模擬Vue實現(xiàn)雙向綁定

模擬Vue實現(xiàn)雙向綁定

使用Vue也有一段時間了筹麸,作為一款MVVM框架罢坝,雙向綁定是其最核心的部分恋追,所以最近動手實現(xiàn)了一個簡單的雙向綁定供常。先上最終成果圖

mvvm.gif

思路

實現(xiàn)MVVM主要包含兩個方面悼凑,一個是數(shù)據(jù)變化更新視圖偿枕,另一個則是對應的試圖變化更新數(shù)據(jù),重點在于怎么實現(xiàn)數(shù)據(jù)變了户辫,如何去更新視圖渐夸,因為視圖更新數(shù)據(jù)使用事件監(jiān)聽的形式就可以實現(xiàn),比如input標簽通過監(jiān)聽input 事件就可以實現(xiàn)渔欢。所以重點是如何實現(xiàn)數(shù)據(jù)改變更新視圖墓塌。

其實是通過Object.defineProperty()對屬性進行數(shù)據(jù)劫持,設置set函數(shù)奥额,當數(shù)據(jù)改變后就回來觸發(fā)這個函數(shù)苫幢,所以要將一些需要更新的方法放在這里面就可以實現(xiàn)data更新view了。

實現(xiàn)功能

  1. 實現(xiàn)一個解析器Compile垫挨,可以掃描和解析每個節(jié)點的相關指令韩肝,并根據(jù)初始化模板數(shù)據(jù)以及初始化相應的訂閱器。

    1. 文本的編譯 例如{{message}}
    2. 指令的編譯 例如v-model
  2. 實現(xiàn)一個監(jiān)聽器Observer九榔,用來劫持并監(jiān)聽所有屬性哀峻,如果有變動的,就通知訂閱者哲泊。

  3. 實現(xiàn)一個訂閱者Watcher剩蟀,可以收到屬性的變化通知并執(zhí)行相應的函數(shù),從而更新視圖攻旦。

flow.jpg

MVVM.js 整合

class MVVM {
    constructor(options) {
        // 先把可用的東西掛載到實例上
        this.$el = options.el;
        this.$data = options.data;

        // 判斷有沒有要編譯的模板
        if(this.$el) {
            // 數(shù)據(jù)劫持 將對象的所有屬性喻旷,都添加 get 和 set 方法
            new Observer(this.$data)
            // 用數(shù)據(jù)和元素進行模板編譯
            new Compile(this.$el, this)
        }
    }
}

模板的編譯(compile.js)

class Compile {
    constructor(el, vm) {
        // 判斷el是不是元素節(jié)點
        this.el = this.isElementNode(el) ? el : document.querySelector(el); 
        this.vm = vm;
        if(this.el) {
            // 1\. 先把真實的DOM移入到內(nèi)存中(fragment),提高性能
            let fragment = this.node2fragment(this.el)
            // 2\. 編譯 -> 提取想要的元素節(jié)點 v-model 和 文本節(jié)點 {{}}
            this.compile(fragment)
            // 3\. 把fragment塞回頁面
            this.el.appendChild(fragment)
        }
    }

    // 對fragment進行編譯
    compile(fragment) {
        let childNodes = fragment.childNodes;
         Array.from(childNodes).forEach( node => {
            // 遍歷fragment的元素節(jié)點
             if(this.isElemenrNode(node)) {
                 // 是元素節(jié)點,需要深度遞歸檢查
                 this.compile(node)
                 // 編譯元素
                 this.compileElement(node)
             } else {
                 // 是文本節(jié)點牢屋,編譯文本
                 this.compileText(node)
             }
         })
    }
}

將數(shù)據(jù)進行劫持且预,添加get 和 set方法

class Observer {
    constructor(data) {
        this.observe(data)
    }
    observe(data) {
        // 要對data數(shù)據(jù)的所有屬性都改為set 和 get 的形式
        if(!data || typeof data === 'object') {
            return ;
        }
        // 取出對象 key 值
        Object.keys(data).forEach( key => {
            // 數(shù)據(jù)劫持
            this.defineReactive(data, key, data[key]);
            this.observe(data[key]); // 遞歸劫持
        })
    }

    // 定義響應式(數(shù)據(jù)劫持)
    defineReactive(obj, key, value) {
        let that = this;
        Object.defineProperty(obj, key, {
            enumerable: true, // 可枚舉
            configurable: true, // 屬性能夠被改變
            get() { // 取值時調(diào)用的方法
                return value;
            },
            set(newVal) { // 當給data屬性中設置值的時候,更改獲取的屬性的值
                if(newVal !== value) {
                   value = newVal;
                    that.observe(newVal); // 如果是對象修改繼續(xù)劫持
                }
            }
        })
    }
}

觀察者(watcher.js)

最后烙无,給需要變化的元素添加一個觀察者锋谐,通過觀察者監(jiān)聽數(shù)據(jù)變化之后執(zhí)行對應的方法。

class Watcher {
    constructor (vm, expr, cb) {
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // 先獲取一下老值
        this.value = this.get()
    }
    getVal() {
        // 獲取實例上對應的數(shù)據(jù)
        expr = expr.split('.');
        return expr.reduce( (prev, next) => {
            return prev[next];
        }, vm.$data)
    }
    get() {
        let value = this.getVal(this.vm, this.expr);
         return value;
    }
    // 對外暴露的方法截酷,老值和新值比對涮拗,如果變化
    update() {
        let newVal = this.getVal(this.vm, this.expr);
        let oldVal = this.value;
        if(newVal !== oldVal) {
            this.cb(newVal); // 對應watch的callback
        }
    }
}

Watch 完成,需要new一下調(diào)用迂苛,首先需要在模板編譯的時候需要調(diào)用三热,在compile.js

CompileUtil = {
    getVal(vm, expr) {
        // 獲取實例上對應的數(shù)據(jù)
        expr = expr.split('.');
        return expr.reduce( (prev, next) => {
            return prev[next];
        }, vm.$data)
    },
    getTextVal(vm, expr) {
        // 獲取編譯后文本的結(jié)果 
        return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
            return this.getVal(vm, arguments[1]);
        })
    },
    text(node, vm, expr) {
        // 文本處理
        let updateFn = this.updater['textUpdater']
        /*  Wather觀察者監(jiān)聽  */
        expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
            new Wathcer(vm, arguments[1], (newVal) => {
                // 如果數(shù)據(jù)變化,文本需要重新獲取依賴的數(shù)據(jù)三幻,更新文本中的內(nèi)容
                updateFn && updateFn(node, this.getTextVal(vm, expr))
            })
        })

        updateFn && updateFn(node, this.getTextVal(vm, expr))
    },
    setVal(vm, expr, value) {
        expr = expr.split('.');
        return expr.reduce( (prev, next,currentIndex) => {
            if(currentIndex === expr.length - 1) {
                return prev[next] = value;
            }
            return prev[next];
        }, vm.$data)
    },

    model(node, vm, expr) {
        // 輸入框處理
        let updateFn = this.updater['modelUpdater']
        /*  Wather觀察者監(jiān)聽  */
        // 這里應該加一個監(jiān)控就漾, 數(shù)據(jù)變化,調(diào)用watch的回調(diào)
        new Wathcer(vm, expr, (newVal) => {
            // 當值變化后會調(diào)用callback念搬,將新值傳遞過來
            updateFn && updateFn(node, this.getVal(vm, expr));
        })
        // 給輸入框加上input事件監(jiān)聽
        node.addEventListener('input', (e) => {
            let newVal = e.target.value;
            this.setVal(vm, expr, newVal)
        })      

        updateFn && updateFn(node, this.getVal(vm, expr));
    },
    updater: {
        // 文本更新
        textUpdater(node, value) {
            node.textContent = value;
        },
        // 輸入框更新
        modelUpdater(node, value) {
            node.value = value;
        }
    }
}

但是此時有一個問題抑堡,Watcher沒有地方調(diào)用,更新函數(shù)不會執(zhí)行朗徊,所以此時需要一個發(fā)布訂閱模式來調(diào)用監(jiān)控者首妖。

class Dep {
    constructor() {
        // 訂閱的數(shù)組
        this.subs = [];
    }
    addSub(watcher) {
        this.subs.push(watcher);
    }
    notify() {
        this.subs.forEach( watcher => {
            watcher.update()
        })
    }
}

此時需要修改watcherget() 這個方法:

get() {
    Dep.target = this;
    let value = this.getVal(this.vm, this.expr)
    Dep.target = null;
    return value;
}

此時要得到對象的值,需要被數(shù)據(jù)劫持攔截:

defineReactive(obj, key, value) {
    let that = this;
    let dep = new Dep();  // 每個變化的數(shù)據(jù)爷恳,都會定義一個數(shù)組有缆,這個數(shù)組存放所有更新的操作
    Object.defineProperty(obj, key, {
        enumerable: true, // 可枚舉
        configurable: true,
        get() { 
            // 當取值時調(diào)用的方法
            Dep.target && dep.addSub(Dep.target); // 最開始編譯的時候不會執(zhí)行
            return value;
        },
        set(newVal) {
            // 當給data屬性中設置值的時候 更改獲取屬性的值
            if(newVal != value) {
                that.observe(newVal); // 如果是對象繼續(xù)劫持
                value = newVal;
                dep.notify(); // 通知所有人數(shù)據(jù)更新了
            }
        }
    });
}

此時就完成了輸入框的雙向綁定。不過此時我們?nèi)?shù)據(jù)是以vm.$data.msg來取到數(shù)據(jù)温亲,理想情況我們是vm.msg來取到數(shù)據(jù)棚壁,為了實現(xiàn)這樣的形式,我們使用proxy進行一下代理實現(xiàn):

    proxyData(data) {
        Object.keys(data).forEach( key => {
            Object.defineProperty(this, key, {
                get() {
                    return data[key]
                },
                set(newVal) {
                    data[key] = newVal
                }
            })
        })
    }

這下我們就可以直接通過vm.msg = 'hello'的形式來進行改變和獲取模板數(shù)據(jù)了铸豁。

歡迎交流指正灌曙,原文地址:https://github.com/hu970804/MVVM

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市节芥,隨后出現(xiàn)的幾起案子在刺,更是在濱河造成了極大的恐慌,老刑警劉巖头镊,帶你破解...
    沈念sama閱讀 216,843評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蚣驼,死亡現(xiàn)場離奇詭異,居然都是意外死亡相艇,警方通過查閱死者的電腦和手機颖杏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來坛芽,“玉大人留储,你說我怎么就攤上這事翼抠。” “怎么了获讳?”我有些...
    開封第一講書人閱讀 163,187評論 0 353
  • 文/不壞的土叔 我叫張陵阴颖,是天一觀的道長。 經(jīng)常有香客問我丐膝,道長量愧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,264評論 1 292
  • 正文 為了忘掉前任帅矗,我火速辦了婚禮偎肃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘浑此。我一直安慰自己累颂,他們只是感情好,可當我...
    茶點故事閱讀 67,289評論 6 390
  • 文/花漫 我一把揭開白布尤勋。 她就那樣靜靜地躺著喘落,像睡著了一般。 火紅的嫁衣襯著肌膚如雪最冰。 梳的紋絲不亂的頭發(fā)上瘦棋,一...
    開封第一講書人閱讀 51,231評論 1 299
  • 那天,我揣著相機與錄音暖哨,去河邊找鬼赌朋。 笑死,一個胖子當著我的面吹牛篇裁,可吹牛的內(nèi)容都是我干的沛慢。 我是一名探鬼主播,決...
    沈念sama閱讀 40,116評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼达布,長吁一口氣:“原來是場噩夢啊……” “哼团甲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起黍聂,我...
    開封第一講書人閱讀 38,945評論 0 275
  • 序言:老撾萬榮一對情侶失蹤躺苦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后产还,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體匹厘,經(jīng)...
    沈念sama閱讀 45,367評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,581評論 2 333
  • 正文 我和宋清朗相戀三年脐区,在試婚紗的時候發(fā)現(xiàn)自己被綠了愈诚。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,754評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖炕柔,靈堂內(nèi)的尸體忽然破棺而出酌泰,到底是詐尸還是另有隱情,我是刑警寧澤汗唱,帶...
    沈念sama閱讀 35,458評論 5 344
  • 正文 年R本政府宣布宫莱,位于F島的核電站丈攒,受9級特大地震影響哩罪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜巡验,卻給世界環(huán)境...
    茶點故事閱讀 41,068評論 3 327
  • 文/蒙蒙 一际插、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧显设,春花似錦框弛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至指攒,卻和暖如春慷妙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背允悦。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評論 1 269
  • 我被黑心中介騙來泰國打工膝擂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人隙弛。 一個月前我還...
    沈念sama閱讀 47,797評論 2 369
  • 正文 我出身青樓架馋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親全闷。 傳聞我的和親對象是個殘疾皇子叉寂,可洞房花燭夜當晚...
    茶點故事閱讀 44,654評論 2 354