Vue原始碼分析之資料驅動

Khazix發表於2020-08-21

響應式特點

  • 資料響應式

修改資料時,檢視自動更新,避免繁瑣Dom操作,提高開發效率

  • 雙向繫結

資料改變,檢視隨之改變。檢視改變,資料隨之改變

  • 資料驅動

開發時僅需要關注資料本身,不需要關心資料如何渲染到檢視

官方教程: https://cn.vuejs.org/v2/guide/reactivity.html
MDN: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

vue 2.x 基於 defineProperty 實現資料捕捉

當你把一個普通的 JavaScript 物件傳入 Vue 例項作為 data 選項,Vue 將遍歷此物件所有的 property,並使用 Object.defineProperty 把這些 property 全部轉為 getter/setter。
Object.defineProperty 是 ES5 中一個無法 shim 的特性,這也就是 Vue 不支援 IE8 以及更低版本瀏覽器的原因。

下面是一段模仿 vue 實現資料捕捉的程式碼

interface Vue{
    data: {
        [prop: string]: any
    }
    [prop: string]: any
}

let vm: Vue = {
    data: {
        name: 'Tom',
        age: 22,
    },
}

//資料劫持
function proxyData(vm: Vue){
    Object.keys(vm.data).forEach(key => {
        console.log(key, vm.data[key])
        vm[key] = vm.data[key];
        Object.defineProperty(vm, key, {
            enumerable: true,   //可列舉
            configurable: true, //可配置:刪除或重定義
            get(){
                console.log('getter:', vm.data[key]);
                return vm.data[key];
            },
            set(newVal){
                console.log('setter', newVal);
                if (newVal === vm.data[key]){
                    return;
                }
                vm.data[key] = newVal;
                document.querySelector('#app')!.textContent = vm.data[key];
            }
        })
    })
}

proxyData(vm);
vm.name = 'karolina'; //模擬資料發生改變,檢視改變
console.log(vm);
// {
//     name: "karolina"
//     age: 33
//     data:{
//         name: "karolina"
//         age: 33
//     }
// }

Vue 3.x 基於 Proxy 代理捕捉資料

MDN: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy

ES6 提供 Proxy 捕捉器, 相比於 Object.defineProperty 代理整個物件而非屬性,程式碼上更簡潔,效能上由瀏覽器優化更快

同樣下面是一段模仿 vue 實現資料捕捉的程式碼

let data ={
    name: 'Tom',
    age: 22,
};

let vm = new Proxy(data, {
    get(target: any, key){
        if (key in target){
            console.log('getter: ',key, target[key]);
            return target[key];
        }
    },
    set(target: any, key, newVal,){
        console.log('setter: ',key, target[key]);
        if (target[key] === newVal){
            return false;
        }
        target[key] = newVal;
        document.querySelector('#app')!.textContent = target[key];
        return true;
    },
})

vm.name = 'Karolina';
console.log(vm);
console.log(vm.age);

釋出訂閱模式

在“釋出者-訂閱者”模式中,稱為釋出者的訊息傳送者不會將訊息程式設計為直接傳送給稱為訂閱者的特定接收者。
這意味著釋出者和訂閱者不知道彼此的存在。存在第三個元件,稱為代理或訊息代理或事件匯流排,它由釋出者和訂閱者都知道,它過濾所有傳入的訊息並相應地分發它們。
換句話說,pub-sub是用於在不同系統元件之間傳遞訊息的模式,而這些元件不知道關於彼此身份的任何資訊。經紀人如何過濾所有訊息?實際上,有幾個訊息過濾過程。最常用的方法有:基於主題和基於內容的。

  • 訂閱者(subscriber)需要在 事件中心 註冊事件
  • 釋出者(publisher)需要於 事件中心 觸發事件
  • 訂閱者和釋出者無需知道對方身份

Vue中的釋出訂閱模式

https://cn.vuejs.org/v2/guide/migration.html#dispatch-和-broadcast-替換

下面是Vue的釋出訂閱虛擬碼,用於兄弟元件之間通訊

//事件中心
let eventHub = new Vue(); 

//ComponetA.vue 訂閱者
willDo: function(){
    eventHub.$on('will-do', (text)=>{console.log(text)});
}

//ComponetB.vue 釋出者
willDo: function(){
    eventHub.$emit('will-do', {text: 'Hello'});
}

下面手寫程式碼來模擬Vue的釋出訂閱模式實現

//儲存主題和控制程式碼,主題為事件名,控制程式碼為hanlder
interface ITopicMap {
    [prop: string]: Array<Function>,
}

//事件中心,封裝訂閱和釋出事件
class EventCenter {
    public topicMap:ITopicMap = {};

    $on(topic: string, handler: Function): void{
        this.topicMap[topic] = this.topicMap[topic] || [];
        this.topicMap[topic].push(handler);
    }

    $emit(topic: string, ...params: any){
        if (topic in this.topicMap){
            this.topicMap[topic].forEach((handler)=>{
                handler(...params);
            })
        }
    }
}

//測試
let hub: EventCenter  = new EventCenter();

//訂閱者註冊事件
hub.$on('click',  ()=>{console.log('you click me')});
hub.$on('custom', (name: string, age: 12)=>{console.log(`your name is ${name}, and age is ${age}`)});

//釋出者觸發事件
hub.$emit('click');              //you click me
hub.$emit('custom', 'Tom', 22);  //your name is Tom, and age is 22

Vue中的觀察者模式

當物件間存在一對多關係時,則使用觀察者模式(Observer Pattern)。比如,當一個物件被修改時,則會自動通知依賴它的物件。觀察者模式屬於行為型模式。

  • 觀察者Watcher Observer通過update()描述當事件發生時需要做的事情

  • 目標Dep subject需要新增認識觀察者,通過notify()通知觸發觀察者事件

  • 沒有事件中心,觀察者和目標需要知道對方身份,抽象耦合

  • 手寫程式碼模擬vue中觀察者模式實現


class Observer {
    constructor(public update: Function){}
}

class Dep {
    public observerList: Array<Observer> = [];

    addObserver(observer: Observer){
        this.observerList.push(observer);
    }

    notify(...params: any){
        this.observerList.forEach(observer => {
            observer.update(...params);
        })
    }
}

//建立事件目標
let dep = new Dep();
let observer = new Observer(
    (name:string)=>{console.log(`my name is ${name}`)}
);

//目標新增觀察者物件
dep.addObserver(observer);

//事件觸發通知
dep.notify('Tim'); //my name is Tim

老生常談的 觀察者模式釋出訂閱模式 區別

  1. 在觀察者模式中,主體維護觀察者列表,因此主體知道當狀態發生變化時如何通知觀察者。然而,在釋出者/訂閱者中,釋出者和訂閱者不需要相互瞭解。它們只需在中間層訊息代理(或訊息佇列)的幫助下進行通訊。
  2. 在釋出者/訂閱者模式中,元件與觀察者模式完全分離。在觀察者模式中,主題和觀察者鬆散耦合。
  3. 觀察者模式主要是以同步方式實現的,即當發生某些事件時,主題呼叫其所有觀察者的適當方法。釋出伺服器/訂閱伺服器模式主要以非同步方式實現(使用訊息佇列)。
  4. 釋出者/訂閱者模式更像是一種跨應用程式模式。釋出伺服器和訂閱伺服器可以駐留在兩個不同的應用程式中。它們中的每一個都通過訊息代理或訊息佇列進行通訊。

其他行為模式參考

其他行為模式學習網站: https://www.runoob.com/design-pattern/observer-pattern.html

相關文章