詳解Vue雙向數(shù)據(jù)綁定原理解析
基本原理
Vue.采用數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式,通過(guò)Object.defineProperty()來(lái)劫持各個(gè)屬性的setter和getter,數(shù)據(jù)變動(dòng)時(shí)發(fā)布消息給訂閱者,觸發(fā)相應(yīng)函數(shù)的回調(diào)。
思路整理
要實(shí)現(xiàn)mvvm的雙向綁定,需要實(shí)現(xiàn)如下幾點(diǎn):
1.實(shí)現(xiàn)一個(gè)數(shù)據(jù)監(jiān)聽(tīng)器Observer,能夠?qū)?duì)象的所有屬性進(jìn)行監(jiān)聽(tīng),發(fā)生變化時(shí)拿到最新值通知訂閱者
2.實(shí)現(xiàn)一個(gè)解析器Compile,對(duì)每個(gè)子元素節(jié)點(diǎn)的指令進(jìn)行掃描和解析,根據(jù)模板指令替換數(shù)據(jù),初始化視圖以及綁定相應(yīng)的回調(diào)函數(shù);
3.實(shí)現(xiàn)一個(gè)Watcher,作為Observer和Compile的橋梁,能夠訂閱屬性變動(dòng)的通知,執(zhí)行指令綁定的回調(diào)函數(shù),更新視圖
4.mvvm的入口,整合以上三者
流程圖如下:
分布實(shí)現(xiàn)
1. MVVM.js
function MVVM(options) { this.$options = options || {}; var data = this._data = this.$options.data; var me = this; // 數(shù)據(jù)代理 // 實(shí)現(xiàn) vm.xxx -> vm._data.xxx Object.keys(data).forEach(function(key) { me._proxyData(key); }); // 代理計(jì)算屬性 // 同樣通過(guò)Object.defineProperty進(jìn)行劫持 this._initComputed(); observe(data, this); this.$compile = new Compile(options.el || document.body, this) } MVVM.prototype = { $watch: function(key, cb, options) { new Watcher(this, key, cb); } }
MVVM入口文件,整合Observer/Compile/Watcher三者,達(dá)到數(shù)據(jù)變化->更新視圖;視圖變化->數(shù)據(jù)變更的雙向綁定效果。(結(jié)合鉤子函數(shù),理解Vue生命周期中各個(gè)階段的作用)
2. Observer.js
function Observer(data) { Object.keys(data).forEach(function() { defineReactive(data, key, data[key]); }); } function defineReactive (data, key, val) { var dep = new Dep(); var childObj = observe(val); Object.defineProperty(data, key, { enumerable: true, // 可枚舉 configurable: false, // 不能再define get: function() { if (Dep.target) { dep.depend(); } return val; }, set: function(newVal) { if (newVal === val) { return; } val = newVal; // 新的值是object的話,進(jìn)行監(jiān)聽(tīng) childObj = observe(newVal); // 通知訂閱者 dep.notify(); } }); }
對(duì)需要監(jiān)測(cè)的對(duì)象的每個(gè)屬性進(jìn)行遞歸遍歷,通過(guò)Object.defineProperty設(shè)置setter和getter。當(dāng)設(shè)置新的屬性值時(shí),觸發(fā)相應(yīng)的setter,通知訂閱者。
function Dep() { this.id = uid++; this.subs = []; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, depend: function() { Dep.target.addDep(this); }, notify: function() { this.subs.forEach(function(sub) { sub.update(); }); } };
訂閱者模式,每個(gè)屬性維護(hù)一個(gè)Dep,記錄自己的訂閱者(即watcher),notify通知每個(gè)訂閱者執(zhí)行相應(yīng)的update方法,更新視圖。
3. Compile.js
Compile做了兩件事情:
1.解析模板指令,替換變量,初始化渲染視圖;
2.生成一個(gè)watcher,注冊(cè)回調(diào)函數(shù),添加監(jiān)聽(tīng)數(shù)據(jù)的訂閱者,數(shù)據(jù)變動(dòng)時(shí),更新視圖
解析流程如下:
1.將DOM轉(zhuǎn)成文檔碎片fragment,提升查詢效率
2.遍歷所有元素節(jié)點(diǎn)及其子節(jié)點(diǎn),調(diào)用對(duì)應(yīng)的指令渲染函數(shù)渲染,并調(diào)用對(duì)應(yīng)的指令更新函數(shù)進(jìn)行綁定
3.將fragment添加回真實(shí)的DOM中
遍歷元素
function compileElement (el) { var childNodes = el.childNodes, me = this; [].slice.call(childNodes).forEach(function(node) { var text = node.textContent; var reg = /\{\{(.*)\}\}/; // 解析元素節(jié)點(diǎn) if (me.isElementNode(node)) { me.compile(node); // {{}}替換變量 } else if (me.isTextNode(node) && reg.test(text)) { me.compileText(node, RegExp.$1); } // 遞歸遍歷子節(jié)點(diǎn) if (node.childNodes && node.childNodes.length) { me.compileElement(node); } }); }
編譯元素節(jié)點(diǎn)
compile: function(node) { var nodeAttrs = node.attributes, me = this; [].slice.call(nodeAttrs).forEach(function(attr) { // 指令以v-xxx命名 // <span v-html="content"></span> var attrName = attr.name; // v-html if (me.isDirective(attrName)) { var exp = attr.value; // content var dir = attrName.substring(2); // 事件指令 if (me.isEventDirective(dir)) { compileUtil.eventHandler(node, me.$vm, exp, dir); // 普通指令 } else { compileUtil[dir] && compileUtil[dir](node, me.$vm, exp); } node.removeAttribute(attrName); } }); }
指令處理與更新函數(shù)
var compileUtil = { html: function(node, vm, exp) { this.bind(node, vm, exp, 'html'); }, bind: function(node, vm, exp, dir) { var updaterFn = updater[dir + 'Updater']; // 第一次初始化視圖 updaterFn && updaterFn(node, this._getVMVal(vm, exp)); // 實(shí)例化Watcher,添加訂閱者 new Watcher(vm, exp, function(value, oldValue) { // 屬性變化的視圖更新函數(shù) updaterFn && updaterFn(node, value, oldValue); }); }, } var Updater = { htmlUpdater: function(node, value) { node.innerHTML = typeof value == 'undefined' ? '' : value; } }
4. Watcher.js
Watcher作為Observer與Compile之間通信的橋梁,屬性變化的訂閱者,做了如下的事情:
1.自身實(shí)例化時(shí)在屬性訂閱器集合dep里添加自己
2.自身需有update方法
3.調(diào)用dep.notice時(shí),watcher調(diào)用自身的update ,觸發(fā)Compile中定義的回調(diào)
function Watcher(vm, expOrFn, cb) { this.cb = cb; this.vm = vm; this.expOrFn = expOrFn; this.value = this.get(); } Watcher.prototype = { update: function() { this.run(); }, run: function() { var value = this.get(); var oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); } }, get: function() { Dep.target = this; var value = this.getter.call(this.vm, this.vm); Dep.target = null; return value; } };
這里需要注意的點(diǎn)是,實(shí)例化watcher的時(shí)候,調(diào)用get方法,通過(guò)Dep.target = curInstance,強(qiáng)行觸發(fā)獲屬性值的getter方法,在屬性的訂閱器中添加當(dāng)前watcher實(shí)例。
小結(jié)
雙向綁定的原理很簡(jiǎn)單,通過(guò)數(shù)據(jù)劫持,當(dāng)設(shè)置新屬性值的時(shí)候通過(guò)訂閱者更新視圖;編譯指令,替換變量,同時(shí)綁定更新函數(shù)到訂閱者;對(duì)應(yīng)事件綁定調(diào)用addEventListener進(jìn)行監(jiān)聽(tīng)。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
解決antd 表單設(shè)置默認(rèn)值initialValue后驗(yàn)證失效的問(wèn)題
這篇文章主要介紹了解決antd 表單設(shè)置默認(rèn)值initialValue后驗(yàn)證失效的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11vue在使用element組件出現(xiàn)<el-input>標(biāo)簽無(wú)法輸入的問(wèn)題
這篇文章主要介紹了vue在使用element組件出現(xiàn)<el-input>標(biāo)簽無(wú)法輸入的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04unplugin-svg-component優(yōu)雅使用svg圖標(biāo)指南
這篇文章主要為大家介紹了unplugin-svg-component優(yōu)雅使用svg圖標(biāo)指南,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03詳解TypeScript+Vue 插件 vue-class-component的使用總結(jié)
這篇文章主要介紹了TypeScript+Vue 插件 vue-class-component的使用總結(jié),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-02-02Vue.js學(xué)習(xí)記錄之在元素與template中使用v-if指令實(shí)例
這篇文章主要給大家介紹了關(guān)于Vue.js學(xué)習(xí)記錄之在元素與template中使用v-if指令的相關(guān)資料,文中給出了詳細(xì)的示例代碼供大家參考學(xué)習(xí),相信對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-06-06vue中echarts圖表大小適應(yīng)窗口大小且不需要刷新案例
這篇文章主要介紹了vue中echarts圖表大小適應(yīng)窗口大小且不需要刷新案例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-07-07vue.js 實(shí)現(xiàn)v-model與{{}}指令方法
這篇文章主要介紹了vue.js 實(shí)現(xiàn)v-model與{{}}指令方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-10-10vue學(xué)習(xí)筆記之作用域插槽實(shí)例分析
這篇文章主要介紹了vue學(xué)習(xí)筆記之作用域插槽,結(jié)合實(shí)例形式分析了vue.js作用域插槽基本使用方法及操作注意事項(xiàng),需要的朋友可以參考下2020-02-02