詳解Vue中雙向綁定原理及簡單實(shí)現(xiàn)
監(jiān)聽器
vue實(shí)現(xiàn)雙向綁定時(shí),首先要實(shí)現(xiàn)目標(biāo)data的監(jiān)聽(通過 Object.defineProperty 來實(shí)現(xiàn))
(1)遍歷整個(gè)data,對(duì)data下面所有的key進(jìn)行Object.defineProperty來監(jiān)聽,然后獲取監(jiān)聽key的get,set事件。
(2)對(duì)每一個(gè)key進(jìn)行Object.defineProperty監(jiān)聽的時(shí)候,都單獨(dú)的創(chuàng)建一個(gè)Dep類用于收集和觸發(fā)依賴,后續(xù)的更新函數(shù)的觸發(fā)和收集都會(huì)存儲(chǔ)在每一個(gè)key對(duì)應(yīng)的Dep實(shí)例中。
// 數(shù)據(jù)監(jiān)聽,監(jiān)聽對(duì)象的所有g(shù)et和set function observe(data) { // 如果data不存在或data不是object if (!data || typeof data !== 'object') { return; } // 遍歷data,獲得當(dāng)前的key for (const key in data) { // 監(jiān)聽data下的當(dāng)前key的屬性變動(dòng) defineReactive(data, key, data[key]); // 一次只處理data和一個(gè)key的監(jiān)聽關(guān)系 } } // 通過Object.defineProperty去監(jiān)聽data下的key function defineReactive(data, key, value) { // 處理key對(duì)應(yīng)的value是一個(gè)對(duì)象的情況 observe(value) // 創(chuàng)建Dep() 實(shí)例 用于存儲(chǔ)key的依賴,以及觸發(fā)key的依賴 let dep = new Dep() Object.defineProperty(data, key, { // 返回value值 get: function () { // get的時(shí)候進(jìn)行依賴收集 if(Dep.target) { // Dep.target默認(rèn)為null,不會(huì)收集依賴,但是創(chuàng)建watcher實(shí)例的時(shí)候 // 會(huì)為Dep.target賦值,并指向當(dāng)前的watcher實(shí)例,同時(shí)會(huì)為watcher掛載上update函數(shù) // 然后get的時(shí)候會(huì)出現(xiàn)觸發(fā)依賴收集,因?yàn)檫@時(shí)候Dep.target指向當(dāng)前的watcher實(shí)例,就可以將這個(gè)watcher實(shí)例收集起來 dep.addSub(Dep.target) } return value }, set: function (newValue) { // get的時(shí)候觸發(fā)依賴 val = newValue; // 依賴觸發(fā),遍歷依賴?yán)锩娴膚atcher并調(diào)用update函數(shù) dep.notify(newValue) } } )} // 創(chuàng)建Dep類,用來存儲(chǔ)依賴 class Dep{ constructor() { this.subs = [] // 定義一個(gè)subs數(shù)組用來存放依賴 } addSub(sub) { // get的時(shí)候存儲(chǔ)依賴 // 注:更新函數(shù)會(huì)被掛載到單獨(dú)創(chuàng)建的watcher實(shí)例,存儲(chǔ)依賴的時(shí)候,實(shí)際上存儲(chǔ)的是創(chuàng)建的watcher實(shí)例 this.subs.push(sub) } notify(newValue) { // set的時(shí)候觸發(fā)依賴 // 注: 遍歷依賴,開始執(zhí)行里面watcher實(shí)例的更新函數(shù) this.subs.forEach(item => { item.update(newValue) }) } target = null }
訂閱器
創(chuàng)建訂閱器,訂閱器作用于觀察器后面,實(shí)際上訂閱器會(huì)被掛載到被監(jiān)聽的key的依賴上。(細(xì)品)
會(huì)緩存上一個(gè)值,以及提供update方法(set的時(shí)候執(zhí)行)
(1) 創(chuàng)建實(shí)例的時(shí)候會(huì)暫存監(jiān)聽的節(jié)點(diǎn)和監(jiān)聽的數(shù)據(jù)對(duì)象等信息,并創(chuàng)建update函數(shù)(用于處理數(shù)據(jù)更新等回調(diào)(2) 初始化的時(shí)候會(huì)調(diào)用訂閱器實(shí)例的get方法,并設(shè)置Dep構(gòu)造函數(shù)的target屬性指向當(dāng)前的訂閱器實(shí)例(注意這個(gè)是一個(gè)實(shí)例,即說明已經(jīng))
class Watcher{ constructor(vm, prop, callback) { this.vm = vm // 監(jiān)聽的節(jié)點(diǎn)和監(jiān)聽的數(shù)據(jù)對(duì)象 this.prop = prop // 監(jiān)聽的數(shù)據(jù)對(duì)象的某個(gè)參數(shù)key,如 name、age 等 this.callback = callback // 存儲(chǔ)回調(diào) this.value = this.get() // 觸發(fā)數(shù)據(jù)get操作,開始依賴收集 } get() { Dep.target = this // 將訂閱器賦值給Dep的target // 調(diào)用watcher前需要調(diào)用observe,所以這里的data已經(jīng)被監(jiān)聽了,get的時(shí)候開始依賴收集因?yàn)镈ep.target = this,所以target非空,Watcher會(huì)被插入到data的key下的依賴集合中 const value = this.vm.data[this.prop] Dep.target = null // 關(guān)閉依賴收集,防止每次都收集依賴 return value // 獲取當(dāng)前的值 } // 注意調(diào)用update的時(shí)候是在監(jiān)聽器的notify里面進(jìn)行的,說明已經(jīng)set操作完了,值已經(jīng)變了,但是watcher的value值在get的時(shí)候先緩存了上一次value值 update(newValue) { // 更新函數(shù) const value = newValue // 獲取當(dāng)前的值 const oldValue = this.value // 獲取之前緩存的值 if(value != oldValue) { // 如果當(dāng)前的值和之前緩存的值不同則觸發(fā)更新函數(shù),并重置value this.value = value this.callback(value); // 觸發(fā)更新回調(diào)函數(shù) } } }
雙向綁定構(gòu)造函數(shù)
基于監(jiān)聽器及訂閱器,我們已經(jīng)可以完成簡單的數(shù)據(jù)更新
具體使用時(shí)會(huì)先創(chuàng)建一個(gè)雙向綁定構(gòu)造函數(shù)實(shí)例,然后將傳入的參數(shù)緩存起來。
然后調(diào)用實(shí)例的init方法,init方法中會(huì)對(duì)傳入的參數(shù)(包括需要監(jiān)聽的data),對(duì)需要監(jiān)聽的data進(jìn)行observe數(shù)據(jù)監(jiān)聽,所以這時(shí)候傳入的data已經(jīng)被我們監(jiān)聽了,我們已經(jīng)是可以獲取到get,set事件的。
然后我們會(huì)顯示的單獨(dú)創(chuàng)建一個(gè)訂閱器實(shí)例
訂閱器的構(gòu)造方法中同樣會(huì)緩存?zhèn)魅氲膮?shù),然后調(diào)用訂閱器的get方法,訂閱器的get方法,會(huì)設(shè)置公共的Dep類,給Dep類的target參數(shù)指向自身,即Dep.target = 訂閱器
然后顯示的觸發(fā)data的get操作,因?yàn)間et操作中會(huì)判斷Dep.target 是否存在,如果存在則存入緩存(dep),再把Dep.target 重新指向 null,防止后面反復(fù)get操作
完成上述步驟之后,我們可以發(fā)現(xiàn)目標(biāo)data的key不僅我們可以監(jiān)聽到它的get,set操作,并成功對(duì)齊設(shè)置了依賴,后續(xù)數(shù)據(jù)變動(dòng)的話實(shí)際上就會(huì)觸發(fā)set操作,會(huì)用更新函數(shù)同步視圖即可。
class MyVue { constructor(options, prop) { this.options = options // 需要雙向綁定的對(duì)象,包括數(shù)據(jù)對(duì)象和對(duì)應(yīng)的節(jié)點(diǎn) this.data = options.data // 需要監(jiān)聽的數(shù)據(jù)對(duì)象 this.el = options.el // 需要更新視圖節(jié)點(diǎn) this.prop = prop // 對(duì)應(yīng)的key值 this.init() } init() { observe(this.data) // 監(jiān)聽data document.getElementById(this.el).textContent = this.data[this.prop] // 將數(shù)據(jù)初始化到頁面上 // 創(chuàng)建watcher實(shí)例,在watcher中對(duì)data進(jìn)行g(shù)et操作,并將watcher本身掛載到data對(duì)應(yīng)的key的依賴下 new Watcher(this.options, this.prop, value => { document.getElementById(this.el).textContent = value }) } }
初步效果:
編譯器
在上面的例子中我們已經(jīng)完成了如何監(jiān)聽一個(gè)數(shù)據(jù),然后當(dāng)數(shù)據(jù)變動(dòng)的時(shí)候,觸發(fā)函數(shù)依賴去更新頁面。
但這是個(gè)單向的流程,我們只能監(jiān)聽數(shù)據(jù)變動(dòng)操作頁面的元素更改,而且在雙向綁定構(gòu)造函數(shù)中我們顯式的創(chuàng)建了watcher實(shí)例(只為id是app的節(jié)點(diǎn)負(fù)責(zé),且這個(gè)節(jié)點(diǎn)下面如果嵌套其他節(jié)點(diǎn)我們就無法處理了) ,顯然是不合理的,我們應(yīng)該要基于頁面元素去分析針對(duì)對(duì)應(yīng)的內(nèi)容進(jìn)行watcher創(chuàng)建。
// 完全是為一個(gè)節(jié)點(diǎn)服務(wù),我們應(yīng)該要針對(duì)不同元素動(dòng)態(tài)的掛載watcher init() { observe(this.data) // 監(jiān)聽data document.getElementById(this.el).textContent = this.data[this.prop] // 將數(shù)據(jù)初始化到頁面上 // 創(chuàng)建watcher實(shí)例,在watcher中對(duì)data進(jìn)行g(shù)et操作,并將watcher本身掛載到data對(duì)應(yīng)的key的依賴下 new Watcher(this.options, this.prop, value => { document.getElementById(this.el).textContent = value }) }
而且watcher的get的依賴收集操作,是直接讀data下的參數(shù),意味著如果data.a.b這種情況是無法使用的。
// 無法處理復(fù)雜數(shù)據(jù)類型,這種只能處理data對(duì)象下的數(shù)據(jù),如果是data.a.b就會(huì)異常 const value = this.vm.data[this.prop]
所以我們需要有一個(gè)編譯器,去自動(dòng)讀取頁面的目標(biāo)節(jié)點(diǎn)下的所有節(jié)點(diǎn),對(duì)每一個(gè)節(jié)點(diǎn)進(jìn)行分析,比如a節(jié)點(diǎn)是個(gè)什么類型的節(jié)點(diǎn)。
如果是文本節(jié)點(diǎn)的話,內(nèi)容里面是否包含{{}}語法,如果包含了{(lán){}}里面的內(nèi)容,那么這個(gè)內(nèi)容是否已經(jīng)在我們data中聲明,聲明的話,我們就需要針對(duì)這個(gè)內(nèi)容給它加上這個(gè)節(jié)點(diǎn)的依賴,告訴它如果你的值要改變那么你的回調(diào)函數(shù)中需要更新這個(gè)節(jié)點(diǎn)下的值。
如果是input標(biāo)簽等元素節(jié)點(diǎn)的話,你需要分析這個(gè)節(jié)點(diǎn)有什么屬性,是否有v-開頭的屬性,當(dāng)然你也可以隨便自定義個(gè)什么東西,如果解析到這種元素節(jié)點(diǎn),你首先要做的是判斷這個(gè)節(jié)點(diǎn)下面有沒有子字節(jié),如果有就開始遞歸調(diào)用一直去解析其子節(jié)點(diǎn)。
如果沒有子節(jié)點(diǎn)就要去分析這個(gè)是什么元素,上面綁了什么屬性。
比如v-html那么在對(duì)這個(gè)節(jié)點(diǎn)創(chuàng)建watcher的時(shí)候,就不能像文本節(jié)點(diǎn)那樣node.textcontent = xxx,而應(yīng)該設(shè)置其html。
同樣的最常見的v-model,我們用在對(duì)應(yīng)的元素節(jié)點(diǎn)上時(shí),我們?cè)跒檫@個(gè)節(jié)點(diǎn)創(chuàng)建watcher的時(shí)候,還得為這個(gè)節(jié)點(diǎn)設(shè)置oninput事件,因?yàn)楫?dāng)其內(nèi)容改變的時(shí)候,我們要用這個(gè)回調(diào)同步的修改我們目標(biāo)data下的值。
下面直接開始上代碼我們需要暴露一個(gè)編譯器類
(1)Compile類
在構(gòu)造函數(shù)中暫存我們雙向綁定構(gòu)造函數(shù)實(shí)例,同時(shí)讀取當(dāng)前目標(biāo)節(jié)點(diǎn)的內(nèi)容,然后將目標(biāo)節(jié)點(diǎn)中的值全部移動(dòng)到我們創(chuàng)建的文檔碎片中,這一步是為了一次性渲染完減少資源損耗,獲取到目標(biāo)節(jié)點(diǎn)下所有的節(jié)點(diǎn)之后,我們要對(duì)這個(gè)節(jié)點(diǎn)進(jìn)行分析主要是為每一個(gè)節(jié)點(diǎn)去建立映射關(guān)系為其設(shè)置對(duì)應(yīng)的watcher,如果是有v-model這種事件還得給這個(gè)節(jié)點(diǎn)加上如oninput或者change這種事件。最后把處理完的節(jié)點(diǎn)重新放回頁面即可。
class Compile { // 對(duì)數(shù)據(jù)進(jìn)行解析 constructor(el, vm) { // el當(dāng)前目標(biāo)元素根節(jié)點(diǎn),vm雙向綁定構(gòu)造函數(shù)實(shí)例 this.el = this.isElementNode(el) ? el : document.getElementById(el) // 獲取目標(biāo)節(jié)點(diǎn) this.vm = vm // 暫存構(gòu)造函數(shù)實(shí)例 // 獲取文檔碎片節(jié)點(diǎn),暫存在內(nèi)容中 const fragmentNode = this.node2Fragment(this.el) // 模板編譯,主要是為每一個(gè)節(jié)點(diǎn)去建立映射關(guān)系為其設(shè)置對(duì)應(yīng)的watcher this.compile(fragmentNode) // 將處理后端 this.el.appendChild(fragmentNode) } .... }
(2)node2Fragment函數(shù)
這個(gè)是讀取目標(biāo)節(jié)點(diǎn)的函數(shù),會(huì)創(chuàng)建一個(gè)新的空白文檔碎片,把原來目標(biāo)節(jié)點(diǎn)下的子節(jié)點(diǎn)一個(gè)一個(gè)放進(jìn)去
// 解析元素節(jié)點(diǎn) node2Fragment(Node) { // 創(chuàng)建空白文檔 const f = document.createDocumentFragment() // 每次先看看Node.firstChild是否存在 while((Node.firstChild)) { // 把子元素加到空白文檔中,加過去之后意味著目標(biāo)節(jié)點(diǎn)第一個(gè)節(jié)點(diǎn)遷移到空白文檔中,那么目標(biāo)節(jié)點(diǎn)的元素就會(huì)越來越少,全部變成空白文檔下的子元素 f.appendChild(Node.firstChild) } return f }
(3)compile函數(shù)
這個(gè)是解析的核心函數(shù),用來解析每一個(gè)節(jié)點(diǎn),判斷這個(gè)節(jié)點(diǎn)是什么類型,然后需要如何為其設(shè)置watcher。
可以看到這個(gè)函數(shù)讀取了我們傳過來的文檔碎片,如果一個(gè)一個(gè)遍歷,判斷你這個(gè)節(jié)點(diǎn)是否是含有子節(jié)點(diǎn)的,如果有子節(jié)點(diǎn)那就繼續(xù)遞歸遍歷,如果沒有就判斷你是什么類型的節(jié)點(diǎn)是文本節(jié)點(diǎn)還是元素節(jié)點(diǎn),文本節(jié)點(diǎn)調(diào)用文本編譯函數(shù),元素節(jié)點(diǎn)調(diào)用元素編譯函數(shù)。
// 編譯模板 compile(fragmentNode) { // 獲取模板的子節(jié)點(diǎn) const childNodes = fragmentNode.childNodes; // 遍歷所有節(jié)點(diǎn) [...childNodes].forEach( child => { // 判斷子節(jié)點(diǎn)中是否還有節(jié)點(diǎn) 遞歸調(diào)用 if (child.childNodes && child.childNodes.length) { this.compile(child); } // 如果是元素節(jié)點(diǎn)類型 else if(this.isElementNode(child)) { // 使用元素編譯 this.elementCompile(child) } // 如果是文本節(jié)點(diǎn)類型 else { // 使用文本編譯 this.textCompile(child) } }) }
1.文本編譯函數(shù)
文本編譯函數(shù),針對(duì)文本類型節(jié)點(diǎn)編譯的函數(shù),主要是負(fù)責(zé)那些{{}}語法的處理,在這里會(huì)匹配這個(gè)節(jié)點(diǎn)里面有沒有{{}}語法,如果有就調(diào)用compileFunc下的text函數(shù),給這個(gè)節(jié)點(diǎn)單獨(dú)添加watcher,使得你數(shù)據(jù)更新的時(shí)候可以用watcher里面的回調(diào)去修改頁面的對(duì)應(yīng)節(jié)點(diǎn)的數(shù)據(jù)。
// 文本編譯函數(shù) textCompile(node) { // 獲取節(jié)點(diǎn)文字內(nèi)容 const content = node.textContent; // 匹配插值語法,如果有{{}}語法則為其設(shè)置watcher if(/{{.+?}}/.test(content)) { compileFunc['text'](node, content, this.vm) } }
下面是compileFunc關(guān)于text模塊的代碼,首先匹配出所有的{{}}內(nèi)的字符,生成一個(gè)數(shù)組,對(duì)這個(gè)數(shù)組去重,因?yàn)橐粋€(gè)text節(jié)點(diǎn)里面是一個(gè)字符串,而一個(gè)字符串可能不只有一個(gè){{}}所以需要找到所有的{{}}內(nèi)的字符,因?yàn)槊恳粋€(gè)字符都相當(dāng)于一個(gè)被監(jiān)聽的key,他們都有自己的回調(diào)都會(huì)對(duì)這個(gè)節(jié)點(diǎn)進(jìn)行操作(ps當(dāng)然這個(gè)操作有點(diǎn)問題后面再說)
找到所有的字符之后就開始循環(huán),為其單獨(dú)設(shè)置回調(diào)函數(shù)即
this.updater.textUpdater(node, value, propList[index], content)
回調(diào)函數(shù)的作用是去設(shè)置這個(gè)節(jié)點(diǎn)下的textcontent的value,然后為這個(gè){{}}內(nèi)的字符創(chuàng)建一個(gè)watcher,值得注意的是因?yàn)閧{}}里面可能有a.b.c這種情況我們還得返回一個(gè)函數(shù),在調(diào)用時(shí)會(huì)取到data下對(duì)應(yīng)的值,只有才能給對(duì)象里面對(duì)象的可以設(shè)置回調(diào)
結(jié)合上面我們對(duì)watcher的設(shè)計(jì)可以知道,如果我們知道了目標(biāo)data和需要監(jiān)聽的data下的key,我們?cè)趧?chuàng)建watcher實(shí)例的時(shí)候就可以將這個(gè)回調(diào)傳入這個(gè)key的依賴中,因?yàn)槲覀冊(cè)诨卣{(diào)中通過閉包的方法我們提前傳入了包括對(duì)應(yīng)元素節(jié)點(diǎn),文案內(nèi)容等信息,所以我們調(diào)用這個(gè)函數(shù)的時(shí)候可以精準(zhǔn)的修改到對(duì)應(yīng)節(jié)點(diǎn)。
const compileFunc = { text(node, content, vm) { const textMatch = /(?<={{)(.+?)(?=})/g; let propList = content.match(textMatch) propList = [...new Set(propList)] for (let index = 0; index < propList.length; index++) { let func = value => { this.updater.textUpdater(node, value, propList[index], content) } new Watcher(vm, this.propFunc(propList[index], vm), func) } }, // 處理取對(duì)象下的值的情況,因?yàn)閧{}}里面會(huì)有{{a.b.c}}這種情況需要通過一個(gè)函數(shù)的形式在watcher中進(jìn)行g(shù)et操作 propFunc(value, vm) { return function() {value.split(".").reduce((p, c) => { return p[c]; }, vm.data)} }, // 實(shí)際操作dom方法 updater: { textUpdater(node, value, prop, content) { node.textContent = content.replaceAll(`{{${prop}}}`, value); }, } }
2.元素編譯函數(shù)(僅做實(shí)例只實(shí)現(xiàn)v-model)
元素編譯函數(shù)主要是針對(duì)元素類型節(jié)點(diǎn)的,不同于文本類型,元素類型主要是處理如v-model這種屬性,會(huì)讀取這個(gè)元素節(jié)點(diǎn)下的屬性,判斷是否v-開頭的
// 判斷是否v-開頭的屬性 isMyVueAttributes(name) { return name.startsWith('v-') }
如果有那就針對(duì)這個(gè)節(jié)點(diǎn)的這個(gè)屬性去調(diào)用compileFunc下的對(duì)應(yīng)屬性函數(shù),如v-model,就調(diào)用model函數(shù)。
// 元素編譯函數(shù) elementCompile(node) { // 獲得目標(biāo)的節(jié)點(diǎn)的屬性 const attributes = node.attributes; [...attributes].forEach(item => { // 解構(gòu)出參數(shù)的name和value, name相當(dāng)于v指令如v-model等,value則是對(duì)應(yīng)的值 name, age等 const {name, value} = item if(!this.isMyVueAttributes(name)) return const attributesName = name.split('-')[1] compileFunc[attributesName](node, value, this.vm) }) }
可以看看compileFunc下的model函數(shù)干了啥,首先定義一個(gè)回調(diào)函數(shù),函數(shù)的核心是modelUpdater,類似于textUpdater,做的事情是data的key改變時(shí)同步更新頁面,即數(shù)據(jù)更新視圖,并將這個(gè)作為回調(diào)傳入對(duì)應(yīng)的watcher中,這時(shí)data的key就可以通過調(diào)用依賴的方式更新視圖了。
同時(shí)我們也看到我們給這個(gè)節(jié)點(diǎn)加了oninput事件,因?yàn)檫@個(gè)是雙向綁定,視圖一樣要改變數(shù)據(jù),所以當(dāng)輸入了內(nèi)容之后通過oninput事件同步修改頁面,這就是雙向綁定
model(node, prop, vm) { let func = value => { this.updater.modelUpdater(node, value) } new Watcher(vm, this.propFunc(prop, vm), func) node.oninput =(item)=>{ // 這里還沒辦法處理字符串轉(zhuǎn)對(duì)應(yīng)對(duì)象 vm.data[prop] = item.srcElement.value } }, ... modelUpdater(node, value) { node.value = value; },
基于上面的實(shí)現(xiàn)我們已經(jīng)完成了監(jiān)聽器,訂閱器,雙向綁定構(gòu)造函數(shù)以及編譯器,但是還有一點(diǎn)問題,我們可以看到雙向綁定構(gòu)造函數(shù)我們的init()方法是這樣子寫的,只針對(duì)一個(gè)節(jié)點(diǎn)創(chuàng)建watcher
init() { observe(this.data) // 監(jiān)聽data document.getElementById(this.el).textContent = this.data[this.prop] // 將數(shù)據(jù)初始化到頁面上 // 創(chuàng)建watcher實(shí)例,在watcher中對(duì)data進(jìn)行g(shù)et操作,并將watcher本身掛載到data對(duì)應(yīng)的key的依賴下 new Watcher(this.options, this.prop, value => { document.getElementById(this.el).textContent = value }) }
又因?yàn)槲覀冊(cè)趯懢幾g器的時(shí)候其實(shí)已經(jīng)將watcher整合進(jìn)compile了,所以之后我們其實(shí)只需要?jiǎng)?chuàng)建一個(gè)complie實(shí)例即可監(jiān)聽下面所有的節(jié)點(diǎn)。
init() { observe(this.data) // 監(jiān)聽data new Compile(this.el, this.options) }
總結(jié)
如何實(shí)現(xiàn)一個(gè)雙向綁定:
(1)監(jiān)聽器:對(duì)你的目標(biāo)data下的所有key進(jìn)行一個(gè)監(jiān)聽,并且提供一個(gè)Dep類用于依賴的收集及執(zhí)行,在進(jìn)行監(jiān)聽的時(shí)候需要對(duì)每一個(gè)key實(shí)例化一個(gè)Dep,對(duì)這個(gè)key進(jìn)行g(shù)et操作時(shí)需要判斷Dep類上target是否有內(nèi)容,如果有那就將這個(gè)內(nèi)容塞入實(shí)例化的Dep中這就是依賴收集,等到set的時(shí)候再將實(shí)例化的Dep中收集的依賴全部執(zhí)行一遍這就是依賴執(zhí)行。
(2)訂閱器:負(fù)責(zé)給監(jiān)聽器設(shè)置依賴,當(dāng)我們實(shí)現(xiàn)一個(gè)依賴函數(shù)時(shí)比如我們更新了數(shù)據(jù)應(yīng)該要修改頁面某個(gè)節(jié)點(diǎn)值時(shí),這個(gè)修改的函數(shù)會(huì)連同data,key一同傳入訂閱器的構(gòu)造函數(shù)中,在訂閱器的構(gòu)造方法會(huì)先緩存執(zhí)行參數(shù),然后里面我們會(huì)調(diào)用一個(gè)get方法這個(gè)方法會(huì)將Dep的target指向watcher實(shí)例本身,然后get里面自己讀取一次對(duì)應(yīng)data下的key,這一步會(huì)觸發(fā)監(jiān)聽器的get操作,因?yàn)楸O(jiān)聽器的get操作會(huì)判斷Dep類上target是否有內(nèi)容,因?yàn)檫@時(shí)候Dep的target指向watcher實(shí)例本身,所以watcher實(shí)例被加入key的實(shí)例化Dep的依賴中,完成了依賴的收集,最后再把Dep的target重新指向null,防止反復(fù)收集。
(3)編譯器:用于動(dòng)態(tài)的為每個(gè)節(jié)點(diǎn)添加watcher的類,會(huì)先創(chuàng)建一個(gè)空白的文檔碎片,然后將目標(biāo)節(jié)點(diǎn)下的所有節(jié)點(diǎn)移動(dòng)到空白碎片文檔中,這一步是為了對(duì)節(jié)點(diǎn)進(jìn)行分析,分析完之后一次性加入到頁面中防止頻繁改動(dòng)。
然后就會(huì)調(diào)用編譯函數(shù),遍歷所有節(jié)點(diǎn),如果有子節(jié)點(diǎn)就遞歸遍歷,如果是元素節(jié)點(diǎn)就調(diào)用元素解析器,如果是文本節(jié)點(diǎn)就調(diào)用文本解析器,對(duì)每一個(gè)節(jié)點(diǎn)設(shè)置相關(guān)key的watcher實(shí)現(xiàn)數(shù)據(jù)更新視圖,如果這個(gè)節(jié)點(diǎn)需要更新數(shù)據(jù)的話還得給他設(shè)置回調(diào)函數(shù),使得其可以在回調(diào)函數(shù)中更新數(shù)據(jù)。這就是雙向綁定的原理。
(4)雙向綁定構(gòu)造函數(shù)
緩存我們傳入的目標(biāo)節(jié)點(diǎn)和data,在init方法中調(diào)用監(jiān)聽器,并且實(shí)例化編譯器對(duì)每一個(gè)節(jié)點(diǎn)掛載對(duì)應(yīng)的watcher和回調(diào)函數(shù)即可。
效果
完整代碼
<!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>VUE雙向綁定原理實(shí)現(xiàn)</title> </head> <body> <div id="app">{{name}}{{name}}<div>{{child.name}}</div><input v-model="name"></input></div> <div id="app1" v-model="a.b.c"></div> <script> // 監(jiān)聽器 // 數(shù)據(jù)監(jiān)聽,監(jiān)聽對(duì)象的所有g(shù)et和set function observe(data) { // 如果data不存在或data不是object if (!data || typeof data !== 'object') { return; } // 遍歷data,獲得當(dāng)前的key for (const key in data) { // 監(jiān)聽data下的當(dāng)前key的屬性變動(dòng) defineReactive(data, key, data[key]); // 一次只處理data和一個(gè)key的監(jiān)聽關(guān)系 } } // 通過Object.defineProperty去監(jiān)聽data下的key function defineReactive(data, key, value) { // 處理key對(duì)應(yīng)的value是一個(gè)對(duì)象的情況 observe(value) // 創(chuàng)建Dep() 實(shí)例 用于存儲(chǔ)key的依賴,以及觸發(fā)key的依賴 let dep = new Dep() Object.defineProperty(data, key, { // 返回value值 get: function () { // get的時(shí)候進(jìn)行依賴收集 if(Dep.target) { // Dep.target默認(rèn)為null,不會(huì)收集依賴,但是創(chuàng)建watcher實(shí)例的時(shí)候 // 會(huì)為Dep.target賦值并指向當(dāng)前的watcher實(shí)例,同時(shí)會(huì)為watcher掛載上update函數(shù) // 然后get的時(shí)候會(huì)出現(xiàn)觸發(fā)依賴收集,因?yàn)檫@時(shí)候Dep.target指向當(dāng)前的watcher實(shí)例,就可以將這個(gè)watcher實(shí)例收集起來 dep.addSub(Dep.target) } return value }, set: function (newValue) { // get的時(shí)候觸發(fā)依賴 val = newValue; // 依賴觸發(fā),遍歷依賴?yán)锩娴膚atcher并調(diào)用update函數(shù) dep.notify(newValue) } } )} // 創(chuàng)建Dep類,用來存儲(chǔ)依賴 class Dep{ constructor() { this.subs = [] // 定義一個(gè)subs數(shù)組用來存放依賴 } addSub(sub) { // get的時(shí)候存儲(chǔ)依賴 // 注:更新函數(shù)會(huì)被掛載到單獨(dú)創(chuàng)建的watcher實(shí)例,存儲(chǔ)依賴的時(shí)候,實(shí)際上存儲(chǔ)的是創(chuàng)建的watcher實(shí)例 this.subs.push(sub) } notify(newValue) { // set的時(shí)候觸發(fā)依賴 // 注: 遍歷依賴,開始執(zhí)行里面watcher實(shí)例的更新函數(shù) this.subs.forEach(item => { item.update(newValue) }) } target = null } // 訂閱器 class Watcher{ constructor(vm, prop, callback) { this.vm = vm // 監(jiān)聽的節(jié)點(diǎn)和監(jiān)聽的數(shù)據(jù)對(duì)象 this.prop = prop // 監(jiān)聽的數(shù)據(jù)對(duì)象的某個(gè)參數(shù)key,如 name、age 等 this.callback = callback // 存儲(chǔ)回調(diào) this.value = this.get() // 觸發(fā)數(shù)據(jù)get操作,開始依賴收集 } get() { Dep.target = this // 將訂閱器賦值給Dep的target // 調(diào)用watcher前需要調(diào)用observe,所以這里的data已經(jīng)被監(jiān)聽了,get的時(shí)候開始依賴收集因?yàn)镈ep.target = this,所以target非空,Watcher會(huì)被插入到data的key下的依賴集合中 const value = this.prop() // 由于要處理取對(duì)象下面的對(duì)象的值的情況,這里需要返回一個(gè)函數(shù)在get()的時(shí)候再顯式的收集依賴 Dep.target = null // 關(guān)閉依賴收集,防止每次都收集依賴 return value // 獲取當(dāng)前的值 } // 注意調(diào)用update的時(shí)候是在監(jiān)聽器的notify里面進(jìn)行的,說明已經(jīng)set操作完了,值已經(jīng)變了,但是watcher的value值在get的時(shí)候先緩存了上一次value值 update(newValue) { // 更新函數(shù) const value = newValue // 獲取當(dāng)前的值 const oldValue = this.value // 獲取之前緩存的值 if(value != oldValue) { // 如果當(dāng)前的值和之前緩存的值不同則觸發(fā)更新函數(shù),并重置value this.value = value this.callback(value); // 觸發(fā)更新回調(diào)函數(shù) } } } // 編譯器 class Compile { // 對(duì)數(shù)據(jù)進(jìn)行解析 constructor(el, vm) { // el當(dāng)前目標(biāo)元素根節(jié)點(diǎn),vm雙向綁定構(gòu)造函數(shù)實(shí)例 this.el = this.isElementNode(el) ? el : document.getElementById(el) // 獲取目標(biāo)節(jié)點(diǎn) this.vm = vm // 暫存構(gòu)造函數(shù)實(shí)例 // 獲取文檔碎片節(jié)點(diǎn),暫存在內(nèi)容中 const fragmentNode = this.node2Fragment(this.el) // 模板編譯,主要是為每一個(gè)節(jié)點(diǎn)去建立映射關(guān)系為其設(shè)置對(duì)應(yīng)的watcher,把上面創(chuàng)建的節(jié)點(diǎn)傳入解析器去編譯分析 this.compile(fragmentNode) // 將處理后端 this.el.appendChild(fragmentNode) } // 解析元素節(jié)點(diǎn) node2Fragment(Node) { // 創(chuàng)建空白文檔 const f = document.createDocumentFragment() // 每次先看看Node.firstChild是否存在 while((Node.firstChild)) { // 把子元素加到空白文檔中,加過去之后意味著目標(biāo)節(jié)點(diǎn)第一個(gè)節(jié)點(diǎn)遷移到空白文檔中,那么目標(biāo)節(jié)點(diǎn)的元素就會(huì)越來越少,全部變成空白文檔下的子元素 f.appendChild(Node.firstChild) } return f } // 編譯模板 compile(fragmentNode) { // 獲取模板的子節(jié)點(diǎn) const childNodes = fragmentNode.childNodes; // 遍歷所有節(jié)點(diǎn) [...childNodes].forEach( child => { // 判斷子節(jié)點(diǎn)中是否還有節(jié)點(diǎn) 遞歸調(diào)用 if (child.childNodes && child.childNodes.length) { this.compile(child); } // 如果是元素節(jié)點(diǎn)類型 else if(this.isElementNode(child)) { // 使用元素編譯 this.elementCompile(child) } // 如果是文本節(jié)點(diǎn)類型 else { // 使用文本編譯 this.textCompile(child) } }) } // 元素編譯函數(shù) elementCompile(node) { // 獲得目標(biāo)的節(jié)點(diǎn)的屬性 const attributes = node.attributes; [...attributes].forEach(item => { // 解構(gòu)出參數(shù)的name和value, name相當(dāng)于v指令如v-model等,value則是對(duì)應(yīng)的值 name, age等 const {name, value} = item if(!this.isMyVueAttributes(name)) return const attributesName = name.split('-')[1] compileFunc[attributesName](node, value, this.vm) }) } // 文本編譯函數(shù) textCompile(node) { // 獲取節(jié)點(diǎn)文字內(nèi)容 const content = node.textContent; // 匹配插值語法,如果有{{}}語法則為其設(shè)置watcher if(/{{.+?}}/.test(content)) { compileFunc['text'](node, content, this.vm) } } // 判斷是否node節(jié)點(diǎn) isElementNode(Node) { return Node.nodeType == 1 } // 判斷是否v-開頭的屬性 isMyVueAttributes(name) { return name.startsWith('v-') } } const compileFunc = { text(node, content, vm) { const textMatch = /(?<={{)(.+?)(?=})/g; let propList = content.match(textMatch) propList = [...new Set(propList)] for (let index = 0; index < propList.length; index++) { let func = value => { this.updater.textUpdater(node, value, propList[index], content) } new Watcher(vm, this.propFunc(propList[index], vm), func) } }, model(node, prop, vm) { let func = value => { this.updater.modelUpdater(node, value) } new Watcher(vm, this.propFunc(prop, vm), func) node.oninput =(item)=>{ // 這里還沒辦法處理字符串轉(zhuǎn)對(duì)應(yīng)對(duì)象 vm.data[prop] = item.srcElement.value } }, // 處理取對(duì)象下的值的情況,因?yàn)閧{}}里面會(huì)有{{a.b.c}}這種情況需要通過一個(gè)函數(shù)的形式在watcher中進(jìn)行g(shù)et操作 propFunc(value, vm) { return function() {value.split(".").reduce((p, c) => { return p[c]; }, vm.data)} }, // 實(shí)際操作dom方法 updater: { textUpdater(node, value, prop, content) { node.textContent = content.replaceAll(`{{${prop}}}`, value); }, modelUpdater(node, value) { node.value = value; }, } } // 雙向綁定構(gòu)造函數(shù) class MyVue { constructor(options, prop) { this.options = options // 需要雙向綁定的對(duì)象,包括數(shù)據(jù)對(duì)象和對(duì)應(yīng)的節(jié)點(diǎn) this.data = options.data // 需要監(jiān)聽的數(shù)據(jù)對(duì)象 this.el = options.el // 需要更新視圖節(jié)點(diǎn) this.prop = prop // 對(duì)應(yīng)的key值 this.init() } init() { observe(this.data) // 監(jiān)聽data new Compile(this.el, this.options) } } const vm = new MyVue({ el: "app", data: { name: 'xiaoshan',child: {name:123} } }); // // 創(chuàng)建一個(gè)對(duì)象,對(duì)這個(gè)對(duì)象數(shù)據(jù)監(jiān)聽 // var user = { name: 'xiaoshan', age: 17, son: {name:'test'} } // // 數(shù)據(jù)監(jiān)聽函數(shù) // observe(user) // user.name = user.name + '小山' // user.son.name = user.son.name + '小小山' </script> </body> </html>
代碼缺陷
上面的代碼沒有做頁面初始化顯示,只是一個(gè)demo
只做了文本節(jié)點(diǎn)和元素節(jié)點(diǎn)v-model的解析
v-model的時(shí)候還不支持a.b.c因?yàn)樵趏ninput事件中set還有點(diǎn)問題
對(duì)于文本節(jié)點(diǎn)時(shí)應(yīng)該以{{}}為一個(gè)單獨(dú)節(jié)點(diǎn)。不然替換的時(shí)候太麻煩了
到此這篇關(guān)于詳解Vue中雙向綁定原理及簡單實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Vue雙向綁定內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue和小程序項(xiàng)目中使用iconfont的方法
這篇文章主要介紹了vue中和小程序中使用iconfont的方法,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-05-05Vue中使用Scss實(shí)現(xiàn)配置、切換主題方式
這篇文章主要介紹了Vue中使用Scss實(shí)現(xiàn)配置、切換主題方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03vue-quill-editor的使用及個(gè)性化定制操作
這篇文章主要介紹了vue-quill-editor的使用及個(gè)性化定制操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-08-08vue 根據(jù)選擇的月份動(dòng)態(tài)展示日期對(duì)應(yīng)的星期幾
這篇文章主要介紹了vue 如何根據(jù)選擇的月份動(dòng)態(tài)展示日期對(duì)應(yīng)的星期幾,幫助大家更好的利用vue框架處理日期需求,感興趣的朋友可以了解下2021-02-02