vue 源碼學習三 vue中如何生成虛擬DOM


vm._render 生成虛擬dom

我們知道在掛載過程中, $mount 會調用 vm._update和vm._render 方法,vm._updata是負責把VNode渲染成真正的DOM,vm._render方法是用來把實例渲染成VNode,這里的_render是實例的私有方法,和前面我們說的vm.render不是同一個,先來看下vm._render定義,vm._render是通過renderMixin(Vue)掛載的,定義在src/core/instance/render.js

// 簡化版本
Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options
  ...
  // render self
  let vnode
  try {
    // _renderProxy生產環境下是vm
    // 開發環境可能是proxy對象
    vnode = render.call(vm._renderProxy, vm.$createElement) // 近似vm.render(createElement)
  } catch (e) {...}
  // return empty vnode in case the render function errored out
  if (!(vnode instanceof VNode)) {
    if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {...}
    vnode = createEmptyVNode()
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}

  • 先緩存vm.$options.rendervm.$options._parentVnodevm.$options.render是在上節的$mount中通過comileToFunctions方法將template/el編譯來的。
  • vnode = render.call(vm._renderProxy, vm.$createElement)調用了render方法,參數是vm._renderProxy,vm.$createElement
  • 拿到vnode后,判斷類型是否為VNode,如果有多個vnode,則是模板上有多個根節點,觸發告警。
  • 掛載vnode父節點,最后返回vnode

簡要概括,vm._render函數最后是通過render執行了createElement方法並返回vnode;下面就來具體看下vm._renderProxy,vm.$createElement,vnode

vm._renderProxy

首先來看下vm._renderProxyvm._renderProxy是在_init()中掛載的:

Vue.prototype._init = function (options?: Object) {
  ...
  if (process.env.NODE_ENV !== 'production') {
    // 對vm對一層攔截處理,當使用vm上沒有的屬性時將告警      
    initProxy(vm)
  } else {
    vm._renderProxy = vm
  }
  ...
}

如果是生產環境,vm._renderProxy直接就是vm;開發環境下,執行initProxy(vm),找到定義:

initProxy = function initProxy (vm) {
  if (hasProxy) {
    // determine which proxy handler to use
    const options = vm.$options
    const handlers = options.render && options.render._withStripped
      ? getHandler
      : hasHandler
    // 對vm對一層攔截處理
    vm._renderProxy = new Proxy(vm, handlers)
  } else {
    vm._renderProxy = vm
  }
}

先判斷當前是否支持Proxy(ES6新語法),支持的話會實例化一個Proxy, 當前例子用的是hasHandler(只要判斷是否vm上有無屬性即可),這樣每次通過vm._renderProxy訪問vm時,都必須經過這層代理:

// 判斷對象是否有某個屬性
const hasHandler = {
  has (target, key) {
    // vm中是否有key屬性
    const has = key in target
    // 當key是全局變量或者key是私有屬性且key沒有在$data中,允許訪問該key
    const isAllowed = allowedGlobals(key) ||
      (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
    // 沒有該屬性且不允許訪問該屬性時發起警告
    if (!has && !isAllowed) {
      if (key in target.$data) warnReservedPrefix(target, key)
      else warnNonPresent(target, key)
    }
    return has || !isAllowed
  }
}

所以,_render中的vnode = render.call(vm._renderProxy, vm.$createElement),實際上是執行vm._renderProxy.render(vm.$createElement)

Virtual DOM 虛擬dom

vue.2.0中引入了virtual dom ,大大提升了代碼的性能。所謂virtual dom ,就是用js對象去描述一個dom節點,這比真實創建dom快很多。在vue中,Virtual dom是用類vnode來表示,vnodesrc/core/vdom/vnode.js中定義,有真實dom上也有的屬性,像tag/text/key/data/children等,還有些是vue的特色屬性,在渲染過程也會用到.

vm.$createElement

vue文檔中介紹了render函數,第一個參數就是createElement,之前的例子轉換成render函數就是:

<div id="app">
  {{ message }}
</div>
// 轉換成render:
render: function (createElement) {
  return createElement('div', {
     attrs: {
        id: 'app'
      },
  }, this.message)
}

可以看出,createElement就是vm.$createElement

找到vm.$createElement定義,在initRender方法中,

// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions. 
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

看到這里定義了2個實例方法都是調用的createElement,一個是用於編譯生成的render方法,一個是用於手寫render方法,createElement最后會返回Vnode,來看下createElement的定義:

export function createElement (
  context: Component, //vm實例
  tag: any,
  data: any, //可以不傳
  children: any,// 子節點
  normalizationType: any,
  alwaysNormalize: boolean
) {
  // 參數判斷,不傳data時,要把children,normalizationType參數往前移
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

先經過參數重載,根據alwaysNormalize傳不同的normalizationType,調用_createElement(),實際上createElement是提前對參數做了一層處理
這里的參數重載有個小點值得注意,normalizationType是關系到后面children的扁平處理,沒有children則不需要對normalizationType賦值,childrennormalizationType就都是空值

_createElement()

  1. 首先校驗data,data是響應式的,調用createEmptyVNode直接返回注釋節點:
export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true//注釋vnode
  return node
}
  1. 處理tag,沒有tag時也返回注釋節點
  2. key做基礎類型校驗
  3. children中有function類型作slot處理,此處先不作分析
  4. childrennormalize 變成vnode一維數組,有2種不同的方式:normalizeChildrensimpleNormalizeChildren
  5. 創建vnode

simpleNormalizeChildren

normalizeChildrensimpleNormalizeChildren是2種對children扁平化處理的方法,先來看下simpleNormalizeChildren定義:

export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    // 把嵌套數組拍平成一維數組
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}

如果children中有一個是數組則將整個children作為參數組用concat連接,可以得到每個子元素都是vnodechildren,這適用於只有一級嵌套數組的情況

normalizeChildren

export function normalizeChildren (children: any): ?Array<VNode> {
  // 判斷是否基礎類型,是:創建文本節點,否:判斷是否數組,是:作normalizeArrayChildren處理
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

普通的children處理:最后也是返回一組一維vnode的數組,當children是Array時,執行normalizeArrayChildren

normalizeArrayChildren

代碼較長,此處就不貼了,可以自己對照源碼來分析:

  • 定義res
  • 遍歷children,當children[i]是空或者是布爾值,跳過該次循環
  • 如果children[i]還是個數組,再對children[i]normalizeArrayChildren處理
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)// 返回vnode數組
        // merge adjacent text nodes 
        // 優化:如果c的第一個vnode和children上一次處理的vnode都是文本節點可以合並成一個vnode
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        res.push.apply(res, c)
      }
    } else if (){...}
    
  • children[i]是基礎類型時
    } else if (isPrimitive(c)) {
      // 當c是基礎類型時
      // children上一次處理的vnode是文本節點,則合並成一個文本節點
      if (isTextNode(last)) {
        // merge adjacent text nodes
        // this is necessary for SSR hydration because text nodes are
        // essentially merged when rendered to HTML strings
        // 這是SSR hydration所必需的,因為文本節點渲染成html時基本上都是合並的
        res[lastIndex] = createTextVNode(last.text + c)
      } else if (c !== '') {
        // convert primitive to vnode
        res.push(createTextVNode(c))// c不為空直接創建文本節點
      }
    } else {
    
  • 其它情況,children[i]vnode時,
    } else {// 當c是vnode時
      if (isTextNode(c) && isTextNode(last)) {
        // merge adjacent text nodes
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        // default key for nested array children (likely generated by v-for)
        // 特殊處理,先略過
        if (isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__`
        }
        // push到res上
        res.push(c)
      }
    }
    
  • 最后返回一組vnode
    主要有2個點,一是normalizeArrayChildren的遞歸調用,二是文本節點的合並

創建vnode

  1. 創建vnode,並返回
  • 判斷tag類型,為字符串時:
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // 判斷tag是否是原生標簽
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component組件部分先略過
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      // 未知標簽,創建vnode
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
    
    
    • tag不是字符串類型時,vnode = createComponent(tag, data, context, children),先略過
    • 最后再對生成的vnode作校驗,返回vnode

小結

到此為止,我們分析了vm._render方法和_createElement方法,知道了創建vnode的整個過程,在$mount中的 vm._update(vm._render(), hydrating)vm._render返回了vnode,再傳入vm._update中,由vm._update渲染成真實dom


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM