Vue源碼分析之數據驅動

響應式特點

  • 數據響應式

修改數據時,視圖自動更新,避免繁瑣Dom操作,提高開發效率

  • 雙向綁定

數據改變,視圖隨之改變。視圖改變,數據隨之改變

  • 數據驅動

開發時僅需要關注數據本身,不需要關心數據如何渲染到視圖

官方教程: //cn.vuejs.org/v2/guide/reactivity.html
MDN: //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: //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中的發布訂閱模式

//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. 發布者/訂閱者模式更像是一種跨應用程式模式。發布伺服器和訂閱伺服器可以駐留在兩個不同的應用程式中。它們中的每一個都通過消息代理或消息隊列進行通訊。

其他行為模式參考

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

Tags: