本文我将继续讲述Vue在挂载中发生的那些事儿
mountComponent
首先来看一下上篇文章说到的mountComponent
// src\core\instance\lifecycle.js
export function mountComponent (vm, el, hydrating) {
vm.$el = el
callHook(vm, 'beforeMount')
let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
Watcher
// src\core\observer\watcher.js
export default class Watcher {
constructor (vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm
if (typeof expOrFn === 'function') {
// 如果是函数,则赋值给getter(expOrFn === updateComponent)
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
// 初始化组件 lazy 为 false,执行 get
this.value = this.lazy
? undefined
: this.get()
}
}
可以看出,挂载时,我们首先在vm上追加当前节点,然后注册一个updateComponent函数,注意此时并每有调用,随后,我们新建了一个Watcher类,进行了初始化的操作,其中可以看到我们转化expOrFn函数后赋值给getter,最后调用get函数获取值,也就是调用了updateComponent。综上,我们可以看到挂载时,新建了一个Watcher以进行响应式处理,然后调用了updateComponent函数。
updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
可以看到这边首先是执行了render函数获取其返回值,然后调用了update函数进行初始化或更新操作。在Vue2 初始化实例过程源码解析(一)中我们通过compileToFunctions函数将template转化为一个render函数,该函数的职责为生成虚拟DOM。所以可得,vm._render的最终返回值为虚拟DOM,而我们的vm._update则是对比新旧(初始化即为创建)虚拟DOM,对比并更新。接下来,让我们来一起看看update函数是如何起作用的吧。
update
// src\core\instance\lifecycle.js
Vue.prototype._update = function (vnode, hydrating) {
const vm = this
// 获取原节点
const prevEl = vm.$el
// 获取原vnode
const prevVnode = vm._vnode
// 通过闭包缓存当前的vm
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
vm.$el = vm.__patch__(prevVnode, vnode)
}
// 通过闭包取出先前的vm
restoreActiveInstance()
// 需要清除原先保存的vnode
if (prevEl) {
prevEl.__vue__ = null
}
// 更新节点
if (vm.$el) {
vm.$el.__vue__ = vm
}
// 节点赋值
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}
可以看到关键点就在于vm.__proto__函数了,显而易见的,传递的第一个参数为真实节点即为初始化操作,传递的是虚拟DOM即为更新操作。而这个函数又是在哪里进行的安装的呢?大家可以这样考虑一下,因为Vue是一个支持多平台的框架,而对于节点的操作,每个平台都有其不同的操作方法,这样想的话,是不是在运行时进行会更好呢?因此,实际的函数安装,是写在/runtime的包中的,具体路径我在Vue2 初始化实例过程源码解析(一)中也有提到。
patch
// src\platforms\web\runtime\patch.js
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
const modules = platformModules.concat(baseModules)
export const patch = createPatchFunction({ nodeOps, modules })
显然,它用了一个工厂函数创建了patch,其中nodeOps是平台特有的节点操作,modules则是一些指令方法。
createPatchFunction
进入到这个函数,你会很惊奇的发现,这个函数有730+行,这是Vue2源码最长的一个函数,但是也不用着急,实际上的处理也很简单。既然说这是一个工厂函数,那我们直接来看看他返回的内容。
// src\core\vdom\patch.js
export function createPatchFunction (backend) {
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// 如果vnode不存在(只有在我们手动调用销毁方法才会进入这边
if (isUndef(vnode)) {
// 销毁存在的oldVnode
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
// 新增vnode插入队列
const insertedVnodeQueue = []
// 如果oldVnode不存在,则空挂载
if (isUndef(oldVnode)) {
isInitialPatch = true
// 创建元素
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 如果oldValue是虚拟DOM,并且和vnode是相同节点
// 不仅仅是key相同,还有tag、inputType相同,是否为异步组件等等等等
// 这是对比更新的函数,在这里用到了diff算法
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// 挂载到真实的节点上
// SSR相关
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
}
}
// 用真实节点创建一个空的虚拟DOM
oldVnode = emptyNodeAt(oldVnode)
}
// 保存节点
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// 创建一个新节点
createElm(
vnode,
// 需要新增的节点
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
// 节点插入位置
nodeOps.nextSibling(oldElm)
)
// 递归地更新父占位符节点元素
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
const insert = ancestor.data.hook.insert
if (insert.merged) {
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// 销毁旧节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
这里是patch过程的所有操作的大致内容,接下来让我们进入函数中,细看创建与更新。
createElm
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested
// 如果是组件创建,直接执行createComponent
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
// 创建根据节点类型element
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
// 设置样式等
setScope(vnode)
// 创建子节点并插入
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)
}
}
patchVnode
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
if (oldVnode === vnode) {
// 如果相同不用更新
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
vnode = ownerArray[index] = cloneVNode(vnode)
}
// 获取当前节点并把节点赋值给vnode
const elm = vnode.elm = oldVnode.elm
// 是否为异步组件
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// 为静态树重用元素
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
// 执行钩子
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
// 获取两者的children递归对比
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
// 钩子
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
if (isUndef(vnode.text)) {
// 如果vnode没有text
if (isDef(oldCh) && isDef(ch)) {
// 如果两者都有children,则对比他们的子元素
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// oldVnode没有children,vnode有children,先清空文本内容,再添加子元素
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// oldVnode有children,vnode为空,删除oldVnode的子元素
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// oldVnode有text,vnode为空,清空文本内容
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// vnode有text,并且和oldVnode不相同,更新文本
nodeOps.setTextContent(elm, vnode.text)
}
// 钩子
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
updateChildren
在这个函数中,进行的是数组与数组的对比,也是最复杂的对比,其中的对比用到的是diff算法
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
const canMove = !removeOnly
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
// 当前头节点已经被移动了,需要调整位置
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
// 当前尾节点已经被移动了,需要调整位置
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 如果两个头节点相同,直接patch这俩节点,并且移动指针
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 如果两个尾节点相同,直接patch这俩节点,并且移动指针
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 如果旧的头节点和新的尾节点相同,patch这俩节点,将头节点移动到尾部
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 如果旧的尾节点和新的头节点相同,patch这俩节点,将尾节点移动到头部
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 以上的头尾比较都不行,则进行key对比,首先是寻找到当前的node的key
// 初始化时需要创建对应的key
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 寻找到相应key的vnode
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) {
// 如果没有寻找到,直接创建新的vnode
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// 如果寻找相同的节点,则对比,并移动指针
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 如果key相同但是元素节点不同,强制创建vnode覆盖
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
// 移动节点位置
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
// 旧节点遍历结束,创建剩余节点
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
// 新节点遍历结束,删除剩余节点
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
以上就是Vue2的挂载的全过程,挂载的过程也是源码阅读中较为重要,难度最大的部分,需要我们细细反复精读。