第一章 變化偵測(1)

??我們想要一個數(shù)據發(fā)生改變時,與其相關的數(shù)據豹障、視圖模型自動發(fā)生變化冯事。首先要知道數(shù)值變化了。
??在Angular的方法是使用zone.js把如setTimeout血公、XHR桅咆、點擊事件等可能引起模型變化的異步操作用一個wrapFn包裹起來,每當有異步操作發(fā)生時Angular就知道數(shù)據可能變化了坞笙。再遍歷組件樹岩饼,通知組件進行變化檢測。若有變化則重新渲染頁面薛夜。
??而Vue采用的方式籍茧,則是利用Object.defineProperty定義setter,再確認數(shù)據變化后梯澜,通知相關的依賴寞冯。

觀察者模式

??先引入一個設計模式——觀察者模式。了解觀察者模式的話可以跳過直接看下文晚伙。我們假設有三種人:

  1. 好事者吮龄,他們對感興趣的事情很上心,發(fā)生了什么事情都想第一時間知道咆疗。
  2. 消息靈通的人漓帚,他們收集信息,提供給感興趣的好事者午磁。
  3. 觀察者尝抖,他們想狗仔隊一樣監(jiān)視著目標,一有發(fā)現(xiàn)就告訴消息靈通的人迅皇。


    觀察者模式

??通過這種消息傳遞的方式昧辽,使得觀察者和好事者解耦,觀察者只管觀察登颓,好事者只管八卦搅荞。

接下來我們來抄襲Vue實現(xiàn)變化偵測,Vue是基于觀察者模式實現(xiàn)的框咙。

1.觀察者

??在js中咕痛,有兩種方式可以獲取對象的變化:Object.definePropertyProxy扁耐。Object.defineProperty只能獲取對象屬性的讀取暇检,是ES5規(guī)范內容瀏覽器兼容性好。Proxy更強大婉称,可以攔截對對象的各種訪問块仆、改變,但是ES6內容所以兼容較差王暗。由于我們只是為了了解響應式框架的原理悔据,不是做實用輪子,所以我們采用Proxy的方式實現(xiàn)俗壹。
??為了獲取對象的變化科汗,我們對讀、寫绷雏、刪除進行攔截头滔。這個代理怖亭,就相當于一個觀察者,監(jiān)視著對象的一舉一動坤检。

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感興趣箭跳,它需要知道m(xù)essage的值是什么它才知道要渲染成怎樣的視圖晨另,即它依賴message了。

<span>{{ message }}</span>

??為此谱姓,我們要搞一個消息靈通的人來用于記錄及管理感興趣的好事者借尿。定義一個類Dep(dep for dependency):

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

    public subs : Array<any>;

    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){
            //發(fā)生變化時,通知感興趣的好事者
            sub.update();
        }
    }
}

??如果有人讀過一個object的屬性逝段,我們就認為這個人對這個object是感興趣的垛玻。那么當這個object發(fā)生變動時,我們就要通知這些感興趣的人奶躯。此時我們改造一下defineReactive方法:

function defineReactive(obj: any): any {
    const dep = new Dep();//創(chuàng)建一個依賴管理
    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.好事者

??好事者會對一件事表示感興趣账嚎,當?shù)玫竭@事的消息時會作出反應。舉個例子儡蔓,我們創(chuàng)建一個好事者郭蕉,他表示對蔡徐坤感興趣,而當他知道蔡徐坤開始打籃球時喂江,會大嚷大叫:

new Watcher('蔡徐坤', (status)=>{
  if(status === '打籃球'){
    console.log('蔡徐坤來打籃球啦U傩狻!');
  }
})

??為實現(xiàn)這樣的功能获询,可以寫出以下代碼:

class Watcher { 
    
    public cb : Function; //回調函數(shù)涨岁,這個人發(fā)現(xiàn)消息之后會做什么事情

    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對應的值,會觸發(fā)proxy中的get
        //進而將這個watcher添加到dep里 即讓消息靈通人的知道我感興趣
        this.value = this.get();
    }   

    public get(){
        Dep.target = this;//記錄自己吉嚣,用于讓上文中的dep知道好事者是誰
        const value = this.getter.call(this.vm, this.vm);//觸發(fā)了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
 * [^]是排除字符組 
 * 這個正則意思是 排除字母 數(shù)組 . $
 */
const bailRE = /[^\w.$]/;
/**
 * 將路徑字符串解析成對應的對象
 */
export function parsePath (path: string): any {
  if (bailRE.test(path)) {//即如果路徑包含字母 數(shù)字 . $ 以外字符梢薪,為非法路徑
    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

??到這里,整個流程已經完成了尝哆。但現(xiàn)在defineReactive只攔截了對象的屬性秉撇。但當對象的屬性的屬性發(fā)生變化時,是偵測不到的。例如下面這種情況:

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

a.b.c = 'world';

我們可以定義一個Observer琐馆,創(chuàng)建觀察者來觀察傳入的值规阀。并遍歷傳入值的子屬性,將他們的行為都攔截下來:

class Observer {

    public value : any;
    public dep : Dep;

    constructor(value : any){
        this.value = value;
        this.dep = new Dep();
        
        def(value, '__ob__', this);//將value和observer關聯(lián)起來

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

}

export function observe(value: any): any{
    //如果這個值已經被觀察了啡捶,就無需再新建Observer 防止循環(huán)嵌套對象無限遞歸
    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

??最后,我們利用上面做好的這套東西瞎暑,實現(xiàn)一個不完整的vm.$watch。首先与帆,我們會用 new ViewModel({data : {}})了赌,這樣的方式創(chuàng)建一個vm對象,并將data加載到vm上玄糟。

class ViewModel{

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

    public $options : any;

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

??我們希望勿她,可以通過vm.key這種方式來訪問到vm._data.key。同理用Proxy來實現(xiàn):

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的值可以被監(jiān)控郭卫。組合以上代碼可以得到:

class ViewModel{

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

    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變?yōu)轫憫降?
    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);
        }
    })
}

現(xiàn)在我們好像已經完成一個簡單的變化偵測了砍聊。但如果執(zhí)行代碼,會發(fā)生什么事情呢贰军?程序會進行一次正確打印之后無限打印'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!';

??這一節(jié)完整的代碼在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屬性來滿足這種需求。但由于我們采用代理的方式實現(xiàn)舵盈,這些行為都能被攔截陋率,則不需要另外添加兩個方法來實現(xiàn)需求了。
老規(guī)矩上測試代碼:

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;
        })
    }
})
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末书释,一起剝皮案震驚了整個濱河市翘贮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌爆惧,老刑警劉巖狸页,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡芍耘,警方通過查閱死者的電腦和手機址遇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來斋竞,“玉大人倔约,你說我怎么就攤上這事“映酰” “怎么了浸剩?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鳄袍。 經常有香客問我绢要,道長,這世上最難降的妖魔是什么拗小? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任重罪,我火速辦了婚禮,結果婚禮上哀九,老公的妹妹穿的比我還像新娘剿配。我一直安慰自己,他們只是感情好阅束,可當我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布呼胚。 她就那樣靜靜地躺著,像睡著了一般围俘。 火紅的嫁衣襯著肌膚如雪砸讳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天界牡,我揣著相機與錄音簿寂,去河邊找鬼。 笑死宿亡,一個胖子當著我的面吹牛常遂,可吹牛的內容都是我干的。 我是一名探鬼主播挽荠,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼克胳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了圈匆?” 一聲冷哼從身側響起漠另,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎跃赚,沒想到半個月后笆搓,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體性湿,經...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年满败,在試婚紗的時候發(fā)現(xiàn)自己被綠了肤频。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡算墨,死狀恐怖宵荒,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情净嘀,我是刑警寧澤报咳,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站挖藏,受9級特大地震影響少孝,放射性物質發(fā)生泄漏。R本人自食惡果不足惜熬苍,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望袁翁。 院中可真熱鬧柴底,春花似錦、人聲如沸粱胜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽焙压。三九已至鸿脓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間涯曲,已是汗流浹背野哭。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留幻件,地道東北人拨黔。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像绰沥,于是被迫代替她去往敵國和親篱蝇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內容

  • 最近總會忘記寫東西徽曲,看來還是不夠重視零截。周六打算練一下計算機的題目,周日又要去超級數(shù)學聲當工作人員秃臣,部門打算期末考完...
    數(shù)學是我的命閱讀 154評論 0 0
  • 本次選擇的分析的產品是Faceu和B612涧衙,兩者在功能方面高度重合,下面主要從產品框架、頁面布局绍撞、流程操作正勒、交互細...
    早羽說閱讀 1,188評論 0 6
  • dd魔武雙修并沒有什么沖突。 一些才華橫溢同時功法適合的人也可以同時進行冥想和打坐修煉傻铣。 能量積累上基本沒有影響章贞。...
    時光勿念閱讀 1,006評論 0 0
  • 閉上雙眼 聆聽春的腳步 草坪上 三兩鳥兒 從容優(yōu)雅 在尋覓 也在播種 振翅聲時遠時近 縱情高飛擁抱藍天 肆意低旋親...
    觀山觀水閱讀 711評論 5 22