使用 Proxy 結合觀察者模式來實現數據雙向綁定

使用 ES6 的 Proxy 結合觀察者模式來實現數據雙向綁定,具體實現步驟大體分爲下面四個步驟html

  • 一、實現 Observer 觀察數據更新觸發更新
  • 二、實現 Compile 模板解析
  • 三、實現 Subscriber 收集訂閱
  • 四、實現 Watcher 觸發 Compile 中綁定的訂閱回調

本文代碼用 TypeScript 寫的,第一次用 Webpack + TS 本身搭項目,很爛,能看就將就看吧。。。node

一、實現 Observer 觀察數據更新

觀察數據更新,而後通知訂閱者更新視圖從而達到數據雙向綁定效果,Vue 使用了 Object.defineProperty() 這個方法來劫持了 VM 實例對象的屬性的讀寫,我這裏主要採用了 ES6 Proxy,實例化的時候 _initSet()data 的數據掛載到 this 上面, 再在把 Biub 傳給 Proxy 實例化一個對象掛在本身身上這樣後面的方法執行時直接 .call(this.proxy),這樣在傳進去的方法裏 this 的指向就是 Biub 自身了,接着執行了 Compile 模板解析完成了渲染。git

class Biub {
    $options: options;
    proxy: Biub;
    deps: {
        [name: string]: Dep
    }
    [propName: string]: any;
    constructor(options: options) {
        this.$options = options;
        this._initSet(); // 把數據掛到 this 上面
        this.deps = {};
        this.proxy = this.defineProxy(); 
        this.$compile = new Compile(options.el, this); // 解析模板
    }
    $watch(key: string, cb: Function) {
        new Watcher(this, key, cb);
    }
    defineProxy() {
        const deps = this.deps;
        return new Proxy(this, {
            get: function (target: Biub, key: string | number) {
                if (target[key]) {
                    if (!deps[key]) {
                        deps[key] = new Dep();
                    } else {
                        if (Dep.target) {
                            deps[key].depend();
                        } 
                    }
                    return target[key];
                }
                return Reflect.get(target, key);
            },
            set: function (target, key: any, value, receiver) {
                const keys = key.split('.');
                if (keys.length > 1) {
                    key = keys[0];
                }
                const dep = deps[key];
                dep && dep.notify(value);
                return Reflect.set(target, key, value);
            }
        })
    }
    _initSet() {
        const { methods, data } = this.$options
        Object.keys(data).forEach((key) => {
            this[key] = data[key];
        });
        Object.keys(methods).forEach((key) => {
            this[key] = methods[key];
        });
    }
    _initComputed() {
        // this.$options.computed && this.$options.computed.call(this.proxy); 
    }
}

複製代碼

二、實現 Compile 模板解析

Compile 主要作的事情是解析模板變量和指令,經過 compileElement遞歸 DOM 將模板中的變量替換成數據,並對 DOM 相應的指令函數,添加訂閱者,下面代碼不全,全文請戳 compile,這裏要感謝 DMQ 大神提供 compile subscribe 兩個模塊,我把大佬的 es5 改爲了 TypeScript 並簡化了模板編譯指令,由好幾個簡化成了兩個,哈哈哈!github

class Compile {
    // 省略。。。
    // 遞歸文檔節點
    compileElement(el: DocumentFragment | Node) {
        var childNodes = el.childNodes;
        childNodes.forEach((node: any) => {
            const text = node.textContent;
            const reg = /\{\{(.*)\}\}/;
            if (this.isElementNode(node)) {
                this.compile(node); // 解析指令
            } else if (this.isTextNode(node) && reg.test(text)) {
                this.compileText(node, RegExp.$1.trim()); // 解析模板變量
            }
            if (node.childNodes && node.childNodes.length) {
                this.compileElement(node);// 遞歸文檔節點
            }
        });
    }

    compile(node: any) {
        const nodeAttrs = node.attributes;
        for (const attr of nodeAttrs) {
            const attrName = attr.name;
            if (this.isDirective(attrName)) {
                const exp = attr.value;
                const dir = attrName.substring(2);
                if (this.isEventDirective(dir)) {                    
                    // 綁定事件指令
                    directives.eventHandler(node, this.$vm, exp, dir);
                } else {
                    // model指令
                    directives.model(node, this.$vm, exp);
                }
                node.removeAttribute(attrName);
            }
        }
    }
    // 省略。。。
}

複製代碼

三、實現 Subscriber 收集訂閱

Dep 經過 subs 數組收集訂閱者,當 proxy 發生數據變動時經過 notify() 通知訂閱更新視圖npm

let depid = 0;
class Dep {
    static target: Watcher | null = null;
    id: number;    
    subs: Array<Watcher>;
    constructor() {
        this.id = depid++;
        this.subs = [];
    }  
    addSub(watcher: Watcher) {
        this.subs.push(watcher);
    }
    depend() {
        if (Dep.target) {
            Dep.target.addDep(this);
        }
    }
    // 通知 Watcher 觸發回調
    notify(vm: Biub) {
        this.subs.forEach((watcher: Watcher) => {
            watcher.update(vm);
        });
    }
};

複製代碼

四、實現 Watcher

Compile 模板解析到指令或者變量的時候實例化一個 WatcherBiubdeps 對應 Dep 裏添加本身並訂閱了一個回調函數 updater(),這樣 proxy 觸發 set 時候就能根據 deps 對應 Dep 發送 notify() 並經過 forEach 遍歷全部訂閱者觸發自身的 update() 並而觸發 Compile 中綁定的回調函數 updater() 更新視圖,這裏很差理解的應該就是 updater() 這個回調方法和添加訂閱 getter 這個方法觸發的閉包在 Biub 產生 get 完成把本身添加進 Dep subs 操做把!數組

export default class Watcher {
    updater: Function;
    expOrFn: string | Function;
    depIds: { [key: number]: Dep };
    value: string | number | symbol;
    vm: Biub;
    getter: any;
    constructor(vm: Biub, expOrFn: string | Function, updater: Function) {
        this.updater = updater;
        this.vm = vm;
        this.expOrFn = expOrFn;
        this.depIds = {};
        if (typeof expOrFn === 'function') {
            this.getter = expOrFn;
        } else {
            this.getter = this.parseGetter(expOrFn.trim());
        }
        this.value = this.get();
    }
    
    update(val: any) {
        this.run(val);
    }
    run(val: any) {
        var oldVal = this.value;
        if (val !== oldVal) {
            this.value = val;
            // 觸發更新 compile 傳進來的
            this.updater.call(this.vm, val, oldVal);
        }
    }
    addDep(dep: Dep) {
        if (!this.depIds.hasOwnProperty(dep.id)) {
            dep.addSub(this);
            this.depIds[dep.id] = dep;
        }
    }
    get() {
        Dep.target = this;
        // 觸發 proxy get,添加訂閱
        const value = this.getter.call(this.vm, this.vm);
        Dep.target = null;
        return value;
    }
    parseGetter(exp: string ) {
        const exps = exp.split('.');
        return function () {
            var val = this.proxy;
            exps.forEach((k: any) => {
                val = val[k]
            });
            return val;
        }
    }
};
複製代碼

編譯一波看看效果

npm run dev
複製代碼
<div id="app">
    <input type="text" v-model="name">
    <input type="text" v-model="obj.kk.childName">
    <p>名字:{{ name }}</p>
    <p>Salary:{{salary}}</p>
    <p>深對象:{{obj.kk.childName}}</p>
    <button v-on:click="click">Add Salary</button>
</div>

複製代碼
let index = 10000;
const vm = new Biub({
    el: '#app',
    data: {
        salary: 10000,
        name: 'nancy',
        obj: {
            kk: {
                childName: 'pony',
            }
        }
    },
    methods: {
        click(e: Event) {
            this.salary = index += 1000;
        }
    }
});
vm.$watch('salary', function () {
    console.log(this.salary);
});

複製代碼

Biub
相關文章
相關標籤/搜索