Vue源码解析系列——组件篇:组件的patch过程

准备

vue版本号2.6.12,为方便分析,选择了runtime+compiler版本。

回顾

如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:《Vue源码分析系列:目录》

createElm

在之前解析patch(点击查看)的时候,我们在createElm中我们已经见过了createComponent
createElm中会先尝试着调用createComponent来确定是不是一个组件,如果是组件就进入createComponent的逻辑,如果不是组件就继续向下。所以我们这次要进入createComponent来看看具体的逻辑。

createComponent(patch.js)

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data;
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
      //判断是否有data.hook以及data.hook.init并调用
      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);
        //在DOM上插入组件
        insert(parentElm, vnode.elm, refElm);
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
        }
        return true;
      }
    }
  }

一开始是keep-alive的逻辑,我们跳过,进入一个判断

//判断是否有data.hook以及data.hook.init并调用
if (isDef((i = i.hook)) && isDef((i = i.init))) {
  i(vnode, false /* hydrating */);
}

这个判断的意思是如果vnode.data中有hook且有init的话,就调用init这个hookhook的赋值详见:Vue源码解析系列——组件篇:createComponent的执行过程。
进入inithook

init hook

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);
 }
}

一开始是keep-alive的逻辑,直接跳过看else:

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

调用createComponentInstanceForVnode,返回一个组件实例。

createComponentInstanceForVnode

进入createComponentInstanceForVnode:

export function createComponentInstanceForVnode(
  // we know it's MountedComponentVNode but flow doesn't
  vnode: any,
  // activeInstance in lifecycle state
  parent: any
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode, //父组件的vnode
    parent, //activeInstance,当前vm的实例
  };
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate;
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render;
    options.staticRenderFns = inlineTemplate.staticRenderFns;
  }
  //实例化组件,顺便调用_init_()
  return new vnode.componentOptions.Ctor(options);
}

首先是定义一个配置,_isComponent: true_parentVnode: vnode将父组件的vnode赋值给_parentVnodeparent:parent,这个parent其实就是activeInstanceactiveInstance定义在lifecycle.js中,我们一会看。
这个方法最后new了一个组件的实例,调用了构造函数,这个构造函数的定义:

const Sub = function VueComponent(options) {
 this._init(options);
};

上一篇有讲,不再过多赘述。
进入_init:

_init

if (options && options._isComponent) {
      //对组件option的合并
      // 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的合并
   vm.$options = mergeOptions(
     resolveConstructorOptions(vm.constructor),
     options || {},
     vm
   );
 }

由于上面定义了_isComponenttrue,所以进入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; //activeInstance,当前vm的实例,也就是组件的父vm实例
  opts._parentVnode = parentVnode; //占位符vnode

  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;
  }
}

其实就是一些options的合并。
回到_init,继续向下,运行initLifecycle(vm);,进入:

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

  // locate first non-abstract parent
  let parent = options.parent;
  //父组件的$children入栈
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent;
    }
    parent.$children.push(vm);
  }
  //将$parent指向options.parent
  vm.$parent = parent;
  vm.$root = parent ? parent.$root : vm;

  vm.$children = [];
  vm.$refs = {};

  vm._watcher = null;
  vm._inactive = null;
  vm._directInactive = false;
  vm._isMounted = false;
  vm._isDestroyed = false;
  vm._isBeingDestroyed = false;
}

这边其实也是对options的一些处理,屡清楚了一些父子关系。
lifecycle.js中我们会看到一个函数:

export function setActiveInstance(vm: Component) {
  const prevActiveInstance = activeInstance;
  activeInstance = vm;
  return () => {
    activeInstance = prevActiveInstance;
  };
}

结合下面的_update中的:

const restoreActiveInstance = setActiveInstance(vm);
//渲染vnode
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
/**
 * 深度优先patch
 * 第一次生成的时候第一个参数是一个真实的dom
 * 之后调用pathc就全部使用vnode了
 */
if (!prevVnode) {
  // initial render
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
  // updates
  vm.$el = vm.__patch__(prevVnode, vnode);
}
//将activeInstance还原成记录中的上一个activeInstance
//深度优先一层一层还原
restoreActiveInstance();

我们可以发现,这个activeInstance其实就是当前激活的vm,由于__patch__是一个深度优先的方法,所以这个activeInstance会一层一层的深度赋值,最后又使用了restoreActiveInstance一层一层还原回父级的vm。以一种巧妙的方式屡清了组件的父子关系。
好了,回到_init:

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

由于组件是没有el的,所以这边的$mount并不会触发,那组件是在哪里挂载的呢?
我们再回到inithook,发现,在createComponentInstanceForVnode之后立即进行了组件的挂载:

const child = (vnode.componentInstance = createComponentInstanceForVnode(
   vnode,
   activeInstance
));
//挂载子组件
child.$mount(hydrating ? vnode.elm : undefined, hydrating);

于是,这就形成了一个递归,$mount又会调用patch,又会执行上面说的所有步骤。
最后执行完inithook之后,又回回到createComponent(patch.js)方法:

 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);
    //在DOM上插入组件
    insert(parentElm, vnode.elm, refElm);
    if (isTrue(isReactivated)) {
      reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
    }
    return true;
  }

调用方法initComponent,之后把组件insert至真实的dom。
进入initComponent:

function initComponent(vnode, insertedVnodeQueue) {
    if (isDef(vnode.data.pendingInsert)) {
      insertedVnodeQueue.push.apply(
        insertedVnodeQueue,
        vnode.data.pendingInsert
      );
      vnode.data.pendingInsert = null;
    }
    //将patch后的dom赋值给vnode.elm以便于insert方法插入至dom
    vnode.elm = vnode.componentInstance.$el;
    if (isPatchable(vnode)) {
      invokeCreateHooks(vnode, insertedVnodeQueue);
      setScope(vnode);
    } else {
      // empty component root.
      // skip all element-related modules except for ref (#3455)
      registerRef(vnode);
      // make sure to invoke the insert hook
      insertedVnodeQueue.push(vnode);
    }
  }

这里的关键代码在vnode.elm = vnode.componentInstance.$el;,将组件实例的$el赋值给vnode.elm,这样insert就可以正常插入组件了。
至此,组件的patch过程分析完毕。
最后奉上一个自己制作的流程图。

附录:组件patch流程图

你可能感兴趣的:(vue,源码)