我们都知道,通过虚拟DOM,可以减少DOM操作,提高页面渲染性能
要实现虚拟DOM,主要三部曲:
- compile view to vnode
- diff vnode 的变化
- patch vnode 变化到实际的DOM
假想的黑粉:"所以这篇文章是要深入虚拟DOM的实现原理和实现细节吗?"
非也非也,我们开始愉快地进入正题吧
三部曲中,diff的性能很关键,所以一般对vnode的type和key作比较,如果不一致,则该vnode及以下孩子们全部干掉(好残忍,无法直视(>﹏<)),用新的直接替换,不再往下对比。
假想的黑粉:“这个大家都懂,所以文章至此可以结束了?”
还没。。。还没开始(# ̄▽ ̄#)
要diff,那就得有diff的两个vnode,一个old vnode,一个new vnode,那new vnode如何产生的呢?
假想的黑粉:“简单啊,把old vnode赋值给new vnode”
额~,像以下这样吗?
let oldVnode = {a: {a1: 1}, b: {b1: 1}}
let newVnode = oldVnode
newVnode === oldVnode // true
可以看到新旧一致,无论如何赋值都是同个对象,无从对比啦
假想的黑粉:“我是想说clone一个啦,shadow就行了” <( ̄︶ ̄)>
额~,像以下这样吗?
let oldVnode = {a: {a1: 1}, b: {b1: 1}}
let newVnode = Object.assign({}, oldVnode)
oldVnode === newVnode // false
newVnode.a.a1 = 2
oldVnode.a.a1 // 2
可以看到更改了new vnode的a1值,old vnode的a1值也被改了,也就无法得知变化了
假想的黑粉:“刚为了性能考虑说了shadow copy,那实在不行就deep copy吧”
额~,像以下这样吗?
const _ = require('lodash');
let oldVnode = {a: {a1: 1}, b: {b1: 1}}
let newVnode = _.cloneDeep(oldVnode)
newVnode.a.a1 = 2
oldVnode === newVnode // false
oldVnode.a.a1 === newVnode.a.a1 // false
看上去没什么问题,功能是可以实现了,但这篇文章是要讲 更 高效diff,上面方案有两个较不好的性能问题:
- deep copy造成资源浪费,没更新的结点也被复制了一份
- 每次要遍历所有vnode进行对比,无论该vnode有没产生变化
假想的黑粉:“看样子你是有更好的方案,有什么花招赶紧使出来吧~”
那就让我慢慢道来,先来个中横线分割一下先( ̄︶ ̄)↗
只要避免上面提到的两点对性能的影响,即可更高效DIFF,对应的措施如下:
- 按需copy:没出现变化的vnode不作copy
- 按需diff:没出现变化的vnode不作diff
假设vnode的数据结构以及图形表示如下:
let oldVnode = {
a: {
a1: { a1_1: 1 },
a2: { a2_1: 1 }
},
b: { b1: { b1_1: 1 } }
}
当把 a1_1 的值更改为 2 时, 我们希望只对以下高亮节点进行shadow copy或赋值,以下即为new vnode
所以在对比old vnode和new vnode时,只有下图高亮的节点需要进行比对
当 a2 和 b 节点下面的子节点越多时,copy 和 diff 所带来的性能收益就越明显
最后献上这种方案的极简单极粗糙的实现(update方法,只考虑对象,没考虑数组)以更好的从代码层面去理解这种思路
const assert = require('assert');
let oldVal = {a: {a1: 1}, b: {b1: 2}}
function update(obj, path, val) {
let fileds = path.split('.');
let shadowCopy = targetObj => Object.assign({}, targetObj);
let result = shadowCopy(obj);
if (fileds.length > 0) {
if (fileds.length === 1) {
result[fileds[0]] = val;
} else {
result[fileds[0]] = update(obj[fileds[0]], fileds.length > 1 ? fileds.splice(1).join('.') : '', val)
}
}
return result;
}
const newVal = update(oldVal, 'a.a1', 2);
assert.notStrictEqual(oldVal, newVal);
assert.notStrictEqual(oldVal.a, newVal.a);
assert.notStrictEqual(oldVal.a.a1, newVal.a.a1);
assert.strictEqual(oldVal.b, newVal.b);