通过案例读 vue2 源码02

62 阅读3分钟

childComp 组件

new VueComponent()

  1. 在进入子组件之前,我们要知道 VueComponent 构造函数是什么。
function VueComponent(options) {
      this._init(options);
};

这里的 this 指向的是当前 VueComponent 实例,而 VueComponent 是继承自 Vue 的,所以这个 _init 方法就是我们初始化 Vue 的时候看到的方法。

传入的 options 为:

{
  _isComponent: true,  
  _parentVnode: vnode,   // `vue-component-1` 组件
  parent: activeInstance  // 也就是我们存入的当前 vm,作为父节点
}
  1. 进入 childComp 组件的 _init() 方法,再次赋值 vm = this注意这个时候的 vm 就改变了,变成 childComp 这个 vueComponent 的实例了
  2. 组件合并配置会走 initInternalComponent 方法,其中将 vm.constructor.options 也就是 Suboptions 属性(childComp 对象)添加到了 vm.$options 属性上。
  3. 然后执行 chilComp 组件的生命周期:

parent beforeCreate

parent created

  1. 最后由于 vm.$options 上没有 el 属性,这是和 Vue 初始化不同的地方,走到这就跳出了 _init 方法,也跳出了 VueComponnet 的构造函数,再跳出了 createComponentInstanceForVnode 方法,来到了 vue-component-1 这个 vnode 的 data.hook.init 方法。
var child = (vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance));
child.$mount(hydrating ? vnode.elm : undefined, hydrating);  // 

也就是我们以上得到的 VueComponent 实例现在复制给了 child,且执行了 child$mounted 方法,这就是 Vue$mounted 方法,只是这时传入的参数变了,elundefined

Vm.$mounted

  1. 这里由于 childComp 组件没有 render,所以需要根据其 template 去生成 render :
// <div>{{msg}}<button-counter/></div>
ƒ anonymous(
) {
  with(this){return _c('div',[_v(_s(msg)),_c('button-counter')],1)}
}
  1. mountedComponent 方法之后,设置了 vm.$elel(undefined),然后执行生命周期

parent beforeMount

  1. 接着又到了 updateComponent 方法,调用 _render_update 方法。

Vm._render()

  1. 在 _render 方法中,将 vm.$vnode 赋值为 vm.$options._parentVnode,当前为 vue-component-1 这个 vnode
  2. 设置 currentInstance=vm,设置成了当前 childCompVueComponent 实例。
  3. 根据生成的 render 函数,我们会依次执行: _c('button-counter'), _s(msg), _v(_s(msg), _c('div',[_v(_s(msg)),_c('button-counter')],1)。也就是先生成两个 vnode 作为 children 再去生成外层的 vnode
  • _s(msg) _s = toString 得到字符串:QAAA

  • _v(_s(msg) _v = createTextVNode 得到 vnode :

{tag: undefined, text: 'QAAA', children: undefined, parent: undefined,...}
  • _c('button-counter')render 过程的 _createElement 方法中,用户手写的 render 传入的 tag 会是一个对象,通过 el 或者 template 生成的 tag 一般都是一个字符串。当前_c('button-counter') 传入的 tagbutton-counter ,这不是一个保留的页面标签,所以会执行:
isDef((Ctor = resolveAsset(context.$options, 'components', tag)))) 
vnode = createComponent(Ctor, data, context, children, tag);

通过 resolveAsset 得到的 Ctor ,其实就是 components 属性中注册的 ButtonCounter 组件,然后将这个组件对象传到 createComponent 方法。 经过 _render 会生成 tagvue-component-2-button-countervnode,由于是组件的 vnode ,所以有 date.hook.init 方法且没有 children

  • _c('div',[_v(_s(msg)),_c('button-counter')],1)

这样将子节点都转换成 vnode 后,然后再执行外层的 _c('div',[_v(_s(msg)),_c('button-counter')],1),由于此时传入的 tagdiv 这个保留标签,所以直接通过 new Vnode() 生成的 vnode

到这就会继续回到 childComp _render() 方法中继续执行,给 vnode 添加 parent 属性,最终生成的 vnode 为:

tag: 'div',
context: {msg: 'QAAA', $vnode: {tag: 'vue-component-1', ...}},
chidren: [
  {tag: undefined, text: 'QAAA',... },
  {
    tag: 'vue-component-2-button-counter', 
    data:{onundefinedhook: {…}},
    children: undefined, 
    parent: undefined, 
    componentOptions: {propsDataundefinedlistenersundefinedtag'button-counter'childrenundefinedCtor: ƒ}
   ... } 
],
parent: {tag'vue-component-1'data: {…}, childrenundefined, …},
componentOptions: undefined,
...

Vm._update()

  1. _update 方法中,将 preEl = vm.$elprevVnode = vm._vnode,当前的 vm.$elvm._vnode 都为 nullvm._vnodeinitRender 的时候就直接赋为 null 了。

  2. 将当前 vm(div VueComponent) 设置为了全局变量 activeInstance,且将 vm 保存在了局部作用域中的 prevActiveInstance 变量中,在执行完 __patch__ 方法后会将保存的 vm 重新设置回 activeInstance 变量中。

  3. vm._vnode 设置为了刚生成的 vnode(div Vnode)

  4. 然后执行 __patch__ 方法,传入了 vm.$el(undefined) 和生成的 vnode

接下来会执行 childComp 组件的 _update,这一次传入 patch 方法的 oldVnode 参数为 vm.$el,其值为 undefined

会走到 createElm(vnode, insertedVnodeQueue),但是再次进入 UcreateComponent 方法后 vnode.data 属性此时为 undefined,因为现在的 vnodediv 元素,不再是一个组件,会跳过这个方法继续向下执行。

vnode.elm = nodeOps.createElement(tag, vnode);

首先会调用 nodeOps 模块的 createElement 方法,根据 tagvnode 生成一个真实 Dom 即一个空的 div 标签:<div></div>

与上个很大不同在,此时的 vnodechildren 有两个 vnode 子节点,会进入 createChildren 方法。

createElm 方法中, 组件 vnode 会走 createComponent 方法。
但是非组件 vnode 不会,如果当前 vnode 节点的 tag 是保留标签,它有 children 所以会走 createChildren 方法,先创建子节点再进行插入。如果是注释或者文本节点,直接根据父节点和兄弟节点进行插入。

  1. createChildren 方法会遍历 children对每个 vnode 子元素执行 createElm 方法
  • 第一个子节点就是一个文本节点的 vnode,所以会走到:
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);

将文本元素插入到 parentElm 这个空的 div 中,此时的 vnode.elm<div>QAAA</div>

  • 第二个子节点是一个组件,所以会走到 UcreateComponent 方法,执行 button-counter 组件的初始化。