课程回顾
- 虚拟DOM库-Snabbdom
- Vue.js 响应式原理模拟实现
- Vue.js 源码剖析 - 响应式原理
什么是虚拟DOM
- 虚拟DOM(Virtual DDOM)是使用JavaScript对象来描述真实DOM。程序的各种状态变化,首先作用于虚拟DOM,最终映射到真实DOM。像Vue这样的MVVM框架会屏蔽这些基本的DOM操作。
- Vue借鉴了Snabbdom的虚拟DOM,比如模块机制、钩子函数、diff算法等等。在这基础上添加了Vue自身的特性,例如:指令和组件机制。
为什么使用虚拟DOM
- 避免用户直接操作DOM,只需关注业务代码实现,也不用关注浏览器兼容性问题。提高开发效率。
- 作为一个中间层可以跨平台,可以SSR、跨移动端平台。
- 虚拟DOM不一定可以提高性能,首次渲染会增加开销,要创建js对象。复杂视图情况可以提升渲染性能。
Vue中的虚拟DOM,大部分和前面说的Snabbdom相似,只是针对一些步骤加入了自己的特性,本节课程重点讲:
- vm._render
- vm._update
- vm.patch
- patchVnode
- updateChildren
VNode的创建过程
createElement
在render方法的定义中,给出了createElement方法调用的地方,在render.js文件中定义了_render方法,在里边通过外界传入的render调用call方法,获取到vnode节点:
vnode = render.call(vm._renderProxy, vm.$createElement)
该render方法最终会返回一个根VNode。在上面代码里vm.$createElement就是h函数。在此处被调用了。
这里先着重记录下代码内部的normalizeChildren方法,文中老师说的是,使用normalizeChildren函数做处理,它最终会返回一个一维数组,方便后续的处理。
但我看了源码,觉得老师说少了,最终是循环遍历children数组,让所有的子节点最终都生成为文本节点。
// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): ?Array<VNode> {
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
看这个方法的递归调用,我觉得是遍历children下的每一层children,然后生成对应的VNode。
function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
const res = []
let i, c, lastIndex, last
for (i = 0; i < children.length; i++) {
c = children[i]
if (isUndef(c) || typeof c === 'boolean') continue
lastIndex = res.length - 1
last = res[lastIndex]
// nested
if (Array.isArray(c)) {
if (c.length > 0) {
c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
// merge adjacent text nodes
if (isTextNode(c[0]) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
c.shift()
}
res.push.apply(res, c)
}
} else if (isPrimitive(c)) {
if (isTextNode(last)) {
// merge adjacent text nodes
// this is necessary for SSR hydration because text nodes are
// essentially merged when rendered to HTML strings
res[lastIndex] = createTextVNode(last.text + c)
} else if (c !== '') {
// convert primitive to vnode
res.push(createTextVNode(c))
}
} else {
if (isTextNode(c) && isTextNode(last)) {
// merge adjacent text nodes
res[lastIndex] = createTextVNode(last.text + c.text)
} else {
// default key for nested array children (likely generated by v-for)
if (isTrue(children._isVList) &&
isDef(c.tag) &&
isUndef(c.key) &&
isDef(nestedIndex)) {
c.key = `__vlist${nestedIndex}_${i}__`
}
res.push(c)
}
}
}
return res
}
相比于Snabbdom的createElement,这里处理了组件的情况,及一些其它额外的事情。
处理Vnode-update(patch)
在渲染watcher中传入的cb,就是被包裹的_update方法,在它源码内部是调用了__patch__方法,这估计和Snabbdom的patch方法差不多。
const vm: Component = this
const prevEl = vm.$el
// _vnode记录的是之前处理过的vnode对象
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
// 初始化渲染时,没有老vnode,这里直接和el根节点作对比
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
首次初始化,vm.$el会在patch方法中被转化为vnode,进行比较。然后将比对的差异更新到真实DOM上,并把比对的结果返回储存到vm.$el中来。
当patch函数处理完,会把最新的vnode存储到_vnode属性中。
Patch函数的初始化
该函数还区分环境:
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
patch函数,由createPatchFunction方法生成,说明它也是一个高阶函数,是一个柯里化的函数。同时看参数,它也和Snabbdom一样,是模块化的。
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'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
- nodeOps:里面和Snabbdom很相似,都是存放的DOM操作方法,不同的是针对createElement方法做了针对
select标签的处理。
export function createElement (tagName: string, vnode: VNode): Element {
const elm = document.createElement(tagName)
if (tagName !== 'select') {
return elm
}
// false or null will remove the attribute but undefined will not
if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
elm.setAttribute('multiple', 'multiple')
}
return elm
}
- modules:这里拼接了基础模块和平台模块,这些模块的基础操作也是处理属性、事件、样式等等,但Vue中多了一个
transition,用于处理过渡动画。
// platformModules 平台模块内容
export default [
attrs,
klass,
events,
domProps,
style,
transition
]
和Snabbdom一样,各个模块导出的都是生命周期钩子函数,基于钩子函数进行模块方法的执行
baseModules:基础模块,处理指令和ref的模块
// baseModules 基础模块内容
export default [
ref,
directives
]
createPatchFunction源码
Vue的钩子函数:
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
在Patch函数内部,定义的大部分方法和Snabbdom相似:
function sameVnode (a, b) {
/**
* 相比于Snabbdom,相同点:
* 都比较了key,tag是否相同
* 不同点:
* 还判断了很多额外的东西
*
*/
return (
a.key === b.key &&
a.asyncFactory === b.asyncFactory && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
isUndef(b.asyncFactory.error)
)
)
)
}
createElm函数
它的作用是,把VNode转化成真实DOM,然后挂载到DOM树上来。
createElm(
vnode: any, 虚拟节点
insertedVnodeQueue: any, inserted钩子队列
parentElm: any, 父节点元素
refElm: any,
nested: any,
ownerArray: any, 子节点数组
index: any
): void {
// ``` //
// 先判断vnode中是否有elm属性,如果有,则说明该vnode层级被渲染过
// ownerArray代表该vnode是否有子节点
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.
// 如果有elm属性,并且有子节点,则要克隆一份。
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
// 处理组件情况
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// ``` //
}
这个函数内部调用了createChildren方法,里边进行了重复Key值的判断,使用方法就是对象方式。
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(children)
}
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
更多详情,看代码注释
patchVNode函数
详情看代码,原理参考Snabbdom。
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
if (oldVnode === vnode) {
return;
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(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;
}
// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
// 如果新旧 VNode 都是静态的,那么只需要替换componentInstance
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);
}
const oldCh = oldVnode.children;
const ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
// 调用 cbs 中的钩子函数,操作节点的属性/样式/事件....
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)) {
// 老节点和老节点都有有子节点
// 对子节点进行 diff 操作,调用 updateChildren
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch)
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
} else if (isDef(ch)) {
// 新vnode有子节点,老vnode没有子节点
if (process.env.NODE_ENV !== "production") {
checkDuplicateKeys(ch);
}
// 先清空老节点 DOM 的文本内容,然后为当前 DOM 节点加入子节点
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "");
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 老节点有子节点,新的没有子节点
// 直接删除老节点中的子节点
removeVnodes(oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
// 老节点有文本,新节点没有文本
// 清空老节点的文本内容
nodeOps.setTextContent(elm, "");
}
} else if (oldVnode.text !== vnode.text) {
// 新老节点都有文本节点
// 修改文本
nodeOps.setTextContent(elm, vnode.text);
}
// 如果有用户定义的data,则判断执行对应钩子函数
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.postpatch)))
i(oldVnode, vnode);
}
}
updateChildren函数
详情参看Snabbdom的相关解析
// 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;
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly;
if (process.env.NODE_ENV !== "production") {
checkDuplicateKeys(newCh);
}
// diff 算法
// 当新节点和旧节点都没有遍历完成
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(
oldStartVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(
oldEndVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
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)) {
// Vnode moved left
patchVnode(
oldEndVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
);
canMove &&
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (isUndef(oldKeyToIdx))
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
if (isUndef(idxInOld)) {
// New element
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 {
// same key but different element. treat as new element
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);
}
}
function sameVnode(a, b) {
/**
* 相比于Snabbdom,相同点:
* 都比较了key,tag是否相同
* 不同点:
* 还判断了很多额外的东西
*
*/
return (
a.key === b.key &&
a.asyncFactory === b.asyncFactory &&
((a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)) ||
(isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
);
}
没设置Key的情况
从示例看,更新了3次DOM,插入了1次DOM,总共执行4次DOM操作。
设置Key的情况
从示例看,只执行了1次插入的操作,Dom操作少很多。
总结:设置key,能更好的利用updateChildren的特性,从头到尾遍历比对不成功,能尽快的从尾到头进行遍历,能最大限度的重用Dom。
没设置key,则可近似看为只要判断tag相同,则会进行patchVnode,增加了不必要的比对、更新次数。设置了key,则会提前判断为比对不一致。
总结
Patch的核心是进行:对数据的更新、真实DOM的操作。所有的虚拟DOM相关的操作,最终由这里挂载到页面上。