vue 源碼學習(三) 建立Virtual 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.jshtml

// 簡化版本
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,vnodevue

vm._renderProxy

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

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),找到定義:es6

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的特點屬性,在渲染過程也會用到.bash

vm.$createElement

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

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

能夠看出,createElement就是vm.$createElementdom

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

// 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
}
複製代碼

若是chilren中有一個是數組則將整個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

相關文章
相關標籤/搜索