前言

前端三大框架:Angular2、React、Vue,都是 MVVM 框架,但是他们各自实现 ViewModel 层 数据绑定的方式都不太一样,在我学习框架的过程中,也想要知道底层实现的方法,所以写写博客记录一下。

Angular2

Angular2 提供一种特殊的双向数据绑定语法:[(x)][(x)] 语法结合了属性绑定的方括号 [x] 和事件绑定的圆括号 (x) 。也就是说,Angular2 数据流向是单向的。

注:这里不讨论 Angularjs 和 Angular2 的区别,因为 Angular2 几乎是 Angularjs 的大改,很多地方都不一样了,具体可以自己去找找。

怎么知道数据变化了

我们知道有一些操作会引起数据的变化

  • 用户输入操作,比如点击,提交等
  • 请求服务端数据
  • 定时事件,比如 setTimeoutsetInterval

这几点有一个共同点,就是它们都是异步的。也就是说,所有的异步操作是可能导致数据变化的根源因素。
Angular2 引入了一个很重要的文件 zone.js(所谓的猴子补丁Monkey patch),它主要是对异步事件做一层代理包裹,也就是说 Zone.js 运行后,调用 setTimeoutaddEventListener 等浏览器异步事件时,不再是调用原生的方法,而是被猴子补丁包装过后的代理方法。
这里就用到了一个模式:代理模式

下面我们来介绍 ngZone 。实际上,ngZone 是基于 Zone.js 来实现的,Angular2 fork了 zone.js,它是 zone 派生出来的一个子zone,在 Angular 环境内注册的异步事件都运行在这个 子zone 上(因为 ngZone 拥有整个 Angular 运行环境的执行上下文),并且 onTurnStartonTurnDone 事件也会在该 子zone 的 run
方法中触发。

在Angular2源码中,有一个 ApplicationRef 类,其作用是用来监听 ngZone 中的
onTurnDone 事件,不论何时只要触发这个事件,那么将会执行一个 tick() 方法用来告诉 Angular 去执行变化监测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// very simplified version of actual source
class ApplicationRef {
changeDetectorRefs:ChangeDetectorRef[] = [];

constructor(private zone: NgZone) {
this.zone.onTurnDone
.subscribe(() => this.zone.run(() => this.tick());
}

tick() {
this.changeDetectorRefs
.forEach((ref) => ref.detectChanges());
}
}

数据更新

Angular2 有一个机制:脏检查,其实就是存储所有变量的值,每当可能有变量发生变化需要检查时,就将所有变量的旧值跟新值进行比较,不相等就说明检测到变化,需要更新对应视图。
在 Angular2 中,任何的一个 Angular2 应用都是由大大小小的组件组成的,可以把它看成是一颗线性的组件树,重要的是,每一个组件都有自己的变化检测器。正是因为每个组件都拥有它的变化检测器,组成了 Angular2 应用的一颗组件树,同样的我们也有变化监测树,它也是线性的,数据的流向也是从上到下,因为变化监测在每个组件中的执行也是从根组件开始,从上往下的执行。


Angular2 在整个运行期间都会为每一个组件创建监测类,用来监测每个组件在每个运行周期是否有异步操作发生。当变化监测被执行时,即发生了异步操作,那么这个组件的值被传递到对应组件的变化检测器来和之前的数据对比是否有改变,如果和参照数据对比有变动的话,Angular 将更新视图。

优化变化检测策略

因为在JavaScript语言中不提供给我们对象的变化通知,所以 Angular 必须保守的要对每一个组件的每一次运行结果执行变化检测,但其实很多组件的输入属性是没有变化的,没必要对这样的组件来一次变化监测,如何减少不必要的监测,我们有两种
OnPush 方式去实现。

1
2
3
4
export enum ChangeDetectionStrategy { 
OnPush, // 表示变化检测对象的状态为`CheckOnce`
Default, // 表示变化检测对象的状态为`CheckAlways`
}

ChangeDetectionStrategy 可以看到,Angular2 有两种变化检测策略。Default
是 Angular2 默认的变化检测策略,也就是上述提到的脏检查(只要有值发生变化,就全部检查)。开发者可以根据场景来设置更加高效的变化检测方式:onPushonPush 策略,就是只有当输入数据的引用发生变化或者有事件触发时,组件才进行变化检测。

不可变对象(Immutable Objects)

不可变对象(Immutable Objects)给我们提供的保障是对象不会改变,即当其内部的属性发生变化时,相对旧有的对象,我们将会保存另一份新的参照。它仅仅依赖输入的属性,也就是当输入属性没有变动(没有变动即没有产生一份新的参照),Angula 将跳过对该组件的全部变化监测,直到有属性变化为止。如果需要在 Angular2 中使用不可变对象,我们需要做的就是设置 changeDetection: ChangeDetectionStrategy.OnPush,如下的例子:

1
2
3
4
5
6
7
8
9
10
11
@Component({
template: `
<h2>{ {vData.name} }</h2>
<span>{ {vData.email} }</span>
`,
// 设置该组件的变化检测策略为onPush
changeDetection: ChangeDetectionStrategy.OnPush
})
class VCardCmp {
@Input() vData;
}

比如上面这个例子,当 vData 的属性值发生变化的时候,这个组件不会发生变化检测,只有当 vData 重新赋值的时候才会。一般,只接受输入的木偶子组件(dumb components),也可以理解为子组件,比较适合采用 onPush 策略。

可观察量(Observables)

和不可变对象类似,但却又和不可变对象不同,它们有相关变化的时候不会提供一份新的参照,可观测对象在输入属性发生变化的时候来触发一个事件来更新组件视图,同样的,我们也是添加 OnPush 来跳过子组件树的监测器,我们看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component({
template: '{ {counter} }',
changeDetection: ChangeDetectionStrategy.OnPush
})
class CartBadgeCmp {

@Input() addItemStream:Observable<any>;
counter = 0;

ngOnInit() {
this.addItemStream.subscribe(() => {
this.counter++; // application state changed
})
}
}

该组件是模拟的当用户触发一个事件后增加 counter 这样一个场景,确切的讲,CartBadgeCmp 设置了一个插值 counter 和一个输入属性 addItemStream,当有异步操作需要更新 counter 的时候,将会触发一个事件流,但是输入属性
addItemStream 作为参考对象将不会更改,意味着该组件树的变化监测将不会发生。那怎么办?我们将怎么来通知 Angular 某区块有改变呢?Angular2 的变化监测总是从组件树的头到尾来执行,我们其实需要的就是在整个组件树的某个发生改变的地方来做出相应即可,Angular 是不知道那一块目录有改变的,但是我们知道,我们可以通过依赖注入给组件来引入一个 ChangeDetectorRef ,这个方法正是我们所需要的,它能标记整颗组件树的目录直到下一次变化监测的执行,代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class CartBadgeCmp {
constructor(private cd: ChangeDetectorRef) {}

@Input() addItemStream:Observable<any>;
counter = 0;

ngOnInit() {
this.addItemStream.subscribe(() => {
this.counter++; // application state changed
this.cd.markForCheck(); // marks path
})
}
}

当这个可监测的 addItemStream 触发一个事件,该事件处理句柄将会从根路径到这个已经改变的 addItemStream 组件来处理监测,一旦变化监测跑遍整个监测路径,它将会存储 OnPush 状态到整个组件树。这样做的好处是,变化监测系统将会走遍整棵树,你可以利用他们来监测树在局部是否有真正的改变,以此来做出相应的改变。

Vue

Vue 和 Angular2 一样,也是使用了单向数据流,实现双向绑定也是属性绑定和事件绑定相结合,但是实现的方法却不一样。

怎么知道数据变化了

vue 是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的。这个函数是重要的概念,不理解可以去看一下 MDN-Object.defineProperty()
Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因。
把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。
vue 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty() 来劫持各个属性的 settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。

vue的发布者-订阅者模式

个人觉得比较好的一篇文章,搬运了,以下是搬运的内容,当做学习与记录。

要实现mvvm的双向绑定,就必须要实现以下几点:
1、实现一个数据监听器 Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器 Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个 Watcher,作为连接 ObserverCompile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm 入口函数,整合以上三者

实现Observer

我们知道可以利用 Obeject.defineProperty() 来监听属性变动
那么将需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 settergetter 这样的话,给这个对象的某个值赋值,就会触发 setter ,那么就能监听到了数据变化。相关代码可以是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function observe(data) {
if (!data || typeof data !== 'object') {
return;
}
// 取出所有属性遍历
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
});
};

function defineReactive(data, key, val) {
observe(val); // 监听子属性
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: function() {
return val;
},
set: function(newVal) {
console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
val = newVal;
}
});
}

这样我们已经可以监听每个数据的变化了,那么监听到变化之后就是怎么通知订阅者了,所以接下来我们需要实现一个消息订阅器,很简单,维护一个数组,用来收集订阅者,数据变动触发 notify ,再调用订阅者的 update 方法,代码改善之后是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ... 省略
function defineReactive(data, key, val) {
var dep = new Dep();
observe(val); // 监听子属性

Object.defineProperty(data, key, {
// ... 省略
set: function(newVal) {
if (val === newVal) return;
console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
val = newVal;
dep.notify(); // 通知所有订阅者
}
});
}

function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};

那么问题来了,谁是订阅者?怎么往订阅器添加订阅者?
没错,上面的思路整理中我们已经明确订阅者应该是 Watcher , 而且 var dep = new Dep(); 是在 defineReactive 方法内部定义的,所以想通过 dep 添加订阅者,就必须要在闭包内操作,所以我们可以在 getter 里面动手脚:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Observer.js
// ...省略
Object.defineProperty(data, key, {
get: function() {
// 由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher, 添加完移除
Dep.target && dep.addDep(Dep.target);
return val;
}
// ... 省略
});

// Watcher.js
Watcher.prototype = {
get: function(key) {
Dep.target = this;
this.value = data[key]; // 这里会触发属性的getter,从而添加订阅者
Dep.target = null;
}
}

这里已经实现了一个 Observer 了,已经具备了监听数据和数据变化通知订阅者的功能。

实现Compile

compile 主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图,如图所示:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Compile(el) {
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
init: function() { this.compileElement(this.$fragment); },
node2Fragment: function(el) {
var fragment = document.createDocumentFragment(), child;
// 将原生节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
}
};

compileElement 方法将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定,详看代码及注释说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
Compile.prototype = {
// ... 省略
compileElement: function(el) {
var childNodes = el.childNodes, me = this;
[].slice.call(childNodes).forEach(function(node) {
var text = node.textContent;
var reg = /{ {(.*)} }/; // 表达式文本
// 按元素节点方式编译
if (me.isElementNode(node)) {
me.compile(node);
} else if (me.isTextNode(node) && reg.test(text)) {
me.compileText(node, RegExp.$1);
}
// 遍历编译子节点
if (node.childNodes && node.childNodes.length) {
me.compileElement(node);
}
});
},

compile: function(node) {
var nodeAttrs = node.attributes, me = this;
[].slice.call(nodeAttrs).forEach(function(attr) {
// 规定:指令以 v-xxx 命名
// 如 <span v-text="content"></span> 中指令为 v-text
var attrName = attr.name; // v-text
if (me.isDirective(attrName)) {
var exp = attr.value; // content
var dir = attrName.substring(2); // text
if (me.isEventDirective(dir)) {
// 事件指令, 如 v-on:click
compileUtil.eventHandler(node, me.$vm, exp, dir);
} else {
// 普通指令
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
}
}
});
}
};

// 指令处理集合
var compileUtil = {
text: function(node, vm, exp) {
this.bind(node, vm, exp, 'text');
},
// ...省略
bind: function(node, vm, exp, dir) {
var updaterFn = updater[dir + 'Updater'];
// 第一次初始化视图
updaterFn && updaterFn(node, vm[exp]);
// 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
new Watcher(vm, exp, function(value, oldValue) {
// 一旦属性值有变化,会收到通知执行此更新函数,更新视图
updaterFn && updaterFn(node, value, oldValue);
});
}
};

// 更新函数
var updater = {
textUpdater: function(node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
}
// ...省略
};

这里通过递归遍历保证了每个节点及子节点都会解析编译到,包括了 { {} } 表达式声明的文本节点。指令的声明规定是通过特定前缀的节点属性来标记,如 <span v-text="content" other-attrv-text 便是指令,而 other-attr 不是指令,只是普通的属性。
监听数据、绑定更新函数的处理是在 compileUtil.bind() 这个方法中,通过 new Watcher() 添加回调来接收数据变化的通知。至此,一个简单的Compile就完成了。

实现Watcher

Watcher 订阅者作为 ObserverCompile 之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器( dep )里面添加自己
2、自身必须有一个 update() 方法
3、待属性变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发
Compile 中绑定的回调,则功成身退。
如果有点乱,可以回顾前面的思路整理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
// 此处为了触发属性的getter,从而在dep添加自己,结合Observer更易理解
this.value = this.get();
}
Watcher.prototype = {
update: function() {
this.run(); // 属性值变化收到通知
},
run: function() {
var value = this.get(); // 取到最新值
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal); // 执行Compile中绑定的回调,更新视图
}
},
get: function() {
Dep.target = this; // 将当前订阅者指向自己
var value = this.vm[exp]; // 触发getter,添加自己到属性订阅器中
Dep.target = null; // 添加完毕,重置
return value;
}
};
// 这里再次列出Observer和Dep,方便理解
Object.defineProperty(data, key, {
get: function() {
// 由于需要在闭包内添加watcher,所以可以在Dep定义一个全局target属性,暂存watcher, 添加完移除
Dep.target && dep.addDep(Dep.target);
return val;
}
// ... 省略
});
Dep.prototype = {
notify: function() {
this.subs.forEach(function(sub) {
sub.update(); // 调用订阅者的update方法,通知变化
});
}
};

实例化 Watcher 的时候,调用 get() 方法,通过 Dep.target = watcherInstance
标记订阅者是当前 watcher 实例,强行触发属性定义的 getter 方法,getter 方法执行的时候,就会在属性的订阅器 dep 添加当前 watcher 实例,从而在属性值有变化的时候, watcherInstance 就能收到更新通知。

实现MVVM

MVVM 作为数据绑定的入口,整合 ObserverCompileWatcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 ObserverCompile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。

一个简单的MVVM构造器是这样子:

1
2
3
4
5
6
function MVVM(options) {
this.$options = options;
var data = this._data = this.$options.data;
observe(data, this);
this.$compile = new Compile(options.el || document.body, this)
}

但是这里有个问题,从代码中可看出监听的数据对象是 options.data ,每次需要更新视图,则必须通过 var vm = new MVVM({data:{name: 'kindeng'} }); vm._data.name = 'dmq';这样的方式来改变数据。

显然不符合我们一开始的期望,我们所期望的调用方式应该是这样的:
var vm = new MVVM({data: {name: 'kindeng'} }); vm.name = 'dmq';

所以这里需要给 MVVM 实例添加一个属性代理的方法,使访问 vm 的属性代理为访问 vm._data 的属性,改造后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function MVVM(options) {
this.$options = options;
var data = this._data = this.$options.data, me = this;
// 属性代理,实现 vm.xxx -> vm._data.xxx
Object.keys(data).forEach(function(key) {
me._proxy(key);
});
observe(data, this);
this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
_proxy: function(key) {
var me = this;
Object.defineProperty(me, key, {
configurable: false,
enumerable: true,
get: function proxyGetter() {
return me._data[key];
},
set: function proxySetter(newVal) {
me._data[key] = newVal;
}
});
}
};

这里主要还是利用了 Object.defineProperty() 这个方法来劫持了 vm 实例对象的属性的读写权,使读写 vm 实例的属性转成读写了 vm._data 的属性值。

React

在React中,数据流是自上而下单向的从父节点传递到子节点,也就是只有单向绑定,并没有 Angular2 和 Vue 的双向绑定 ,但是我们也可以自己实现双向绑定,这里就不说了,给出一篇文章,里面说到了 React 双向绑定的实现方法,可以自己去看看。

怎么知道数据变化了

React 通过setState()通知变化

数据更新

React 最出名的就是 虚拟DOM(virtual DOM),Vue(2.0版本)也有虚拟DOM(Vue宣称可以更快地计算出Virtual DOM的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。)但我们不在 Vue 里面讲 virtual DOM 而是放到 React 来讲。

什么是Virtual DOM

Virtual DOM并没有完全实现 DOM,Virtual DOM最主要的还是保留了 Element 之间的层次关系和一些基本属性。Virtual DOM 里每一个 Element 实际上只有几个属性,并且没有那么多乱七八糟的引用。所以哪怕是直接把 Virtual DOM 删了,根据新传进来的数据重新创建一个新的 Virtual DOM 出来都非常非常非常快。
我们可以看看下面这个列表在HTML中的代码是如何写的:

1
2
3
4
<ul class="list">
<li>item 1</li>
<li>item 2</li>
</ul>

而在JavaScript中,我们可以用对象简单地创造一个针对上面例子的映射:

1
2
3
4
5
6
7
8
{
type: 'ul',
props: {'class': 'list'},
children: [
{ type: 'li', props: {}, children: ['item 1'] },
{ type: 'li', props: {}, children: ['item 2'] }
]
}

真实的 Virtual DOM 会比上面的例子更复杂,但它本质上是一个嵌套着数组的原生对象。

为什么要用 Virtual DOM

这是因为更新 DOM 的花费时间非常长,当我们使用 JavaScript 来改变页面的时候,浏览器不得不做一些工作来找到需要的DOM节点。在如今的应用程序的DOM中大概有成千上万的节点,因此更新所花费的时间就更长了。有很多不可避免的很小很频繁的更新拖慢了页面的速度。

注意:如果你的应用中,交互复杂,需要处理大量的UI变化,那么使用 Virtual DOM 是一个好主意。如果你更新元素并不频繁,那么 Virtual DOM 并不一定适用,性能很可能还不如直接操控 DOM。

为什么 Virtual DOM 可以提高网页性能

状态变更->重新渲染整个视图的方式可以稍微修改一下:用 JavaScript 对象表示 DOM 信息和结构,当状态变更的时候,重新渲染这个 JavaScript 的对象结构。当然这样做其实没什么卵用,因为真正的页面其实没有改变。
但是可以用新渲染的对象树去和旧的树进行对比,记录这两棵树差异。记录下来的不同就是我们需要对页面真正的 DOM 操作,然后把它们应用在真正的 DOM 树上,页面就变更了。这样就可以做到:视图的结构确实是整个全新渲染了,但是最后操作DOM的时候确实只变更有不同的地方。
这就是所谓的 Virtual DOM 算法。包括几个步骤:

  1. 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
  2. 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
  3. 把第 2 步所记录的差异应用到第 1 步所构建的真正的DOM树上,视图就更新了

Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。

diff算法

比较两棵DOM树的差异是 Virtual DOM 算法最核心的部分,这也是所谓的 Virtual DOM 的 diff 算法。两个树的完全的 diff 算法是一个时间复杂度为 O(n^3) 的问题。但是在前端当中,你很少会跨越层级地移动 DOM 元素。所以 Virtual DOM 只会对同一个层级的元素进行对比:


上面的 div只会和同一层级的 div 对比,第二层级的只会跟第二层级对比。这样算法复杂度就可以达到 O(n)。

在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记:


在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比。如果有差异的话就记录到一个对象里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// diff 函数,对比两棵树
function diff (oldTree, newTree) {
var index = 0 // 当前节点的标志
var patches = {} // 用来记录每个节点差异的对象
dfsWalk(oldTree, newTree, index, patches)
return patches
}

// 对两棵树进行深度优先遍历
function dfsWalk (oldNode, newNode, index, patches) {
// 对比oldNode和newNode的不同,记录下来
patches[index] = [...]

diffChildren(oldNode.children, newNode.children, index, patches)
}

// 遍历子节点
function diffChildren (oldChildren, newChildren, index, patches) {
var leftNode = null
var currentNodeIndex = index
oldChildren.forEach(function (child, i) {
var newChild = newChildren[i]
currentNodeIndex = (leftNode && leftNode.count) // 计算节点的标识
? currentNodeIndex + leftNode.count + 1
: currentNodeIndex + 1
dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍历子节点
leftNode = child
})
}

例如,上面的 div 和新的 div 有差异,当前的标记是0,那么:
patches[0] = [{difference}, {difference}, ...] // 用数组存储新旧节点的不同
同理 ppatches[1]ulpatches[3] ,类推。

如果想了解 Vue Virtual DOM具体算法,可以参考这篇文章

注意:由于hexo的问题,不能在代码块中出现{{或}} 的字符,所以只能在{{中间加一个空格

参考
深入理解Angular2变化监测和ngZone
Angular变化检测机制:改善的脏检查
angular2 脏检查总述–zone.js 原理
剖析Vue原理&实现双向绑定MVVM
对比其他框架
深度剖析:如何实现一个 Virtual DOM 算法
如何理解虚拟DOM?