Vue雙向綁定原理與實(shí)現(xiàn)

前言

實(shí)現(xiàn)Vue的數(shù)據(jù)的雙向綁定 是采用數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式 通過Object.defineProperty()來劫持各個屬性的setter,getter休傍,在數(shù)據(jù)變動時發(fā)布消息給訂閱者掀亩,觸發(fā)相應(yīng)的監(jiān)聽回調(diào) 需要實(shí)現(xiàn)以下幾點(diǎn):
1 實(shí)現(xiàn)一個 Observer 觀察者對data中的數(shù)據(jù)進(jìn)行監(jiān)聽置侍,若有變化,通知相應(yīng)的訂閱者
2 實(shí)現(xiàn)一個 Compile 編譯器將頁面中子節(jié)點(diǎn)拷貝到DocumentFragment對象中,然后對每個元素節(jié)點(diǎn)的指令進(jìn)行掃描與解析
3 實(shí)現(xiàn)一個 Watcher 用來連接ObserverCompile豌习,并為每個屬性綁定相應(yīng)的訂閱者苏潜,當(dāng)數(shù)據(jù)發(fā)生變化時银萍,執(zhí)行相應(yīng)的回調(diào)函數(shù),從而更新視圖恤左。
4 new MVVM的類實(shí)例對象 作為入口整合使用

雙向綁定.png

準(zhǔn)備工作

下面是文件目錄贴唇,由于瀏覽器不支持 es6 的export default / import 語法所以用webpack來打包解析es6語法


711563808099_.pic.jpg

實(shí)現(xiàn)vue的類

  • 新創(chuàng)建一個Vue的類,接收 new實(shí)例對象傳過來的參數(shù)飞袋, el:vue實(shí)例被掛載的Dom對象戳气, _data: 接收了初始化的data數(shù)據(jù)
  • 利用Object.defineProperty()的方法利用set、get 的方法對指定的屬性進(jìn)行數(shù)據(jù)代理巧鸭,也就是將示例對象上的屬性 映射到內(nèi)部_data對象屬性上去 實(shí)現(xiàn) MVVM.xxx => MVVM._data.xxx
  • 接下來實(shí)現(xiàn)一個Observer 對data中的屬性進(jìn)行數(shù)據(jù)劫持將屬性與發(fā)布訂閱者綁定
  • 進(jìn)行模版指令解析

vue.js

import Observer from './Observer' //觀察者
import Compiler from './Compiler' //編譯模版

class Vue {
    constructor(options) {
        this.$options = options
        //保存掛載元素的ID
        this.$el = this.$options.el
        //保存實(shí)例對象中的data對象數(shù)據(jù)
        this._data = this.$options.data
        //遍歷屬性添加數(shù)據(jù)代理
        Object.keys(this._data).forEach(key => {
            this._proxy(key)
        })
        //通過數(shù)據(jù)劫持實(shí)現(xiàn)雙向綁定 通知發(fā)布訂閱
        new Observer(this._data)
        //模版指令解析
        new Compiler(this.$el, this)
    }
    //將實(shí)例對象上的屬性 映射到內(nèi)部_data對象屬性上面 (實(shí)現(xiàn) MVVM.xxx => MVVM._data.xxx)
    _proxy(key) {
        //屬性描述符
        Object.defineProperty(this, key, {
            //讀取屬性值時調(diào)用
            get() {
                //返回_data 中屬性名的對應(yīng)的屬性值
                return this._data[key]
            },
            //監(jiān)聽設(shè)置屬性調(diào)用
            set(value) {
                //賦值到_data屬性名對應(yīng)的屬性值
                this._data[key] = value
            }
        })
    }
}

export default Vue

實(shí)現(xiàn)Observer 觀察者

  • 接收并保存vue實(shí)例對象傳遞過來的數(shù)據(jù)
  • 通過Object.keys()的方法返回對象所有屬性名組成的數(shù)組瓶您,遍歷對每個屬性執(zhí)行數(shù)據(jù)劫持實(shí)現(xiàn)雙向綁定。在每次遍歷中都會new一個Dep()的實(shí)例對象纲仍,并且Dep()的實(shí)例對象與屬性是一一對應(yīng)的關(guān)系
  • 在對屬性進(jìn)行數(shù)據(jù)劫持的同時觸發(fā)get()方法會去判斷當(dāng)前是否存在Watcher對象呀袱,存在就調(diào)用Dep中的listen方法將當(dāng)前的Watcher對象添加到訂閱者數(shù)組中,并返回當(dāng)前的value值
  • 在通過MVVM.name = 'xxx' 賦值的過程中會觸發(fā)set()函數(shù)郑叠,簡單判斷后賦值的同時調(diào)用myDep 中的notify()方法遍歷執(zhí)行Watcher中的update()方法進(jìn)行頁面對應(yīng)視圖的更新

Observer.js

import Dep from "./Dep";

class Observer {
    constructor(data) {
        //保存數(shù)據(jù)
        this.data = data

        //對數(shù)據(jù)中所有屬性設(shè)置setter getter 通過數(shù)據(jù)劫持實(shí)現(xiàn)雙向綁定
        Object.keys(data).forEach(key => {
            this._bind(data, key, data[key])
        })
    }
    _bind(data, key, value) {
        //new Dep對象 保存訂閱者 遍歷調(diào)用訂閱者的update對象 (myDep實(shí)例對象與data中的屬性一一對應(yīng))
        let myDep = new Dep()

        //屬性描述符
        Object.defineProperty(data, key, {
            //獲取屬性值
            get() {
                //獲取屬性值 判斷是否存在wathcer實(shí)例對象 存在將其保存到Dep當(dāng)中
                if (Dep.target) myDep.listen(Dep.target)
                return value
            },
            //監(jiān)視屬性值的變化
            set(newValue) {
                if (newValue === value) return
                //賦值
                value = newValue

                //觀察者改變完數(shù)據(jù) 通知發(fā)布訂閱
                myDep.notify()
            }
        })
    }
}

export default Observer

發(fā)布訂閱

  • 初始化 target 保存Watcher訂閱者對象 夜赵,list 存放多個Watcher訂閱者對象
  • 實(shí)現(xiàn)了一個 listen()方法用于將Watcher訂閱者對象添加到list當(dāng)中
  • 實(shí)現(xiàn)了一個 notify()方法遍歷訂閱者對象調(diào)用其 update()的方法進(jìn)行頁面視圖的更新

Dep .js

class Dep {
    constructor() {
        this.target = null
        this.list = []
    }
    //添加訂閱者 watcher
    listen(subs) {
        this.list.push(subs)
    }
    //調(diào)用watcher中的update方法更新視圖
    notify() {
        this.list.forEach(item => {
            item.update()
        })
    }
}
export default Dep

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

  • 通過頁面元素的id獲取當(dāng)前元素的節(jié)點(diǎn)對象并保存,保存當(dāng)前的vue的實(shí)例對象
  • 創(chuàng)建通過 createFragment()方法創(chuàng)建了一個Dom碎片對象乡革, 循環(huán)將this.$el中的子節(jié)點(diǎn)依次轉(zhuǎn)移到 fragment對象中經(jīng)過compileElement()編譯元素節(jié)點(diǎn)之后return 在次添加到頁面的節(jié)點(diǎn)對象
  • 通過判斷元素的節(jié)點(diǎn)類型的值再做對應(yīng)的編譯解析操作
    • 當(dāng)數(shù)據(jù)類型是元素節(jié)點(diǎn)并且包含 v-model 的屬性指令時直接將綁定的屬性名對應(yīng)的屬性值賦值到元素節(jié)點(diǎn)的vlaue值實(shí)現(xiàn)視圖更新寇僧, 并且給元素添加 'input' 的監(jiān)聽事件,事件發(fā)生時將value值賦值給 對象中對應(yīng)屬性名的屬性值
    • 當(dāng)數(shù)據(jù)類型為文本類型 {{ name }} 時沸版,通過正則解析判斷當(dāng)前格式獲取到 'name' 屬性名 new Watcher()實(shí)例對象初始化調(diào)用update()方法實(shí)現(xiàn)視圖更新

Compiler.js

import Watcher from "./Watcher";
const reg = /\{\{(.*)\}\}/
class Compiler {
    constructor(el, vm) {
        //保存根據(jù)獲取到ID對應(yīng)的頁面節(jié)點(diǎn)對象
        this.$el = document.querySelector(el)
        //保存當(dāng)前的MVVM對象
        this.$vm = vm
        //將頁面中的子節(jié)點(diǎn)轉(zhuǎn)移到fragment中
        this.fragment = this.createFragment()
        //將fragment 轉(zhuǎn)移回到頁面當(dāng)中
        this.$el.appendChild(this.fragment)
    }
    createFragment() {
        //創(chuàng)建Dom碎片對象
        let fragment = document.createDocumentFragment(), child
        //將原生節(jié)點(diǎn)拷貝到fragment
        while (child = this.$el.firstChild) {
            //編譯元素節(jié)點(diǎn)
            this.compileElement(child)
            //將節(jié)點(diǎn)加入fragment對象
            fragment.appendChild(child)
        }
        return fragment
    }
    //編譯元素節(jié)點(diǎn)
    compileElement(node) {
        //元素節(jié)點(diǎn)
        if (node.nodeType == 1) {
            //獲取當(dāng)前元素節(jié)點(diǎn)的屬性值數(shù)組
            let attr = node.attributes
            //判斷屬性值數(shù)組中是否存在v-model
            if (attr.hasOwnProperty('v-model')) {
                let self = this
                //獲取屬性為 v-model 的元素節(jié)點(diǎn) 并獲取節(jié)點(diǎn)值
                let name = attr['v-model'].nodeValue
                //初始化顯示輸入框內(nèi)容
                node.value = this.$vm[name]

                //給輸入框添加input事件監(jiān)聽
                node.addEventListener('input', function(e){
                    self.$vm[name] = e.target.value
                })
            }
        }
        //文本節(jié)點(diǎn)
        if (node.nodeType == 3) {
            //判斷是否符合 雙大括號表達(dá)式 {{ }}
            if (reg.test(node.nodeValue)) {
                //獲取表達(dá)式內(nèi)容
                let name = RegExp.$1
                //去空格
                name = name.trim()

                //new一個Watcher對象更新雙大括號表達(dá)式內(nèi)容
                new Watcher(node, name, this.$vm)
            }
        }
    }
}
export default Compiler

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

  • 接收并保存初始化編譯模版?zhèn)鬟f過來的 節(jié)點(diǎn)元素婉宰,屬性名(‘name’) 當(dāng)前的vue實(shí)例對象
  • 將自身在觸發(fā)get()方法時添加到Dep中的list數(shù)組后清空
  • 初始化調(diào)用update()方法實(shí)現(xiàn)視圖更新

Watcher.js

import Dep from './Dep'

//頁面上所有訂閱數(shù)據(jù)的地方 (watcher 與頁面的表達(dá)式一一對應(yīng))
class Watcher {
    constructor(node, name, vm) {
        this.node = node
        this.name = name
        this.vm = vm

        //將Dep.target 賦值為 this (Watcher的示例對象)
        Dep.target = this
        //調(diào)用update() 方法更新節(jié)點(diǎn)數(shù)據(jù)
        this.update()
        Dep.target = null
    }
    update() {
        //將當(dāng)前的標(biāo)簽節(jié)點(diǎn)下的值改成 vm對象中對應(yīng)屬性的值
        //觸發(fā)Observer中對MVVM對象監(jiān)聽的name屬性添加的get屬性 從而將當(dāng)前的{{}}的表達(dá)式注入到 Dep中
        this.node.nodeValue = this.vm[this.name]
    }
}

export default Watcher

vue實(shí)例對象

main.js入口文件

import Vue from './vue'

const MVVM = new Vue({
   el: "#app",
   data: {
       name: 'hello world',
   }
})  
window.MVVM = MVVM

HTML代碼

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="name" />
        {{ name }}
    </div>
    <script src="./dist/main.js"></script>
</body>
</html>

總結(jié)

至此就實(shí)現(xiàn)了一個簡單的數(shù)據(jù)雙向綁定的例子,主要的目的是為了更好的理解雙向綁定的實(shí)現(xiàn)原理與設(shè)計思想

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末推穷,一起剝皮案震驚了整個濱河市心包,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌馒铃,老刑警劉巖蟹腾,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痕惋,死亡現(xiàn)場離奇詭異,居然都是意外死亡娃殖,警方通過查閱死者的電腦和手機(jī)值戳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來炉爆,“玉大人堕虹,你說我怎么就攤上這事》沂祝” “怎么了赴捞?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長郁稍。 經(jīng)常有香客問我赦政,道長,這世上最難降的妖魔是什么耀怜? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任恢着,我火速辦了婚禮,結(jié)果婚禮上财破,老公的妹妹穿的比我還像新娘掰派。我一直安慰自己,他們只是感情好左痢,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布碗淌。 她就那樣靜靜地躺著,像睡著了一般抖锥。 火紅的嫁衣襯著肌膚如雪亿眠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天磅废,我揣著相機(jī)與錄音纳像,去河邊找鬼。 笑死拯勉,一個胖子當(dāng)著我的面吹牛竟趾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播宫峦,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼岔帽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了导绷?” 一聲冷哼從身側(cè)響起犀勒,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后贾费,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钦购,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年褂萧,在試婚紗的時候發(fā)現(xiàn)自己被綠了押桃。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡导犹,死狀恐怖唱凯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情谎痢,我是刑警寧澤磕昼,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站舶得,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏爽蝴。R本人自食惡果不足惜沐批,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蝎亚。 院中可真熱鬧九孩,春花似錦、人聲如沸发框。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽梅惯。三九已至宪拥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間铣减,已是汗流浹背她君。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留葫哗,地道東北人缔刹。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像劣针,于是被迫代替她去往敵國和親校镐。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評論 2 353