根據調試工具看Vue源碼之虛擬dom(二)

前言

上回咱們提到,在子組件存在的狀況下,父組件在執行完created鉤子函數以後生成子組件的實例,子組件執行created鉤子函數,同時也檢查是否也有子組件,有則重複父組件的步驟,不然子組件的dom元素渲染javascript

深刻了解vnode

在上一篇文章中其實咱們提到一個函數 —— createComponentInstanceForVnode👇前端

function createComponentInstanceForVnode ( vnode, // we know it's MountedComponentVNode but flow doesn't parent // activeInstance in lifecycle state ) {
  var options = {
    _isComponent: true,
    _parentVnode: vnode,
    parent: parent
  };
  // check inline-template render functions
  var inlineTemplate = vnode.data.inlineTemplate;
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render;
    options.staticRenderFns = inlineTemplate.staticRenderFns;
  }
  return new vnode.componentOptions.Ctor(options)
}
複製代碼

與之相關的代碼👇java

...
var child = vnode.componentInstance = createComponentInstanceForVnode(
    vnode,
    activeInstance
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
複製代碼

從中咱們能夠得知:node

  • 子組件的實例是由createComponentInstanceForVnode生成的
  • 上面的結論與vnode.componentOptions.Ctor(options)有關

VNode

經過全局檢索componentOptions,可知存在以下代碼👇瀏覽器

var VNode = function VNode ( tag, data, children, text, elm, context, componentOptions, asyncFactory ) {
  this.tag = tag;
  this.data = data;
  this.children = children;
  this.text = text;
  this.elm = elm;
  ...
}
複製代碼

實際上,在beforeMount鉤子和mounted鉤子之間,有段奇怪的代碼😅微信

new Watcher(vm, updateComponent, noop, {
    before: function before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate');
      }
    }
  }, true /* isRenderWatcher */);
複製代碼

看過前面的文章的你其實已經知道Watcher的執行邏輯:閉包

  1. 初始化相關屬性,其中包括getter屬性
  2. value賦值的同時執行getter

updateComponent實現:app

updateComponent = function () {
    vm._update(vm._render(), hydrating);
};
複製代碼

這意味着函數 updateComponent 將被執行,同時存在這樣的調用順序(從上往下執行):dom

  • vm._render
  • vm_update

同時dom元素確定也是在這兩個函數調用時渲染async

vm._render

Vue.prototype._render = function () {
    var vm = this;
    var ref = vm.$options;
    var render = ref.render;
    var _parentVnode = ref._parentVnode;

    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      );
    }

    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode;
    // render self
    var vnode;
    try {
      // There's no need to maintain a stack becaues all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm;
      vnode = render.call(vm._renderProxy, vm.$createElement);
    } catch (e) {
      handleError(e, vm, "render");
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e);
        } catch (e) {
          handleError(e, vm, "renderError");
          vnode = vm._vnode;
        }
      } else {
        vnode = vm._vnode;
      }
    } finally {
      currentRenderingInstance = null;
    }
    // if the returned array contains only a single node, allow it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0];
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        );
      }
      vnode = createEmptyVNode();
    }
    // set parent
    vnode.parent = _parentVnode;
    return vnode
  };
複製代碼

簡單梳理下函數 _render 的執行過程(從上往下):

  • _parentVnode(父組件的 vnode) 賦值給 vm.$vnode
  • 執行 normallizeScopedSlots,將父子組件的 $slots$scopedSlots 合併
  • 執行 render 函數並賦值給 vnode(即獲得現有的 vnode
  • 若是 vnode 爲空則執行 createEmptyVNode 函數
  • 返回 vnode

這裏咱們優先把斷點打入 render 函數,理所固然的會獲得如下執行過程:

  • render
  • vm.$createElement

因爲最早執行的是 new Vue({...}),因此看上去斷點好像停在了「奇怪的地方」👇

new Vue({
  render: h => h(App),
}).$mount('#app')
複製代碼

render 函數

細心的同窗會注意到這樣一行代碼👇

vnode = render.call(vm._renderProxy, vm.$createElement);
複製代碼

步進以後斷點立刻跳到了這裏👇

new Vue({
    render: h => h(App),
    ...
}).$mount('#app')
複製代碼

其實,這裏將 vm._renderProxy 做爲了 render 函數的上下文對象,而 vm.$createElement 返回一個閉包函數做爲 render 函數的參數傳入

相關代碼:

vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
複製代碼

總結

總結下生成 vnode 的完整邏輯:

  • 執行 $mount 函數
  • 判斷是否在瀏覽器環境下,是則獲取 dom 元素並賦值給 el 變量,不然 el 變量取值 undefined
  • 執行 mountComponent 函數
  • 執行 new Watcher(vm, updateComponent, noop, ...)
  • 因爲 Watcher 的「特性」(傳入的 updateComponent 賦值給 getter 以後執行),_render 函數在這以後會被觸發
  • 執行 $createElement
  • 執行 createElement
  • 執行 _createElement
  • 判斷參數 datadata.is 是否不爲空,是則將 data.is 賦值給 tag
  • 若是 tag 爲空,那麼認爲這是一個空白節點,此時調用 createEmptyVNode 建立一個「空白節點」,並把 isComment 標記爲 true
  • 判斷 tag 是不是「保留字」,是則屬於 HTML 標籤,生成對應的 vnode,不然調用 createComponent 函數生成對應的 vnode
  • 最後返回 vnode

相關函數

createEmptyVNode

var createEmptyVNode = function (text) {
  if ( text === void 0 ) text = '';

  var node = new VNode();
  node.text = text;
  node.isComment = true;
  return node
};
複製代碼

掃描下方的二維碼或搜索「tony老師的前端補習班」關注個人微信公衆號,那麼就能夠第一時間收到個人最新文章。

相關文章
相關標籤/搜索