摘要:兼容性更詳細(xì)的可以看一下實(shí)現(xiàn)思路系列的雙向綁定,關(guān)鍵步驟實(shí)現(xiàn)數(shù)據(jù)監(jiān)聽(tīng)器,用重寫數(shù)據(jù)的,值更新就在中通知訂閱者更新數(shù)據(jù)。
前言
現(xiàn)在的前端面試不管你用的什么框架,總會(huì)問(wèn)你這個(gè)框架的雙向綁定機(jī)制,有的甚至要求你現(xiàn)場(chǎng)實(shí)現(xiàn)一個(gè)雙向綁定出來(lái),那對(duì)于沒(méi)有好好研究過(guò)這方面知識(shí)的同學(xué)來(lái)說(shuō),當(dāng)然是很難的,接下來(lái)本文用160行代碼帶你實(shí)現(xiàn)一個(gè)極簡(jiǎn)的雙向綁定機(jī)制。如果喜歡的話可以點(diǎn)波贊/關(guān)注,支持一下,希望大家看完本文可以有所收獲。
本文是在面試題:你能寫一個(gè)Vue的雙向數(shù)據(jù)綁定嗎?的基礎(chǔ)上仔細(xì)研究+改動(dòng),并添加了詳細(xì)注釋,而成的。
個(gè)人博客了解一下:obkoro1.com效果GIF: demo地址:
codepen:仿Vue極簡(jiǎn)雙向綁定
Github:仿Vue極簡(jiǎn)雙向綁定
了解Object.defineProperty():這個(gè)API是實(shí)現(xiàn)雙向綁定的核心,最主要的作用是重寫數(shù)據(jù)的get、set方法。
使用方式:let obj = { singer: "周杰倫" }; let value = "青花瓷"; Object.defineProperty(obj, "music", { // value: "七里香", // 設(shè)置屬性的值 下面設(shè)置了get set函數(shù) 所以這里不能設(shè)置 configurable: false, // 是否可以刪除屬性 默認(rèn)不能刪除 // writable: true, // 是否可以修改對(duì)象 下面設(shè)置了get set函數(shù) 所以這里不能設(shè)置 enumerable: true, // music是否可以被枚舉 默認(rèn)是不能被枚舉(遍歷) // ☆ get,set設(shè)置時(shí)不能設(shè)置writable和value,要一對(duì)一對(duì)設(shè)置,交叉設(shè)置/同時(shí)存在 就會(huì)報(bào)錯(cuò) get() { // 獲取obj.music的時(shí)候就會(huì)調(diào)用get方法 // let value = "強(qiáng)行設(shè)置get的返回值"; // 打開(kāi)注釋 讀取屬性永遠(yuǎn)都是‘強(qiáng)行設(shè)置get的返回值’ return value; }, set(val) { // 將修改的值重新賦給song value = val; } }); console.log(obj.music); // 青花瓷 delete obj.music; // configurable設(shè)為false 刪除無(wú)效 console.log(obj.music); // 青花瓷 obj.music = "聽(tīng)媽媽的話"; console.log(obj.music); // 聽(tīng)媽媽的話 for (let key in obj) { // 默認(rèn)情況下通過(guò)defineProperty定義的屬性是不能被枚舉(遍歷)的 // 需要設(shè)置enumerable為true才可以 否則只能拿到singer 屬性 console.log(key); // singer, music }示例demo:
對(duì),這里有個(gè)demo。
畫一下重點(diǎn):get,set設(shè)置時(shí)不能設(shè)置writable和value, 他們是一對(duì)情侶的存在,交叉設(shè)置或同時(shí)存在,會(huì)報(bào)錯(cuò)
通過(guò)defineProperty設(shè)置的屬性,默認(rèn)不能刪除,不能遍歷,當(dāng)然你可以通過(guò)設(shè)置更改他們。
get、set 是函數(shù),可以做的事情很多。
兼容性:IE 9,Firefox 4, Chorme 5,Opera 11.6,Safari 5.1
更詳細(xì)的可以看一下MDN
實(shí)現(xiàn)思路: mvvm系列的雙向綁定,關(guān)鍵步驟:實(shí)現(xiàn)數(shù)據(jù)監(jiān)聽(tīng)器Observer,用Object.defineProperty()重寫數(shù)據(jù)的get、set,值更新就在set中通知訂閱者更新數(shù)據(jù)。
實(shí)現(xiàn)模板編譯Compile,深度遍歷dom樹(shù),對(duì)每個(gè)元素節(jié)點(diǎn)的指令模板進(jìn)行替換數(shù)據(jù)以及訂閱數(shù)據(jù)。
實(shí)現(xiàn)Watch用于連接Observer和Compile,能夠訂閱并收到每個(gè)屬性變動(dòng)的通知,執(zhí)行指令綁定的相應(yīng)回調(diào)函數(shù),從而更新視圖。
mvvm入口函數(shù),整合以上三者。
流程圖:這部分講的很清楚,現(xiàn)在有點(diǎn)懵逼也沒(méi)關(guān)系,看完代碼,自己copy下來(lái)玩一玩之后,回頭再看實(shí)現(xiàn)思路,相信會(huì)有收獲的。
具體代碼實(shí)現(xiàn): html結(jié)構(gòu):{{ testData1 }}
{{ testData2 }}
看到這個(gè)模板,相信用過(guò)Vue的同學(xué)都不會(huì)陌生。
調(diào)用方法:采用類Vue方式來(lái)使用雙向綁定:
window.onload = function () { var app = new myVue({ el: "#app", // dom data: { // 數(shù)據(jù) testData1: "仿Vue", testData2: "極簡(jiǎn)雙向綁定", name: "OBKoro1" } }) }創(chuàng)建myVue函數(shù):
實(shí)際上這里是我們實(shí)現(xiàn)思路中的第四步,用于整合數(shù)據(jù)監(jiān)聽(tīng)器this._observer()、指令解析器this._compile()以及連接Observer和Compile的_watcherTpl的watch池。
function myVue(options = {}) { // 防止沒(méi)傳,設(shè)一個(gè)默認(rèn)值 this.$options = options; // 配置掛載 this.$el = document.querySelector(options.el); // 獲取dom this._data = options.data; // 數(shù)據(jù)掛載 this._watcherTpl = {}; // watcher池 this._observer(this._data); // 傳入數(shù)據(jù),執(zhí)行函數(shù),重寫數(shù)據(jù)的get set this._compile(this.$el); // 傳入dom,執(zhí)行函數(shù),編譯模板 發(fā)布訂閱 };Watcher函數(shù):
這是實(shí)現(xiàn)思路中的第三步,因?yàn)橄路綌?shù)據(jù)監(jiān)聽(tīng)器_observer()需要用到Watcher函數(shù),所以這里就先講了。
像實(shí)現(xiàn)思路中所說(shuō)的,這里起到了連接Observer和Compile的作用:
在模板編譯_compile()階段發(fā)布訂閱
在賦值操作的時(shí)候,更新視圖
// new Watcher() 為this._compile()發(fā)布訂閱+ 在this._observer()中set(賦值)的時(shí)候更新視圖 function Watcher(el, vm, val, attr) { this.el = el; // 指令對(duì)應(yīng)的DOM元素 this.vm = vm; // myVue實(shí)例 this.val = val; // 指令對(duì)應(yīng)的值 this.attr = attr; // dom獲取值,如value獲取input的值 / innerHTML獲取dom的值 this.update(); // 更新視圖 } Watcher.prototype.update = function () { this.el[this.attr] = this.vm._data[this.val]; // 獲取data的最新值 賦值給dom 更新視圖 }
沒(méi)有看錯(cuò),代碼量就這么多,可能需要把整個(gè)代碼連接起來(lái),多看幾遍才能夠理解。
實(shí)現(xiàn)數(shù)據(jù)監(jiān)聽(tīng)器_observer():實(shí)現(xiàn)思路中的第一步,用Object.defineProperty()遍歷data重寫所有屬性的get set。
然后在給對(duì)象的某個(gè)屬性賦值的時(shí)候,就會(huì)觸發(fā)set。
在set中我們可以監(jiān)聽(tīng)到數(shù)據(jù)的變化,然后就可以觸發(fā)watch更新視圖。
myVue.prototype._observer = function (obj) { var _this = this; Object.keys(obj).forEach(key => { // 遍歷數(shù)據(jù) _this._watcherTpl[key] = { // 每個(gè)數(shù)據(jù)的訂閱池() _directives: [] }; var value = obj[key]; // 獲取屬性值 var watcherTpl = _this._watcherTpl[key]; // 數(shù)據(jù)的訂閱池 Object.defineProperty(_this._data, key, { // 雙向綁定最重要的部分 重寫數(shù)據(jù)的set get configurable: true, // 可以刪除 enumerable: true, // 可以遍歷 get() { console.log(`${key}獲取值:${value}`); return value; // 獲取值的時(shí)候 直接返回 }, set(newVal) { // 改變值的時(shí)候 觸發(fā)set console.log(`${key}更新:${newVal}`); if (value !== newVal) { value = newVal; watcherTpl._directives.forEach((item) => { // 遍歷訂閱池 item.update(); // 遍歷所有訂閱的地方(v-model+v-bind+{{}}) 觸發(fā)this._compile()中發(fā)布的訂閱Watcher 更新視圖 }); } } }) }); }實(shí)現(xiàn)Compile 模板編譯
這里是實(shí)現(xiàn)思路中的第三步,讓我們來(lái)總結(jié)一下這里做了哪些事情:
首先是深度遍歷dom樹(shù),遍歷每個(gè)節(jié)點(diǎn)以及子節(jié)點(diǎn)。
將模板中的變量替換成數(shù)據(jù),初始化渲染頁(yè)面視圖。
把指令綁定的屬性添加到對(duì)應(yīng)的訂閱池中
一旦數(shù)據(jù)有變動(dòng),收到通知,更新視圖。
myVue.prototype._compile = function (el) { var _this = this, nodes = el.children; // 獲取app的dom for (var i = 0, len = nodes.length; i < len; i++) { // 遍歷dom節(jié)點(diǎn) var node = nodes[i]; if (node.children.length) { _this._compile(node); // 遞歸深度遍歷 dom樹(shù) } // 如果有v-model屬性,并且元素是INPUT或者TEXTAREA,我們監(jiān)聽(tīng)它的input事件 if (node.hasAttribute("v-model") && (node.tagName = "INPUT" || node.tagName == "TEXTAREA")) { node.addEventListener("input", (function (key) { var attVal = node.getAttribute("v-model"); // 獲取v-model綁定的值 _this._watcherTpl[attVal]._directives.push(new Watcher( // 將dom替換成屬性的數(shù)據(jù)并發(fā)布訂閱 在set的時(shí)候更新數(shù)據(jù) node, _this, attVal, "value" )); return function () { _this._data[attVal] = nodes[key].value; // input值改變的時(shí)候 將新值賦給數(shù)據(jù) 觸發(fā)set=>set觸發(fā)watch 更新視圖 } })(i)); } if (node.hasAttribute("v-bind")) { // v-bind指令 var attrVal = node.getAttribute("v-bind"); // 綁定的data _this._watcherTpl[attrVal]._directives.push(new Watcher( // 將dom替換成屬性的數(shù)據(jù)并發(fā)布訂閱 在set的時(shí)候更新數(shù)據(jù) node, _this, attrVal, "innerHTML" )) } var reg = /{{s*([^}]+S)s*}}/g, txt = node.textContent; // 正則匹配{{}} if (reg.test(txt)) { node.textContent = txt.replace(reg, (matched, placeholder) => { // matched匹配的文本節(jié)點(diǎn)包括{{}}, placeholder 是{{}}中間的屬性名 var getName = _this._watcherTpl; // 所有綁定watch的數(shù)據(jù) getName = getName[placeholder]; // 獲取對(duì)應(yīng)watch 數(shù)據(jù)的值 if (!getName._directives) { // 沒(méi)有事件池 創(chuàng)建事件池 getName._directives = []; } getName._directives.push(new Watcher( // 將dom替換成屬性的數(shù)據(jù)并發(fā)布訂閱 在set的時(shí)候更新數(shù)據(jù) node, _this, placeholder, "innerHTML" )); return placeholder.split(".").reduce((val, key) => { return _this._data[key]; // 獲取數(shù)據(jù)的值 觸發(fā)get 返回當(dāng)前值 }, _this.$el); }); } } }完整代碼&demo地址
GitHub完整代碼
codepen:仿Vue極簡(jiǎn)雙向綁定
Github:仿Vue極簡(jiǎn)雙向綁定
如果覺(jué)得還不錯(cuò)的話,就給個(gè)Star??鼓勵(lì)一下我吧~
結(jié)語(yǔ)本文只是一個(gè)簡(jiǎn)單的實(shí)現(xiàn)雙向綁定的方法,主要目的是幫助各位同學(xué)理解mvvm框架的雙向綁定機(jī)制,也并沒(méi)有很完善,這里還是有很多缺陷,比如:沒(méi)有實(shí)現(xiàn)數(shù)據(jù)的深度對(duì)數(shù)據(jù)進(jìn)行get、set等。希望看完本文,大家能有所收獲。
希望看完的朋友可以點(diǎn)個(gè)喜歡/關(guān)注,您的支持是對(duì)我最大的鼓勵(lì)。個(gè)人blog and 掘金個(gè)人主頁(yè),如需轉(zhuǎn)載,請(qǐng)放上原文鏈接并署名。碼字不易,感謝支持!本人寫文章本著交流記錄的心態(tài),寫的不好之處,不撕逼,但是歡迎指點(diǎn)。
如果喜歡本文的話,歡迎關(guān)注我的訂閱號(hào),漫漫技術(shù)路,期待未來(lái)共同學(xué)習(xí)成長(zhǎng)。
以上2018.6.24
參考資料:剖析Vue原理&實(shí)現(xiàn)雙向綁定MVVM
面試題:你能寫一個(gè)Vue的雙向數(shù)據(jù)綁定嗎?
不好意思!耽誤你的十分鐘,讓MVVM原理還給你
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/95662.html
摘要:的雙向數(shù)據(jù)綁定方法我覺(jué)得很巧妙我是尤雨溪腦殘粉為什么選美團(tuán)外賣美團(tuán)外賣項(xiàng)目估計(jì)差不多能夠設(shè)計(jì)到大部分技術(shù)點(diǎn),不包括服務(wù)端渲染作為練習(xí)夠用了。平日里我點(diǎn)外賣一直用美團(tuán)。用多了可能也比較了解美團(tuán)外賣吧。 前言 很多初學(xué)者尤其是像我這樣的公司有且只有一個(gè)前端的時(shí)候,硬著頭皮去學(xué)習(xí)一門新框架,周圍無(wú)人幫忙,平日里遇到問(wèn)題只能求助于思否,百度,google。點(diǎn)擊我的個(gè)人頭像去看我的提問(wèn)你們就知道...
摘要:仿滴滴出行項(xiàng)目最近,各大社區(qū)出現(xiàn)很多小伙伴的項(xiàng)目,趁著這股熱潮我也用擼了一個(gè)滴滴出行的項(xiàng)目。可是,后來(lái)在手機(jī)上發(fā)現(xiàn),輸入的時(shí)候居然調(diào)不出軟鍵盤,寫項(xiàng)目的時(shí)候沒(méi)考慮到設(shè)備問(wèn)題,簡(jiǎn)直是大大的失誤。也就是說(shuō)可以在組件內(nèi)部進(jìn)行請(qǐng)求,不需要提交。 Vue2.0 仿滴滴出行項(xiàng)目 最近,各大社區(qū)出現(xiàn)很多小伙伴的vue項(xiàng)目,趁著這股熱潮我也用vue擼了一個(gè)滴滴出行的項(xiàng)目。 效果預(yù)覽 showImg(h...
摘要:目標(biāo)你好上面是最常見(jiàn)的的用法現(xiàn)在我就只實(shí)現(xiàn)一件事改變執(zhí)行這一句時(shí)頁(yè)面會(huì)及時(shí)更新開(kāi)始動(dòng)工第一步先聲明一個(gè)類我們一開(kāi)始定義的屬性是定義在中的我們想要這樣賦值的時(shí)候和的相關(guān)聯(lián)需要中間做一個(gè)代理修改代碼執(zhí)行函數(shù)實(shí)現(xiàn)代理觀察的屬性要想實(shí)現(xiàn)這樣賦值的時(shí) 目標(biāo) html {{ someStr }} js let myMvvm = new Mvvm({ el: documen...
摘要:目標(biāo)你好上面是最常見(jiàn)的的用法現(xiàn)在我就只實(shí)現(xiàn)一件事改變執(zhí)行這一句時(shí)頁(yè)面會(huì)及時(shí)更新開(kāi)始動(dòng)工第一步先聲明一個(gè)類我們一開(kāi)始定義的屬性是定義在中的我們想要這樣賦值的時(shí)候和的相關(guān)聯(lián)需要中間做一個(gè)代理修改代碼執(zhí)行函數(shù)實(shí)現(xiàn)代理觀察的屬性要想實(shí)現(xiàn)這樣賦值的時(shí) 目標(biāo) html {{ someStr }} js let myMvvm = new Mvvm({ el: documen...
閱讀 2766·2019-08-30 15:53
閱讀 536·2019-08-29 17:22
閱讀 1072·2019-08-29 13:10
閱讀 2329·2019-08-26 13:45
閱讀 2761·2019-08-26 10:46
閱讀 3209·2019-08-26 10:45
閱讀 2516·2019-08-26 10:14
閱讀 477·2019-08-23 18:23