第一章 变化侦测(1)

  我们想要一个数据发生改变时,与其相关的数据、视图模型自动发生变化。首先要知道数值变化了。
  在Angular的方法是使用zone.js把如setTimeout、XHR、点击事件等可能引起模型变化的异步操作用一个wrapFn包裹起来,每当有异步操作发生时Angular就知道数据可能变化了。再遍历组件树,通知组件进行变化检测。若有变化则重新渲染页面。
  而Vue采用的方式,则是利用Object.defineProperty定义setter,再确认数据变化后,通知相关的依赖。

观察者模式

  先引入一个设计模式——观察者模式。了解观察者模式的话可以跳过直接看下文。我们假设有三种人:

  1. 好事者,他们对感兴趣的事情很上心,发生了什么事情都想第一时间知道。
  2. 消息灵通的人,他们收集信息,提供给感兴趣的好事者。
  3. 观察者,他们想狗仔队一样监视着目标,一有发现就告诉消息灵通的人。


    观察者模式

  通过这种消息传递的方式,使得观察者和好事者解耦,观察者只管观察,好事者只管八卦。

接下来我们来抄袭Vue实现变化侦测,Vue是基于观察者模式实现的。

1.观察者

  在js中,有两种方式可以获取对象的变化:Object.definePropertyProxy。Object.defineProperty只能获取对象属性的读取,是ES5规范内容浏览器兼容性好。Proxy更强大,可以拦截对对象的各种访问、改变,但是ES6内容所以兼容较差。由于我们只是为了了解响应式框架的原理,不是做实用轮子,所以我们采用Proxy的方式实现。
  为了获取对象的变化,我们对读、写、删除进行拦截。这个代理,就相当于一个观察者,监视着对象的一举一动。

function defineReactive(obj: any): any {
    return new Proxy(obj, {
        get: function (target, property, receiver) {
            console.log('属性被读');
            return Reflect.get(target, property, receiver);
        },
        set: function (obj, prop: (keyof Object), value, receiver) {
            if (value === obj[prop]) {
                return false;
            }
            console.log('属性被修改');
            return Reflect.set(obj, prop, value, receiver);
        },
        deleteProperty: function (target: any, p: string | number | symbol) {
            console.log('属性被删除');
            return Reflect.deleteProperty(target, p);
        }
    })
}
2.消息灵通的人

  我们通过打印得知了对象的变化,但这并没什么卵用。我们需要谁对它感兴趣。例如,我们有如下模板时,这个视图模型就对message感兴趣,它需要知道message的值是什么它才知道要渲染成怎样的视图,即它依赖message了。

{{ message }}

  为此,我们要搞一个消息灵通的人来用于记录及管理感兴趣的好事者。定义一个类Dep(dep for dependency):

export class Dep {
    static target;//用来存放好事者

    public subs : Array;

    constructor (){
        this.subs = [];
    }   

    public addSub(sub){
        this.subs.push(sub);
    }

    public removeSub(sub){
        //有一个好事者说不感兴趣了
    }

    public depend(){
        //假设我们用target这个全局变量存放一个好事者
        //我们把它添加到感兴趣的人群里
        this.addSub(Dep.target);
    }

    public notify(){
        for(let sub of this.subs){
            //发生变化时,通知感兴趣的好事者
            sub.update();
        }
    }
}

  如果有人读过一个object的属性,我们就认为这个人对这个object是感兴趣的。那么当这个object发生变动时,我们就要通知这些感兴趣的人。此时我们改造一下defineReactive方法:

function defineReactive(obj: any): any {
    const dep = new Dep();//创建一个依赖管理
    return new Proxy(obj, {
        get: function (target, property, receiver) {
            dep.depend();//告诉dep,有人感兴趣
            return Reflect.get(target, property, receiver);
        },
        set: function (obj, prop: (keyof Object), value, receiver) {
            if (value === obj[prop]) {
                return false;
            }
            dep.notify();//让dep通知感兴趣的人,有值被改了
            return Reflect.set(obj, prop, value, receiver);
        },
        deleteProperty: function (target: any, p: string | number | symbol) {
            dep.notify();//让dep通知感兴趣的人,有值被删除了
            return Reflect.deleteProperty(target, p);
        }
    })
}
3.好事者

  好事者会对一件事表示感兴趣,当得到这事的消息时会作出反应。举个例子,我们创建一个好事者,他表示对蔡徐坤感兴趣,而当他知道蔡徐坤开始打篮球时,会大嚷大叫:

new Watcher('蔡徐坤', (status)=>{
  if(status === '打篮球'){
    console.log('蔡徐坤来打篮球啦!!');
  }
})

  为实现这样的功能,可以写出以下代码:

class Watcher { 
    
    public cb : Function; //回调函数,这个人发现消息之后会做什么事情

    public vm : ViewModel;
    private getter: Function;//用来获取感兴趣的消息
    private value: any;//消息

    constructor (
        expOrFn : string | Function, 
        cb : Function
    ){
        this.cb = cb;
        if(typeof expOrFn === 'function'){
            this.getter = expOrFn;
        }else{
            this.getter = parsePath(expOrFn);
        }

        //get()方法会去访问expOrFn对应的值,会触发proxy中的get
        //进而将这个watcher添加到dep里 即让消息灵通人的知道我感兴趣
        this.value = this.get();
    }   

    public get(){
        Dep.target = this;//记录自己,用于让上文中的dep知道好事者是谁
        const value = this.getter.call(this.vm, this.vm);//触发了proxy的get!
        Dep.target = undefined;
        return value;
    }

    public update(){
        const oldValue = this.value;
        this.value = this.get();
        this.cb.call(this.vm, this.value, oldValue);
    }
}

/**
 * \w为 a-z A-Z 0-9
 * [^]是排除字符组 
 * 这个正则意思是 排除字母 数组 . $
 */
const bailRE = /[^\w.$]/;
/**
 * 将路径字符串解析成对应的对象
 */
export function parsePath (path: string): any {
  if (bailRE.test(path)) {//即如果路径包含字母 数字 . $ 以外字符,为非法路径
    return
  }
  const segments = path.split('.')
  return function (obj : any) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}
Observer

  到这里,整个流程已经完成了。但现在defineReactive只拦截了对象的属性。但当对象的属性的属性发生变化时,是侦测不到的。例如下面这种情况:

let a = {
  b : {
    c : 'hello'
  }
};

a.b.c = 'world';

我们可以定义一个Observer,创建观察者来观察传入的值。并遍历传入值的子属性,将他们的行为都拦截下来:

class Observer {

    public value : any;
    public dep : Dep;

    constructor(value : any){
        this.value = value;
        this.dep = new Dep();
        
        def(value, '__ob__', this);//将value和observer关联起来

        if(!Array.isArray(value)){
            this.value = defineReactive(value);
        }
    }

}

export function observe(value: any): any{
    //如果这个值已经被观察了,就无需再新建Observer 防止循环嵌套对象无限递归
    if(hasOwn(value, '__ob__') && value.__ob__ instanceof Observer){
        return;
    }else{
        return (new Observer(value)).value;
    }
}

export function defineReactive(obj: any ): any{
    const dep = new Dep();

    const keys = Object.keys(obj);
    for(let key of keys){
        if(typeof obj[key] === 'object'){
            //如果子属性是对象,我们需要递归添加代理
            obj[key] = observe(obj[key]);
        }
    }

    return new Proxy(obj, {
        get: function (target, property, receiver) {
            dep.depend();
            return Reflect.get(target, property, receiver);
        },
        set: function(obj, prop: (keyof Object), value, receiver){
            if(value === obj[prop]){//值无变化
                return false;
            }
            const result = Reflect.set(obj, prop, value, receiver);
            dep.notify();
            return result;
        },
        deleteProperty: function(target: any, p: string | number | symbol){
            return Reflect.deleteProperty(target, p);
        }
    });
}

我们可以写一个测试代码测试一下:

test('observe a object', () => {
    const obj = {
        a : "123",
        b : {
            test : {
                text : "hello"
            }
        }
    }
    new Observer(obj);

    expect(hasOwn(obj, '__ob__')).toBe(true);
    expect(hasOwn(obj.b, '__ob__')).toBe(true);
    expect(hasOwn(obj.b.test, '__ob__')).toBe(true);
});
vm.$watch

  最后,我们利用上面做好的这套东西,实现一个不完整的vm.$watch。首先,我们会用 new ViewModel({data : {}}),这样的方式创建一个vm对象,并将data加载到vm上。

class ViewModel{

    public _data : Object = {};
    public _watchers : Array = [];

    public $options : any;

    constructor(options: any){
        this.$options = options;
        this._data = this.$options.data;
    }
}

  我们希望,可以通过vm.key这种方式来访问到vm._data.key。同理用Proxy来实现:

new Proxy(vm, {
        get: function (target, property, receiver) {
            if( property in target._data){//如果_data里有同名的属性,则读取_data里的值
                return Reflect.get(target._data, property, receiver);
            }
            return Reflect.get(target, property, receiver);
        },
        set: function(target, property: (keyof Object), value, receiver){
            if( property in target._data){
                return Reflect.set(target._data, property, value);
            }
            return Reflect.set(target, property, value, receiver);
        },
        deleteProperty: function(target, property){
            if( property in target._data){
                return Reflect.deleteProperty(target._data, property);
            }
            return Reflect.deleteProperty(target, property);
        }
    })

  最后,我们希望data的值是响应式的,且vm提供$watch方法使得data的值可以被监控。组合以上代码可以得到:

class ViewModel{

    public _uid : number;
    public _data : Object = {};
    public _watchers : Array = [];

    public $options : any;

    constructor(options: any){
        this._uid = _vmUid++;
        this.$options = options;
        return initState(this);
    }

    public $watch(expOrFn : string | Function, cb : Function){
        const watcher = new Watcher(this, expOrFn, cb);
        this._watchers.push(watcher);
    }
}

export function initState(vm: ViewModel) {
    const opts = vm.$options;
    if(opts.data){
        vm = initData(vm);
    }
    return vm;
}

function initData(vm: ViewModel) {
    let data = vm.$options.data;

    vm._data = defineReactive(data);//将data变为响应式的

    return new Proxy(vm, {
        get: function (target, property, receiver) {
            if( property in target._data){
                return Reflect.get(target._data, property, receiver);
            }
            return Reflect.get(target, property, receiver);
        },
        set: function(target, property: (keyof Object), value, receiver){
            if( property in target._data){
                return Reflect.set(target._data, property, value);
            }
            return Reflect.set(target, property, value, receiver);
        },
        deleteProperty: function(target, property){
            if( property in target._data){
                return Reflect.deleteProperty(target._data, property);
            }
            return Reflect.deleteProperty(target, property);
        }
    })
}

现在我们好像已经完成一个简单的变化侦测了。但如果执行代码,会发生什么事情呢?程序会进行一次正确打印之后无限打印'text changed!'!思考一下为什么。

const vm = new ViewModel({
    data: {
        text: 'hello world!'
    }
});

vm.$watch('text',(value : any, oldValue : any)=>{    
    console.log(value);    
    console.log(oldValue);    
});

(vm as any)['text'] = 'text changed!';

  这一节完整的代码在github 可以看到哦。
  最后的最后,编写测试代码验证结果:

test('watch', async ()=>{
    const vm = new ViewModel({
        data: {
            text: 'hello world!'
        }
    });

    const result = await watchChanged() as any;
    
    expect(result.oldValue).toBe('hello world!');
    expect(result.value).toBe('text changed!');

    function watchChanged(){
        return new Promise((resolve)=>{            
            vm.$watch('text',(value : any, oldValue : any)=>{
                console.log(value, oldValue);
                resolve({
                    value,
                    oldValue
                })
            });
            
            (vm as any)['text'] = 'text changed!';
        })
    }
})
vm.$set、vm.$delete

  由于Vue采用的Object.defineProperty对属性进行读写的拦截。所以它不能侦测到属性的删除以及data添加新属性。所以Vue提供了set和delete属性来满足这种需求。但由于我们采用代理的方式实现,这些行为都能被拦截,则不需要另外添加两个方法来实现需求了。
老规矩上测试代码:

test('watch add property', async ()=>{
    const vm = new ViewModel({
        data: {
            message : {}
        }
    });

    const result = await watchChanged() as any;
    
    expect(result.oldValue).toBe(undefined);
    expect(result.value).toBe('hello!');

    function watchChanged(){
        return new Promise((resolve)=>{            
            vm.$watch('message.a',(value : any, oldValue : any)=>{
                resolve({
                    value,
                    oldValue
                })
            });
            
            (vm as any).message.a = 'hello!';
        })
    }
})

test('watch delete property', async ()=>{
    const vm = new ViewModel({
        data: {
            message : {
                a : 'hello!'
            }
        }
    });

    const result = await watchChanged() as any;
    
    expect(result.oldValue).toBe('hello!');
    expect(result.value).toBe(undefined);

    function watchChanged(){
        return new Promise((resolve)=>{            
            vm.$watch('message.a',(value : any, oldValue : any)=>{
                resolve({
                    value,
                    oldValue
                })
            });
            
            delete (vm as any).message.a;
        })
    }
})

你可能感兴趣的:(第一章 变化侦测(1))