Vue源码之组件化

1,198 阅读6分钟

组件化

Vue另⼀个核⼼思想是组件化,把⻚⾯拆分成多个组件,每个组件有自己的 template、js、CSS。组件可复⽤,组件和组件之间可嵌套。

接下来用下面代码为例,来分析⼀下 Vue 组件初始化的⼀个过程。

createComponent

  1. 哪里开始说起


  1. src/core/vdom/create-component.js

createComponent核⼼流程三个:构造⼦类构造函数 安装组件钩⼦函数 和 实例化 vnode

  1. 构造⼦类构造函数
  • 看⼀下 Vue.extend 函数的定义,在src/core/global-api/extend.js

Vue.extend 的作⽤就是构造⼀个 Vue 的⼦类,它使⽤⼀种⾮常经典的原型继承的⽅式把⼀个纯对象转换⼀个继承于 Vue 的构造器 Sub 并返回,然后对 Sub 这个对象本⾝扩展了⼀些属性,如扩展 options 、添加全局静态方法等;并且对配置中的 props 和 computed 做了初始化⼯作;最后对于这个 Sub 构造函数做了缓存,避免多次执⾏ Vue.extend 的时候对同⼀个⼦组件重复构造。

  1. 安装组件钩⼦函数

installComponentHooks 的过程就是把 componentVNodeHooks 的钩⼦函数合并到data.hook 中,在 VNode 执⾏ patch 的过程中执⾏相关的钩⼦函数,具体的执⾏我们稍后在说patch 过程中再提及。在合并过程中,如果某个时机的钩⼦已经存在 data.hook 中,那么通过执⾏ mergeHook 函数做合并,这个逻辑很简单,就是在最终执⾏的时候,依次执⾏这两个钩⼦函数即可。

  1. 实例化 VNode

通过 new VNode 实例化⼀个 vnode 并返回。这和普通元素节点的vnode 不同,组件的 vnode 是没有 children 的,这点很关键,在之后的 patch 过程中我们会再提。

  1. 总结

createComponent 在渲染⼀个组件的时候的 3 个关键点:构造⼦类构造函数,安装组件钩⼦函数和实例化 vnode 。 createComponent 后返回的是组件vnode ,它也⼀样下一步是执行 vm._update ⽅法,进⽽执⾏了 patch 函数,我们在上篇对 patch 函数做了简单的分析,那么我们对它做进⼀步的分析。

patch

执⾏ vm.patch 去把 VNode 转换成真正的 DOM 节点。这个过程我们在上篇已经分析过了,但是针对⼀个普通的 VNode 节点,接下来我们来看看组件的 VNode 会有哪些不⼀样的地⽅。

  1. src/core/vdom/patch.js

这⾥会判断 createComponent(vnode,insertedVnodeQueue, parentElm, refElm) 的返回值。

  1. createComponent

得到 i 就是 init 钩⼦函数,上面我们在创建组件 VNode 的时候合并钩⼦函数中就包含 init 钩⼦函数。

  1. src/core/vdom/create-component.js

init 钩⼦函数先不考虑 keepAlive 的情况,它是通过createComponentInstanceForVnode 创建⼀个 Vue 的实例,然后调⽤ $mount ⽅法挂载⼦组件.

  1. createComponentInstanceForVnode

_parentVnode为组件vnode也叫占位符vnode,createComponentInstanceForVnode 函数构造⼀个参数options,然后执⾏
new vnode.componentOptions.Ctor(options) 。这⾥的 vnode.componentOptions.Ctor 对应的就是⼦组件的构造函数,上面分析了它实际上是继承于 Vue 的⼀个构造器 Sub ,相当于 new Sub(options) 这⾥有⼏个参数, _isComponent 为 true 表⽰它是⼀个组件, parent 表⽰当前激活的组件实例。所以⼦组件的实例化实际上就是在这个时机执⾏的,并且它会执⾏实例的 _init ⽅法。

  1. src/core/instance/init.js

抽取主要代码


⾸先是合并 options 的过程有变化, _isComponent 为 true,所以⾛到了initInternalComponent 过程,

initLifecycle(vm) 这里确立了父子关系

_init 函数最后执⾏的代码

  1. src/core/instance/render.js

我们知道在执⾏完 vm._render ⽣成 VNode 后,接下来就要执⾏ vm._update 去渲染 VNode 了。

  1. src/core/instance/lifecycle.js

  1. 回到 _update ,最后就是调⽤ patch 渲染 VNode 了,patch中之前分析过负责渲染成 DOM 的函数是 createElm

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

  • 然后在 createComponent 有这么⼀段逻辑:将整个组件dom插入到父节点中。

  • 接着大循环结束整个dom构建完毕将整个树插入body insert(parentElm, vnode.elm, refElm)

  • path.js最后删除旧节点

总结

结论一:如果组件 patch 过程中⼜创建了⼦组件,那么DOM 的插⼊顺序是先⼦后⽗,组件执行顺序先父后子,渲染顺序先子后父。

结论二:一个组件一个watcher。

这篇捋通这个路程图,头真的裂开,过程很痛苦,捋通了顿时舒服了,先捋个流程然后真的去多打断点去看看才知道怎么个闭环的。脑中想不出来逻辑跟不上就多动手,大不了写个大白话,写着写着就通了。

<div id="app">
    <h1>组件化</h1>
    <div>{{name}}</div>
    <demo></demo>
</div>

patch时
createElm(
   vnode,
   insertedVnodeQueue,
   oldElm._leaveCb ? null : parentElm,
   nodeOps.nextSibling(oldElm)
)
此时parentElm为Body

createComponent返回false tag = div
vnode.elm = nodeOps.createElement(tag, vnode) = div

然后createChildren遍历div子节点 执行 createElm 传入vnode.elm=div

1.第一次循环
createComponent返回false tag = h1
vnode.elm = nodeOps.createElement(tag, vnode) = h1
然后createChildren遍历h1子节点 执行 createElm 传入vnode.elm=h1
createComponent返回false tag = undefined
执行
vnode.elm = nodeOps.createTextNode(vnode.text) = test
 insert(parentElm, vnode.elm, refElm)  parentElm=h1 此时把test插入了h1
第一次循环结束
createChildren(vnode, children, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm) 执行这个 parentElm=div 此时把h1插入了div

2.第二次循环
createComponent返回false tag = undefinded
执行
vnode.elm = nodeOps.createTextNode(vnode.text) = test
 insert(parentElm, vnode.elm, refElm)  parentElm=div 此时把文本节点插入了div

3.第三次循环
createComponent返回false tag = div
vnode.elm = nodeOps.createElement(tag, vnode) = div
然后createChildren遍历div子节点 执行 createElm 传入vnode.elm=div当做父节点使用
createComponent返回false tag = undefined
执行
vnode.elm = nodeOps.createTextNode(vnode.text) = test
 insert(parentElm, vnode.elm, refElm)  parentElm=div 此时把test插入了div
第三次循环结束
createChildren(vnode, children, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm) 执行这个 parentElm=div 此时把div插入了div

4.第四次循环
createComponent返回false tag = undefinded
执行
vnode.elm = nodeOps.createTextNode(vnode.text) = test
 insert(parentElm, vnode.elm, refElm)  parentElm=div 此时把文本节点插入了div

5.第五次循环
createComponent返回true 
createComponent有两个方法 
一 :执行i(vnode, false /* hydrating */) 执行构造函数 vm._update(vm._render) 创建虚拟dom 生成Dom
插入parentElm=div  执行完一执行二
二 :insert(parentElm, vnode.elm, refElm)  parentElm=div 此时把组件插入了div

然后结束大循环
createChildren(vnode, children, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm) 执行这个 parentElm=body 此时把整个div插入了body

然后patch结束逻辑删除
removeVnodes([oldVnode], 0, 0)