Vue源码学习 - 组件化(二) patch
学习内容和文章内容来自 黄轶老师
黄轶老师的慕课网视频教程地址:
《Vue.js2.0 源码揭秘》、
黄轶老师拉钩教育教程地址:
《Vue.js 3.0 核心源码解析》
这里分析的源码是Runtime + Compiler 的 Vue.js
调试代码在:node_modules\vue\dist\vue.esm.js 里添加
vue版本:Vue.js 2.5.17-beta
你越是认真生活,你的生活就会越美好
——弗兰克·劳埃德·莱特
《人生果实》经典语录
patch
通过前一章的分析我们知道,当我们通过createComponent
创建了组件 VNode
,接下来会走到 vm._update
,执行 vm.__patch__
去把 VNode 转换成真正的 DOM 节点
。这个过程我们在前一章已经分析过了,但是针对一个普通的 VNode 节点
,接下来我们来看看组件的 VNode
会有哪些不一样的地方。
重点
patch的整体流程: createComponent => 子组件初始化 => 子组件render => 子组件patch
activeInstance为当前激活的vm实例 vm.$vnode为组件的占位vnode vm._vnode为组件的渲染vnode
嵌套组件的插入顺序 先子后父
debugger添加地方
patch
的过程会调用 createElm 创建元素节点
,回顾一下 createElm
的实现,它的定义在 src/core/vdom/patch.js
中:
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// ...
}
src/App.vue
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
createComponent
我们删掉多余的代码,只保留关键的逻辑,这里会判断 createComponent(vnode, insertedVnodeQueue, parentElm, refElm)
的返回值,如果为 true 则直接结束,那么接下来看一下 createComponent
方法的实现
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
// keep-alive相关 这里先不看
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
做了一些判断:
let i = vnode.data
if (isDef(i)) {
// ...
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
// ...
}
// ..
}
如果 vnode
是一个组件 VNode
,那么条件会满足,并且得到i
就是 init 钩子函数
,回顾上节我们在创建组件 VNode
的时候合并钩子函数
中就包含init 钩子函数
,定义在 src/core/vdom/create-component.js
中:
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
的情况,它是通过 createComponentInstanceForVnode
创建一个 Vue 的实例
,然后调用 $mount 方法挂载子组件
, 先来看一下 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
函数构造的一个内部组件的参数,然后执行 new vnode.componentOptions.Ctor(options)
。这里的vnode.componentOptions.Ctor
对应的就是子组件的构造函数
,
我们上一节分析了它实际上是继承于 Vue 的一个构造器 Sub
,相当于 new Sub(options)
这里有几个关键参数要注意几个点,
_isComponent
为 true
表示它是一个组件,
parent
表示当前激活的组件实例(注意,这里比较有意思的是如何拿到组件实例,后面会介绍。
所以子组件的实例化
实际上就是在这个时机执行的,并且它会执行实例的_init
方法,这个过程有一些和之前不同的地方需要挑出来说,代码在src/core/instance/init.js
中:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// merge options
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 的过程
有变化,_isComponent
为true
,所以走到了 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
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
}
}
这个过程我们重点记住以下几个点即可:
opts.parent = options.parent
、
opts._parentVnode = parentVnode
,
它们是把之前我们通过createComponentInstanceForVnode
函数传入的几个参数合并到内部的选项 $options
里了。
initLifecycle(vm)
方法 单步调试时 也可以看看
再来看一下 _init
函数最后执行的代码:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
由于组件初始化
的时候是不传 el
的,因此组件是自己接管了$mount
的过程,这个过程的主要流程在上一章介绍过了,回到组件init
的过程,componentVNodeHooks
的 init 钩子函数
,在完成实例化的_init
后,接着会执行
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
。
这里 hydrating
为 true 一般是服务端渲染的情况,我们只考虑客户端渲染
,所以这里$mount
相当于执行 child.$mount(undefined, false)
,它最终会调用 mountComponent
方法,进而执行 vm._render()
方法:
mountComponent
方法会走到vm._update(vm._render(), hydrating)
方法
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
// ...
}
// set parent
vnode.parent = _parentVnode
return vnode
}
我们只保留关键部分的代码,这里的_parentVnode
就是当前组件的父 VNode
,而 render
函数生成的 vnode
是当前组件的渲染 vnode
,vnode 的 parent
指向了 _parentVnode
,也就是 vm.$vnode
,它们是一种父子
的关系。
我们知道在执行完 vm._render
生成 VNode
后,接下来就要执行 vm._update 去渲染 VNode
了。来看一下组件渲染的过程中有哪些需要注意的,vm._update
的定义在 src/core/instance/lifecycle.js
中:
export let activeInstance: any = null
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
过程中有几个关键的代码,首先 vm._vnode = vnode
的逻辑,这个 vnode
是通过 vm._render() 返回的组件渲染 VNode
,vm._vnode
和 vm.$vnode
的关系就是一种父子关系
,用代码表达就是 vm._vnode.parent === vm.$vnode
。还有一段比较有意思的代码:
export let activeInstance: any = null
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
// ...
const prevActiveInstance = activeInstance
activeInstance = vm
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// ...
}
这个activeInstance
作用就是保持当前上下文的 Vue 实例
,它是在lifecycle
模块的全局变量,定义是 export let activeInstance: any = null
,并且在之前我们调用 createComponentInstanceForVnode
方法的时候从lifecycle
模块获取,并且作为参数传入的。
PS:
activeInstance为当前激活的vm实例 vm.$vnode为组件的占位vnode vm._vnode为组件的渲染vnode
因为实际上 JavaScript
是一个单线程
,Vue 整个初始化
是一个深度遍历的过程
,在实例化子组件的过程中,它需要知道当前上下文的 Vue 实例
是什么,并把它作为子组件的父 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 = activeInstance
用 prevActiveInstance
保留上一次的 activeInstance
。
实际上,prevActiveInstance
和当前的 vm
是一个父子关系
,当一个vm
实例完成它的所有子树的 patch
或者 update
过程后,activeInstance
会回到它的父实例,这样就完美地保证了 createComponentInstanceForVnode
整个深度遍历
过程中,我们在实例化子组件的时候能传入当前子组件的父 Vue 实例
,并在_init
的过程中,通过 vm.$parent
把这个父子关系保留。
那么回到_update
,最后就是调用__patch__
渲染VNode
了。
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
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 {
// ...
}
// ...
}
这里又回到了本节开始
的过程,之前分析过负责渲染成 DOM 的函数是 createElm
,注意这里我们只传了 2 个参数,所以对应的 parentElm
是 undefined
。我们再来看看它的定义:
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)
/* istanbul ignore if */
if (__WEEX__) {
// ...
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
// ...
} 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
(createElm方法渲染生成DOM节点),在遍历的过程中,如果遇到子 VNode
是一个组件的 VNode
,则重复本节开始的过程
,这样通过一个递归
的方式就可以完整地构建了整个组件树
。
由于我们这个时候传入的parentElm
是空,所以对组件的插入,在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
}
}
}
function insert (parent, elm, ref$$1) {
if (isDef(parent)) {
if (isDef(ref$$1)) {
if (nodeOps.parentNode(ref$$1) === parent) {
nodeOps.insertBefore(parent, elm, ref$$1);
}
} else {
nodeOps.appendChild(parent, elm);
}
}
}
function insertBefore (parentNode, newNode, referenceNode) {
parentNode.insertBefore(newNode, referenceNode);
}
先将HelloWorld组件插入到ID为app的Dom节点上 然后再将ID为app的Dom节点插入到父节点上 也就是body节点上
在完成组件的整个 patch 过程
后,最后执行insert(parentElm, vnode.elm, refElm)
完成组件的 DOM 插入
,如果组件 patch 过程中又创建了子组件
,那么DOM 的插入顺序
是先子后父
。
重点
patch的整体流程: createComponent => 子组件初始化 => 子组件render => 子组件patch
activeInstance为当前激活的vm实例 vm.$vnode为组件的占位vnode vm._vnode为组件的渲染vnode
嵌套组件的插入顺序 先子后父
总结
那么到此,一个组件的 VNode
是如何创建、初始化、渲染的过程
也就介绍完毕了。在对组件化的实现有一个大概了解后,接下来我们来介绍一下这其中的一些细节。
我们知道编写一个组件实际上是编写一个 JavaScript 对象
,对象的描述就是各种配置,之前我们提到在_init
的最初阶段执行的就是merge options
的逻辑,那么下一节我们从源码角度来分析合并配置
的过程。
Vue源码学习 - 组件化(一) createComponent
Vue源码学习 - 组件化(三) 配置合并
谢谢你阅读到了最后~
期待你关注、收藏、评论、点赞~
让我们一起 变得更强