当数据变化时,会触发渲染函数,执行
vm._update(vm._render(), hydrating)
_render
的作用就是生成VNode
,而_update
则是将vnode
渲染为真实dom
,这一个过程称为patch
当前执行update时的上下文的Vue实例,在更新结束的时候会回到父实例
export let activeInstance: any = null
export let isUpdatingChildComponent: boolean = false
function setActiveInstance(vm: Component) {
const prevActiveInstance = activeInstance
activeInstance = vm
return () => {
activeInstance = prevActiveInstance
}
}
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 restoreActiveInstance = setActiveInstance(vm) // 设置当前的上下文
vm._vnode = vnode
// __patch__ 见下面
restoreActiveInstance() // 更新结束,回到父实例
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
}
}
}
可参考react中的对diff算法的说明:
由于直接比价两个树,复杂度为O(n^3 )
,
React 和 Vue 做优化的前提是“放弃了最优解“,本质上是一种权衡,有利有弊,他们假设是:
- 检测VDOM的变化只发生在同一层
- 检测VDOM的变化依赖于用户指定的key
进而得到启发式的解: 2. 不同类型的元素,会产生两个不同的dom树 3. 开发者可以通过设置 key 属性,来告知渲染哪些子元素在不同的渲染下可以保存不变
那么在比较过程中会有:
- 不同的元素,则直接删除旧结点,添加新结点,而不会比较两颗子树
- 对比同一类型的元素,仅更新属性
- 同一类型的结点会继续对子节点进行递归比较:
- 如果同一位置的结点不同,则跳步骤1
- 但可以用key表示,只是位置移动了,则会复用key相同的dom,只对位子进行移动
- 文本结点或者注释结点,如果字符串不一样直接替换,不需要递归
总的操作就是替换、删除、新增
src/core/vdom/patch.js
createPatchFunction () {
// ... 省略patch过程调用的函数,后面用到在写
return function patch (oldVnode, vnode, hydrating, removeOnly) {}
}
src/platforms/web/runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
- 占位符 vnode:vm.$vnode 只有组件实例才有。在 _render 过程中赋值,在父组件中的组件标签vnode,如
vue-component-button-counter
- 渲染 vnode:vm._vnode 可以直接映射成真实 DOM。在_update 过程中赋值
- 它们是父子关系:vm._vnode.parent = vm.$vnode
function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) { // vnode不存在则直接销毁组件
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) { // 没有旧结点,直接新创建
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType) // 第一个渲染的时候为vm.$el,是dom
if (!isRealElement && sameVnode(oldVnode, vnode)) { // 相同的vnode结点(key和tag一样)
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) // diff过程
} else {
// 新旧结点不同,见下面
// 更新父占位符,子组件创建过程会有parent属性
if(vnode.parent) {}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
- 如果是组件vnode,走组件vnode的实例化,然后插入
- 如果是html标签,则创建dom结点,并递归创建子节点,然后插入
- 如果是注释和文本结点,创建后直接插入
function createElm (
vnode,insertedVnodeQueue,
parentElm,refElm,
nested,ownerArray,index
) {
// 是否是组件vnode,见下面
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
if (isDef(tag)) { // 是html标签
vnode.elm = nodeOps.createElement(tag, vnode) // 创建真实dom结点
createChildren(vnode, children, insertedVnodeQueue) // 递归createElm子节点
insert(parentElm, vnode.elm, refElm) // 插入
} else if (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)
}
}
function createComponent (vnode/*组件vnode*/, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
// ... 执行componentInstance(init钩子,在创建组件vnode的时候注入)
// 实例化组件,得到 _render、_update,并执行mount
vnode.componentInstance(vnode)
// 上面执行完成,但不存在parent(可挂在的)节点,所以子组件的vnode不会插入到真dom上
if (isDef(vnode.componentInstance)) {
// 赋值vnode.elm = vnode.componentInstance.$el(子组件的$el 到 占位vnode(ButtonCounter))
// 然后调用creathooks钩子,执行updateAttr、指令等
initComponent(vnode, insertedVnodeQueue) // 设置占位vnode的elm
// 将子组件的dom append 到parentElm下,代替当前的占位vnode,具体过程见diff
insert(parentElm, vnode.elm, refElm)
// ...
return true
}
}
}
// src/core/vdom/create-component.js
import {activeInstance} from '../instance/lifecycle'
// init钩子
const componentVNodeHooks = {
init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive) {
// keepAlive 相关...
} else {
// 创建一个 Vue 的实例,见下面
const child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance)
// 如果是组件,此时el也undefined,不在这里挂载,init执行结束,回到上面
child.$mount(vnode.el, hydrating)
}
},
}
createComponentInstanceForVnode
里面实际上返回了一个vue
实例
return new vnode.componentOptions.Ctor({
_isComponent: true,
_parentVnode: vnode, // 当前的组件vnode
parent // 当前组件实例 activeInstance
})
componentOptions.Ctor
来自 创建vnode
的函数createComponent
时加入- 所以这里
new
的时候又回到了_init()
,但和之前的流程有个分支不一样
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
if (options && options._isComponent) {
initInternalComponent(vm, options) // 子组件mergeOptions,见下
} else {
// mergeOptions...
}
// ...
initLifecycle(vm) // 将组件实例放入非抽象父组件.$children
if (vm.$options.el) {vm.$mount(vm.$options.el)} // 如果是子组件,则没有el,也就是不在这里挂载,回到上hook
}
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
// 创建 vm.$options 对象
const opts = vm.$options = Object.create(vm.constructor.options)
const parentVnode = options._parentVnode
opts.parent = options.parent // 正在实例化的vue组件本身
opts._parentVnode = parentVnode // 保存组件占位符vnode
// 传递绑定在父占位符上的props/事件等
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
}
}
在上面patch
函数中,如果存在oldVnode
和vnode
,并且结点不同,则进入进入这里。一般情况下不会走到这里,除非这么写:
<template>
<div v-if="flag">true</div>
<ul v-else>
<li>1</li>
<li>2</li>
</ul>
</template>
看代码
function patch () {
// ... 新旧结点不同
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 新旧vnode相同
} else {
// 创建一个空的旧vnode(children为空)
if (isRealElement) { oldVnode = emptyNodeAt(oldVnode)}
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm) // 旧vnode的父dom
// 创建新结点
createElm( vnode,
insertedVnodeQueue,
parentElm, // 插入旧vnode的父节点中
nodeOps.nextSibling(oldElm)) // 插入位置,空的时候直接append。否则insertbefore
// 更新父占位vnode的elm(上面的例子不是组件vndoe,所以没有), <my-comp/> 就是占位符
if (isDef(vnode.parent)) {...}
// 详情见 https://juejin.cn/post/6844904155056701453#heading-3
// 删除旧结点或者销毁
if (isDef(parentElm)) {removeVnodes([oldVnode], 0, 0)}
else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode)}
}
}
在上面patch
函数中,如果存在oldVnode
和vnode
,并且是相同结点的话,就会进入patchVnode
过程,比较子节点的更新。
function patch (oldVnode, vnode){
if (isUndef(oldVnode)) {
// ...
} else {
if (!isRealElement && sameVnode(oldVnode, vnode)) { // 新旧vnode相同
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 新旧结点不同
}
}
}
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
if (oldVnode === vnode) {
return
}
const elm = vnode.elm = oldVnode.elm
// 省略
// 1. 异步结点的处理
// 2. 静态结点的复用
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode) // prepatch钩子
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
// update钩子,里面有指令update,属性更新等
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)) {
if (isDef(oldCh) && isDef(ch)) {
// 子节点不同则diff比较,,见下面
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) { // 只有ch存在,表示旧节点不需要了
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) // 批量将ch插入elm下
} else if (isDef(oldCh)) { // 只有旧节点,则说是删除操作
removeVnodes(oldCh, 0, oldCh.length - 1) // 清除elm所有的子节点
} else if (isDef(oldVnode.text)) { // 不满足上面,可能旧节点是文本节点,那么就清空文本
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) { // 文本结点就是字符串的比较
nodeOps.setTextContent(elm, vnode.text) // 直接修改textContent
}
if (isDef(data)) { // postpatch钩子
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
src/core/vdom/patch.js
实际上调用的就是`updateChildren``函数
遍历整个newVnode[]
,oldVnode[]
, 从两头向中间比较,newVnode[]
或oldVnode[]
中任意一组都扫描过了结束遍历。比较的场景如下:
- oldStartIndex || oldEndIndex 所在节点不存在: ++oldStartIndex || --oldEndIndex
- newStartIndex === oldStartIndex:
- dom没有变化,patch(children);
- ++newStartIndex,++oldStartIndex
- newEndIndex === oldEndIndex,:
- 同上;
- --newStartIndex,--oldStartIndex
- newEndIndex === oldStartIndex:
- 先判断节点右移,patchVnode(children),处理完子节点
- 移动oldStartIndex节点到oldEndndex的下一个兄弟节点之前;
- ++oldStartIndex,--newEndIndex
- newStartIndex === oldEndIndex:
- 节点左移,patchVnode(children),处理完子节点
- 移动oldEndIndex节点到oldStartIndex的位置之前
- ++newStartIndex,--oldEndIndex
先判断在old VNode
中是否存在newStartIndex
节点:
- 建立索引表
kyes: [oldStartIndex,oldEndIndex]
- 根据
sameVnode
方法判断索引表中是否存在newStartIndex
节点 - 如果不存在,则创建新节点,插入到
oldStartIndex
节点之前,如8
号位置的节点 - 如果存在,表示也是移动的节点,如上面的
4
号节点:- patchVnode(children),处理完子节点
- 令
oldCh[idxInOld]
= undefined,表示这个oldVNode
已经比较过,下次直接跳过(上面步骤1的情况) - 将上面找到的
idxInOld
节点移动到oldStartIndex
节点之前
- newStartIndex++,继续往下对比
oldStartIdx > oldEndIdx
:new VNode
没比较完,则将剩余[newEndIndex,newEndIndex]
个节点添加到newEndIndex+1
的位置之前- newStartIdx > newEndIdx :oldVnode比较长,删除[oldStartIndex,oldEndIndex]间的节点,已经设置为undefined的会忽略
- 创建节点的函数
createElm
,如果用过其他方式删除页面上的dom节点(例如自己进行dom操作的话),将会导致插入失败,插入函数是
if (targetDom.parentNode === parent) {
parent.insertBefore(newDom, targetDom);
}
其他方式从页面移除targetDom
,就不会插入失败