数据状态更新时的差异 diff 及 patch 机制
作者: 彭凯
之前讲到,在对 model 进行操作对时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。最终是将新产生的 VNode 节点与老 VNode 进行一个 patch 的过程,比对得出「差异」,最终将这些「差异」更新到视图上。
这一章就来介绍一下这个 patch 的过程,因为 patch 过程本身比较复杂,这一章的内容会比较多,但是不要害怕,我们逐块代码去看,一定可以理解。...
跨平台
因为使用了 Virtual DOM 的原因,Vue.js具有了跨平台的能力,Virtual DOM 终归只是一些 JavaScript 对象罢了,那么最终是如何调用不同平台的 API 的呢?
这就需要依赖一层适配层了,将不同平台的 API 封装在内,以同样的接口对外提供。
const nodeOps = {
setTextContent (text) {
if (platform === 'weex') {
node.parentNode.setAttr('value', text);
} else if (platform === 'web') {
node.textContent = text;
}
},
parentNode () {
//......
},
removeChild () {
//......
},
nextSibling () {
//......
},
insertBefore () {
//......
}
}
举个例子,现在我们有上述一个 nodeOps 对象做适配,根据 platform 区分不同平台来执行当前平台对应的API,而对外则是提供了一致的接口,供 Virtual DOM 来调用...
一些API
接下来我们来介绍其他的一些 API,这些API在下面 patch 的过程中会被用到,他们最终都会调用 nodeOps 中的相应函数来操作平台。
insert 用来在 parent 这个父节点下插入一个子节点,如果指定来 ref 则插入到 ref 这个子节点前面。
function insert (parent, elm, ref) {
if (parent) {
if (ref) {
if (ref.parentNode === parent) {
nodeOps.insertBefore(parent, elm, ref);
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
createElm 用来新建一个节点, tag 存在创建一个标签节点,否则创建一个文本节点。
function createElm (vnode, parentElm, refElm) {
if (vnode.tag) {
insert(parentElm, nodeOps.createElement(vnode.tag), refElm);
} else {
insert(parentElm, nodeOps.createTextNode(vnode.text), refElm);
}
}
addVnodes 用来批量调用 createElm 新建节点。
function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
createElm(vnodes[startIdx], parentElm, refElm);
}
}
removeNode 用来移除一个节点。
function removeNode (el) {
const parent = nodeOps.parentNode(el);
if (parent) {
nodeOps.remove...
patch
首先说一下 patch 的核心 diff 算法,我们用 diff 算法可以比对出两颗树的「差异」,我们来看一下,假设我们现在有如下两颗树,它们分别是新老 VNode 节点,这时候到了 patch 的过程,我们需要将他们进行比对。
diff 算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有 O(n),是一种相当高效的算法,如下图。
这张图中的相同颜色的方块中的节点会进行比对,比对得到「差异」后将这些「差异」更新到视图上。因为只进行同层级的比对,所以十分高效。
patch 的过程相当复杂,我们先用简单的代码来看一下。
function patch (oldVNode, vnode, parentElm) {
if (!oldVnode) {
addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
} else if (!vnode) {
removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
} else {
if (sameVnode(oldVNode, vnode)) {
patchVnode(oldVNode, vnode);
} else {
removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
}
}
}
因为 patch 的主要功能是比对两个 VNode 节点,将「差异」更新到视图上,所以入参有新老两个 VNode 以及父节点的 element 。我们来逐步捋一下逻辑, addVnodes 、 removeVnodes 等函数后面会讲。
首先在 oldVnode(老 VNode 节点)不存在的时候,相当于新的 VNode 替代原本没有的节点,所以直接用 addVnodes 将这些节点批量添加到 parentElm 上。
if (!oldVnode) {
addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
}
然后同理,在 vnode(新 VNode 节点)不存在的时候,相当于要把老的节点删除,所以直接使用 removeVnodes 进行批量的节点删除即可。
else if (!vnode) {
removeVnodes(parentElm, oldVnode, 0, oldVn...
未完待续。。。。。
放下心中挂念。加油!