这篇文章介绍一下脏检查机制实现响应式编程原理。使用脏检查机制的框架有Angularjs
和网易内部使用比较多的Regularjs
。
在脏检查机制当中主要涉及到:
Watcher
的绑定- 脏检查触发时机
- 执行
listener
和更新View
最终的效果:
//js
let vm = new MVVM({
data: {
name: "luoxia",
year: 21,
intro: "大家好,我来自美丽的山城重庆!"
},
methods: {
sayName(){
this.$data.name = "Jack"; // 这里改变data能够自动触发脏检查
console.log("hello,my name is", this.$data.name);
}
}
});
vm.$watch("name",(newVal,oldVal)=>{
console.log(`name的值变化了,新值:${newVal},旧值:${oldVal}`);
});
vm.$watch("year",(newVal,oldVal)=>{
console.log(`year的值变化了,新值:${newVal},旧值:${oldVal}`);
})
vm.$data.year = 22; // 这里修改data需要手动触发脏检查
vm.$apply();
<button lx-click="sayName">更改姓名</button>
<div>
<p lx-bind="intro">
</p>
介绍者:<span lx-bind="name"></span>
</div>
在MVVM
实例化过程中,有一个模板编译的过程。在编译过程中,做以下事情:
- 遇到数据绑定,添加相应的
Watcher
到观察者队列 - 因为UI事件是触发脏检查的一个时机,所以对事件绑定做处理
$compile(){
let bindList = document.querySelectorAll('[lx-bind]');
let this_ = this;
for(let i=0,length=bindList.length; i<length; i++){
let bindData = bindList[i].getAttribute('lx-bind');
// 添加一个对应的观察者
let bindWatcher = new Watcher(bindData,(newVal,oldVal)=>{
bindList[i].innerHTML = newVal; // 监听绑定数据的变化,更新view
});
this.$watcherList.push(bindWatcher);
}
// 对数据绑定做处理
let clickList = document.querySelectorAll('[lx-click]');
for(let i=0,length = clickList.length; i<length; i++){
clickList[i].onclick = function(){
let method = this.getAttribute('lx-click');
this_.$methods[method].call(this_);
// 执行事件回调后自动触发一次脏检查
this_.$apply();
}
}
}
我们通过lx-click
绑定点击事件而不是直接onclick
,因为我们要在事件触发时,自动触发脏检查。
这里的Watcher
其实比Vue
方式更简单,记录自己监听的属性、回调和上一次的值:
class Watcher {
constructor(prop,listener){
this.prop = prop;
this.listener = listener || function(){};
this.last = undefined;
}
getNewValue(scope){
return scope.$data[this.prop];
}
}
$apply(){
this.$digest();
}
$digest(){
let dirty = true,
checkTimes = 0; // 一次脏检查周期内循环脏检查的次数
while(dirty){
dirty = this.$dirtyOnce(); //调用脏检查
checkTimes++;
if(checkTimes>10&&dirty){ // 循环脏检查次数达到10次,dirty还是为true则报错
throw new Error("脏检查超过10次,建议优化代码");
}
}
}
$dirtyOnce(){
let dirty = false;
let list = this.$watcherList;
for(let i=0,length=list.length; i<length; i++){
let watcher = list[i];
let newVal = watcher.getNewValue(this); // 获取watcher监听的属性的最新值
let oldVal = watcher.last;
if(newVal !== oldVal){ // 如果和上次的值不同则调用相应的回调,且标记dirty为true
dirty = true;
watcher.listener(newVal,oldVal);
watcher.last = newVal;
}
}
return dirty;
}
在调用$apply()
时,会触发进入一次脏检查周期,在一个周期内是一个循环检查的过程,每次检查都会遍历所有的watcher
,看它们自己监听的数据是否发生变化,如果有则调用相应的listener
,且标记dirty
为true
,只要dirty
为true
则会继续进行脏检查确保所有数据达到稳定状态,这样在listener
里对data
的变化也能立即生效。但是也要做一个次数限制防止过深的循环检查。
整个完整的流程如下:
- 对实例化传入的
options
进行处理 - 模板编译,编译过程中没解析到一个数据绑定,就添加相应的观察者,对事件绑定进行处理
- 编译完成后,触发一次脏检查
- 当特定场景时(主要是UI事件触发和手动触发脏检查),进入脏检查周期
- 每个脏检查周期都会有两次以上的脏检查,直到没有数据发生变化
- 脏检查中,每个观察者如果对应的数据发生变化则调用
listener
,view
上绑定数据的watcher
有默认的listener
用于更新view
即:
class MVVM {
constructor(options) {
this.$data = options.data;
this.$methods = options.methods;
this.$watcherList = [];
this.$compile();
this.$apply(); //编译完成触发一次脏检查
}
// ...
}
完整代码见:https://github.com/laoqiren/Reactive/blob/master/dirty.js