Vue響應(yīng)式原理模擬實(shí)現(xiàn)原理探究
前置知識(shí)
數(shù)據(jù)驅(qū)動(dòng)
數(shù)據(jù)響應(yīng)式——Vue 最標(biāo)志性的功能就是其低侵入性的響應(yīng)式系統(tǒng)。組件狀態(tài)都是由響應(yīng)式的 JavaScript 對(duì)象組成的。當(dāng)更改它們時(shí),視圖會(huì)隨即自動(dòng)更新。
雙向綁定——數(shù)據(jù)改變,視圖改變;視圖改變,數(shù)據(jù)也隨之改變
數(shù)據(jù)驅(qū)動(dòng)是Vue最獨(dú)特的特性之一 —— 開(kāi)發(fā)者只需關(guān)注數(shù)據(jù)本身,而無(wú)需關(guān)心數(shù)據(jù)如何渲染到視圖
數(shù)據(jù)響應(yīng)式的核心原理
Vue 2.x
<!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> <div id="app"> hello </div> <script> // 模擬Vue中的data選項(xiàng) let data = { msg: 'hello' } // 模擬Vue的實(shí)例 let vm = {} // 數(shù)據(jù)劫持,當(dāng)訪(fǎng)問(wèn)或設(shè)置vm中的成員的時(shí)候,做一些操作 Object.defineProperty(vm, 'msg', { // 是否可以枚舉 enumerable: true, // 是否可配置,即delete刪除 configurable: true, get() { console.log('get: ', data.msg); return data.msg }, set(newValue) { console.log('set: ', newValue) if (newValue === data.msg) return data.msg = newValue // 更新DOM document.getElementById('app').textContent = data.msg } }) // 測(cè)試 vm.msg = 'Hello World' console.log(vm.msg) </script> </body> </html>
當(dāng)data中有多個(gè)對(duì)象時(shí),需要對(duì)其進(jìn)行遍歷,此時(shí)需要對(duì)上述代碼進(jìn)行一些改造。
let data = { msg: 'hello', count: 10 } let vm = {} proxyData(data) function proxyData(data) { Object.keys(data).forEach(key => { Object.defineProperty(vm, key, { enumerable: true, configurable: true, get() { return data[key] }, set(newValue) { if (newValue === data[key]) return data[key] = newValue document.querySelector('#app').textContent = data[key] } }) }) }
Vue 3.x
步入Vue3,尤小右使用Proxy對(duì)其進(jìn)行了改造,不僅拋棄了如 $delete 之類(lèi)的雞肋API(因?yàn)镻roxy可以監(jiān)聽(tīng)刪除屬性),還提升了性能。
<!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> <div id="app">hello</div> <script> let data = { msg: 'hello', count: 0 } let vm = new Proxy(data, { get(target, key) { return target[key] }, set(target, key, newValue) { if (target[key] === newValue) return target[key] = newValue document.querySelector('#app').textContent = target[key] } }) </script> </body> </html>
發(fā)布訂閱和觀(guān)察者模式
發(fā)布/訂閱模式
注:為簡(jiǎn)便起見(jiàn),代碼實(shí)現(xiàn)并未加入對(duì)傳參的考慮。
<!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> <script> class EventEmitter { constructor() { this.subs = Object.create(null) } // 注冊(cè)事件 $on(eventType, handler) { this.subs[eventType] = this.subs[eventType] || [] this.subs[eventType].push(handler) } $emit(eventType) { if (this.subs[eventType]) { this.subs[eventType].forEach(handler => { handler() }) } } } // 測(cè)試 let em = new EventEmitter() em.$on('click', () => { console.log('click1'); }) em.$on('click', () => { console.log('click2'); }) em.$emit('click') </script> </body> </html>
觀(guān)察者模式
注:為簡(jiǎn)便起見(jiàn),代碼實(shí)現(xiàn)并未加入對(duì)傳參的考慮。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>觀(guān)察者模式</title> </head> <body> <script> // 發(fā)布者-目標(biāo) class Dep { constructor() { this.subs = [] } addSub(sub) { if (sub && sub.update) { this.subs.push(sub) } } notify() { this.subs.forEach(sub => { sub.update() }) } } class Watcher { update() { console.log('update') } } // 測(cè)試 let dep = new Dep() let watcher = new Watcher() dep.addSub(watcher) dep.notify() </script> </body> </html>
Vue響應(yīng)式原理模擬實(shí)現(xiàn)
Vue
功能
- 接收初始化參數(shù)
- 將data中的數(shù)據(jù)注入實(shí)例并轉(zhuǎn)換成getter/setter
- 調(diào)用observer監(jiān)聽(tīng)data中屬性的變化
- 調(diào)用compiler解析指令/插值表達(dá)式
class Vue { constructor(options) { // 1. 通過(guò)屬性保存選項(xiàng)的數(shù)據(jù) this.$options = options || {} this.$data = options.data || {} this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el // 2. 把data中的數(shù)據(jù)轉(zhuǎn)換為getter和setter,并注入到Vue實(shí)例中 this._proxyData(this.$data) // 3. 調(diào)用observer對(duì)象,監(jiān)聽(tīng)數(shù)據(jù)的變化 new Observer(this.$data) // 4. 調(diào)用compiler對(duì)象,解析指令和插值表達(dá)式 new Compiler(this) } _proxyData(data) { Object.keys(data).forEach(key => { Object.defineProperty(this, key, { enumerable: true, configurable: true, get() { return data[key] }, set(newValue) { if (newValue === data[key]) return data[key] = newValue } }) }) } }
Observer對(duì)data中的屬性進(jìn)行監(jiān)聽(tīng)
class Observer { constructor(data) { this.walk(data) } walk(data) { // 1.判斷data是否是對(duì)象 if (!data || typeof data !== 'object') { return } // 2.遍歷data對(duì)象的所有屬性 Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]) }) } defineReactive(obj, key, val) { let that = this // 負(fù)責(zé)收集依賴(lài) let dep = new Dep() // 如果val是對(duì)象,會(huì)將其內(nèi)部的對(duì)象也變成響應(yīng)式數(shù)據(jù) this.walk(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // 收集依賴(lài) Dep.target && dep.addSub(Dep.target) return val }, set(newValue) { if (newValue === val) { return } val = newValue that.walk(newValue) // 發(fā)送通知 dep.notify() } }) } }
Compiler
class Compiler { constructor(vm) { this.el = vm.$el this.vm = vm this.compile(this.el) } // 編譯模板,處理文本節(jié)點(diǎn)和元素節(jié)點(diǎn) compile(el) { let childNodes = el.childNodes Array.from(childNodes).forEach(node => { // 處理文本節(jié)點(diǎn) if (this.isTextNode(node)) { this.compileText(node) } else if (this.isElementNode(node)) { // 處理元素節(jié)點(diǎn) this.comipleElement(node) } // 判斷node節(jié)點(diǎn)是否有子節(jié)點(diǎn) if (node.childNodes && node.childNodes.length) { this.compile(node) } }) } // 編譯元素節(jié)點(diǎn),處理指令 comipleElement(node) { Array.from(node.attributes).forEach(attr => { let attrName = attr.name if (this.isDirective(attrName)) { // v-text => text attrName = attrName.substr(2) let key = attr.value this.update(node, key, attrName) } }) } update(node, key, attrName) { let updateFn = this[attrName + 'Updater'] updateFn && updateFn.call(this, node, this.vm[key], key) } // 處理 v-text 指令 textUpdater(node, value, key) { node.textContent = value new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } // v-model modelUpdater(node, value, key) { node.value = value new Watcher(this.vm, key, (newValue) => { node.value = newValue }) // 雙向綁定 node.addEventListener('input', () => { this.vm[key] = node.value }) } // 編譯文本節(jié)點(diǎn),處理插值表達(dá)式 compileText(node) { // console.dir(node); let reg = /\{\{(.+?)\}\}/ let value = node.textContent if (reg.test(value)) { let key = RegExp.$1.trim() node.textContent = value.replace(reg, this.vm[key]) // 創(chuàng)建watcher對(duì)象,當(dāng)數(shù)據(jù)改變更新視圖 new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } } // 判斷元素屬性是否是指令 isDirective(attrName) { return attrName.startsWith('v-') } // 判斷節(jié)點(diǎn)是否是文本節(jié)點(diǎn) isTextNode(node) { return node.nodeType === 3 } // 判斷節(jié)點(diǎn)是否是元素節(jié)點(diǎn) isElementNode(node) { return node.nodeType === 1 } }
Watcher
class Watcher { constructor(vm, key, cb) { this.vm = vm // data中的屬性名稱(chēng) this.key = key // 回調(diào)函數(shù)負(fù)責(zé)更新視圖 this.cb = cb // 把watcher對(duì)象記錄到Dep類(lèi)的靜態(tài)屬性target Dep.target = this // 觸發(fā)get方法,在get方法中會(huì)調(diào)用addSub this.oldValue = vm[key] Dep.target = null } // 當(dāng)數(shù)據(jù)發(fā)生變化的時(shí)候更新視圖 update() { let newValue = this.vm[this.key] if (this.oldValue === newValue) { return } this.cb(newValue) } }
Dep
class Dep { constructor() { this.subs = [] } // 添加觀(guān)察者 addSub(sub) { if (sub && sub.update) { this.subs.push(sub) } } // 發(fā)送通知 notify() { this.subs.forEach(sub => { sub.update() }) } }
測(cè)試代碼
<!DOCTYPE html> <html lang="cn"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Mini Vue</title> </head> <body> <div id="app"> <h1>差值表達(dá)式</h1> <h3>{{ msg }}</h3> <h3>{{ count }}</h3> <h1>v-text</h1> <div v-text="msg"></div> <h1>v-model</h1> <input type="text" v-model="msg"> <input type="text" v-model="count"> </div> <script src="./js/dep.js"></script> <script src="./js/watcher.js"></script> <script src="./js/compiler.js"></script> <script src="./js/observer.js"></script> <script src="./js/vue.js"></script> <script> let vm = new Vue({ el: '#app', data: { msg: 'Hello Vue', count: 100 } }) vm.test = 'abc' </script> </body> </html>
到此這篇關(guān)于 Vue響應(yīng)式原理模擬實(shí)現(xiàn)原理探究的文章就介紹到這了,更多相關(guān) Vue響應(yīng)式原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue3父子組件傳參有關(guān)sync修飾符的用法詳解
這篇文章主要給大家介紹關(guān)于前端Vue3父子組件傳參有關(guān)sync修飾符的用法詳細(xì)解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-09-09vue獲取元素寬、高、距離左邊距離,右,上距離等還有XY坐標(biāo)軸的方法
今天小編就為大家分享一篇vue獲取元素寬、高、距離左邊距離,右,上距離等還有XY坐標(biāo)軸的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-09-09Vue3初探之ref、reactive以及改變數(shù)組的值
在setup函數(shù)中,可以使用ref函數(shù)和reactive函數(shù)來(lái)創(chuàng)建響應(yīng)式數(shù)據(jù),下面這篇文章主要給大家介紹了關(guān)于Vue3初探之ref、reactive以及改變數(shù)組值的相關(guān)資料,需要的朋友可以參考下2022-09-09vue如何實(shí)現(xiàn)el-select下拉選項(xiàng)的懶加載
這篇文章主要介紹了vue如何實(shí)現(xiàn)el-select下拉選項(xiàng)的懶加載,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04Vuex之module使用方法及場(chǎng)景說(shuō)明
這篇文章主要介紹了Vuex之module使用方法及場(chǎng)景說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10html-webpack-plugin修改頁(yè)面的title的方法
這篇文章主要介紹了html-webpack-plugin修改頁(yè)面的title的方法 ,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06Vue+Element UI+vue-quill-editor富文本編輯器及插入圖片自定義
這篇文章主要為大家詳細(xì)介紹了Vue+Element UI+vue-quill-editor富文本編輯器及插入圖片自定義,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08vue利用v-for嵌套輸出多層對(duì)象,分別輸出到個(gè)表的方法
今天小編就為大家分享一篇vue利用v-for嵌套輸出多層對(duì)象,分別輸出到個(gè)表的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-09-09vue-editor-bridge報(bào)錯(cuò)的解決方案
這篇文章主要介紹了vue-editor-bridge報(bào)錯(cuò)的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04element-ui?tree?異步樹(shù)實(shí)現(xiàn)勾選自動(dòng)展開(kāi)、指定展開(kāi)、指定勾選功能
這篇文章主要介紹了element-ui?tree?異步樹(shù)實(shí)現(xiàn)勾選自動(dòng)展開(kāi)、指定展開(kāi)、指定勾選,項(xiàng)目中用到了vue的element-ui框架,用到了el-tree組件,由于數(shù)據(jù)量很大,使用了數(shù)據(jù)懶加載模式,即異步樹(shù),需要的朋友可以參考下2022-08-08