Vue 核心之?dāng)?shù)據(jù)雙向綁定

Vue雙向綁定原理:Vue內(nèi)部通過 Object.defineProperty方法以屬性攔截的方式俺驶,把data對象的每個(gè)數(shù)據(jù)的讀寫轉(zhuǎn)化為getter / setter,當(dāng)數(shù)據(jù)變化時(shí)通知視圖更新。

一赞赖、MVVM數(shù)據(jù)雙向綁定

MVVM數(shù)據(jù)雙向綁定主要是指:數(shù)據(jù)變化更新視圖,視圖變化更新數(shù)據(jù)。


image.png

即:

  • 輸入框內(nèi)容變化時(shí)界睁,Data 中的數(shù)據(jù)同步變化。View 導(dǎo)致 Data 的變化兵拢。(通過事件監(jiān)聽的方式來實(shí)現(xiàn))
  • Data 中的數(shù)據(jù)變化時(shí)翻斟,文本節(jié)點(diǎn)的內(nèi)容同步變化。Data 導(dǎo)致 View 的變化说铃。(通過操作DOM實(shí)現(xiàn))

監(jiān)聽器 Observer 只要是讓對象變的 "可觀測"访惜,即每次讀寫數(shù)據(jù)時(shí),我們能感知到數(shù)據(jù)被讀取了或數(shù)據(jù)被改寫了腻扇。Vue2.0源碼中用到Object.defineProperty()來劫持各個(gè)數(shù)據(jù)屬性的setter / getter债热。關(guān)于Object.defineProperty 方法,在 MDN 上是這么定義的:

Object.defineProperty() 方法會直接在一個(gè)對象上定義一個(gè)新屬性幼苛,或者修改一個(gè)對象的現(xiàn)有屬性窒篱,并返回這個(gè)對象。

二、Object.defineProperty() 語法

Object.defineProperty(obj, prop, descriptor)

參數(shù):

  • obj: 要在其上定義屬性的對象墙杯。
  • prop:要定義或修改的屬性的名稱配并。
  • descriptor:將被定義或修改的屬性描述符。

返回值:被傳遞給函數(shù)的對象高镐。
屬性描述符:
Object.defineProperty() 為對象定義屬性溉旋,分為數(shù)據(jù)描述符和存取描述符,兩種形式不能混用嫉髓。

數(shù)據(jù)描述符和存取描述符均具有以下可選鍵值:

  • configurable:一個(gè)總開關(guān)观腊,一旦將它設(shè)為false,就不能刪除或重新設(shè)置defineProperty監(jiān)聽的屬性算行。為true時(shí)可以進(jìn)行刪除或重新使用defineProperty設(shè)置新值梧油。默認(rèn)為false。
  • enumerable:當(dāng)屬性值為true時(shí)纱意,該屬性才能出現(xiàn)在對象枚舉的屬性中婶溯。默認(rèn)為false。

數(shù)據(jù)描述符具有以下可選鍵值:

  • value:該屬性對應(yīng)的值偷霉,可以為任意有效的 JavaScript值(數(shù)值迄委、對象、函數(shù)等)类少。默認(rèn) undefined叙身。
  • writable:設(shè)置屬性值是否允許被賦值運(yùn)算符改變。true為允許硫狞,false為不允許被重寫信轿。默認(rèn)false。

存取描述符具有以下可選鍵值:

  • get:用于給屬性提供 getter 方法残吩,當(dāng)訪問該屬性時(shí)财忽,該方法會被執(zhí)行,執(zhí)行時(shí)不需要傳入?yún)?shù)泣侮,但可以拿到this對象即彪。默認(rèn)為undefined。
  • set:用于給屬性提供 setter 方法活尊,當(dāng)屬性修改時(shí)隶校,該方法會被執(zhí)行。該方法接收唯一參數(shù)蛹锰,即該屬性新的參數(shù)值深胳。默認(rèn)為undefined。
通過 Object.defineProperty() 實(shí)現(xiàn)一個(gè)簡單的輸入框雙向綁定:
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>defineProperty實(shí)現(xiàn)綁定</title>
    <script type="text/javascript" src="./example.js"></script>
</head>
<body>
    <div id="myApp">
        <input type="text" id="myInput" />
        <div id="myDiv"></div>
    </div>
</body>
<script type="text/javascript">
    var myInput = document.getElementById('myInput');
    var myDiv = document.getElementById('myDiv');
    
    let obj = {
        value: '監(jiān)聽數(shù)據(jù)'
    }
    
    // 將初始化數(shù)據(jù)賦值給元素
    myInput.value = obj.value;
    myDiv.innerHTML = obj.value;
    
    Object.defineProperty(obj, 'value', {
        set(newVal) {
            // 監(jiān)聽對象屬性值改變,更新div元素innerHTML屬性
            myDiv.innerHTML = newVal;
        }
    })
    
    myInput.oninput = function(e) {
        // 更新對象值,來觸發(fā)Object.defineProperty的set方法
        obj.value = e.target.value;
    }
</script>
</html>

要了解Vue雙向綁定原理铜犬,首先要明白三個(gè)概念:

1舞终、觀察者( observer ):數(shù)據(jù)監(jiān)聽器轻庆,負(fù)責(zé)對數(shù)據(jù)對象的所有屬性進(jìn)行監(jiān)聽劫持,并將消息發(fā)送給訂閱者進(jìn)行數(shù)據(jù)更新权埠。
2榨了、訂閱者( watcher ):負(fù)責(zé)接收數(shù)據(jù)的變化,并執(zhí)行更新視圖(view)攘蔽。數(shù)據(jù)與訂閱者是一對多的關(guān)系。
3呐粘、解析器( compile ):負(fù)責(zé)對你的每個(gè)節(jié)點(diǎn)元素指令進(jìn)行掃描和解析满俗,負(fù)責(zé)相關(guān)指令的數(shù)據(jù)綁定初始化及創(chuàng)造數(shù)據(jù)對應(yīng)的訂閱者(每個(gè)通過指令綁定該屬性數(shù)據(jù)的元素都是一個(gè)訂閱者)。
html:
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>雙向綁定</title>
    <script type="text/javascript" src="./example.js"></script>
</head>
<body>
    <div id="myApp">
        <input type="button" value="加個(gè)!" z-on:click="fun1" />
        <input type="button" value="加個(gè)作岖?" @click="fun2" />
        <input type="text" style="width:400px" z-model="site">
        <p z-html="site"></p>
        <p z-text="site"></p>
    </div>
</body>
<script type="text/javascript">
    var vm = new Example({
        el: '#myApp',
        data: {
            site: 'Vue雙向綁定原理',
            age: 12,
            sex: '男'
        },
        methods: {
            fun1() {
                this.site += '!'
            },
            fun2() {
                this.site += '?'
            }
        }
    })
</script>
</html>
example.js雙向綁定代碼:
function Example(options) { // 創(chuàng)建構(gòu)造函數(shù)Example,并接收對象結(jié)構(gòu)體options
    this.$el = document.querySelector(options.el); // 獲取指定掛載的元素
    this.$data = options.data; // 將數(shù)據(jù)掛載到實(shí)例
    this.$methods = options.methods; // 存放對象的方法
    this.binding = {}; // 所有與數(shù)據(jù)相關(guān)的訂閱者對象都存放于此,$data下每個(gè)數(shù)據(jù)對應(yīng)一個(gè)數(shù)組,用于對應(yīng)多個(gè)訂閱者
    
    this.observer(); // 調(diào)用觀察者,對數(shù)據(jù)進(jìn)行劫持
    this.compile(this.$el); // 對元素上綁定的指令如(v-model)進(jìn)行解析,并創(chuàng)建訂閱者.(所有綁定$data下該屬性的元素都將成為該屬性數(shù)據(jù)的訂閱者)
}

// 觀察者
Example.prototype.observer = function() {
    if (!this.$data || typeof this.$data !== 'object') return;
    
    var value = ''; // 記錄$data每個(gè)屬性的屬性值
    for (var key in this.$data) { // 遍歷數(shù)據(jù)對象
        value = this.$data[key]; // 對象屬性值
        this.binding[key] = []; // 初始化數(shù)據(jù)訂閱者,一對多關(guān)系,為一個(gè)數(shù)組
        var binding = this.binding[key]; // 存放當(dāng)前數(shù)據(jù)相關(guān)的所有訂閱者
        
        // 開始監(jiān)聽劫持
        this.defineReactive(this.$data, key, value, binding); // 通過創(chuàng)建方法實(shí)現(xiàn)數(shù)據(jù)分離,私有化,實(shí)現(xiàn)閉包
    }
}

Example.prototype.defineReactive = function (data, key, value, binding) {
    Object.defineProperty(data, key, {
        get() {
            return value; // 返回當(dāng)前值
        },
        set(newVal) { // newVal 為設(shè)置修改后的新值
            if (newVal !== value) {
                value = newVal; // 更新數(shù)據(jù)
                // 以后該屬性數(shù)據(jù)值改變后都會執(zhí)行一次數(shù)據(jù)更新
                binding.forEach(watcher => {
                    watcher.update(); // 通知與本數(shù)據(jù)相關(guān)的訂閱者們(即綁定該數(shù)據(jù)的DOM元素)進(jìn)行視圖更新
                })
            }
        }
    })
}

// 解析器 (解析指令并創(chuàng)建訂閱者)
Example.prototype.compile = function(el) {
    var nodes = el.children; // 獲取所有子節(jié)點(diǎn)(元素節(jié)點(diǎn))
    for (var i = 0; i < nodes.length; i ++) { // 遍歷子節(jié)點(diǎn)
        var node = nodes[i]; // 具體節(jié)點(diǎn)
        if (node.children.length > 0) { // 判斷是否具有子節(jié)點(diǎn)
            this.compile(node); // 遞歸
        }
        
        if (node.hasAttribute("z-on:click")) { // 該節(jié)點(diǎn)是否擁有 z-on:click 指令
            var attrVal = node.getAttribute('z-on:click'); // 獲取指令對應(yīng)的方法名
            // 為元素綁定click事件,事件方法為$methods下的方法,并將this指向this.$data
            node.addEventListener('click', this.$methods[attrVal].bind(this.$data));
        }
        
        if (node.hasAttribute("@click")) { // 該節(jié)點(diǎn)是否擁有@click指令
            var attrVal = node.getAttribute('@click'); // 獲取指令對應(yīng)的方法名
            // 為元素綁定click事件,事件方法為$methods下的方法,并將this指向this.$data
            node.addEventListener('click', this.$methods[attrVal].bind(this.$data));
        }
        
        if (node.hasAttribute("z-model")) { // 該節(jié)點(diǎn)是否擁有z-model指令
            var attrVal = node.getAttribute('z-model'); // 獲取指令對應(yīng)的數(shù)據(jù)屬性
            node.addEventListener("input", ((i) => { // 為指令添加input事件
                this.binding[attrVal].push(new Watcher(node, "value", this, attrVal)); // 將該元素添加為當(dāng)前數(shù)據(jù)的訂閱者唆垃,并將數(shù)據(jù)初始值作用與綁定指令的元素上

                return () => { // input事件處理函數(shù)
                    this.$data[attrVal] = nodes[i].value; // 更新$data的屬性值,會在觀察者中劫持
                }
            })(i));
        }
        
        if (node.hasAttribute("z-html")) { // 該節(jié)點(diǎn)是否擁有z-html指令
            var attrVal = node.getAttribute('z-html'); // 獲取指令對應(yīng)的數(shù)據(jù)屬性
            this.binding[attrVal].push(new Watcher(node, 'innerHTML', this, attrVal));
        }
        
        if (node.hasAttribute('z-text')) { // 該節(jié)點(diǎn)是否用擁有z-text指令
            var attrVal = node.getAttribute('z-text'); // 獲取指令對應(yīng)的數(shù)據(jù)屬性
            this.binding[attrVal].push(new Watcher(node, 'innerText', this, attrVal));
        }
    }
}

// 訂閱者
function Watcher(el, attr, vm, val) {
    this.el = el; // 指令對應(yīng)的元素
    this.attr = attr; // 要更改的元素屬性
    this.vm = vm; // 指令所在實(shí)例
    this.val = val; // 指令綁定的值
    this.update(); // 更新視圖view
}
// 數(shù)據(jù)變化,更新視圖痘儡。
Watcher.prototype.update = function() {
    this.el[this.attr] = this.vm.$data[this.val];
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末辕万,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子沉删,更是在濱河造成了極大的恐慌渐尿,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件矾瑰,死亡現(xiàn)場離奇詭異砖茸,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)殴穴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門凉夯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人采幌,你說我怎么就攤上這事劲够。” “怎么了休傍?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵征绎,是天一觀的道長。 經(jīng)常有香客問我尊残,道長炒瘸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任寝衫,我火速辦了婚禮顷扩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘慰毅。我一直安慰自己隘截,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著婶芭,像睡著了一般东臀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上犀农,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天惰赋,我揣著相機(jī)與錄音,去河邊找鬼呵哨。 笑死赁濒,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的孟害。 我是一名探鬼主播拒炎,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼挨务!你這毒婦竟也來了击你?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤谎柄,失蹤者是張志新(化名)和其女友劉穎丁侄,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谷誓,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡绒障,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了捍歪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片户辱。...
    茶點(diǎn)故事閱讀 40,675評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖糙臼,靈堂內(nèi)的尸體忽然破棺而出庐镐,到底是詐尸還是另有隱情,我是刑警寧澤变逃,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布必逆,位于F島的核電站,受9級特大地震影響揽乱,放射性物質(zhì)發(fā)生泄漏名眉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一凰棉、第九天 我趴在偏房一處隱蔽的房頂上張望损拢。 院中可真熱鬧,春花似錦撒犀、人聲如沸福压。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽荆姆。三九已至蒙幻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間胆筒,已是汗流浹背邮破。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留腐泻,地道東北人决乎。 一個(gè)月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像派桩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子蚌斩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評論 2 360