通俗易懂講解並手寫一個vue資料雙向繫結案例
問題描述
面試中,面試官除了問基礎知識以外,還喜歡問一些框架原理。比如: 你對vue的資料雙向繫結mvvm是如何理解的?
網上的部分貼子可能寫的有點抽象,不便於快速閱讀理解。本篇文章就使用通俗易懂的簡單方式,來講解並實現一個簡單的vue資料雙向繫結原理demo,希望對大家有一定的幫助
先複習基本知識
為了便於大家更好的理解下文資料雙向繫結的程式碼,我們最好先複習一下舊知識,如果基礎知識紮實的道友,可以直接跳過這一段。
DOM.children屬性返回DOM元素有哪些元素子節點
程式碼:
<body> <div class="divClass"> <span>孫悟空</span> <h4>豬八戒</h4> <input type="text" value="沙和尚"> </div> <script> let divBox = document.querySelector('.divClass') console.log('元素節點', divBox); console.log('元素節點的子節點偽陣列', divBox.children); </script> </body>
示例圖:
注意區分: DOM.childNodes得到所有的節點
,比如元素節點、文字節點、註釋節點;而, DOM.children只得到所有的元素節點
。二者返回的都是一個偽陣列,但偽陣列有length長度,代表有多少個節點,且可以迴圈遍歷, 遍歷的每一項都是一個dom元素標籤!
不過偽陣列不能使用陣列的方法
DOM.hasAttribute(key)/getAttribute(key)判斷元素標籤是否有key屬性以及訪問對應value值
程式碼:
<body> <h3 class="styleCss" like="coding" v-bind="fire in the hole">穿越火線</h3> <script> let h3 = document.querySelector('h3') console.log(h3.hasAttribute('v-hello')); // 看看此標籤有沒有加上v-hello這個屬性,沒的,故列印:false console.log(h3.hasAttribute('like')); // 看看此標籤有沒有加上like這個屬性,有,故列印:true console.log(h3.getAttribute('like')); // 訪問此標籤上加上的這個v-bind屬性值是啥,列印:coding console.log(h3.hasAttribute('v-bind')); // 看看此標籤有沒有加上v-bind這個屬性,,有的,故列印:true console.log(h3.getAttribute('v-bind')); // 訪問此標籤上加上的這個v-bind屬性值是啥,列印:fire in the hole console.log(h3.attributes); // 可以看到所有的在標籤上繫結的屬性名和屬性值(key="value"),是一個偽陣列 </script> </body>
示例圖:
這兩個api可以用來看標籤上是否綁定了vue的指令,以及看看vue指令值是啥,以便於我們去與data中的相應資料做對應
DOM.innerHTML與DOM.innerText的區別
二者均可以修改dom的文字內容。innerHTML是符合W3C標準的屬性,所以是主流使用的dom的api。而innerText雖然相容性要好一些,不過主流還是innerHTML
程式碼:
<body> <h3>西遊記</h3> <button>更改dom內容</button> <script> let h3 = document.querySelector('h3') let btn = document.querySelector('button') btn.onclick = () => { h3.innerHTML = h3.innerHTML + '6' } </script> </body>
示例圖:
DOM.innerHtml這個api可用於更改vue中的差值表示式{{key}}對應的內容值
資料雙向繫結成品效果圖
我們先看一下,我們所要實現的成品的效果圖
需求分析
- 輸入框輸入值內容發生變化,頁面也發生對應變化
- 點選按鈕,輸入框和頁面都發生對應變化
即: - 頁面變化(輸入框引起)觸發資料data變化,最終觸發頁面變化;
- 資料data變化(按鈕引起),觸發頁面變化
關於MVVM的理解
簡單理解
mvvm即為m v vm分別對應的是:
- m是model資料層(就是vue中的data、computed、watch啊之類的資料配置項)
- v是view檢視層(檢視層效果是dom堆疊出來的,所以檢視層可以理解為dom元素)
- vm是model資料層和view檢視層的中間層view_model(vm)層,是vue中的核心,功能強大
vm可以監聽檢視層dom的變化
,比如監聽input標籤dom的value值變化,去更改model資料層中的data對應值, vm也可以監聽model資料層中的data對應key的value的值的變化,
去更改input標籤dom的value值。
即: vm相當於一個擺渡人,可把此岸人渡到彼岸、彼岸人渡到此岸
核心理解
所以,MVVM的核心是,所以,MVVM的核心是,所以,MVVM的核心是(重要的事情說三遍:)
-
監聽頁面的DOM的內容值變化,從而通知到data中做對應資料變化(主要是監聽表單標籤)
監聽表單標籤的變化,是使用dom.addEventListener()這個方法
-
當data中資料變化以後,再去及時更新頁面DOM的內容變化
監聽data中資料的變化,是使用Object.defineProperty()的set方法,自動幫我們監聽變化,至於更新dom,就是首先找到要更新哪個dom,如果是普通標籤就更新其innerHTML值、如果是表單標籤,就更改其value即可
關於Object.defineProperty的理解
關於Object.defineProperty這個方法,一言以蔽之,給物件定義響應式。論壇有很多資料帖子,在此不贅述。推薦看官方文件: https://developer.mozilla.org...
關於這個方法,我們先理解下面案例就差不多了:
案例需求
有一個物件obj,裡面有name和age屬性,要讓這個obj的每一個屬性,都是響應式的,訪問和修改的時候,都要對應列印資訊。
案例程式碼
複製貼上跑一下,大致就明白了
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="nameId">修改名字</button> <button id="ageId">修改年齡</button> <script> let obj = { name: '孫悟空', age: 500, } for (const key in obj) { // 因為是給物件中每一個屬性都新增響應式,所以要遍歷物件 let value = obj[key] // 存一份對應value值,用於訪問返回,以及新值修改賦值 Object.defineProperty(obj, key, { // 給這個obj物件的每一個屬性名key都定義響應式 get() { console.log('訪問之(自動觸發),訪問值為:', value); return value }, set(newVal) { console.log('修改之(自動觸發),修改的屬性名為:', key, '屬性值為:', newVal); value = newVal } }) } let nameBtn = document.querySelector('#nameId') let ageBtn = document.querySelector('#ageId') nameBtn.onclick = () => { obj.name = obj.name + '^_^ | ' } ageBtn.onclick = () => { obj.age = obj.age + 1 } // 這樣的話,訪問和修改的時候都會觸發啦(修改的時候是要先訪問找到,再去修改,故列印兩次) </script> </body> </html>
案例效果圖
完整程式碼
程式碼中寫了不少註釋,大家跟著註釋步驟閱讀應該就可以了。演示的話直接複製貼上即可。注意程式碼中的subArr,蒐集依賴,目的是看看有哪些dom元素需要做後續的響應式更新內容
列印new出來的Vue例項
如果下方的完整程式碼,有助於各位道友更好的理解mvvm的話,那就給咱點個贊鼓勵一下創作唄 ^_^
完整MVVM程式碼
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> #app { width: 600px; height: 216px; background-color: #ccc; padding: 24px; } button { cursor: pointer; } </style> </head> <body> <!-- view檢視層dom,為了便於理解,這裡以#app的根元素內部只有一層dom為例(多層需要遞迴) --> <div id="app"> <input v-model="name" placeholder="請填寫名字"> <span>名字是:</span><span v-bind="name"></span> <br> <br> <input v-model="age" placeholder="請填寫年齡"> <span>年齡是:</span><span v-bind="age"></span> <br> <br> <h3>{{name}}</h3> <h3>{{age}}</h3> <button id="nameId">更改名字</button> <button id="ageId">更改年齡</button> <button id="resetId">恢復預設</button> <button id="removeId">全部清空</button> </div> <script> // 簡單函式封裝 之 判斷標籤內是否包含雙差值表示式 function isIncludesFourKuoHao(str) { // 不過這裡不是特別嚴謹。嚴謹需要使用正則限制,大家明白思路即可 if (str.length <= 4) { // 得大於4個字元 return false } if ( // 且要有雙差值表示式, str[0] == '{' & str[1] == '{' & str[str.length - 1] == '}' & str[str.length - 2] == '}' ) { return true } else { return false } } // 簡單函式封裝 之 獲取雙差值表示式之間的變數名 function getKuoHaoBetweenValue(params) { // 這裡也不是特別嚴謹,嚴謹也需要使用正則,大家明白思路即可 return params.slice(2, params.length - 2) // {{name}} --> name } // 這裡使用建構函式,使之擁有new的功能。當然也可以使用class類程式設計 function Vue(options) { /** * 第一步,獲取根節點dom元素,這一步的作用是有了根節點dom以後,可以通過dom.children獲取其所有子節點的dom元素, * 便於我們對子節點的dom進行操作,比如給子節點的input標籤繫結input事件監聽,這樣就可以通過dom.value * 實時拿到使用者在輸入框輸入的值了 * */ this.$el = document.querySelector(options.el); /** * 第二步,把data中的資料{name:'jack',age:'500'}存一份,因為我們除了修改this.name要是響應式的,同樣: * this.$data.name也要是響應式的 */ this.$data = options.data; /** * 第三步,定義一個數組蒐集要變化的dom元素,當我們修改data中資料的時候,觸發Object.defineProperty()的set方法執行 * 然後去subArr陣列中去尋找,看看是要修改那個dom元素的資料值即可,大家列印一下,就會發現subArr存放的是一個又 * 一個物件,物件中記錄的是 哪一個dom,什麼屬性名key,以及對應更改innerHTML或value * */ this.subArr = [] /** * 第四步,執行模板編譯操作,把data中的資料做頁面呈現。這裡又分為兩部分 * 4.1 給相應的互動輸入類標籤繫結事件監聽,比如input標籤繫結input事件,select標籤繫結change事件等。為便於理解 * 本案例中只以input標籤為例說明(當然前提是:加了v-model指令做資料雙向繫結才會去操作這一步) * 4.2 把v-bind和插值表示式{{}}做內容呈現,即:把model中的對應資料值,並找到對應dom,更改其innerHTML的值為對應資料值 * */ this.useDataToCompileRenderPage(); // 使用data中的資料做模板編譯並渲染到頁面上 /** * 第五步,給m中的資料使用Object.defineProperty做資料劫持,這樣的話,訪問或者修改物件的屬性值時,都可以得知。即: * 訪問時,不用額外操作。不過修改時,model中的data的值變化了,於此同時,還需同時更新dom,因為m變v也要跟著變 * 即:dataChangeUpdatePage方法的執行,只要一set更新,我就讓dataChangeUpdatePage方法去更新對應的dom值 * (因為第四步以後,data中資料是渲染到頁面上了,但還需讓data中的資料變化,頁面也跟著變化,故要做資料劫持) * */ this.definePropertyAllDataKey(); // 資料劫持data中的所有key使之成為響應式的 } // 先把data中的資料,去編譯渲染到頁面上 Vue.prototype.useDataToCompileRenderPage = function () { let _this = this; // 存一份this例項物件 let nodes = this.$el.children; // 獲取根元素下的所有的子節點dom;值為偽陣列,列印結果:[input, span, span, br, br, input, span, span, br, br, button] for (let i = 0; i < nodes.length; i++) { // 迴圈這個子節點dom偽陣列, let node = nodes[i]; // 所有的標籤,一個一個去判斷,判斷這個標籤有沒有加上v-model,有沒有加上v-bind,有沒有差值表示式{{}} ,以這三種情況為例 // 若dom標籤節點上加上了v-model指令 if (node.hasAttribute('v-model')) { let dataKey = node.getAttribute('v-model');// 去獲取v-model繫結的那個屬性值,本例中為dataKey的值分別為:name、age node.addEventListener('input', function () { // 以input輸入框為例:給標籤繫結input輸入事件監聽,即:<input/>.addEventListener('input',function(){}) /** 注意,這裡是頁面到資料的處理,即v --> vm --> m的流程 */ _this.$data[dataKey] = node.value; // 如果是input標籤,可以直接通過inputDom.value獲取到input標籤中使用者輸入的值 _this[dataKey] = node.value; // 上一行是$data更改,即:this.$data.name或age獲取dom最新的值、這一行是this.name或age獲取最新的值 }); /** 把model中的資料更新賦值(編譯)到頁面上 */ node['value'] = _this.$data[dataKey]; // inputDom.value = this.$data.name或age 賦值 /** 所以,經過這一波操作,成功的把輸入框(變化)的值,更改到資料層中了 即:v --> vm --> m */ /** 注意這裡,就是蒐集依賴,可以提取一個方法的,為了便於理解,就不提取了 */ _this.subArr.push({ nodeLabelDom: node, // 哪個dom標籤元素 whichAttribute: dataKey, // 哪一個屬性name或age valueOrInnerHtml: 'value', // 更改value還是innerHTML }) } // 若dom標籤節點上加上了v-bind指令 if (node.hasAttribute('v-bind')) { /** 如果是v-bind指令,只需要新增watcher即可 * */ let dataKey = node.getAttribute('v-bind'); // 去獲取v-bind繫結的那個屬性值,本例中為dataKey的值分別為:name、age node['innerHTML'] = _this.$data[dataKey]; // normalDom.innerHtml = this.$data.name或age 普通dom顯示賦值操作 /** 注意這裡,就是蒐集依賴,可以提取一個方法的,為了便於理解,就不提取了 */ _this.subArr.push({ nodeLabelDom: node, // 哪個dom標籤元素 whichAttribute: dataKey, // 哪一個屬性name或age valueOrInnerHtml: 'innerHTML', // 更改value還是innerHTML }) } // 如果包含雙差值表示式{{}} if (isIncludesFourKuoHao(node.textContent)) { let dataKey = getKuoHaoBetweenValue(node.textContent) // 就拿到雙差值表示式中間的key,屬性名,這裡的dataKey分別為:name、age node['innerHTML'] = _this.$data[dataKey]; // 把雙差值表示式中的key做一個替換對應值 /** 注意這裡,就是蒐集依賴,可以提取一個方法的,為了便於理解,就不提取了 */ _this.subArr.push({ nodeLabelDom: node, // 哪個dom標籤元素 whichAttribute: dataKey, // 哪一個屬性name或age valueOrInnerHtml: 'innerHTML', // 更改value還是innerHTML }) } } } // 再做資料劫持,遍歷給data中的每一個數據都劫持,使之,都用於set和get方法 Vue.prototype.definePropertyAllDataKey = function () { let _this = this; // 存一份this以便使用 for (let key in _this.$data) { // 遍歷物件{name:'孫悟空',age: 500} let value = _this.$data[key]; // value值為孫悟空、500 key的值自然是name和age Object.defineProperty(_this.$data, key, { // 使用defineProperty去新增攔截、劫持(劫持到$data身上) get: function () { // return value; // 訪問key,訪問name或者age,就返回對應的值 }, set: function (newVal) { value = newVal; // 修改key的屬性值,修改name或者age的屬性值,在做正常操作value = newVal賦值的同時 // 每當更新this.$data資料時,如:this.$data.name = 'newVal'就去做對應dom的更新即可 _this.dataChangeUpdatePage(key, newVal) } }) Object.defineProperty(_this, key, { // 劫持到自己身上 get: function () { return value; }, set: function (newVal) { value = newVal; // 每當更新this資料時,如:this.name = 'newVal'就去做對應dom的更新即可 _this.dataChangeUpdatePage(key, newVal) } }) } } // 公共方法,當更新觸發的時候,去根據資料做頁面渲染 Vue.prototype.dataChangeUpdatePage = function (key, newVal) { let _this = this; // 存一份this例項物件 // 也要去更新對應dom的內容 _this.subArr.forEach((item) => { if (key == item.whichAttribute) { // 哪個dom的 // innerText或者value // 賦新值 item.nodeLabelDom[item.valueOrInnerHtml] = newVal; } }) } let vm = new Vue({ el: '#app', // 指定vue的根元素 /** * model資料層,為了便於理解,這裡也是舉例data中資料只有一層,多層需要遞迴 * */ data: { name: '孫悟空', age: 500, } }); console.log('vmvm', vm); // 更改名字 let nameBtn = document.querySelector('#nameId') nameBtn.onclick = () => { vm.name = vm.name + '^' // 直接訪問 } // 更改年齡 let ageBtn = document.querySelector('#ageId') ageBtn.onclick = () => { vm.$data.age = vm.$data.age * 1 + 1 // 通過$data間接訪問 } // 恢復預設的名字和年齡 let resetBtn = document.querySelector('#resetId') resetBtn.onclick = () => { vm.$data.name = '孫悟空' vm.age = 500 } // 清空名字和年齡 let removeBtn = document.querySelector('#removeId') removeBtn.onclick = () => { vm.name = '' vm.$data.age = null } </script> </body> </html>
好記性不如爛筆頭,記錄一下唄
- 不要在 Python 中使用迴圈,這些方法其實更棒!
- 分享 6 個 Vue3 開發必備的 VSCode 外掛
- 微服務架構的外部 API 整合模式
- 前端該如何優雅地 Mock 資料
- 微服務架構的通訊設計模式
- 我做了一個線上白板(二)
- 3 款非常實用的 Node.js 版本管理工具
- 636. 函式的獨佔時間 : 簡單棧運用模擬題
- TCP 學習筆記(三) 可靠傳輸
- 一文讀懂微服務架構的分解設計
- Python中常用最神祕的函式! lambda 函式深度總結!
- 技術分享| 小程式實現音影片通話
- Birdseye 極其強大的Python除錯工具
- Android技術分享| 影片通話開發流程(一)
- 細胞影象資料的主動學習
- React報錯之Cannot find name
- 智慧合約安全——私有資料訪問
- 30 個數據工程必備的Python 包
- 萬字長文Python面試題,建議先收藏
- 6個可解釋AI (XAI)的Python框架推薦