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

初次探索

什麼是虛擬dom

Vue 經過創建一個虛擬 DOM 對真實 DOM 發生的變化保持追蹤。請仔細看這行代碼:javascript

return createElement('h1', this.blogTitle)
複製代碼

createElement 到底會返回什麼呢?其實不是一個實際的 DOM 元素。它更準確的名字多是 createNodeDescription,由於它所包含的信息會告訴 Vue 頁面上須要渲染什麼樣的節點,及其子節點。咱們把這樣的節點描述爲「虛擬節點 (Virtual Node)」,也常簡寫它爲「VNode」。「虛擬 DOM」是咱們對由 Vue 組件樹創建起來的整個 VNode 樹的稱呼。html

以上這段對虛擬Dom的簡短介紹來自Vue官網前端

第一個斷點

咱們一開始的斷點先打在app.vue的兩個hook上:vue

export default {
    name: 'app',
    created () {
        debugger
    },
    mounted () {
        debugger
    }
}
複製代碼

刷新頁面,此時調用棧中顯示的函數跟預想中的不太同樣:
java

avatar

created這個hook執行以前,多出了一些比較奇怪的函數:node

  • createComponentInstanceForVnode
  • Vue._update
  • mountComponent

🤔看完之後我心中出現了一個疑問:express

爲何在created鉤子執行以前就出現了mountComponent這個方法,究竟是文檔出問題了,仍是文檔出問題了呢?帶着這個疑惑咱們接着往下看瀏覽器

mountComponent作了什麼?

經過上面打第一個斷點,其實不難看出這樣的執行順序(從上往下):微信

  • (annoymous)
  • Vue.$mount
  • mountComponent

(annoymous)這步其實就是在執行咱們的main.js,代碼很短:閉包

...
new Vue({
    render: h => h(App)
}).$mount('#app')
複製代碼
Vue.$mount
Vue.prototype.$mount = function ( el, hydrating ) {
    // 判斷是否處於瀏覽器的環境
    el = el && inBrowser ? query(el) : undefined;
    // 執行mountComponent
    return mountComponent(this, el, hydrating)
};
複製代碼
mountComponent
function mountComponent ( vm, el, hydrating ) {
  vm.$el = el;
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode;
    // 開發環境下給出警告提示
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        );
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        );
      }
    }
  }
  callHook(vm, 'beforeMount');

  var updateComponent;
  /* istanbul ignore if */
  // 這裏對測試環境跟正式環境的updateComponent 作了實現上的一個區分
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = function () {
      var name = vm._name;
      var id = vm._uid;
      var startTag = "vue-perf-start:" + id;
      var endTag = "vue-perf-end:" + id;

      mark(startTag);
      var vnode = vm._render();
      mark(endTag);
      measure(("vue " + name + " render"), startTag, endTag);

      mark(startTag);
      vm._update(vnode, hydrating);
      mark(endTag);
      measure(("vue " + name + " patch"), startTag, endTag);
    };
  } else {
    updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined

  new Watcher(vm, updateComponent, noop, {
    before: function before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate');
      }
    }
  }, true /* isRenderWatcher */);
  hydrating = false;

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true;
    callHook(vm, 'mounted');
  }
  return vm
}
複製代碼

簡單羅列下上面這兩段代碼的邏輯👇:

  • 調用beforeMount鉤子函數
  • 封裝一個updateComponent函數
  • 執行new Watcher並將updateComponent當作參數傳入
  • 調用vm._update方法

_update方法是如何被觸發的?

Watcher
var Watcher = function Watcher ( vm, expOrFn, cb, options, isRenderWatcher ) {
  ...
  // 將函數賦值給this.getter,這裏是updateComponent函數
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = noop;
      process.env.NODE_ENV !== 'production' && warn(
        "Failed watching path: \"" + expOrFn + "\" " +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.',
        vm
      );
    }
  }
  // 根據this.lazy決定是否觸發get方法
  this.value = this.lazy
    ? undefined
    : this.get();
};
Watcher.prototype.get = function get () {
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    // 這裏調用getter方法,實際上也就是調用updateComponent方法並拿到返回值
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  // 返回函數(updateComponent)執行結果
  return value
};
複製代碼

簡單梳理下上面這段代碼的邏輯:

  • 新建Watcher實例時,將updateComponent賦值給getter屬性
  • 經過this.get方法,觸發updateComponent函數
  • 最終拿到函數的執行結果

小結

經過上面的分析咱們能夠初步得出一個結論:

組件的渲染跟Watcher離不開關係,父組件在執行完created鉤子函數以後,會調用updateComponent函數對子組件進行處理

深刻研究

若是前面你動手跟着斷點一直走,那麼不可貴知存在這樣的調用關係(從上往下):

  • ...
  • mountComponent
  • Watcher
  • get
  • updateComponent
  • Vue._update
  • patch
  • createElm
  • createComponent
  • init
  • createComponentInstanceForVnode
  • VueComponent
  • Vue._init
  • callHook
  • invokeWithErrorHandling
  • created

Vue.prototype._update

Vue.prototype._update = function (vnode, hydrating) {
    var vm = this;
    var prevEl = vm.$el;
    var prevVnode = vm._vnode;
    // 重存儲當前父實例
    var restoreActiveInstance = setActiveInstance(vm);
    vm._vnode = vnode;
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
    } else {
      // 執行patch函數
      vm.$el = vm.__patch__(prevVnode, vnode);
    }
    restoreActiveInstance();
    ...
  };
複製代碼

固然,咱們經過全局檢索能夠得知_patch函數相關的代碼👇:

// 只在瀏覽器環境下patch函數有效
Vue.prototype.__patch__ = inBrowser ? patch : noop;
複製代碼
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
function createPatchFunction (backend) {
    ...
    return function patch (oldVnode, vnode, hydrating, removeOnly) {
        ...
    }
}
複製代碼

這裏先不深究patch的實現,咱們只要知道patch是使用createPatchFunction來生成的一個閉包函數便可。

子組件的渲染

咱們注意到,在子組件created鉤子執行以前存在一個init方法👇:

var componentVNodeHooks = {
  init: function init (vnode, hydrating) {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      var mountedNode = vnode; // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    } else {
      // 建立子組件實例
      var child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      );
      // 對子組件執行$mount方法
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    }
  },
  ...
複製代碼

相關代碼:

createComponentInstanceForVnode
function createComponentInstanceForVnode ( vnode, // we know it's MountedComponentVNode but flow doesn't parent // activeInstance in lifecycle state ) {
  // 初始化一個子組件的vnode配置
  var options = {
    _isComponent: true,
    _parentVnode: vnode,
    parent: parent
  };
  // 檢查render函數內是否有template模板
  var inlineTemplate = vnode.data.inlineTemplate;
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render;
    options.staticRenderFns = inlineTemplate.staticRenderFns;
  }
  // 返回子組件實例
  return new vnode.componentOptions.Ctor(options)
}
複製代碼

總結

  1. 存在子組件時,先初始化父組件,在created鉤子執行以後,生成子組件的vnode實例
  2. 子組件的created鉤子執行完,檢查子組件是否也有子組件
  3. 子組件也存在子組件時,則重複1,不然直接執行$mount函數,渲染子組件

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

相關文章
相關標籤/搜索