vue雙向綁定原理及實(shí)現(xiàn)

vue是采用數(shù)據(jù)劫持配合發(fā)布者-訂閱模式的方式,通過(guò)Object.definePropery()來(lái)劫持各個(gè)屬性的settergetter,在數(shù)據(jù)變動(dòng)時(shí)發(fā)布消息給消息訂閱器-Dep,通知訂閱者-Watcher,觸發(fā)相應(yīng)回調(diào)函數(shù),去更新視圖。

vue創(chuàng)建實(shí)例的時(shí)候,MVVM作為綁定的入口,整合Observer, CompileWatcher三種,通過(guò)Observer來(lái)監(jiān)聽(tīng)model數(shù)據(jù)變化,通過(guò)Compile解析編譯模板指令,最終利用Watcher搭起了Observer, Compile之前的通信橋梁,達(dá)到數(shù)據(jù)變化=>視圖更新;視圖交互變化=>數(shù)據(jù)model變更的雙向綁定效果。

數(shù)據(jù)雙向綁定流程圖

實(shí)現(xiàn) Observer

  • Observer是一個(gè)數(shù)據(jù)監(jiān)聽(tīng)器,用來(lái)劫持監(jiān)聽(tīng)所有屬性,如果有變動(dòng),就通知訂閱者
  • 核心方法是用Object.defineProperty(),遞歸遍歷所有屬性,給每個(gè)屬性加上settergetter,當(dāng)給對(duì)象的某個(gè)屬性賦值,就會(huì)觸發(fā) setter, 那么就能監(jiān)聽(tīng)到了數(shù)據(jù)變化。

怎么通知訂閱者?消息訂閱器(Dep)-調(diào)度中心

  • 需要實(shí)現(xiàn)一個(gè)消息訂閱器-Dep,來(lái)收集所有訂閱者-Watcher
  • Observer中植入消息訂閱器-Dep
  • 數(shù)據(jù)變動(dòng)觸發(fā)Dep的notify,再調(diào)用訂閱者的update方法
  • 訂閱器Depwatcher的方法放在Observer的getter里面(原因看watcher實(shí)現(xiàn))

如下代碼,實(shí)現(xiàn)了一個(gè)Observer

//實(shí)現(xiàn)一個(gè)`Observer`對(duì)象
class Observer{
    constructor(data){
        this.observe(data);
    }
    // data是一個(gè)對(duì)象,可能嵌套其它對(duì)象,需要采用遞歸遍歷的方式進(jìn)行觀察者綁定
    observe(data){
        if(data && typeof data === 'object'){
            Object.keys(data).forEach(key =>{
                this.defineReactive(data, key, data[key]);
            })
        }
    }
    // 通過(guò) object.defineProperty方法對(duì)對(duì)象屬性進(jìn)行劫持
    defineReactive(obj, key, value){
        // 遞歸觀察
        this.observe(value);
        const dep = new Dep();
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: false,
            get(){
                //初始化
                if (Dep.target) {// 判斷是否需要添加訂閱者
                    dep.addWatcher(Dep.target); // 在這里添加一個(gè)訂閱者
                }
                return value;
            },
            // 采用箭頭函數(shù)在定義時(shí)綁定this的定義域
            set: (newVal)=>{
                if(newVal !== value){
                    console.log('哈哈哈,監(jiān)聽(tīng)到值變化了 ', val, ' --> ', newVal);
                    this.observe(newVal);
                    value = newVal;
                    dep.notify(); // 如果數(shù)據(jù)變化,通知所有訂閱者
                }
            }
        })
    }
}

// Dep類存儲(chǔ)watcher對(duì)象,并在數(shù)據(jù)變化時(shí)通知訂閱者
class Dep{
    constructor(){
        this.watcherCollector = [];
    }
    // 添加watcher
    addWatcher(watcher){
        console.log('觀察者', this.watcherCollector);
        this.watcherCollector.push(watcher);
    }
    // 數(shù)據(jù)變化時(shí)通知watcher更新
    notify(){
        this.watcherCollector.forEach(w=>w.update());
    }
}

實(shí)現(xiàn)Watcher

訂閱者Watcher在初始化的時(shí)候需要將自己添加進(jìn)訂閱器Dep中,那該如何添加呢?

  1. 在監(jiān)聽(tīng)器Observergetter函數(shù)中執(zhí)行添加訂閱者Watcher的操作
  2. 只要在訂閱者Watcher初始化的時(shí)候觸發(fā)對(duì)應(yīng)的getter函數(shù)去執(zhí)行添加訂閱者操作即可(只要獲取對(duì)應(yīng)的屬性值就可以觸發(fā)了)
  3. 只有在訂閱者Watcher初始化的時(shí)候才需要添加訂閱者,所以需要做一個(gè)判斷操作,因此可以在訂閱器上做一下手腳:在Dep.target上緩存下訂閱者,添加成功后再將其去掉就可以了

訂閱者Watcher的實(shí)現(xiàn)如下:

class Watcher{
    // 通過(guò)回調(diào)函數(shù)實(shí)現(xiàn)更新的數(shù)據(jù)通知到視圖
    constructor(expr, vm, cb){
        this.expr = expr;
        this.vm = vm;
        this.cb = cb;
        this.oldVal = this.getOldVal();
    }
    // 獲取舊數(shù)據(jù)
    getOldVal(){
        // 在利用getValue獲取數(shù)據(jù)調(diào)用getter()方法時(shí)先把當(dāng)前觀察者掛載
        Dep.target = this;// 緩存自己
        const oldVal = compileUtil.getValue(this.expr, this.vm);
        // 掛載完畢需要注銷(xiāo),防止重復(fù)掛載 (數(shù)據(jù)一更新就會(huì)掛載)
        Dep.target = null;
        return oldVal;
    }
    // 通過(guò)回調(diào)函數(shù)更新數(shù)據(jù)
    update(){
        const newVal = compileUtil.getValue(this.expr, this.vm);
        if(newVal !== this.oldVal){
            this.cb(newVal);
        }
    }
}

至此,監(jiān)聽(tīng)器-Observer、消息訂閱器-Dep、訂閱者-Watcher的實(shí)現(xiàn),已經(jīng)具備了監(jiān)聽(tīng)數(shù)據(jù)和數(shù)據(jù)變化通知訂閱者的功能。那么接下來(lái)就是實(shí)現(xiàn)Compile了。

實(shí)現(xiàn)Compile

  • 指令解析器:解析模板指令,并替換模板數(shù)據(jù),初始化視圖
  • 在編譯工具中綁定Watcher:將每個(gè)指令對(duì)應(yīng)的節(jié)點(diǎn)綁定更新函數(shù),添加監(jiān)聽(tīng)數(shù)據(jù)的訂閱者,一旦數(shù)據(jù)有變動(dòng),收到通知,更新視圖,如圖所示:
//Complier編譯類設(shè)計(jì)

const compileUtil = {
    getValue(expr, vm){
        // 處理 person.name 這種對(duì)象類型,取出真正的value
        return expr.split('.').reduce((data,currentVal)=>{
            return data[currentVal];
        }, vm.$data)
    },
    setVal(expr, vm, inputValue){
        expr.split('.').reduce((data,currentVal)=>{
            data[currentVal] = inputValue;
        }, vm.$data)
    },
    text(node, expr, vm) {
        let value;
        if(expr.indexOf('{{')!==-1){
            value = expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
                 // text的 Watcher應(yīng)在此綁定,因?yàn)槭菍?duì)插值{{}}進(jìn)行雙向綁定
                // Watcher的構(gòu)造函數(shù)的 getOldVal()方法需要接受數(shù)據(jù)或者對(duì)象,而{{person.name}}不能接收
                new Watcher(args[1], vm, ()=>{
                    this.updater.textUpdater(node, this.getContent(expr, vm));
                });
                return this.getValue(args[1], vm);
            });
        }else{
            value = this.getValue(expr, vm);
        }
        this.updater.textUpdater(node, value);  
    },
    html(node, expr, vm) {
        const value = this.getValue(expr, vm);
         // html對(duì)應(yīng)的 Watcher
        new Watcher(expr, vm, (newVal)=>{
            this.updater.htmlUpdater(node, newVal);
        })
        this.updater.htmlUpdater(node, value);
    },
    model(node, expr, vm) {
        const value = this.getValue(expr, vm);
        // v-model綁定對(duì)應(yīng)的 Watcher, 數(shù)據(jù)驅(qū)動(dòng)視圖:數(shù)據(jù)=>視圖
        new Watcher(expr, vm, (newVal)=>{
            this.updater.modelUpdater(node, newVal);
        })
         // 視圖 => 數(shù)據(jù) => 視圖
        node.addEventListener('input', (e)=>{
            this.setVal(expr, vm, e.target.value);
        })
        this.updater.modelUpdater(node, value);
    },
    on(node, expr, vm, detailStr) {
        let fn = vm.$options.methods && vm.$options.methods[expr];
        node.addEventListener(detailStr,fn.bind(vm), false);
    },
    bind(node, expr, vm, detailStr){
        // v-on:href='...' => href='...'
        node.setAttribute(detailStr, expr);
    },
    // 視圖更新函數(shù)
    updater: {
        textUpdater(node, value) {
            node.textContent = value;
        },
        htmlUpdater(node, value){
            node.innerHTML = value;
        },
        modelUpdater(node, value){
            node.value = value;
        }
    }

}

// 編譯HTML模版對(duì)象
class Compiler {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        // 1. 將預(yù)編譯的元素節(jié)點(diǎn)放入文檔碎片對(duì)象中,避免DOM頻繁的回流與重繪,提高渲染性能
        const fragments = this.node2fragments(this.el);
        // 2. 編譯模版
        this.compile(fragments);
        // 3. 追加子元素到根元素
        this.el.appendChild(fragments);
    }
    compile(fragments) {
        // 1.獲取子節(jié)點(diǎn)
        const childNodes = fragments.childNodes;
        // 2.遞歸循環(huán)編譯
        [...childNodes].forEach(child => {
            // 如果是元素節(jié)點(diǎn)
            if (this.isElementNode(child)) {
                this.compileElement(child);
            } else {
                // 文本節(jié)點(diǎn)
                this.compileText(child);
            }
            //遞歸遍歷
            if(child.childNodes && child.childNodes.length){
                this.compile(child);
            }
        })
    }
    compileElement(node) {
        let attributes = node.attributes;
        // 對(duì)于每個(gè)屬性進(jìn)行遍歷編譯
        // attributes是類數(shù)組,因此需要先轉(zhuǎn)數(shù)組
        [...attributes].forEach(attr => {
            let {name,value} = attr; // v-text="msg"  v-html=htmlStr  type="text"  v-model="msg"
            if (this.isDirector(name)) { // v-text  v-html  v-mode  v-bind  v-on:click v-bind:href=''
                let [, directive] = name.split('-');
                let [compileKey, detailStr] = directive.split(':');
                // 更新數(shù)據(jù),數(shù)據(jù)驅(qū)動(dòng)視圖
                compileUtil[compileKey](node, value, this.vm, detailStr);
                // 刪除有指令的標(biāo)簽屬性 v-text v-html等,普通的value等原生html標(biāo)簽不必刪除
                node.removeAttribute('v-' + directive);
            }else if(this.isEventName(name)){
                // 如果是事件處理 @click='handleClick'
                let [, detailStr] = name.split('@');
                compileUtil['on'](node, value, this.vm, detailStr);
                node.removeAttribute('@' + detailStr);
            }

        })

    }
    compileText(node) {
        // 編譯文本中的{{person.name}}--{{person.age}}
        const content = node.textContent;
        if(/\{\{(.+?)\}\}/.test(content)){
            compileUtil['text'](node, content, this.vm);
        }
    }
    isEventName(attrName){
        // 判斷是否@開(kāi)頭
        return attrName.startsWith('@');
    }
    isDirector(attrName) {
        // 判斷是否為Vue特性標(biāo)簽
        return attrName.startsWith('v-');
    }
    node2fragments(el) {
        // 創(chuàng)建文檔碎片對(duì)象
        const f = document.createDocumentFragment();
        let firstChild;
        while (firstChild = el.firstChild) {
            f.appendChild(firstChild);
        }
        return f;
    }
    isElementNode(node) {
        // 元素節(jié)點(diǎn)的nodeType屬性為 1
        return node.nodeType === 1;
    }
}

MVue入口類設(shè)計(jì)

Mvue類接收一個(gè)參數(shù)對(duì)象作為初始輸入,然后利用Compiler類對(duì)模版進(jìn)行編譯及渲染、創(chuàng)建觀察者,觀察數(shù)據(jù)。


class MVue {
    constructor(options) {
        // 初始元素與數(shù)據(jù)通過(guò)options對(duì)象綁定
        this.$el = options.el;
        this.$data = options.data;
        this.$options = options;
        // 通過(guò)Compiler對(duì)象對(duì)模版進(jìn)行編譯,例如{{}}插值、v-text、v-html、v-model等Vue語(yǔ)法
        if (this.$el) {
            // 1. 創(chuàng)建觀察者,利用Observer對(duì)象對(duì)數(shù)據(jù)進(jìn)行劫持
            new Observer(this.$data);
            // 2. 編譯模版
            new Compiler(this.$el, this);
            // 3. 通過(guò)數(shù)據(jù)代理實(shí)現(xiàn) this.person.name = '子非魚(yú)-cool'功能,而不是this.$data.person.name = '子非魚(yú)-cool'
            this.proxyData(this.$data);
        }
    }
     //用vm代理vm.$data
     proxyData(data){
        for(let key in data){
            Object.defineProperty(this,key,{
                get(){
                    return data[key];
                },
                set(newVal){
                    data[key] = newVal;
                }
            })
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容