Skip to content

源码笔记(七):mount 阶段生成 dom

vnode 渲染为真实 dom

接上文,vm._render 通过 render 生成 vnode 后,然后执行 vm._update(vm._render(),hydrating) 来首次渲染成真实 dom,里面执行:

js
if (!prevVnode) {
  // initial render
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
  // updates
  vm.$el = vm.__patch__(prevVnode, vnode);
}

此处为初始渲染,执行第一个分支 vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */); 即执行 patch

patch 中因为 oldVnodevm.$el 是真实节点,则 oldVnode 替换为空 vnode,然后执行:

js
createElm(vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm));

createElm

createElm 主要逻辑:

  • 是组件 vnode,则在 createComponent 处理
  • 不是组件 vnode
    • 是元素节点的 vnode,则创建该标签并设置 css scope,然后通过 createChildren 循环构建子 vnode,然后触发 invokeCreateHooks 处理标签的属性
    • 是注释节点的 vnode,则创建注释节点
    • 其他情况就创建文件节点
    • 最后将创建的真实节点插入到根节点里

createChildren

js
createChildren(vnode, children, insertedVnodeQueue);

createChildren 标志开始构建子 vnode。方法里先通过 checkDuplicateKeys 检查 key 是否重复,然后执行:

js
for (var i = 0; i < children.length; ++i) {
  createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
}

即递归对每一个子 vnode 执行 createElm

invokeCreateHooks

js
if (isDef(data)) {
  // `data` 为:`{ref: "btn", staticClass: "side", on: {click: ƒ}}`。
  invokeCreateHooks(vnode, insertedVnodeQueue);
}

invokeCreateHooks 处理该 vnode 上的 data 属性,里循环调用 cbs.create 数组里的 8 个方法如 updateAttrs,updateClass,updateDOMListeners 等,做一些 style,class,event,$refs 等相关的处理。如果是组件 vnode 且有 insert 方法则将该组件 vnode pushinsertedVnodeQueue

insertedVnodeQueue 记录子节点组件创建顺序的队列。每创建一个组件实例就会往这个队列中插入当前的组件节点 VNode, 当整个 VNode 对象全部转换成为真实的 DOM 树时,会依次调用这个队列中的 VNode hookinsert 方法。

invokeInsertHook

js
function invokeInsertHook(vnode, queue, initial) {
  if (isTrue(initial) && isDef(vnode.parent)) {
    vnode.parent.data.pendingInsert = queue;
  } else {
    for (var i = 0; i < queue.length; ++i) {
      queue[i].data.hook.insert(queue[i]);
    }
  }
}

暂时不考虑服务端渲染,invokeInsertHook 钩子在 patch 方法的最后执行。如果是新建的组件实例 vnode(如未挂载、组件实例)且有父 vnode,则将 queue 即之前的 insertedVnodeQueue 队列存到父 vnode 上的 data.pendingInsert 上,在 initComponent 时,会把其 pushinsertedVnodeQueue

如果不是,则就依次调用每个组件 vnodeinsert 方法,如果组件还未 mounted,则触发 mounted 钩子,如果是 keepAlive 包裹的组件,则执行:

js
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        //已装载好,说明在更新,将 componentInstance push 到 activatedChildren
        //后面组件更新结束后,在 flushSchedulerQueue 执行 callActivatedHooks,即遍历整个activatedChildren队列 执行 activateChildComponent
        queueActivatedComponent(componentInstance);
      } else {
        //触发子元素的 activated 钩子和自己的 activated 钩子。同理,keep-alive 的 deactivated 钩子原理一致
        activateChildComponent(componentInstance, true);
      }

分析 demo

对于本例:

html
<div id="main">
  <!-- 节点 1 -->
  <Bpp></Bpp>
  <!-- 节点 2 -->
  <div v-on:click="plus">info.name:{{info.name}},计算属性:{{compute}}</div>
  <!-- 节点 3 -->
  <App name="one" v-bind:num="info.age"></App>
  <!-- 节点 4 -->
  <div v-on:click="hide">====点击让第二个App组件卸载====</div>
  <!-- 节点 5 -->
  <App name="two" v-if="isShow"></App>
</div>

除开 4 个空节点和一个异步组件占位的空注释节点,还有 4 个子节点。

接下来逐个分析创建流程:

  1. 创建 4 个空节点,即在 createElm 里直接执行 createTextNode 生成空的文本节点后插入到对应的位置,下面的节点分析将略过空节点;
  2. 创建第一个节点(称为节点 1,下一个就为节点 2,以此类推)异步占位节点,即在 createElm 里直接执行 createComment 生成空的注释节点后插入到对应的位置;
  3. 创建节点 2,同前面逻辑一样走 createElm -> createChildren -> createElm -> createChildren ···,递归执行 createElm 判断到 tag 为空,则执行 createTextNode 创建文本节点 info.name:korey,计算属性:29 并调 insert 插入到父节点。然后回到节点 2 的 createElm 继续执行 invokeCreateHooks,然后调 insert 插入到父节点即根组件的 div,此时生成的 dom 节点都赋值在各自 vnode.elm 下;
  4. 创建节点 3,vnodeApp 组件节点,在 createElm 里走 createComponent 方法。下面单独分析;
  5. 创建节点 4,与节点 2 一致;
  6. 创建节点 5,与节点 3 一致;

组件 vnode 渲染为真实 dom

组件节点在 createElm 里走 createComponent。因为组件 vnode.datahook.init 等渲染 vnode 时安装的钩子函数,故执行:

js
i(vnode, false /* hydrating */); //其中 i 为 componentVNodeHooks.init

实例化子组件

执行 componentVNodeHooks.init,方法里先判断如果存在已被 keep-alive 缓存的组件实例 vnode.componentInstance,则调用 componentVNodeHooks.prepatch 直接 updateChildComponent 更新子组件实例即可,就不用去初始化和装载子组件,即不会走一系列常规生命周期钩子。否则执行:

js
var child = (vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance));

createComponentInstanceForVnode 用于创建 component 实例。

方法里执行:new vnode.componentOptions.Ctor(options),即实例化 VueComponent,内部执行 this._init(options) 即原型链上的 Vue 的方法 _init,到此,App 子组件开始走 vueinit 流程(具体与 Vue 实例化差异可以参考第一章分析),触发 App beforeCreate 钩子->App created 钩子,然后执行:

js
if (vm.$options.el) {
  vm.$mount(vm.$options.el);
}

vm.$options.el 不存在,所以子组件的 _init 的执行结束(即子组件实例化完成),将子组件实例赋给 vnode.componentInstancechild

构建子组件

回到 componentVNodeHooks.init 继续执行:

js
child.$mount(hydrating ? vnode.elm : undefined, hydrating);

执行 child.$mount 即原型链上的 Vue 的方法 $mount。因为存在 render,故触发 App beforeMount 钩子 后,直接进入 mount 阶段。

前面已分析,在实例化 渲染 watcher 里触发 updateComponent 先执行 vm._render 生成 vnode

子组件 render 函数

js
var render = function () {
  var _vm = this;
  var _h = _vm.$createElement;
  var _c = _vm._self._c || _h;
  return _c(
    'div',
    { attrs: { id: 'app' } },
    [
      _c('div', [_vm._v(_vm._s(_vm.num ? 'num:' + _vm.num : ''))]),
      _vm._v(' '),
      _c('Child', { staticClass: 'example' }),
    ],
    1,
  );
};

生成 vnode 后,执行 vm._update 渲染成真实 dom。在 patch 方法里,由于 oldVnodevm.$el 不存在,执行 createElm->createChildren->·· 递归生成每一个 vnode 及其子 vnode 对应的真实 dom

构建孙组件

在子组件 Apprender 函数里,创建孙组件 Child。与子组件流程一致,他同样会经历 App 组件的构建逻辑(触发 Child beforeCreate->Child created->Child beforeMount 钩子),执行 vm._update 渲染成真实 dom, 通过 createElm->createChildren->·· 递归在其各层 vnode 里得到对应的 vnode.elm

在孙组件 Child 里的根节点时(createElm 里),由于没有父节点 parentElm,所以执行 insert 时,不会插入节点。

然后回到 patch 触发 invokeInsertHook(此时不存在 insertedVnodeQueue),然后返回 vnode.elmvnodeChild 组件根 vnode) 赋给 vm.$elvmChild 组件实例)。回到 mountComponent 中返回孙组件实例, Child 构建完毕。

真实 dom 插入到文档

孙组件 dom 插入到子组件 dom

回到 createComponent 方法里继续执行:

js
if (isDef(vnode.componentInstance)) {
  initComponent(vnode, insertedVnodeQueue);
  insert(parentElm, vnode.elm, refElm);
  if (isTrue(isReactivated)) {
    reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
  }
  return true;
}

initComponent

js
function initComponent(vnode, insertedVnodeQueue) {
  if (isDef(vnode.data.pendingInsert)) {
    insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
    vnode.data.pendingInsert = null;
  }

  vnode.elm = vnode.componentInstance.$el;

  if (isPatchable(vnode)) {
    invokeCreateHooks(vnode, insertedVnodeQueue);
    setScope(vnode);
  } else {
    registerRef(vnode); // make sure to invoke the insert hook

    insertedVnodeQueue.push(vnode);
  }
}

initComponent 里, vnodeChild 组件 vnode。若vnode.data.pendingInsert 存在(存在说明有子组件 vnode push 进入)则与 insertedVnodeQueue 合并,然后将 Child 组件实例生成的真实 dom 节点 vnode.componentInstance.$el 赋给 Child 组件 vnode.elm 上。

然后判断如果 Child 组件实例 vnode 有标签,则执行 invokeCreateHooks(此处会把 Child 组件 vnode pushinsertedVnodeQueue)和 setScope

initComponent 执行完成后,此时调用 insertvnode.elm 插入到 App 组件对应的 dom 上。到此 Child 孙组件渲染结束。

子组件 dom 插入到根组件 dom

父级即 App 组件的 children 渲染循环也结束,然后回到 patch 触发 invokeInsertHookinsertedVnodeQueue 存入 vnode.parent.data.pendingInsert,然后返回 vnode.elmvnodeApp 组件根 vnode) 赋给 vm.$elvmApp 组件实例)。回到 mountComponent 中返回子组件实例, App 构建完毕。

Child 的流程一致,回到 createComponent 方法执行了 initComponent 后,此时调用 insertvnode.elm 插入到根组件对应的 dom 上。到此,App 组件渲染结束。

整体生命周期

根组件下的 createChildren 里继续循环 createElm,遇到下一个 App 组件的构建 dom 跟之前的逻辑一样,其中构造函数复用。

待到根组件的 createChildren 里渲染循环也结束,在 createElm 里因为有 parentElmbody 节点,所以执行 insert 将整个父组件 vnode.elm 插入到 body 元素中。

回到 patch 方法里继续执行,删掉老的 dom 节点,然后触发 invokeInsertHook,此时 insertedVnodeQueue 里包含之前按顺序 push 进去的 4 个组件 vnode

  1. 第一个孙组件 Child
  2. 第一个子组件 App
  3. 第二个孙组件 Child
  4. 第二个子组件 App

循环依次执行 insert 钩子,方法里会触发各组件生命周期钩子:mounted

然后 patch 方法返回 vnode.elm 赋给根实例 vm.$el, vm._update 执行结束,实例化渲染 watcher 亦结束。

然后回到 mountComponent 继续执行:

js
callHook(vm, 'mounted');

触发生命周期钩子 mounted。在这之后,异步组件才加载结束,开始构建异步组件的生命周期(上一篇文章的异步组件第二阶段已分析),构建完成后,Vue 初始化全部完成。

整个过程中,生命周期顺序为:

js
// 开始加载 Vue ↓↓
vue beforeCreate
vue created
vue beforeMount
// 开始加载第一个 App 组件 ↓↓
App beforeCreate
App one created
App one beforeMount
// 开始加载第一个 App 内的 Child 组件 ↓↓
Child beforeCreate
Child created
Child beforeMount
// 开始加载第二个 App 组件 ↓↓
App beforeCreate
App two created
App two beforeMount
// 开始加载第二个 App 内的 Child 组件 ↓↓
Child beforeCreate
Child created
Child beforeMount
// 调用 invokeInsertHook 依次触发各组件 mounted
// 第一个 App 内的 Child 组件 ↓↓
Child mounted
// 第一个 App 组件 ↓↓
App one mounted
// 第二个 App 内的 Child 组件 ↓↓
Child mounted
// 第二个 App 组件 ↓↓
App two mounted
// Vue加载完成 ↓↓
vue mounted
// 开始加载异步 Bpp 组件 ↓↓
vue beforeUpdate
async Bpp beforeCreate
async Bpp created
async Bpp beforeMount
async Bpp mounted
vue updated

本章小结

  1. 本章介绍了 vue 执行的 Mount 阶段中的通过 vnode 渲染为真实 dom 部分。
  2. 同时分析了组件里子组件,孙组件的渲染过程以及如何将渲染结果 dom 添加到对应的组件 vnode 上。
  3. 分析了 Vue 的整体生命周期。