您好,登錄后才能下訂單哦!
這篇文章主要講解了“如何實現一個vue雙向綁定”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“如何實現一個vue雙向綁定”吧!
開局一張圖
從圖上可以看出new Vue()
分為了兩步走
代理監聽所有數據,并與Dep
進行關聯,通過Dep
通知訂閱者進行視圖更新。【相關推薦:vuejs視頻教程】
解析所有模板,并將模板中所用到的數據進行訂閱,并綁定一個更新函數,數據發生改變時Dep
通知訂閱者執行更新函數。
接下里就是分析如何去實現,并且都需要寫什么,先看一段vue的基礎代碼,我們從頭開始分析
<div id="app"> <input v-model="message" /> <p>{{message}}</p> </div>
let app = new Vue({ el:"#app", data:{ message:"測試這是一個內容" } })
從上面代碼我們可以看到new Vue
的操作,里面攜帶了el
和data
屬性,這算是最基礎的屬性,而在html代碼中我們知道<div id="app">
是vue渲染的模板根節點,所以vue要渲染頁面就要去實現一個模板解析的方法Compile
類,解析方法中還需要去處理{{ }}
和v-model
兩個指令,除了解析模板之后我們還需要去實現數據代理也就是實現Observer
類
如下代碼所示,這就寫完了Vue
類,夠簡單吧,如果對class
關鍵字不熟悉的,建議先去學習一下,從下面我們可能看到,這里實例化了兩個類,一個是代理數據的類,一個是解析模板的類。
class Vue { constructor(options) { // 代理數據 new Observer(options.data) // 綁定數據 this.data = options.data // 解析模板 new Compile(options.el, this) } }
接著往下我們先寫一個Compile
類用于解析模板,我們再來分析一波,解析模板要做什么事
我們要解析模板不可能直接對dom繼續操作,所以我們要創建一個文檔片段(虛擬dom),然后將模板DOM節點復制一份到虛擬DOM節點中,對虛擬DOM節點解析完成之后,再將虛擬DOM節點替換掉原來的DOM節點
虛擬節點復制出來之后,我們要遍歷整個節點樹進行解析,解析過程中會對DOM的atrr屬性進行遍歷找到Vue相關的指令,除此之外還要對 textContent
節點內容進行解析,判斷是否存在雙花括號
將解析出來所用到的屬性進行一個訂閱
下面我們將逐步實現
構建Compile
類,先把靜態節點和Vue實例獲取出來,再定義一個虛擬dom的屬性用來存儲虛擬dom
class Compile { constructor(el, vm) { // 獲取靜態節點 this.el = document.querySelector(el); // vue實例 this.vm = vm // 虛擬dom this.fragment = null // 初始化方法 this.init() } }
實現初始化方法init()
,該方法主要是用于創建虛擬dom和調用解析模板的方法,解析完成之后再將DOM節點替換到頁面中
class Compile { //...省略其他代碼 init() { // 創建一個新的空白的文檔片段(虛擬dom) this.fragment = document.createDocumentFragment() // 遍歷所有子節點加入到虛擬dom中 Array.from(this.el.children).forEach(child => { this.fragment.appendChild(child) }) // 解析模板 this.parseTemplate(this.fragment) // 解析完成添加到頁面 this.el.appendChild(this.fragment); } }
實現解析模板方法parseTemplate
,主要是遍歷虛擬DOM中的所有子節點并進行解析,根據子節點類型進行不同的處理。
class Compile { //...省略其他代碼 // 解析模板 parseTemplate(fragment) { // 獲取虛擬DOM的子節點 let childNodes = fragment.childNodes || [] // 遍歷節點 childNodes.forEach((node) => { // 匹配大括號正則表達式 var reg = /\{\{(.*)\}\}/; // 獲取節點文本 var text = node.textContent; if (this.isElementNode(node)) { // 判斷是否是html元素 // 解析html元素 this.parseHtml(node) } else if (this.isTextNode(node) && reg.test(text)) { //判斷是否文本節點并帶有雙花括號 // 解析文本 this.parseText(node, reg.exec(text)[1]) } // 遞歸解析,如果還有子元素則繼續解析 if (node.childNodes && node.childNodes.length != 0) { this.parseTemplate(node) } }); } }
根據上面的代碼我們得出需要實現兩個簡單的判斷,也就是判斷是否是html元素和文字元素,這里通過獲取nodeType
的值來進行區分,不了解的可以直接看一下 傳送門:Node.nodeType,這里還擴展了一個isVueTag
方法,用于后面的代碼中使用
class Compile { //...省略其他代碼 // 判斷是否攜帶 v- isVueTag(attrName) { return attrName.indexOf("v-") == 0 } // 判斷是否是html元素 isElementNode(node) { return node.nodeType == 1; } // 判斷是否是文字元素 isTextNode(node) { return node.nodeType == 3; } }
實現parseHtml
方法,解析html代碼主要是遍歷html元素上的attr屬性
class Compile { //...省略其他代碼 // 解析html parseHtml(node) { // 獲取元素屬性集合 let nodeAttrs = node.attributes || [] // 元素屬性集合不是數組,所以這里要轉成數組之后再遍歷 Array.from(nodeAttrs).forEach((attr) => { // 獲取屬性名稱 let arrtName = attr.name; // 判斷名稱是否帶有 v- if (this.isVueTag(arrtName)) { // 獲取屬性值 let exp = attr.value; //切割 v- 之后的字符串 let tag = arrtName.substring(2); if (tag == "model") { // v-model 指令處理方法 this.modelCommand(node, exp, tag) } } }); } }
實現modelCommand
方法,在模板解析階段來說,我們只要把 vue實例中data的值綁定到元素上,并實現監聽input方法更新數據即可。
class Compile { //...省略其他代碼 // 處理model指令 modelCommand(node, exp) { // 獲取數據 let val = this.vm.data[exp] // 解析時綁定數據 node.value = val || "" // 監聽input事件 node.addEventListener("input", (event) => { let newVlaue = event.target.value; if (val != newVlaue) { // 更新data數據 this.vm.data[exp] = newVlaue // 更新閉包數據,避免雙向綁定失效 val = newVlaue } }) } }
處理Text元素就相對簡單了,主要是將元素中的textContent
內容替換成數據即可
class Compile { //...省略其他代碼 //解析文本 parseText(node, exp) { let val = this.vm.data[exp] // 解析更新文本 node.textContent = val || "" } }
至此已經完成了Compile
類的初步編寫,測試結果如下,已經能夠正常解析模板
下面就是我們目前所實現的流程圖部分
坑點一:
在第6點modelCommand
方法中并沒有實現雙向綁定,只是單向綁定,后續要雙向綁定時還需要繼續處理
坑點二:
第7點parseText
方法上面的代碼中并沒有去訂閱數據的改變,所以這里只會在模板解析時綁定一次數據
這里主要是用于代理data中的所有數據,這里會用到一個Object.defineProperty
方法,如果不了解這個方法的先去看一下文檔傳送門:
文檔:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
Observer
類主要是一個遞歸遍歷所有data中的屬性然后進行數據代理的的一個方法
defineReactive
中傳入三個參數data
, key
, val
data
和key
都是Object.defineProperty
的參數,而val
將其作為一個閉包變量供Object.defineProperty
使用
// 監聽者 class Observer { constructor(data) { this.observe(data) } // 遞歸方法 observe(data) { //判斷數據如果為空并且不是object類型則返回空字符串 if (!data || typeof data != "object") { return "" } else { //遍歷data進行數據代理 Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]) }) } } // 代理方法 defineReactive(data, key, val) { // 遞歸子屬性 this.observe(data[key]) Object.defineProperty(data, key, { configurable: true, //可配置的屬性 enumerable: true, //可遍歷的屬性 get() { return val }, set(newValue) { val = newValue } }) } }
下面我們來測試一下是否成功實現了數據代理,在Vue的構造函數輸出一下數據
class Vue { constructor(options) { // 代理數據 new Observer(options.data) console.log(options.data) // 綁定數據 this.data = options.data // 解析模板 new Compile(options.el, this) } }
結果如下,我們可以看出已經實現了數據代理。
對應的流程圖如下所示
坑點三:
這里雖然實現了數據代理,但是按照圖上來說,還需要引入管理器,在數據發生變化時通知管理器數據發生了變化,然后管理器再通知訂閱者更新視圖,這個會在后續的填坑過程過講到。
上面我們已經實現了模板解析到初始化視圖,還有數據代理。而下面要實現的Dep
類主要是用于管理訂閱者和通知訂閱者,這里會用一個數組來記錄每個訂閱者,而類中也會給出一個notify
方法去調用訂閱者的update
方法,實現通知訂閱者更新功能。這里還定義了一個target
屬性用來存儲臨時的訂閱者,用于加入管理器時使用。
class Dep { constructor() { // 記錄訂閱者 this.subList = [] } // 添加訂閱者 addSub(sub) { // 先判斷是否存在,防止重復添加訂閱者 if (this.subList.indexOf(sub) == -1) { this.subList.push(sub) } } // 通知訂閱者 notify() { this.subList.forEach(item => { item.update() //訂閱者執行更新,這里的item就是一個訂閱者,update就是訂閱者提供的方法 }) } } // Dep全局屬性,用來臨時存儲訂閱者 Dep.target = null
管理器實現完成之后我們也就實現了流程圖中的以下部分。要注意下面幾點
Observer
通知Dep
主要是通過調用notify
方法
Dep
通知Watcher
主要是是調用了Watcher
類中的update
方法
訂閱者代碼相對少,但是理解起來還是有點難度的,在Watcher
類中實現了兩個方法,一個是update
更新視圖方法,一個putIn
方法(我看了好幾篇文章都是定義成 get 方法,可能是因為我理解的不夠好吧)。
update:主要是調用傳入的cb
方法體,用于更新頁面數據
putIn:主要是用來手動加入到Dep
管理器中。
// 訂閱者 class Watcher { // vm:vue實例本身 // exp:代理數據的屬性名稱 // cb:更新時需要做的事情 constructor(vm, exp, cb) { this.vm = vm this.exp = exp this.cb = cb this.putIn() } update() { // 調用cb方法體,改變this指向并傳入最新的數據作為參數 this.cb.call(this.vm, this.vm.data[this.exp]) } putIn() { // 把訂閱者本身綁定到Dep的target全局屬性上 Dep.target = this // 調用獲取數據的方法將訂閱者加入到管理器中 let val = this.vm.data[this.exp] // 清空全局屬性 Dep.target = null } }
坑點四:
Watcher
類中的putIn
方法再構造函數調用后并沒有加入到管理器中,而是將訂閱者本身綁定到target
全局屬性上而已
通過上面的代碼我們已經完成了每一個類的構建,如下圖所示,但是還是有幾個流程是有問題的,也就是上面的坑點。所以下面要填坑
埋坑 1 和 2
完成坑點一和坑點二,在modelCommand
和parseText
方法中增加實例化訂閱者代碼,并自定義要更新時執行的方法,其實就是更新時去更新頁面中的值即可
modelCommand(node, exp) { // ...省略其他代碼 // 實例化訂閱者,更新時直接更新node的值 new Watcher(this.vm, exp, (value) => { node.value = value }) } parseText(node, exp) { // ...省略其他代碼 // 實例化訂閱者,更新時直接更新文本內容 new Watcher(this.vm, exp, (value) => { node.textContent = value }) }
埋坑 3
完成坑點三,主要是為了引入管理器,通知管理器發生改變,主要是在Object.defineProperty set
方法中調用dep.notify()
方法
// 監聽方法 defineReactive(data, key, val) { // 實例化管理器--------------增加這一行 let dep = new Dep() // ...省略其他代碼 set(newValue) { val = newValue // 通知管理器改變--------------增加這一行 dep.notify() } }
埋坑 4
完成坑點四,主要四將訂閱者加入到管理器中
defineReactive(data, key, val) { // ...省略其他代碼 get() { // 將訂閱者加入到管理器中--------------增加這一段 if (Dep.target) { dep.addSub(Dep.target) } return val }, // ...省略其他代碼 }
完成了坑點四可能就會有靚仔疑惑了,這里是怎么加入的呢Dep.target
又是什么呢,我們不妨從頭看看代碼并結合下面這張圖
至此我們已經實現了一個簡單的雙向綁定,下面測試一下
感謝各位的閱讀,以上就是“如何實現一個vue雙向綁定”的內容了,經過本文的學習后,相信大家對如何實現一個vue雙向綁定這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。