【Vue源码】09.组件化-patch

245 阅读4分钟

组件化-patch

08.组件化-createComponent 中,我们通过 createComponent 创建了组件的 VNode,然后执行 _update 函数,也就是 patch 函数,把 VNode 转换为真正的 浏览器DOM 节点。patch 函数在之前分析普通对象时已经大概了解 【Vue 源码】数据驱动-7._update,接下来我们具体分析一下当组件的 VNode 是如何被解析的。

patch 会调用 createElm 生成真实的 浏览器DOM 并插入到对应的节点,createElm 定义在 src/core/vdom/patch.js 中:

function createElm(
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  // ...
  vnode.isRootInsert = !nested; // for transition enter check
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return;
  }
  // ...
}

createComponent

这里对判断 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的结果,如果为 true 直接结束。createComponent 也是定义在 src/core/vdom/patch.js 中:

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data;
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
    if (isDef((i = i.hook)) && isDef((i = i.init))) {
      i(vnode, false /* hydrating */);
    }
    // after calling the init hook, if the vnode is a child component
    // it should've created a child instance and mounted it. the child
    // component also has set the placeholder vnode's elm.
    // in that case we can just return the element and be done.
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue);
      insert(parentElm, vnode.elm, refElm);
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
      }
      return true;
    }
  }
}

createComponent 中,首先对 vnode.data 做判断,如果是 component 组件那么这些条件就会满足,执行 i.init 方法。

let i = vnode.data;
if (isDef(i)) {
  const isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
  if (isDef((i = i.hook)) && isDef((i = i.init))) {
    i(vnode, false /* hydrating */);
  }
  // ...
}

i.init 出现在上一节中,就是在创建组件 VNode 时,执行的合并钩子函数 installComponentHooks

const componentVNodeHooks = {
  init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode; // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    } else {
      const child = (vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      ));
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    }
  },
  // ...
};

init 钩子除去 keepAlive 的情况下,会走到下面逻辑:

const child = (vnode.componentInstance = createComponentInstanceForVnode(
  vnode,
  activeInstance
));

createComponentInstanceForVnode

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

createComponentInstanceForVnode 方法会创建一个 创建组件的 options,然后调用 new vnode.componentOptions.Ctor(options), 这里的 vnode.componentOptions.Ctor 就是子组件对应的构造函数, 我们在 08.组件化-createComponent 中已经知道它实际上是继承于 Vue 的一个构造器 Sub

Vue.extend = function (extendOptions: Object): Function {
  // ...
  // Vue实例的初始化逻辑,初始化的同时添加全局api
  const Sub = function VueComponent(options) {
    this._init(options);
  };
  // ...
  return Sub;
};

所以 子组件实例化时机 就是在这里执行的,并且执行 _init 方法,在执行 _init 方法时,和之前有点不同:

// 给Vue的原型prototype添加 _init方法,在new Vue初始化实例时调用
Vue.prototype._init = function (options?: Object) {
  const vm: Component = this;
  // ...

  if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options);
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    );
  }

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

在这次的流程中,因为 options._isComponenttrue,所以执行 initInternalComponent(vm, options) 进行合并配置。

initInternalComponent

export function initInternalComponent(
  vm: Component,
  options: InternalComponentOptions
) {
  const opts = (vm.$options = Object.create(vm.constructor.options));
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode;
  // 父实例
  opts.parent = options.parent;
  // 父 VNode
  opts._parentVnode = parentVnode;

  const vnodeComponentOptions = parentVnode.componentOptions;
  opts.propsData = vnodeComponentOptions.propsData;
  opts._parentListeners = vnodeComponentOptions.listeners;
  opts._renderChildren = vnodeComponentOptions.children;
  opts._componentTag = vnodeComponentOptions.tag;

  if (options.render) {
    opts.render = options.render;
    opts.staticRenderFns = options.staticRenderFns;
  }
}

initInternalComponent 流程中,只有几个关键的点需要记住:

  1. opts.parent = options.parent
  2. opts._parentVnode = parentVnode

他是把之前我们通过 createComponentInstanceForVnode 函数传入的几个参数合并到组件内部的 $options 选项里了。

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

回到 _init,因为在组件初始化的时候,没有 el,所以在 _init 方法中不执行 mount

组件的 mount 时机是在 i.init,即上面写到的这里执行的

const componentVNodeHooks = {
  init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode; // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    } else {
      const child = (vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      ));
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    }
  },
  // ...
};

在这里完成实例化 _init 之后执行 child.$mount(hydrating ? vnode.elm : undefined, hydrating)hydrating 为服务端渲染标识,所以这里为 child.$mount(undefined, false),最终也会调用 mountComponent,进而执行 vm._render

Vue.prototype._render = function (): VNode {
  const vm: Component = this;
  const { render, _parentVnode } = vm.$options;
  //...
  vm.$vnode = _parentVnode;
  let vnode;
  try {
    // 主要执行此方法
    vnode = render.call(vm._renderProxy, vm.$createElement);
  } catch (e) {
    // ...
  }
  // ...
  // set parent
  vnode.parent = _parentVnode;
  return vnode;
};

这里的 _parentVnode 就是当前组件的父 VNode,而 render 函数生成的 vnode 当前组件的渲染 vnodevnodeparent 指向了 _parentVnode,也就是 vm.$vnode,它们是一种父子的关系。

我们知道在 _render 执行完生成 VNode 之后又会去执行 _update 去渲染 VNode,来看一下在渲染的过程中,组件VNode 是什么流程。 vm._update 的定义在 src/core/instance/lifecycle.js 中:

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this;
  const prevEl = vm.$el;
  const prevVnode = vm._vnode;
  const prevActiveInstance = activeInstance;
  activeInstance = 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 {
    // updates
    // 更新
    vm.$el = vm.__patch__(prevVnode, vnode);
  }
  activeInstance = prevActiveInstance;
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null;
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm;
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el;
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
};

_update 有几个关键的点:

  1. vm._vnode = vnode,vnode 是通过 _render 生成的
  2. vm._vnodevm.$vnode 是一种父子关系,可以表示为 vm._vnode.parent === vm.$vnode
  3. activeInstance 的作用是保持当前上下文中 Vue 的实例,它定义在 src/core/instance/lifecycle.js 中,是模块全局变量。我们之前调用 createComponentInstanceForVnode 时传入的 activeInstance 参数,就是从 lifecycle 导入的。因为 JavaScript 是单线程的,所以在父子、不同组件进行不同操作时,锁定当前组件实例,并把它作为子组件的父 Vue 实例。

之前我们提到过对子组件的实例化过程先会调用 initInternalComponent(vm, options) 合并 options,把 parent 存储在 vm.$options 中,在 $mount 之前会调用 initLifecycle(vm) 方法:

export function initLifecycle(vm: Component) {
  const options = vm.$options;

  // locate first non-abstract parent
  let parent = options.parent;
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent;
    }
    parent.$children.push(vm);
  }

  vm.$parent = parent;
  // ...
}

可以看到 vm.$parent 就是用来保留当前 vm 的父实例,并且通过 parent.$children.push(vm) 来把当前的 vm 存储到父实例的 $children 中。

vm._update 的过程中,把当前的 vm 赋值给 activeInstance,同时通过 const prevActiveInstance = activeInstanceprevActiveInstance 保留上一次的 activeInstance。实际上,prevActiveInstance 和当前的 vm 是一个父子关系,当一个 vm 实例完成它的所有子树的 patch 或者 update 过程后,activeInstance 会回到它的父实例,这样就完美地保证了 createComponentInstanceForVnode 整个深度遍历过程中,我们在实例化子组件的时候能传入当前子组件的 父Vue 实例,并在 _init 的过程中,通过 vm.$parent 把这个父子关系保留。

最后回到 patch,就是调用 __patch__ 来渲染 VNode

function patch(oldVnode, vnode, hydrating, removeOnly) {
  // ...
  let isInitialPatch = false;
  const insertedVnodeQueue = [];

  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true;
    createElm(vnode, insertedVnodeQueue);
  } else {
    // ...
  }
  // ...
}

这里又回到了文章的开始,在这里我们只传了 2 个参数,所以 parentElmundefined。我们来看 createElm 的定义:

function createElm(
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  // ...
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return;
  }

  const data = vnode.data;
  const children = vnode.children;
  const tag = vnode.tag;
  if (isDef(tag)) {
    // ...
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode);
    setScope(vnode);

    if (__WEEX__) {
      // ...
    } else {
      createChildren(vnode, children, insertedVnodeQueue);
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue);
      }
      insert(parentElm, vnode.elm, refElm);
    }

    if (process.env.NODE_ENV !== "production" && data && data.pre) {
      creatingElmInVPre--;
    }
  } else if (isTrue(vnode.isComment)) {
    vnode.elm = nodeOps.createComment(vnode.text);
    insert(parentElm, vnode.elm, refElm);
  } else {
    vnode.elm = nodeOps.createTextNode(vnode.text);
    insert(parentElm, vnode.elm, refElm);
  }
}

我们在这里传入的 vnode 是组件渲染的 vnode,也就是之前说的 vm._vnode。如果组件的根节点是普通元素,那么 vm._vnode 也是普通的 vnode,这里 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的值是 false,接下来就和我们之前的分析一样了,先创建一个父节点占位符,然后再遍历所有子 VNode 递归调用 createElm,在遍历的过程中,如果遇到子 VNode 是一个组件的 VNode,则重复本节开始的过程,这样通过一个递归的方式就可以完整地构建了整个组件树。

由于我们传入的 parentElmundefined,所以对组件的插入,在 createComponent 中有那么一段逻辑:

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data;
  if (isDef(i)) {
    // ....
    if (isDef((i = i.hook)) && isDef((i = i.init))) {
      i(vnode, false /* hydrating */);
    }
    // ...
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue);
      insert(parentElm, vnode.elm, refElm);
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
      }
      return true;
    }
  }
}

在完成组件的整个 patch 过程后,最后执行 insert(parentElm, vnode.elm, refElm) 完成组件的 DOM 插入,如果组件 patch 过程中又创建了子组件,那么 DOM 的插入顺序是先子后父。

总结

09.组件化-patch.png

目前我们已经知道,一个组件 VNode 是如何创建、初始化、渲染的过程了,在这个过程中先渲染 <App/> 这样的占位符节点,然后渲染 App.vue 里面的内容,如果碰到里面还有组件的话再进行递归。我们知道组件的编写其实是返回一个 JavaScript 对象,那么下一节将深入了解组件的合并配置。

源码分析 GitHub 地址

参考:Vue.js 技术揭秘