# 脏检查机制

这篇文章介绍一下脏检查机制实现响应式编程原理。使用脏检查机制的框架有`Angularjs`和网易内部使用比较多的`Regularjs`。

在脏检查机制当中主要涉及到：

* `Watcher`的绑定
* 脏检查触发时机
* 执行`listener`和更新`View`

最终的效果：

```js
//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();
```

```html
<button lx-click="sayName">更改姓名</button>
<div>
    <p lx-bind="intro">

    </p>
    介绍者：<span lx-bind="name"></span>
</div>
```
## `Compile`过程

在`MVVM`实例化过程中，有一个模板编译的过程。在编译过程中，做以下事情：

* 遇到数据绑定，添加相应的`Watcher`到观察者队列
* 因为UI事件是触发脏检查的一个时机，所以对事件绑定做处理
```js
$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

这里的`Watcher`其实比`Vue`方式更简单，记录自己监听的属性、回调和上一次的值：

```js
class Watcher {
    constructor(prop,listener){
        this.prop = prop;
        this.listener = listener || function(){};
        this.last = undefined;
    }
    getNewValue(scope){
        return scope.$data[this.prop];
    }
}
```

## 脏检查实现
```js
$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`

即：

```js
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](https://github.com/laoqiren/Reactive/blob/master/dirty.js)
## 存在的问题