站長(zhǎng)資訊網(wǎng)
最全最豐富的資訊網(wǎng)站

手把手帶你了解VUE響應(yīng)式原理

本篇文章我們來(lái)了解 Vue2.X 響應(yīng)式原理,然后我們來(lái)實(shí)現(xiàn)一個(gè) vue 響應(yīng)式原理(寫的內(nèi)容簡(jiǎn)單)實(shí)現(xiàn)步驟和注釋寫的很清晰,大家有興趣可以耐心觀看,希望對(duì)大家有所幫助!

手把手帶你了解VUE響應(yīng)式原理

如何快速入門VUE3.0:進(jìn)入學(xué)習(xí)

Vue2.X響應(yīng)式原理

1.defineProperty 的應(yīng)用

Vue2.X 響應(yīng)式中使用到了 defineProperty 進(jìn)行數(shù)據(jù)劫持,所以我們對(duì)它必須有一定的了解,那么我們先來(lái)了解它的使用方法把, 這里我們來(lái)使用 defineProperty來(lái)模擬 Vue 中的 data。(學(xué)習(xí)視頻分享:vue視頻教程)

<body>     <div id="app"></div>     <script>         // 模擬 Vue的data         let data = {             msg: '',         }         // 模擬 Vue 實(shí)例         let vm = {}         // 對(duì) vm 的 msg 進(jìn)行數(shù)據(jù)劫持         Object.defineProperty(vm, 'msg', {             // 獲取數(shù)據(jù)             get() {                 return data.msg             },             // 設(shè)置 msg             set(newValue) {                 // 如果傳入的值相等就不用修改                 if (newValue === data.msg) return                 // 修改數(shù)據(jù)                 data.msg = newValue                 document.querySelector('#app').textContent = data.msg             },         })         // 這樣子就調(diào)用了 defineProperty vm.msg 的 set         vm.msg = '1234'     </script> </body>

手把手帶你了解VUE響應(yīng)式原理

可以看見 上面 vm.msg 數(shù)據(jù)是響應(yīng)式

2.defineProperty修改多個(gè)參數(shù)為響應(yīng)式

修改多個(gè)參數(shù)

看了上面的方法只能修改一個(gè)屬性,實(shí)際上我們 data 中數(shù)據(jù)不可能只有一個(gè),我們何不定義一個(gè)方法把data中的數(shù)據(jù)進(jìn)行遍歷都修改成響應(yīng)式呢

<body>     <div id="app"></div> 	<script>         // 模擬 Vue的data         let data = {             msg: '哈哈',             age: '18',         }         // 模擬 Vue 實(shí)例         let vm = {}         // 把多個(gè)屬性轉(zhuǎn)化 響應(yīng)式         function proxyData() {             // 把data 中每一項(xiàng)都[msg,age] 拿出來(lái)操作             Object.keys(data).forEach((key) => {                 // 對(duì) vm 的 屬性 進(jìn)行數(shù)據(jù)劫持                 Object.defineProperty(vm, key, {                     // 可枚舉                     enumerable: true,                     // 可配置                     configurable: true,                     // 獲取數(shù)據(jù)                     get() {                         return data[key]                     },                     // 設(shè)置 屬性值                     set(newValue) {                         // 如果傳入的值相等就不用修改                         if (newValue === data[key]) return                         // 修改數(shù)據(jù)                         data[key] = newValue                         document.querySelector('#app').textContent = data[key]                     },                 })             })         }         // 調(diào)用方法         proxyData(data)  	</script> </body>

3.Proxy

Vue3 中使用 Proxy 來(lái)設(shè)置響應(yīng)式的屬性

先來(lái)了解下 Proxy 的兩個(gè)參數(shù)

new Proxy(target,handler)

  • target :要使用 Proxy 包裝的目標(biāo)對(duì)象(可以是任何類型的對(duì)象,包括原生數(shù)組,函數(shù),甚至另一個(gè)代理)
  • handler:一個(gè)通常以函數(shù)作為屬性的對(duì)象,各屬性中的函數(shù)分別定義了在執(zhí)行各種操作時(shí)代理 p 的行為

其實(shí) 和 Vue2.X實(shí)現(xiàn)的邏輯差不多,不過實(shí)現(xiàn)的方法不一樣

那么就放上代碼了

<body>     <div id="app"></div>     <script>             // 模擬 Vue data             let data = {                 msg: '',                 age: '',             }             // 模擬 Vue 的一個(gè)實(shí)例             // Proxy 第一個(gè)             let vm = new Proxy(data, {                 // get() 獲取值                 // target 表示需要代理的對(duì)象這里指的就是 data                 // key 就是對(duì)象的 鍵                 get(target, key) {                     return target[key]                 },                 // 設(shè)置值                 // newValue 是設(shè)置的值                 set(target, key, newValue) {                     // 也先判斷下是否和之前的值一樣 節(jié)省性能                     if (target[key] === newValue) return                     // 進(jìn)行設(shè)置值                     target[key] = newValue                     document.querySelector('#app').textContent = target[key]                 },             })     </script> </body>

觸發(fā)setget 的方法

// 觸發(fā)了set方法 vm.msg = 'haha' // 觸發(fā)了get方法 console.log(vm.msg)

4.發(fā)布訂閱模式

在Vue 響應(yīng)式中應(yīng)用到了 發(fā)布訂閱模式 我們先來(lái)了解下

首先來(lái)說(shuō)簡(jiǎn)單介紹下 一共有三個(gè)角色

發(fā)布者訂閱者信號(hào)中心 舉個(gè)現(xiàn)實(shí)中例子 作者(發(fā)布者)寫一篇文章 發(fā)到了掘金(信號(hào)中心) ,掘金可以處理文章然后推送到了首頁(yè),然后各自大佬(訂閱者)就可以訂閱文章

在Vue 中的例子 就是EventBus $on $emit

那么我們就簡(jiǎn)單模仿一下 Vue 的事件總線吧

之前代碼縮進(jìn)4個(gè)單位有點(diǎn)寬,這里改成2個(gè)

<body>   <div id="app"></div>   <script>     class Vue {       constructor() {         // 用來(lái)存儲(chǔ)事件         // 存儲(chǔ)的 例子 this.subs = { 'myclick': [fn1, fn2, fn3] ,'inputchange': [fn1, fn2] }         this.subs = {}       }       // 實(shí)現(xiàn) $on 方法 type是任務(wù)隊(duì)列的類型 ,fn是方法       $on(type, fn) {         // 判斷在 subs是否有當(dāng)前類型的 方法隊(duì)列存在         if (!this.subs[type]) {           // 沒有就新增一個(gè) 默認(rèn)為空數(shù)組           this.subs[type] = []         }         // 把方法加到該類型中         this.subs[type].push(fn)       }       // 實(shí)現(xiàn) $emit 方法       $emit(type) {         // 首先得判斷該方法是否存在         if (this.subs[type]) {           // 獲取到參數(shù)           const args = Array.prototype.slice.call(arguments, 1)           // 循環(huán)隊(duì)列調(diào)用 fn           this.subs[type].forEach((fn) => fn(...args))         }       }     }      // 使用     const eventHub = new Vue()     // 使用 $on 添加一個(gè) sum 類型的 方法到 subs['sum']中     eventHub.$on('sum', function () {       let count = [...arguments].reduce((x, y) => x + y)       console.log(count)     })     // 觸發(fā) sum 方法     eventHub.$emit('sum', 1, 2, 4, 5, 6, 7, 8, 9, 10)   </script> </body>

5.觀察者模式

與 發(fā)布訂閱 的差異

與發(fā)布訂閱者不同 觀察者中 發(fā)布者和訂閱者(觀察者)是相互依賴的 必須要求觀察者訂閱內(nèi)容改變事件 ,而發(fā)布訂閱者是由調(diào)度中心進(jìn)行調(diào)度,那么看看觀察者模式 是如何相互依賴,下面就舉個(gè)簡(jiǎn)單例子

<body>   <div id="app"></div>   <script>     // 目標(biāo)     class Subject {       constructor() {         this.observerLists = []       }       // 添加觀察者       addObs(obs) {         // 判斷觀察者是否有 和 存在更新訂閱的方法         if (obs && obs.update) {           // 添加到觀察者列表中           this.observerLists.push(obs)         }       }       // 通知觀察者       notify() {         this.observerLists.forEach((obs) => {           // 每個(gè)觀察者收到通知后 會(huì)更新事件           obs.update()         })       }       // 清空觀察者       empty() {         this.subs = []       }     }      class Observer {       // 定義觀察者內(nèi)容更新事件       update() {         // 在更新事件要處理的邏輯         console.log('目標(biāo)更新了')       }     }      // 使用     // 創(chuàng)建目標(biāo)     let sub = new Subject()     // 創(chuàng)建觀察者     let obs1 = new Observer()     let obs2 = new Observer()     // 把觀察者添加到列表中     sub.addObs(obs1)     sub.addObs(obs2)     // 目標(biāo)開啟了通知 每個(gè)觀察者者都會(huì)自己觸發(fā) update 更新事件     sub.notify()   </script> </body>

6.模擬Vue的響應(yīng)式原理

這里來(lái)實(shí)現(xiàn)一個(gè)小型簡(jiǎn)單的 Vue 主要實(shí)現(xiàn)以下的功能

  • 接收初始化的參數(shù),這里只舉幾個(gè)簡(jiǎn)單的例子 el data options
  • 通過私有方法 _proxyDatadata 注冊(cè)到 Vue 中 轉(zhuǎn)成getter setter
  • 使用 observerdata 中的屬性轉(zhuǎn)為 響應(yīng)式 添加到 自身身上
  • 使用 observer 方法監(jiān)聽 data 的所有屬性變化來(lái) 通過觀察者模式 更新視圖
  • 使用 compiler 編譯元素節(jié)點(diǎn)上面指令 和 文本節(jié)點(diǎn)差值表達(dá)式

1.vue.js

在這里獲取到 el data

通過 _proxyDatadata的屬性 注冊(cè)到Vue 并轉(zhuǎn)成 getter setter

/* vue.js */  class Vue {   constructor(options) {     // 獲取到傳入的對(duì)象 沒有默認(rèn)為空對(duì)象     this.$options = options || {}     // 獲取 el     this.$el =       typeof options.el === 'string'         ? document.querySelector(options.el)         : options.el     // 獲取 data     this.$data = options.data || {}     // 調(diào)用 _proxyData 處理 data中的屬性     this._proxyData(this.$data)   }   // 把data 中的屬性注冊(cè)到 Vue   _proxyData(data) {     Object.keys(data).forEach((key) => {       // 進(jìn)行數(shù)據(jù)劫持       // 把每個(gè)data的屬性 到添加到 Vue 轉(zhuǎn)化為 getter setter方法       Object.defineProperty(this, key, {         // 設(shè)置可以枚舉         enumerable: true,         // 設(shè)置可以配置         configurable: true,         // 獲取數(shù)據(jù)         get() {           return data[key]         },         // 設(shè)置數(shù)據(jù)         set(newValue) {           // 判斷新值和舊值是否相等           if (newValue === data[key]) return           // 設(shè)置新值           data[key] = newValue         },       })     })   } }

2.observer.js

在這里把 data 中的 屬性變?yōu)轫憫?yīng)式加在自身的身上,還有一個(gè)主要功能就是 觀察者模式在 第 4.dep.js 會(huì)有詳細(xì)的使用

/* observer.js */  class Observer {   constructor(data) {     // 用來(lái)遍歷 data     this.walk(data)   }   // 遍歷 data 轉(zhuǎn)為響應(yīng)式   walk(data) {     // 判斷 data是否為空 和 對(duì)象     if (!data || typeof data !== 'object') return     // 遍歷 data     Object.keys(data).forEach((key) => {       // 轉(zhuǎn)為響應(yīng)式       this.defineReactive(data, key, data[key])     })   }   // 轉(zhuǎn)為響應(yīng)式   // 要注意的 和vue.js 寫的不同的是   // vue.js中是將 屬性給了 Vue 轉(zhuǎn)為 getter setter   // 這里是 將data中的屬性轉(zhuǎn)為getter setter   defineReactive(obj, key, value) {     // 如果是對(duì)象類型的 也調(diào)用walk 變成響應(yīng)式,不是對(duì)象類型的直接在walk會(huì)被return     this.walk(value)     // 保存一下 this     const self = this     Object.defineProperty(obj, key, {       // 設(shè)置可枚舉       enumerable: true,       // 設(shè)置可配置       configurable: true,       // 獲取值       get() {         return value       },       // 設(shè)置值       set(newValue) {         // 判斷舊值和新值是否相等         if (newValue === value) return         // 設(shè)置新值         value = newValue         // 賦值的話如果是newValue是對(duì)象,對(duì)象里面的屬性也應(yīng)該設(shè)置為響應(yīng)式的         self.walk(newValue)       },     })   } }

在html中引入的話注意順序

<script src="./js/observer.js"></script> <script src="./js/vue.js"></script>

然后在vue.js 中使用 Observer

/* vue.js */  class Vue {   constructor(options) {     ...     // 使用 Obsever 把data中的數(shù)據(jù)轉(zhuǎn)為響應(yīng)式     new Observer(this.$data)   }   // 把data 中的屬性注冊(cè)到 Vue   _proxyData(data) {    ...   } }

看到這里為什么做了兩個(gè)重復(fù)性的操作呢?重復(fù)性兩次把 data的屬性轉(zhuǎn)為響應(yīng)式

obsever.js 中是把 data 的所有屬性 加到 data 自身 變?yōu)轫憫?yīng)式 轉(zhuǎn)成 getter setter方式

vue.js 中 也把 data的 的所有屬性 加到 Vue 上,是為了以后方面操作可以用 Vue 的實(shí)例直接訪問到 或者在 Vue 中使用 this 訪問

使用例子:

<body>     <div id="app"></div>     <script src="./js/observer.js"></script>     <script src="./js/vue.js"></script>     <script>       let vm = new Vue({         el: '#app',         data: {           msg: '123',           age: 21,         },       })     </script>   </body>

手把手帶你了解VUE響應(yīng)式原理

這樣在Vue$data 中都存在了 所有的data 屬性了 并且是響應(yīng)式的

3.compiler.js

comilper.js在這個(gè)文件里實(shí)現(xiàn)對(duì)文本節(jié)點(diǎn) 和 元素節(jié)點(diǎn)指令編譯 主要是為了舉例子 當(dāng)然這個(gè)寫的很簡(jiǎn)單 指令主要實(shí)現(xiàn) v-text v-model

/* compiler.js */  class Compiler {   // vm 指 Vue 實(shí)例   constructor(vm) {     // 拿到 vm     this.vm = vm     // 拿到 el     this.el = vm.$el     // 編譯模板     this.compile(this.el)   }   // 編譯模板   compile(el) {     // 獲取子節(jié)點(diǎn) 如果使用 forEach遍歷就把偽數(shù)組轉(zhuǎn)為真的數(shù)組     let childNodes = [...el.childNodes]     childNodes.forEach((node) => {       // 根據(jù)不同的節(jié)點(diǎn)類型進(jìn)行編譯       // 文本類型的節(jié)點(diǎn)       if (this.isTextNode(node)) {         // 編譯文本節(jié)點(diǎn)         this.compileText(node)       } else if (this.isElementNode(node)) {         //元素節(jié)點(diǎn)         this.compileElement(node)       }       // 判斷是否還存在子節(jié)點(diǎn)考慮遞歸       if (node.childNodes && node.childNodes.length) {         // 繼續(xù)遞歸編譯模板         this.compile(node)       }     })   }   // 編譯文本節(jié)點(diǎn)(簡(jiǎn)單的實(shí)現(xiàn))   compileText(node) {     // 核心思想利用把正則表達(dá)式把{{}}去掉找到里面的變量     // 再去Vue找這個(gè)變量賦值給node.textContent     let reg = /{{(.+?)}}/     // 獲取節(jié)點(diǎn)的文本內(nèi)容     let val = node.textContent     // 判斷是否有 {{}}     if (reg.test(val)) {       // 獲取分組一  也就是 {{}} 里面的內(nèi)容 去除前后空格       let key = RegExp.$1.trim()       // 進(jìn)行替換再賦值給node       node.textContent = val.replace(reg, this.vm[key])     }   }   // 編譯元素節(jié)點(diǎn)這里只處理指令   compileElement(node) {     // 獲取到元素節(jié)點(diǎn)上面的所有屬性進(jìn)行遍歷     ![...node.attributes].forEach((attr) => {       // 獲取屬性名       let attrName = attr.name       // 判斷是否是 v- 開頭的指令       if (this.isDirective(attrName)) {         // 除去 v- 方便操作         attrName = attrName.substr(2)         // 獲取 指令的值就是  v-text = "msg"  中msg         // msg 作為 key 去Vue 找這個(gè)變量         let key = attr.value         // 指令操作 執(zhí)行指令方法         // vue指令很多為了避免大量個(gè) if判斷這里就寫個(gè) uapdate 方法         this.update(node, key, attrName)       }     })   }   // 添加指令方法 并且執(zhí)行   update(node, key, attrName) {     // 比如添加 textUpdater 就是用來(lái)處理 v-text 方法     // 我們應(yīng)該就內(nèi)置一個(gè) textUpdater 方法進(jìn)行調(diào)用     // 加個(gè)后綴加什么無(wú)所謂但是要定義相應(yīng)的方法     let updateFn = this[attrName + 'Updater']     // 如果存在這個(gè)內(nèi)置方法 就可以調(diào)用了     updateFn && updateFn(node, key, this.vm[key])   }   // 提前寫好 相應(yīng)的指定方法比如這個(gè) v-text   // 使用的時(shí)候 和 Vue 的一樣   textUpdater(node, key, value) {     node.textContent = value   }        // v-model   modelUpdater(node, key, value) {     node.value = value   }        // 判斷元素的屬性是否是 vue 指令   isDirective(attr) {     return attr.startsWith('v-')   }   // 判斷是否是元素節(jié)點(diǎn)   isElementNode(node) {     return node.nodeType === 1   }   // 判斷是否是 文本 節(jié)點(diǎn)   isTextNode(node) {     return node.nodeType === 3   } }

4.dep.js

寫一個(gè)Dep類 它相當(dāng)于 觀察者中的發(fā)布者 每個(gè)響應(yīng)式屬性都會(huì)創(chuàng)建這么一個(gè) Dep 對(duì)象 ,負(fù)責(zé)收集該依賴屬性的Watcher對(duì)象 (是在使用響應(yīng)式數(shù)據(jù)的時(shí)候做的操作)

當(dāng)我們對(duì)響應(yīng)式屬性在 setter 中進(jìn)行更新的時(shí)候,會(huì)調(diào)用 Depnotify 方法發(fā)送更新通知

然后去調(diào)用 Watcher 中的 update 實(shí)現(xiàn)視圖的更新操作(是當(dāng)數(shù)據(jù)發(fā)生變化的時(shí)候去通知觀察者調(diào)用觀察者的update更新視圖)

總的來(lái)說(shuō) 在Dep(這里指發(fā)布者) 中負(fù)責(zé)收集依賴 添加觀察者(這里指Wathcer),然后在 setter 數(shù)據(jù)更新的時(shí)候通知觀察者

說(shuō)的這么多重復(fù)的話,大家應(yīng)該知道是在哪個(gè)階段 收集依賴 哪個(gè)階段 通知觀察者了吧,下面就來(lái)實(shí)現(xiàn)一下吧

先寫Dep

/* dep.js */  class Dep {   constructor() {     // 存儲(chǔ)觀察者     this.subs = []   }   // 添加觀察者   addSub(sub) {     // 判斷觀察者是否存在 和 是否擁有update方法     if (sub && sub.update) {       this.subs.push(sub)     }   }   // 通知方法   notify() {     // 觸發(fā)每個(gè)觀察者的更新方法     this.subs.forEach((sub) => {       sub.update()     })   } }

obsever.js 中使用Dep

get 中添加 Dep.target (觀察者)

set 中 觸發(fā) notify (通知)

/* observer.js */  class Observer {   ...   }   // 遍歷 data 轉(zhuǎn)為響應(yīng)式   walk(data) {    ...   }   // 這里是 將data中的屬性轉(zhuǎn)為getter setter   defineReactive(obj, key, value) { 	...     // 創(chuàng)建 Dep 對(duì)象     let dep = new Dep()     Object.defineProperty(obj, key, { 	  ...       // 獲取值       get() {         // 在這里添加觀察者對(duì)象 Dep.target 表示觀察者         Dep.target && dep.addSub(Dep.target)         return value       },       // 設(shè)置值       set(newValue) {         if (newValue === value) return         value = newValue         self.walk(newValue)         // 觸發(fā)通知 更新視圖         dep.notify()       },     })   } }

5.watcher.js

**watcher **的作用 數(shù)據(jù)更新后 收到通知之后 調(diào)用 update 進(jìn)行更新

/* watcher.js */  class Watcher {   constructor(vm, key, cb) {     // vm 是 Vue 實(shí)例     this.vm = vm     // key 是 data 中的屬性     this.key = key     // cb 回調(diào)函數(shù) 更新視圖的具體方法     this.cb = cb     // 把觀察者的存放在 Dep.target     Dep.target = this     // 舊數(shù)據(jù) 更新視圖的時(shí)候要進(jìn)行比較     // 還有一點(diǎn)就是 vm[key] 這個(gè)時(shí)候就觸發(fā)了 get 方法     // 之前在 get 把 觀察者 通過dep.addSub(Dep.target) 添加到了 dep.subs中     this.oldValue = vm[key]     // Dep.target 就不用存在了 因?yàn)樯厦娴牟僮饕呀?jīng)存好了     Dep.target = null   }   // 觀察者中的必備方法 用來(lái)更新視圖   update() {     // 獲取新值     let newValue = this.vm[this.key]     // 比較舊值和新值     if (newValue === this.oldValue) return     // 調(diào)用具體的更新方法     this.cb(newValue)   } }

那么去哪里創(chuàng)建 Watcher 呢?還記得在 compiler.js中 對(duì)文本節(jié)點(diǎn)的編譯操作嗎

在編譯完文本節(jié)點(diǎn)后 在這里添加一個(gè) Watcher

還有 v-text v-model 指令 當(dāng)編譯的是元素節(jié)點(diǎn) 就添加一個(gè) Watcher

/* compiler.js */  class Compiler {   // vm 指 Vue 實(shí)例   constructor(vm) {     // 拿到 vm     this.vm = vm     // 拿到 el     this.el = vm.$el     // 編譯模板     this.compile(this.el)   }   // 編譯模板   compile(el) {     let childNodes = [...el.childNodes]     childNodes.forEach((node) => {       if (this.isTextNode(node)) {         // 編譯文本節(jié)點(diǎn)         this.compileText(node)       }         ...   }   // 編譯文本節(jié)點(diǎn)(簡(jiǎn)單的實(shí)現(xiàn))   compileText(node) {     let reg = /{{(.+)}}/     let val = node.textContent     if (reg.test(val)) {       let key = RegExp.$1.trim()       node.textContent = val.replace(reg, this.vm[key])       // 創(chuàng)建觀察者       new Watcher(this.vm, key, newValue => {         node.textContent = newValue       })     }   }   ...   // v-text    textUpdater(node, key, value) {     node.textContent = value      // 創(chuàng)建觀察者2     new Watcher(this.vm, key, (newValue) => {       node.textContent = newValue     })   }   // v-model   modelUpdater(node, key, value) {     node.value = value     // 創(chuàng)建觀察者     new Watcher(this.vm, key, (newValue) => {       node.value = newValue     })     // 這里實(shí)現(xiàn)雙向綁定 監(jiān)聽input 事件修改 data中的屬性     node.addEventListener('input', () => {       this.vm[key] = node.value     })   } }

當(dāng) 我們改變 響應(yīng)式屬性的時(shí)候 觸發(fā)了 set() 方法 ,然后里面 發(fā)布者 dep.notify 方法啟動(dòng)了,拿到了 所有的 觀察者 watcher 實(shí)例去執(zhí)行 update 方法調(diào)用了回調(diào)函數(shù) cb(newValue) 方法并把 新值傳遞到了 cb() 當(dāng)中 cb方法是的具體更新視圖的方法 去更新視圖

比如上面的例子里的第三個(gè)參數(shù) cb方法

new Watcher(this.vm, key, newValue => {     node.textContent = newValue })

還有一點(diǎn)要實(shí)現(xiàn)v-model的雙向綁定

不僅要通過修改數(shù)據(jù)來(lái)觸發(fā)更新視圖,還得為node添加 input 事件 改變 data數(shù)據(jù)中的屬性

來(lái)達(dá)到雙向綁定的效果

7.測(cè)試下自己寫的

到了目前為止 響應(yīng)式 和 雙向綁定 都基本實(shí)現(xiàn)了 那么來(lái)寫個(gè)例子測(cè)試下

<body>   <div id="app">     {{msg}} <br />     {{age}} <br />     <div v-text="msg"></div>     <input v-model="msg" type="text" />   </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: '123',         age: 21,       },     })   </script> </body>

手把手帶你了解VUE響應(yīng)式原理

OK 基本實(shí)現(xiàn)了 通過 觀察者模式 來(lái) 實(shí)現(xiàn) 響應(yīng)式原理

8.五個(gè)文件代碼

這里直接把5個(gè)文件個(gè)代碼貼出來(lái) 上面有的地方省略了,下面是完整的方便大家閱讀

vue.js

/* vue.js */  class Vue {   constructor(options) {     // 獲取到傳入的對(duì)象 沒有默認(rèn)為空對(duì)象     this.$options = options || {}     // 獲取 el     this.$el =       typeof options.el === 'string'         ? document.querySelector(options.el)         : options.el     // 獲取 data     this.$data = options.data || {}     // 調(diào)用 _proxyData 處理 data中的屬性     this._proxyData(this.$data)     // 使用 Obsever 把data中的數(shù)據(jù)轉(zhuǎn)為響應(yīng)式     new Observer(this.$data)     // 編譯模板     new Compiler(this)   }   // 把data 中的屬性注冊(cè)到 Vue   _proxyData(data) {     Object.keys(data).forEach((key) => {       // 進(jìn)行數(shù)據(jù)劫持       // 把每個(gè)data的屬性 到添加到 Vue 轉(zhuǎn)化為 getter setter方法       Object.defineProperty(this, key, {         // 設(shè)置可以枚舉         enumerable: true,         // 設(shè)置可以配置         configurable: true,         // 獲取數(shù)據(jù)         get() {           return data[key]         },         // 設(shè)置數(shù)據(jù)         set(newValue) {           // 判斷新值和舊值是否相等           if (newValue === data[key]) return           // 設(shè)置新值           data[key] = newValue         },       })     })   } }

obsever.js

/* observer.js */  class Observer {   constructor(data) {     // 用來(lái)遍歷 data     this.walk(data)   }   // 遍歷 data 轉(zhuǎn)為響應(yīng)式   walk(data) {     // 判斷 data是否為空 和 對(duì)象     if (!data || typeof data !== 'object') return     // 遍歷 data     Object.keys(data).forEach((key) => {       // 轉(zhuǎn)為響應(yīng)式       this.defineReactive(data, key, data[key])     })   }   // 轉(zhuǎn)為響應(yīng)式   // 要注意的 和vue.js 寫的不同的是   // vue.js中是將 屬性給了 Vue 轉(zhuǎn)為 getter setter   // 這里是 將data中的屬性轉(zhuǎn)為getter setter   defineReactive(obj, key, value) {     // 如果是對(duì)象類型的 也調(diào)用walk 變成響應(yīng)式,不是對(duì)象類型的直接在walk會(huì)被return     this.walk(value)     // 保存一下 this     const self = this     // 創(chuàng)建 Dep 對(duì)象     let dep = new Dep()     Object.defineProperty(obj, key, {       // 設(shè)置可枚舉       enumerable: true,       // 設(shè)置可配置       configurable: true,        // 獲取值       get() {         // 在這里添加觀察者對(duì)象 Dep.target 表示觀察者         Dep.target && dep.addSub(Dep.target)         return value       },       // 設(shè)置值       set(newValue) {         // 判斷舊值和新值是否相等         if (newValue === value) return         // 設(shè)置新值         value = newValue         // 賦值的話如果是newValue是對(duì)象,對(duì)象里面的屬性也應(yīng)該設(shè)置為響應(yīng)式的         self.walk(newValue)         // 觸發(fā)通知 更新視圖         dep.notify()       },     })   } }

compiler.js

/* compiler.js */  class Compiler {   // vm 指 Vue 實(shí)例   constructor(vm) {     // 拿到 vm     this.vm = vm     // 拿到 el     this.el = vm.$el     // 編譯模板     this.compile(this.el)   }   // 編譯模板   compile(el) {     // 獲取子節(jié)點(diǎn) 如果使用 forEach遍歷就把偽數(shù)組轉(zhuǎn)為真的數(shù)組     let childNodes = [...el.childNodes]     childNodes.forEach((node) => {       // 根據(jù)不同的節(jié)點(diǎn)類型進(jìn)行編譯       // 文本類型的節(jié)點(diǎn)       if (this.isTextNode(node)) {         // 編譯文本節(jié)點(diǎn)         this.compileText(node)       } else if (this.isElementNode(node)) {         //元素節(jié)點(diǎn)         this.compileElement(node)       }       // 判斷是否還存在子節(jié)點(diǎn)考慮遞歸       if (node.childNodes && node.childNodes.length) {         // 繼續(xù)遞歸編譯模板         this.compile(node)       }     })   }   // 編譯文本節(jié)點(diǎn)(簡(jiǎn)單的實(shí)現(xiàn))   compileText(node) {     // 核心思想利用把正則表達(dá)式把{{}}去掉找到里面的變量     // 再去Vue找這個(gè)變量賦值給node.textContent     let reg = /{{(.+?)}}/     // 獲取節(jié)點(diǎn)的文本內(nèi)容     let val = node.textContent     // 判斷是否有 {{}}     if (reg.test(val)) {       // 獲取分組一  也就是 {{}} 里面的內(nèi)容 去除前后空格       let key = RegExp.$1.trim()       // 進(jìn)行替換再賦值給node       node.textContent = val.replace(reg, this.vm[key])       // 創(chuàng)建觀察者       new Watcher(this.vm, key, (newValue) => {         node.textContent = newValue       })     }   }   // 編譯元素節(jié)點(diǎn)這里只處理指令   compileElement(node) {     // 獲取到元素節(jié)點(diǎn)上面的所有屬性進(jìn)行遍歷     ![...node.attributes].forEach((attr) => {       // 獲取屬性名       let attrName = attr.name       // 判斷是否是 v- 開頭的指令       if (this.isDirective(attrName)) {         // 除去 v- 方便操作         attrName = attrName.substr(2)         // 獲取 指令的值就是  v-text = "msg"  中msg         // msg 作為 key 去Vue 找這個(gè)變量         let key = attr.value         // 指令操作 執(zhí)行指令方法         // vue指令很多為了避免大量個(gè) if判斷這里就寫個(gè) uapdate 方法         this.update(node, key, attrName)       }     })   }   // 添加指令方法 并且執(zhí)行   update(node, key, attrName) {     // 比如添加 textUpdater 就是用來(lái)處理 v-text 方法     // 我們應(yīng)該就內(nèi)置一個(gè) textUpdater 方法進(jìn)行調(diào)用     // 加個(gè)后綴加什么無(wú)所謂但是要定義相應(yīng)的方法     let updateFn = this[attrName + 'Updater']     // 如果存在這個(gè)內(nèi)置方法 就可以調(diào)用了     updateFn && updateFn.call(this, node, key, this.vm[key])   }   // 提前寫好 相應(yīng)的指定方法比如這個(gè) v-text   // 使用的時(shí)候 和 Vue 的一樣   textUpdater(node, key, value) {     node.textContent = value     // 創(chuàng)建觀察者     new Watcher(this.vm, key, (newValue) => {       node.textContent = newValue     })   }   // v-model   modelUpdater(node, key, value) {     node.value = value     // 創(chuàng)建觀察者     new Watcher(this.vm, key, (newValue) => {       node.value = newValue     })     // 這里實(shí)現(xiàn)雙向綁定     node.addEventListener('input', () => {       this.vm[key] = node.value     })   }    // 判斷元素的屬性是否是 vue 指令   isDirective(attr) {     return attr.startsWith('v-')   }   // 判斷是否是元素節(jié)點(diǎn)   isElementNode(node) {     return node.nodeType === 1   }   // 判斷是否是 文本 節(jié)點(diǎn)   isTextNode(node) {     return node.nodeType === 3   } }

dep.js

/* dep.js */  class Dep {   constructor() {     // 存儲(chǔ)觀察者     this.subs = []   }   // 添加觀察者   addSub(sub) {     // 判斷觀察者是否存在 和 是否擁有update方法     if (sub && sub.update) {       this.subs.push(sub)     }   }   // 通知方法   notify() {     // 觸發(fā)每個(gè)觀察者的更新方法     this.subs.forEach((sub) => {       sub.update()     })   } }

watcher.js

/* watcher.js */  class Watcher {   constructor(vm, key, cb) {     // vm 是 Vue 實(shí)例     this.vm = vm     // key 是 data 中的屬性     this.key = key     // cb 回調(diào)函數(shù) 更新視圖的具體方法     this.cb = cb     // 把觀察者的存放在 Dep.target     Dep.target = this     // 舊數(shù)據(jù) 更新視圖的時(shí)候要進(jìn)行比較     // 還有一點(diǎn)就是 vm[key] 這個(gè)時(shí)候就觸發(fā)了 get 方法     // 之前在 get 把 觀察者 通過dep.addSub(Dep.target) 添加到了 dep.subs中     this.oldValue = vm[key]     // Dep.target 就不用存在了 因?yàn)樯厦娴牟僮饕呀?jīng)存好了     Dep.target = null   }   // 觀察者中的必備方法 用來(lái)更新視圖   update() {     // 獲取新值     let newValue = this.vm[this.key]     // 比較舊值和新值     if (newValue === this.oldValue) return     // 調(diào)用具體的更新方法     this.cb(newValue)   } }

(學(xué)習(xí)視頻分享:web前端開發(fā)、編程基礎(chǔ)視頻)

贊(0)
分享到: 更多 (0)
網(wǎng)站地圖   滬ICP備18035694號(hào)-2    滬公網(wǎng)安備31011702889846號(hào)
国产精品揄拍一区二区久久| 日韩视频一区二区| 久久国产香蕉一区精品 | 国产日韩视频在线观看| 亚洲欧洲精品视频在线观看| 国产日韩久久久精品影院首页| 国产精品玖玖玖在线资源| 国产AV午夜精品一区二区入口| 日韩av无码国产精品| 欧美黑人巨大精品videos| 久久99国产精品| 九色精品视频在线观看| 日韩精品视频一区二区三区| 999久久久免费精品国产| 国内精品久久久久久99| 日韩欧毛片免费视频| 青春草国产成人精品久久| 67194国产精品免费观看| 国产美女亚洲精品久久久综合 | 日韩专区在线观看| 精品久久免费视频| 日本精品视频一视频高清| 亚洲精品无码不卡| 国产最新进精品视频| 精品一区二区三区在线观看l| 97视频精品视频人人一二区| 久久精品成人免费观看| 精品久久久久久久久久久久久久久| 手机看片日韩福利| 影院成人区精品一区二区婷婷丽春院影视 | 国产精品内射婷婷一级二| 日韩人妻无码精品久久免费一| 麻豆精品国产免费观看 | 国产精品九九久久免费视频 | 亚洲精品第一国产综合精品99| 日韩精品无码一区二区三区不卡| 国产乱人伦精品一区二区在线观看| 亚洲精品国产专区91在线| 欧洲精品色在线观看| 久久精品国产一区| 国产亚洲色婷婷久久99精品|