重学Vue源码,根据​​黄轶大佬​​的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 ​github​ 上。

正文

前面提到 ​​createElement​​​ 创建了组件VNode,接着调用 ​​vm._update​​​ ,执行 ​​vm._patch​​ 方法把VNode转换为真实节点,这是针对于一个普通的 VNode节点,下面看下在组件中的VNode的区别。

​patch​​​ 会调用 ​​createEml​​​ 创建元素节点,它在 ​​src/core/vdom/patch.js​​ 中:

function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// ...
}

和普通VNode最不一样的地方就是: ​​createComponent​​,因为这里的vnode是一个组件vnode,所以它在创建的时候有些不太一样:

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

首先判断 ​​vnode.data​​​ 有没有,keepAlive先忽略,接着判断有没有 ​​data.hook​​​ 以及这个 ​​hook​​​ 中有没有 ​​init​​ 方法,如果有就调用这个方法。

回忆一下,在创建组件 ​​createComponent​​​ 方法里面,有一个 ​​installComponentHooks​​​ 方法,这个方法会把上面定义的4个hook都初始化一遍(init,prepatch,insert,destroy),然后挂载到 ​​data.hook​​​ 上,所以在上面的判断中,​​init​​​ 是有的,然后就执行到了 ​​hook​​​ 上的 ​​init​​ 方法中:

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

keepAlive跳过,后面有个 ​​createComponentInstanceForVnode​​​,这个方法返回了 ​​vnode.componentInstance​​​,也就是返回了vm的实例,然后调用了 ​​$mount​​​ 方法挂载子组件。看下 ​​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)
}

可以看到传入了两个参数,第一个是组件vnode,第二个参数其实是当前vm的一个实例,定义了一个 ​​options​​​,有三个键, 中间的 ​​_parentVnode​​​ 就是父vnode,它其实是一个占位vnode,一个占位节点。最后返回了一个 ​​new vnode.componentOptions.Ctor(options)​​​,回忆一下,在创建子组件vnode的时候,用了一个 ​​context.$options.__base​​​,也就是 ​​Vue.extend​​​,扩展了一个子组件构造器 ​​Ctor​​​,接着在创建vnode的时候,有个参数是 ​​{ Ctor, propsData, listeners, tag, children }​​​,这里的 ​​Ctor​​​ 就是组件构造器,那么再执行 ​​vnode.componentOptions.Ctor​​​ 的时候其实就是执行了 ​​Sub​​​ 的构造函数,​​Sub​​​ 在 ​​src/core/global-api/extend.js​​​ 中,然后它执行 了 ​​_init​​​,这个 ​​_init​​​ 又回到了 Vue 的初始化,因为子组件的构造器其实是继承了 Vue 的构造器,来再次看下 ​​_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
)
}
// ...
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

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

首先合并了 ​​options​​​,参数 ​​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
}
}

创建了一个 ​​vm.constructor.options​​​ 对象,然后赋值给 ​​vm.$options​​​,接下来是重点,它把 ​​_parentVnode​​​ 和 ​​parent​​​ 传了进来,​​_parentVnode​​​ 就是上面的 ​​createComponentInstanceForVnode ​​​ 的参数 ​​_parentVnode​​​,就是占位符的vnode,​​parent​​​ 是当前vm的实例,也就是当前子组件的父vm实例,继续看 ​​initInternalComponent​​​,把 ​​vnodeComponentOptions​​​ 里的一些参数拿出来赋值给 ​​opts​​​,到此 ​​initInternalComponent​​​ 结束。所以这里做的操作就是把通过 ​​createComponentInstanceForVnode​​​ 函数传入的参数合并到内部的 ​​$options​​ 里了。

接着看 ​​_init​​​, ​​initLifecycle​​​ 定义在 ​​src/core/instance/lifecycle.js​​,看下这个方法:

export let activeInstance: any = null
export let isUpdatingChildComponent: boolean = false

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.$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.parent​​​,这个 ​​parent​​​ 实际上就是 ​​activeInstance​​​,注意 ​​activeInstance​​​ 在这个文件中是一个全局变量,它的赋值在 ​​lifecycleMixin​​ 方法中:

export function lifecycleMixin (Vue: Class<Component>) {
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​​​ 的时候,赋值了 ​​activeInstance​​​,也就是说每次调用 ​​_update​​​ ,就会把当前的vm实例赋值给 ​​activeInstance​​:

activeInstance = vm

同时用 ​​prevActiveInstance​​​ 来保留上一次的 ​​activeInstance​​,这么做是什么意思?

这里把当前的vm给了 ​​activeInstance​​​,然后在当前vm实例的vnode在 ​​patch​​​ 的过程中,把当前实例作为父vm实例,传给子组件,这样 ​​patch​​​ 其实就是一个深度遍历,将当前激活的vm实例给 ​​activeInstance​​​,然后在初始化子组件的时候,将这个 ​​activeInstance​​​ 作为parent参数传入,然后在 ​​initLifecycle​​ 里,就可以拿到当前激活的vm实例,然后把实例作为parent:

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.$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

此时 ​​parent​​​ 是vm实例,​​parent.$children​​​ 塞一个子组件的vm。上面代码中的 ​​vm​​​ 是子组件,​​parent​​​ 是它的父组件,然后他们有一层 ​​push​​​ 的关系,接着把 ​​vm.$parent​​​ 赋值为父组件实例 ​​parent​​​,至此 ​​initLifecycle​​ 就把这一层父子关系给建立起来了。

继续看 ​​_init​​​,最后的 ​​vm.$mount ​​​是走不到的,因为现在的 ​​$options​​​ 没有 ​​el​​:

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

所以此时 ​​_init​​​ 返回的是一个子组件的实例,然后回到 ​​createComponent​​​ 里面的 ​​init​​​ 钩子,​​createComponentInstanceForVnode​​ 其实就是返回了一个子组件的实例,接着:

child.$mount(hydrating ? vnode.elm : undefined, hydrating)

手动调用了 ​​$mount​​​ 来挂载,也就是执行之前的 ​​Vue.prototype.$mount​​​ 和 ​​mountComponent​​​ 方法,接着执行 ​​_update​​​ 的 ​​updateComponent​​​ 方法,最后调用 ​​__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 {
// ...
}
// ...
}

然后调用 ​​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)

/* 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)
}
}

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

此时传入的 ​​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
}
}
}

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

下面附一张自己整理的图,还有一张更大的图在​​github仓库​​​,有需要的自取~ 重学Vue【Vue的patch】_Vue.js

附述

由于我在这一块花费了好多时间,所以在附上一点自己的理解。

App.vue
<template>
<div id="app">
<HelloWorld />
</div>
</template>

id为app作根,是一个渲染vnode,里面有个children,children里有个child就是:​​<HelloWorld />​​​ ,这个 ​​<HelloWorld />​​ 就是 HelloWorld.vue 文件的 _parentVnode,也就是占位符vnode。

而 ​​<HelloWorld />​​​ 真正渲染的是 HelloWorld.vue 文件,调用它的 render 渲染出一个 渲染vnode,这个渲染vnode的_parentVnode 就是 ​​<HelloWorld /> ​​ 这个占位符vnode。


vm.$vnode = __parentVnode = vnode._parent



vm._vnode = vnode



vm._vnode.parent = vm.$vnode


update方法执行下面两个方法:

  1. initComponent 是把patch的返回值赋值给 vnode.elm(HelloWorld占位符节点)
  2. insert是把HelloWorld.vue 插入到父占位符里

此时 HelloWorld插入到App.vue结束


vm._vnode是渲染vnode,vm.$vnode是占位vnode