Skip to content

Latest commit

 

History

History
164 lines (138 loc) · 5.22 KB

脏检查机制.md

File metadata and controls

164 lines (138 loc) · 5.22 KB

脏检查机制

这篇文章介绍一下脏检查机制实现响应式编程原理。使用脏检查机制的框架有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>

Compile过程

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

这里的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,且标记dirtytrue,只要dirtytrue则会继续进行脏检查确保所有数据达到稳定状态,这样在listener里对data的变化也能立即生效。但是也要做一个次数限制防止过深的循环检查。

完整流程

整个完整的流程如下:

  • 对实例化传入的options进行处理
  • 模板编译,编译过程中没解析到一个数据绑定,就添加相应的观察者,对事件绑定进行处理
  • 编译完成后,触发一次脏检查
  • 当特定场景时(主要是UI事件触发和手动触发脏检查),进入脏检查周期
  • 每个脏检查周期都会有两次以上的脏检查,直到没有数据发生变化
  • 脏检查中,每个观察者如果对应的数据发生变化则调用listenerview上绑定数据的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

存在的问题