從發佈訂閱到雙向數據綁定

前言

雙向數據綁定已是一個談爛的話題,若談及原理,想必你們都能提到個defineProperty。可是,對於如何完整地實現一個雙向數據綁定僞代碼,我想大概不少人都沒有去深究。因而,本文藉着梳理髮布訂閱模式由淺到深地實現一下雙向數據綁定。node

發佈訂閱模式

雙向數據綁定的底層設計模式爲:發佈訂閱模式。
  • 發佈訂閱模式也稱爲觀察者模式
  • 觀察者同時訂閱一個對象,當對象發生改變時,每個觀察者都能接收到通知

通俗舉例

舉一個最通俗的🌰:小紅、小明、小白同時關注拼多多的AJ1,當AJ1一降價,三我的都能接到通知。設計模式

class Pinduoduo {
  constructor() {
    // 訂閱者
    this.subscribers = [];
  }
  // 訂閱方法
  subscribe({name, callback}) {
    if (~this.subscribers.indexOf(name)) return;
    this.subscribers.push({
      name, callback
    });
  }
  // 發佈降價消息
  publish() {
    this.subscribers.forEach(({name, callback}) => {
      let prize = 666;
      if (name === '小明') prize = 100;
      callback && callback(name, prize);
    })
  }
}


const pinInstance = new Pinduoduo();
const commonFn = (name, prize) => {
  console.log(`${name}接收到了降價信息,AJ1如今的價格是${prize}`)
}


// 訂閱
pinInstance.subscribe({
  name: '小紅',
  callback: commonFn
});
pinInstance.subscribe({
  name: '小明',
  callback: commonFn
});
pinInstance.subscribe({
  name: '小白',
  callback: commonFn
});


// 發佈
pinInstance.publish();
// 輸出
// 小紅接收到了降價信息,AJ1如今的價格是666
// 小明接收到了降價信息,AJ1如今的價格是100
// 小白接收到了降價信息,AJ1如今的價格是666

因此——記住實現發佈訂閱模式的兩個要點:
發佈(觸發) & 訂閱(監聽)緩存

EventEmitter

藉此咱們還能夠實現一下EventEmitter的僞代碼。app

function EventEmitter() {
    this.events = Object.create(null);
}

// 實現監聽方法
EventEmitter.prototype.on = (type, event) => {
    if (!this.events) this.events = Object.create(null);
    if (!this.events[type]) this.events[type] = [];
    this.events[type].push(event);
}

// 實現觸發方法
EventEmitter.prototype.emit = (type, ...args) => {
    if (!this.events[type]) return;
    this.events[type].forEach(event => {
        event.call(this, ...args);
    })
}

// 執行
function Girl() {}
// 實現繼承
Girl.prototype = Object.create(EventEmitter.prototype);
const lisa = new Girl();
lisa.on('逛街', () => {
    console.log('買買買!');
});

lisa.emit('逛街');

// console: 買買買!

深刻淺出雙向數據綁定

下述例子dom結構基於以下代碼dom

<div id="app">
    <input type="text" v-model="data">
    <p v-text="data"></p>
</div>

最最簡單的實現

不註釋了,相信你們都能看懂。性能

const inputDom = document.getElementsByTagName('input')[0];
const textDom = document.getElementsByTagName('p')[0];
inputDom.addEventListener('input', e => {
    const val = e.target.value;
    textDom.innerText = val;
});

defineProperty實現

一、極簡版this

const vm = {
    data: ''
};
const inputDom = document.getElementsByTagName('input')[0];
const textDom = document.getElementsByTagName('p')[0];

Object.defineProperty(vm, 'data', {
    set(newVal) {
        if (vm['data'] === newVal) return;
        // 同時觸發視圖更新
        textDom.innerText = newVal;
    }
});


inputDom.addEventListener('input', e => {
    vm.data = e.target.value;
});

二、進階版
假如咱們更換個屬性,或添加v-model上述代碼就不能複用了。咱們迭代一下,能夠適應多個v-model的狀況。prototype

首先梳理一下須要作什麼設計

  • 添加對象監聽器 Object.defineProperty
  • 添加訂閱者收集器,新增訂閱者和通知訂閱者更新
  • 遍歷Dom Treev-modelv-text進行解析。對v-model進行事件綁定監聽變化,對v-text添加訂閱者,訂閱vm變化實現視圖更新。
/*
 * 定義對象監聽
*/
const vm = {
    data: ''
};

function observe(obj) {
    Object.keys(obj).forEach(key => {
        let val = obj[key];
        Object.defineProperty(obj, key, {
            get() {
                return val;
            },
            set(newVal) {
                if (newVal === val) return;
                // 更新vm中的數據
                val = newVal;
            }
        })
    })
}
/*
 * 加入發佈訂閱模式
*/

const Dep = {
    target: null,
    subs: [],
    addSubs(sub) {
        this.subs.push(sub)
    },
    notify() {
        this.subs.forEach(sub => {
            sub.update();
        });
    }
}

在getter中,添加watcher
在setter觀測到數據變化時,觸發全部【訂閱者】更新code

// ...
get() {
    // 此時的target已經賦值成當前的watcher實例
    if (Dep.target) Dep.addSubs(Dep.target);
    return val;
},
set(newVal) {
    if (newVal === val) return;
    // 更新vm中的數據
    val = newVal;
    Dep.notify();
}
// ...

接下來定義【訂閱者】watcher,在本例中能夠理解成每個node節點

function Watcher(node, vm, name) {
    Dep.target = this;
    this.node = node;
    this.vm = vm;
    // name是綁定數據的key
    this.name = name;
    // 將watcher添加進dep中
    this.update();
    Dep.target = null;
}

// Watcher包含update方法和get方法
Watcher.prototype = {
    update() {
        this.get();
        this.node.innerText = this.value;
    },
    // 這裏主要是爲了觸發getter中Dep.addSub
    get() {
        this.value = this.vm[this.name];
    }
}

而後是對相應節點進行解析處理

function complie(node, vm) {
    if (node.nodeType === 1) {
        [...node.attributes].forEach(attr => {
            const name = attr.nodeValue;
            if (attr.nodeName === 'v-model') {
                node.addEventListener('input', e => {
                    vm[name] = e.target.value;
                })
            } else if (attr.nodeName === 'v-text') {
                new Watcher(node, vm, name)
            }
        })
    }
}

如今能夠對每一個節點進行綁定處理了

function MVVM(id, vm) {
    observe(vm);
    const node = document.getElementById(id);
    // 用fragment緩存節點,節約性能開支
    const fragment = document.createDocumentFragment();
    let child;
    while(child = node.firstChild) {
        fragment.appendChild(child)
    }
}

調用

MVVM(vm);

整個代碼初看起來會比較繞,但只要理解observecomplieDepWacther這幾個概念,相信就能基本看懂MVVM了。


(未完待續...待更新Proxy的MVVM實現方法)

相關文章
相關標籤/搜索