vue 數(shù)據(jù)雙向綁定的實(shí)現(xiàn)方法
1. 前言
本文適合于學(xué)習(xí)Vue源碼的初級(jí)學(xué)者,閱讀后,你將對(duì)Vue的數(shù)據(jù)雙向綁定原理有一個(gè)大致的了解,認(rèn)識(shí)Observer、Compile、Wathcer三大角色(如下圖所示)以及它們所發(fā)揮的功能。
本文將一步步帶你實(shí)現(xiàn)簡(jiǎn)易版的數(shù)據(jù)雙向綁定,每一步都會(huì)詳細(xì)分析這一步要解決的問(wèn)題以及代碼為何如此寫(xiě),因此,在閱讀完本文后,希望你能自己動(dòng)手實(shí)現(xiàn)一個(gè)簡(jiǎn)易版數(shù)據(jù)雙向綁定。
2. 代碼實(shí)現(xiàn)
2.1 目的分析
本文要實(shí)現(xiàn)的效果如下圖所示:
本文用到的HTML和JS主體代碼如下:
<div id="app"> <h1 v-text="msg"></h1> <input type="text" v-model="msg"> <div> <h1 v-text="msg2"></h1> <input type="text" v-model="msg2"> </div> </div>
let vm = new Vue({ el: "#app", data: { msg: "hello world", msg2: "hello xiaofei" } })
我們將按照下面三個(gè)步驟來(lái)實(shí)現(xiàn):
- 第一步:將data中的數(shù)據(jù)同步到頁(yè)面上,實(shí)現(xiàn) M ==> V 的初始化;
- 第二步:當(dāng)input框中輸入值時(shí),將新值同步到data中,實(shí)現(xiàn) V ==> M 的綁定;
- 第三步:當(dāng)data數(shù)據(jù)發(fā)生更新的時(shí)候,觸發(fā)頁(yè)面發(fā)生變化,實(shí)現(xiàn) M ==> V 的綁定。
2.2 實(shí)現(xiàn)過(guò)程
2.2.1 入口代碼
首先,我們要?jiǎng)?chuàng)造一個(gè)Vue類,這個(gè)類接收一個(gè) options 對(duì)象,同時(shí),我們要對(duì) options 對(duì)象中的有效信息進(jìn)行保存;
然后,我們有三個(gè)主要模塊:Observer、Compile、Wathcer,其中,Observer用來(lái)數(shù)據(jù)劫持的,Compile用來(lái)解析元素,Wathcer是觀察者??梢詫?xiě)出如下代碼:(Observer、Compile、Wathcer這三個(gè)概念,不用細(xì)究,后面會(huì)詳解講解)。
class Vue { // 接收傳進(jìn)來(lái)的對(duì)象 constructor(options) { // 保存有效信息 this.$el = document.querySelector(options.el); this.$data = options.data; // 容器: {屬性1: [wathcer1, wathcer2...], 屬性2: [...]},用來(lái)存放每個(gè)屬性觀察者 this.$watcher = {}; // 解析元素: 實(shí)現(xiàn)Compile this.compile(this.$el); // 要解析元素, 就得把元素傳進(jìn)去 // 劫持?jǐn)?shù)據(jù): 實(shí)現(xiàn) Observer this.observe(this.$data); // 要劫持?jǐn)?shù)據(jù), 就得把數(shù)據(jù)傳入 } compile() {} observe() {} }
2.2.2 頁(yè)面初始化
在這一步,我們要實(shí)現(xiàn)頁(yè)面的初始化,即解析出v-text和v-model指令,并將data中的數(shù)據(jù)渲染到頁(yè)面中。
這一步的關(guān)鍵在于實(shí)現(xiàn)compile方法,那么該如何解析el元素呢?思路如下:
- 首先要獲取到el下面的所有子節(jié)點(diǎn),然后遍歷這些子節(jié)點(diǎn),如果子節(jié)點(diǎn)還有子節(jié)點(diǎn),那我們就需要用到遞歸的思想;
- 遍歷子節(jié)點(diǎn)找到所有有指令的元素,并將對(duì)應(yīng)的數(shù)據(jù)渲染到頁(yè)面中。
代碼如下:(主要看compile那部分)
class Vue { // 接收傳進(jìn)來(lái)的對(duì)象 constructor(options) { // 獲取有用信息 this.$el = document.querySelector(options.el); this.$data = options.data; // 容器: {屬性1: [wathcer1, wathcer2...], 屬性2: [...]} this.$watcher = {}; // 2. 解析元素: 實(shí)現(xiàn)Compile this.compile(this.$el); // 要解析元素, 就得把元素傳進(jìn)去 // 3. 劫持?jǐn)?shù)據(jù): 實(shí)現(xiàn) Observer this.observe(this.$data); // 要劫持?jǐn)?shù)據(jù), 就得把數(shù)據(jù)傳入 } compile(el) { // 解析元素下的每一個(gè)子節(jié)點(diǎn), 所以要獲取el.children // 備注: children 返回元素集合, childNodes返回節(jié)點(diǎn)集合 let nodes = el.children; // 解析每個(gè)子節(jié)點(diǎn)的指令 for (var i = 0, length = nodes.length; i < length; i++) { let node = nodes[i]; // 如果當(dāng)前節(jié)點(diǎn)還有子元素, 遞歸解析該節(jié)點(diǎn) if(node.children){ this.compile(node); } // 解析帶有v-text指令的元素 if (node.hasAttribute("v-text")) { let attrVal = node.getAttribute("v-text"); node.textContent = this.$data[attrVal]; // 渲染頁(yè)面 } // 解析帶有v-model指令的元素 if (node.hasAttribute("v-model")) { let attrVal = node.getAttribute("v-model"); node.value = this.$data[attrVal]; } } } observe(data) {} }
這樣,我們就實(shí)現(xiàn)頁(yè)面的初始化了。
2.2.3 視圖影響數(shù)據(jù)
因?yàn)閕nput帶有v-model指令,因此我們要實(shí)現(xiàn)這樣一個(gè)功能:在input框中輸入字符,data中綁定的數(shù)據(jù)發(fā)生相應(yīng)的改變。
我們可以在input這個(gè)元素上綁定一個(gè)input事件,事件的效果就是:將data中的相應(yīng)數(shù)據(jù)修改為input中的值。
這一部分的實(shí)現(xiàn)代碼比較簡(jiǎn)單,只要看標(biāo)注那個(gè)地方就明白了,代碼如下:
class Vue { constructor(options) { this.$el = document.querySelector(options.el); this.$data = options.data; this.$watcher = {}; this.compile(this.$el); this.observe(this.$data); } compile(el) { let nodes = el.children; for (var i = 0, length = nodes.length; i < length; i++) { let node = nodes[i]; if(node.children){ this.compile(node); } if (node.hasAttribute("v-text")) { let attrVal = node.getAttribute("v-text"); node.textContent = this.$data[attrVal]; } if (node.hasAttribute("v-model")) { let attrVal = node.getAttribute("v-model"); node.value = this.$data[attrVal]; // 看這里??!只多了三行代碼!! node.addEventListener("input", (ev)=>{ this.$data[attrVal] = ev.target.value; // 可以試著在這里執(zhí)行:console.log(this.$data), // 就可以看到每次在輸入框輸入文字的時(shí)候,data中的msg值也發(fā)生了變化 }) } } } observe(data) {} }
2.2.4 數(shù)據(jù)影響視圖
至此,我們已經(jīng)實(shí)現(xiàn)了:當(dāng)我們?cè)趇nput框中輸入字符的時(shí)候,data中的數(shù)據(jù)會(huì)自動(dòng)發(fā)生更新;
本小節(jié)的主要任務(wù)是:當(dāng)data中的數(shù)據(jù)發(fā)生更新的時(shí)候,綁定了該數(shù)據(jù)的元素會(huì)在頁(yè)面上自動(dòng)更新視圖。具體思路如下:
1) 我們將要實(shí)現(xiàn)一個(gè) Wathcer 類,它有一個(gè)update方法,用來(lái)更新頁(yè)面。觀察者的代碼如下:
class Watcher{ constructor(node, updatedAttr, vm, expression){ // 將傳進(jìn)來(lái)的值保存起來(lái),這些數(shù)據(jù)都是渲染頁(yè)面時(shí)要用到的數(shù)據(jù) this.node = node; this.updatedAttr = updatedAttr; this.vm = vm; this.expression = expression; this.update(); } update(){ this.node[this.updatedAttr] = this.vm.$data[this.expression]; } }
2) 試想,我們?cè)摻o哪些數(shù)據(jù)添加觀察者?何時(shí)給數(shù)據(jù)添加觀察者?
在解析元素的時(shí)候,當(dāng)解析到v-text和v-model指令的時(shí)候,說(shuō)明這個(gè)元素是需要和數(shù)據(jù)雙向綁定的,因此我們?cè)谶@時(shí)往容器中添加觀察者。我們需用到這樣一個(gè)數(shù)據(jù)結(jié)構(gòu):{屬性1: [wathcer1, wathcer2...], 屬性2: [...]},如果不是很清晰,可以看下圖:
可以看到:vue實(shí)例中有一個(gè)$wathcer對(duì)象,$wathcer的每個(gè)屬性對(duì)應(yīng)每個(gè)需要綁定的數(shù)據(jù),值是一個(gè)數(shù)組,用來(lái)存放觀察了該數(shù)據(jù)的觀察者。(備注:Vue源碼中專門(mén)創(chuàng)造了Dep這么一個(gè)類,對(duì)應(yīng)這里所說(shuō)的數(shù)組,本文屬于簡(jiǎn)易版本,就不過(guò)多介紹了)
3) 劫持?jǐn)?shù)據(jù):利用對(duì)象的訪問(wèn)器屬性getter和setter做到當(dāng)數(shù)據(jù)更新的時(shí)候,觸發(fā)一個(gè)動(dòng)作,這個(gè)動(dòng)作的主要目的就是讓所有觀察了該數(shù)據(jù)的觀察者執(zhí)行update方法。
總結(jié)一下,在本小節(jié)我們需要做的工作:
- 實(shí)現(xiàn)一個(gè)Wathcer類;
- 在解析指令的時(shí)候(即在compile方法中)添加觀察者;
- 實(shí)現(xiàn)數(shù)據(jù)劫持(實(shí)現(xiàn)observe方法)。
完整代碼如下:
class Vue { // 接收傳進(jìn)來(lái)的對(duì)象 constructor(options) { // 獲取有用信息 this.$el = document.querySelector(options.el); this.$data = options.data; // 容器: {屬性1: [wathcer1, wathcer2...], 屬性2: [...]} this.$watcher = {}; // 解析元素: 實(shí)現(xiàn)Compile this.compile(this.$el); // 要解析元素, 就得把元素傳進(jìn)去 // 劫持?jǐn)?shù)據(jù): 實(shí)現(xiàn) Observer this.observe(this.$data); // 要劫持?jǐn)?shù)據(jù), 就得把數(shù)據(jù)傳入 } compile(el) { // 解析元素下的每一個(gè)子節(jié)點(diǎn), 所以要獲取el.children // 拓展: children 返回元素集合, childNodes返回節(jié)點(diǎn)集合 let nodes = el.children; // 解析每個(gè)子節(jié)點(diǎn)的指令 for (var i = 0, length = nodes.length; i < length; i++) { let node = nodes[i]; // 如果當(dāng)前節(jié)點(diǎn)還有子元素, 遞歸解析該節(jié)點(diǎn) if (node.children) { this.compile(node); } if (node.hasAttribute("v-text")) { let attrVal = node.getAttribute("v-text"); // node.textContent = this.$data[attrVal]; // Watcher在實(shí)例化時(shí)調(diào)用update, 替代了這行代碼 /** * 試想Wathcer要更新節(jié)點(diǎn)數(shù)據(jù)的時(shí)候要用到哪些數(shù)據(jù)? * e.g. p.innerHTML = vm.$data[msg] * 所以要傳入的參數(shù)依次是: 當(dāng)前節(jié)點(diǎn)node, 需要更新的節(jié)點(diǎn)屬性, vue實(shí)例, 綁定的數(shù)據(jù)屬性 */ // 往容器中添加觀察者: {msg1: [Watcher, Watcher...], msg2: [...]} if (!this.$watcher[attrVal]) { this.$watcher[attrVal] = []; } this.$watcher[attrVal].push(new Watcher(node, "innerHTML", this, attrVal)) } if (node.hasAttribute("v-model")) { let attrVal = node.getAttribute("v-model"); node.value = this.$data[attrVal]; node.addEventListener("input", (ev) => { this.$data[attrVal] = ev.target.value; }) if (!this.$watcher[attrVal]) { this.$watcher[attrVal] = []; } // 不同于上處用的innerHTML, 這里input用的是vaule屬性 this.$watcher[attrVal].push(new Watcher(node, "value", this, attrVal)) } } } observe(data) { Object.keys(data).forEach((key) => { let val = data[key]; // 這個(gè)val將一直保存在內(nèi)存中,每次訪問(wèn)data[key],都是在訪問(wèn)這個(gè)val Object.defineProperty(data, key, { get() { return val; // 這里不能直接返回data[key],不然會(huì)陷入無(wú)限死循環(huán) }, set(newVal) { if (val !== newVal) { val = newVal;// 同理,這里不能直接對(duì)data[key]進(jìn)行設(shè)置,會(huì)陷入死循環(huán) this.$watcher[key].forEach((w) => { w.update(); }) } } }) }) } } class Watcher { constructor(node, updatedAttr, vm, expression) { // 將傳進(jìn)來(lái)的值保存起來(lái) this.node = node; this.updatedAttr = updatedAttr; this.vm = vm; this.expression = expression; this.update(); } update() { this.node[this.updatedAttr] = this.vm.$data[this.expression]; } } let vm = new Vue({ el: "#app", data: { msg: "hello world", msg2: "hello xiaofei" } })
至此,代碼就完成了。
3. 未來(lái)的計(jì)劃
用設(shè)計(jì)模式的知識(shí),分析上面這份源碼存在的問(wèn)題,并和Vue源碼進(jìn)行比對(duì),算是對(duì)Vue源碼的解析
以上就是vue 數(shù)據(jù)雙向綁定的實(shí)現(xiàn)方法的詳細(xì)內(nèi)容,更多關(guān)于vue 數(shù)據(jù)雙向綁定的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- vue子組件改變父組件傳遞的prop值通過(guò)sync實(shí)現(xiàn)數(shù)據(jù)雙向綁定(DEMO)
- Vue.js實(shí)現(xiàn)數(shù)據(jù)雙向綁定的代碼示例
- 使用Vue.js實(shí)現(xiàn)數(shù)據(jù)的雙向綁定
- Vue父子組件數(shù)據(jù)雙向綁定(父?jìng)髯印⒆觽鞲福┘皉ef、$refs、is、:is的使用與區(qū)別
- 淺析Vue3中通過(guò)v-model實(shí)現(xiàn)父子組件的雙向數(shù)據(jù)綁定及利用computed簡(jiǎn)化父子組件雙向綁定
- vue中如何解除數(shù)據(jù)之間的雙向綁定
- vue3中reactive數(shù)據(jù)被重新賦值后無(wú)法雙向綁定的解決
- proxy實(shí)現(xiàn)vue3數(shù)據(jù)雙向綁定原理
- vue自定義組件實(shí)現(xiàn)v-model雙向綁定數(shù)據(jù)的實(shí)例代碼
- Vue項(xiàng)目開(kāi)發(fā)實(shí)現(xiàn)父組件與子組件數(shù)據(jù)間的雙向綁定原理及適用場(chǎng)景
相關(guān)文章
vue webpack開(kāi)發(fā)訪問(wèn)后臺(tái)接口全局配置的方法
今天小編就為大家分享一篇vue webpack開(kāi)發(fā)訪問(wèn)后臺(tái)接口全局配置的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-09-09vue學(xué)習(xí)筆記之指令v-text && v-html && v-bind詳解
這篇文章主要介紹了vue學(xué)習(xí)筆記之指令v-text && v-html && v-bind詳解,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-05-05Vue 設(shè)置axios請(qǐng)求格式為form-data的操作步驟
今天小編就為大家分享一篇Vue 設(shè)置axios請(qǐng)求格式為form-data的操作步驟,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-10-10Vue3子組件向父組件傳值的兩種實(shí)現(xiàn)方式
近期學(xué)習(xí)vue3的父子組件之間的傳值,發(fā)現(xiàn)跟vue2的并沒(méi)有太大的區(qū)別,這篇文章主要給大家介紹了關(guān)于Vue3子組件向父組件傳值的兩種實(shí)現(xiàn)方式,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-04-04elementUI中input回車(chē)觸發(fā)頁(yè)面刷新問(wèn)題與解決方法
這篇文章主要給大家介紹了關(guān)于elementUI中input回車(chē)觸發(fā)頁(yè)面刷新問(wèn)題與解決方法,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用elementUI具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-07-07vue實(shí)現(xiàn)未登錄跳轉(zhuǎn)到登錄頁(yè)面的方法
這篇文章主要介紹了vue實(shí)現(xiàn)未登錄跳轉(zhuǎn)到登錄頁(yè)面的方法,主要目的是實(shí)現(xiàn)未登錄跳轉(zhuǎn),需要的朋友參考下吧2018-07-07vue+springboot用戶注銷功能實(shí)現(xiàn)代碼
這篇文章主要介紹了vue+springboot用戶注銷功能,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-05-05