vue修改數(shù)據(jù)視圖更新原理學(xué)習(xí)
MVVM模式
在vue使用中,我們只要修改了數(shù)據(jù),所見視圖便會進(jìn)行相應(yīng)更新。
為什么?原理是什么?
在這期間做三件事(發(fā)布訂閱者模式)
- 數(shù)據(jù)劫持
- 依賴收集
- 派發(fā)更新
Object.defineProperty()詳解
詳見:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Refer...該方法有三個入?yún)?/p>
obj
要定義屬性的對象。
prop
一個字符串或 Symbol
,指定了要定義或修改的屬性鍵。
descriptor
要定義或修改的屬性的描述符。
通過該方法進(jìn)行賦值(無需關(guān)注隱藏屬性)
// 定義一個對象 var obj = {} // 通過Object.defineProperty對obj進(jìn)行賦值 Object.defineProperty(obj, 'name', { value: '張三' }) console.log(obj) // {name: '張三'} console.log(obj.name) // 張三
引入get
// 定義一個對象 var obj = {} // 通過Object.defineProperty對obj進(jìn)行賦值 Object.defineProperty(obj, 'name', { get() { console.log('正在訪問name') // 當(dāng)訪問時(shí),會執(zhí)行該代碼 } }) Object.defineProperty(obj, 'age', { value: 18 }) console.log(obj.name) // undefined 因?yàn)闆]有被賦值、所以認(rèn)為不存在 console.log(obj.age) // 18 console.log(obj) // {age: 18}
引入set
// 定義一個對象 var obj = {} // 通過Object.defineProperty對obj進(jìn)行賦值 Object.defineProperty(obj, 'name', { get() { console.log('正在訪問name') // 當(dāng)訪問時(shí),會執(zhí)行該代碼 }, set() { console.log('嘗試改變name') // 當(dāng)對name進(jìn)行更改時(shí),執(zhí)行該行代碼 } }) Object.defineProperty(obj, 'age', { value: 18 }) console.log(obj.name) // undefined obj.name = '李四' console.log(obj.name) // undefined 雖然改變了name, // 但并沒有進(jìn)行具體的賦值,此時(shí)會觸發(fā)set內(nèi)代碼
如何賦值?
// 定義一個對象 var obj = {} // 通過Object.defineProperty對obj進(jìn)行賦值 Object.defineProperty(obj, 'name', { // getter return出的值為name值, get() { console.log('正在訪問name') // 當(dāng)訪問時(shí),會執(zhí)行該代碼 return 'TEST' // 便于理解我們寫死 }, set(newValue) { console.log('嘗試改變name', newValue) // 當(dāng)對name進(jìn)行更改時(shí),執(zhí)行該行代碼 } }) console.log(obj.name) // 輸出TEST, obj.name = '李四' // setter內(nèi)輸出 '李四' console.log(obj.name) // 輸出TEST、因?yàn)樵趃etter中賦值為TEST,setter未成功賦值
引入defineReactive()
//為什么引入該函數(shù)? //因?yàn)間et和set不好用,并且不希望引入變量進(jìn)行周轉(zhuǎn),例子: // 定義一個對象 var obj = {} // 定義一個變量 var temp // 通過Object.defineProperty對obj進(jìn)行賦值 Object.defineProperty(obj, 'name', { // getter return出的值為name值, get() { console.log('正在訪問name') // 當(dāng)訪問時(shí),會執(zhí)行該代碼 return temp // 返回temp,此時(shí)已被賦值 }, set(newValue) { temp = newValue // 賦值給全局變量 console.log('嘗試改變name', newValue) // 當(dāng)對name進(jìn)行更改時(shí),執(zhí)行該行代碼 } }) console.log(obj.name) // undefined,因?yàn)閠emp未賦值,且setter未觸發(fā) obj.name = '李四' // 賦值進(jìn)行setter觸發(fā),將 '李四'賦值給temp console.log(obj.name) // 李四
defineReactive提供一個閉包環(huán)境,利用閉包特性進(jìn)行周轉(zhuǎn)
var obj = {} function defineReactive(data, key, val) { console.log(data, key, val) Object.defineProperty(data, key, { get() { return val }, set(newValue) { // 如果你要設(shè)置的值和原值相等,直接返回 if(newValue === val) return val = newValue } }) } // 進(jìn)行賦值操作 defineReactive(obj, 'name', '張三') console.log(obj.name) // 張三 obj.name = '李四' console.log(obj.name) // 李四
引入遞歸偵測對象(observer)處理對象
// 不進(jìn)行遞歸操作的話,如果一個對象有好幾層,那么我們是檢測不到深層變量的 // eg: var obj = { a: { b: { c: 'i am c' } }, b: 'i am b' }
實(shí)現(xiàn)遞歸
定義observer
把一個正常的object轉(zhuǎn)換為每個層級都是響應(yīng)式的
//defineReactive function defineReactive(data, key, val) { console.log(data, key, val) if (arguments.length == 2) { val = data[key] } // 子元素要進(jìn)行遞歸 let childOb = observe(val) Object.defineProperty(data, key, { get() { return val }, set(newValue) { // 如果你要設(shè)置的值和原值相等,直接返回 if(newValue === val) return val = newValue // 設(shè)置了新值,也要被observe childOb = observe(newValue) } }) } // Observer class Observer { constructor(value) { // 給實(shí)例添加__ob__屬性,數(shù)值是這次new的實(shí)例 def(value, '__ob__', this, false) this.walk(value) } // 遍歷 walk(value) { for (let k in value) { defineReactive(value, k) } } } // 不可遍歷的方法 function def(obj, key, value, enumerable) { Object.defineProperty(obj, key, { value, enumerable, writable: true, configurable: true }) } // 創(chuàng)建observe函數(shù)用于輔助 function observe(value) { // 只為對象服務(wù) if(typeof value !== 'object') return // 定義ob if(typeof value.__ob__ !== 'undefined'){ ob = value.__ob__ } else { ob = new Observer(value) } } // 循環(huán)監(jiān)測 observe(obj) // 此時(shí)obj每個屬性都被掛載了__ob__標(biāo)識,且為響應(yīng)式 obj.a.b.c.e = 10 console.log(obj.a.b.c.e) // 10 // 簡單來說就是通過__ob__這個標(biāo)識來確認(rèn)你這個obj是不是響應(yīng)式的,如果不是就遞歸遍歷 // observe(obj) ==》 是否有__ob__ ==》 沒有就new Observer(obj) ==》逐個響應(yīng)式defineReactive
- 處理數(shù)組
vue
對數(shù)組的方法進(jìn)行了改寫(push
、pop
、shift
、unshift
、splice
、sort
、reverse
)
以Array.prototype
為原型創(chuàng)建了一個arrayMethods
的對象,用es6
的Object.setPrototypeOf()
,強(qiáng)制指向arrayMethods
// 拿到Array.prototype const arrayPrototype = Array.prototype // 以Array.prototype為原型創(chuàng)建arrayMethods對象 const arrayMethods = Object.create(arrayPrototype) const methodsNeedChange = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] // 遍歷 methodsNeedChange.foreach(methodName => { // 備份原來的方法, 因?yàn)閿?shù)組的功能不能被剝奪 const original = arrayPrototype[methodName] // 定義新的方法 def(arrayMethods, methodName, functio(this){ // 恢復(fù)原來的功能 const result = original.apply(this, arguments) // 把類數(shù)組對象變?yōu)閿?shù)組 const args = [...arguments] // 把數(shù)組身上的__ob__取出來,__ob__已經(jīng)被添加了,因?yàn)閿?shù)組不是最高層,在遍歷obj對象第一層的時(shí)候,已經(jīng)添加了__ob__屬性 const ob = this.__ob__ //push, unshift, splice可以插入新項(xiàng),因此也要把插入的新項(xiàng)也變?yōu)閛bserve let inserted = [] switch(methodName){ case 'push': case 'unshift': inserted = arguments; break; case 'splice': // splice格式是splice(下標(biāo)、數(shù)量、插入的新項(xiàng)) inserted = args.slice(2); break; } // 判斷有沒有要插入的新項(xiàng), 如果有讓新項(xiàng)也變?yōu)轫憫?yīng)的 if (inserted) { ob.observeArray(inserted) } return result }, false) }) // 在observer 中處理數(shù)組 // Observer class Observer { constructor(value) { // 給實(shí)例添加__ob__屬性,數(shù)值是這次new的實(shí)例 def(value, '__ob__', this, false) // 如果是數(shù)組,需要把數(shù)組的原型,指向arrayMethods if(Array.isArray(value)) { Object.setPrototypeOf(value, arrayMethods) // 讓數(shù)組可以響應(yīng)式、既observe this.observeArray(value) } else { this.walk(value) } } // 遍歷 walk(value) { for (let k in value) { defineReactive(value, k) } } // 數(shù)組的特殊遍歷 observeArray(arr) { for(let i = 0, l = arr.length; i < l; i++) { // 逐項(xiàng)進(jìn)行observe observe(arr[i]) } } }
依賴收集
- 需要用到數(shù)據(jù)的地方,稱為依賴
- vue1.X, 細(xì)粒度依賴、用到數(shù)據(jù)的DOM依賴
- vue2.X, 中等粒度依賴、用到數(shù)據(jù)的組件是依賴
- 在getter收集依賴、在setter觸發(fā)依賴
dep類
- 把依賴收集的代碼封裝,專門用來管理依賴,每個Observer的實(shí)例,成員中都有一個Dep的實(shí)例
- dep使用發(fā)布訂閱模式、當(dāng)數(shù)據(jù)發(fā)生變化時(shí),會循環(huán)依賴列表,把所有的watcher都通知一遍
watcher類
- 中介、數(shù)據(jù)發(fā)生變化時(shí)通過Watcher進(jìn)行中轉(zhuǎn),通知組件
- 依賴就是watcher、只有watcher觸發(fā)的getter才會收集依賴、哪個watcher觸發(fā)了getter、就把哪個watcher收集到dep中
- whatcher把自己設(shè)置到一個全局指定的位置,然后讀取數(shù)據(jù),由于讀取了數(shù)據(jù),會觸發(fā)這個數(shù)據(jù)的getter。
- 在getter中就能得到當(dāng)前正在讀取數(shù)據(jù)的watcher,并把這個watcher收集到dep中
- dep類
class Dep{ constructor(){ // dep類的構(gòu)造器 // 用數(shù)組存儲自己訂閱者, 存儲的是watcher的實(shí)例 this.subs = [] } // 添加訂閱 addSub(){ this.subs.push(sub) } // 添加依賴 depend() { // Dep.target是我們自己指定的全局的位置,主要是全局唯一,沒歧義 if(Dep.target){ this.addSub(Dep.target) } } // 通知更新 notify() { // 淺克隆一份 const subs = this.subs.slice() // 遍歷 for ()let i = 0, l = subs.length; i < 1; i++){ subs[i].update() } } }
- watcher類
class Watcher{ constructor(target, expression, callback){ // watcher類的構(gòu)造器 this.target = target // parsePath是一個高階函數(shù)、用于拿到obj最底層的值 // getter被賦值的為函數(shù) this.getter = parsePath(expression) this.callback = callback this.value = this.get() } update() { this.run() } get() { // 進(jìn)入依賴收集階段,讓全局的Dep.target設(shè)置為watcher本身,那么就是進(jìn)入依賴收集階段 Dep.target = this const obj = this.traget // 取出最底層的值 try { this.getter(obj) } finally { Dep.target = null } return value } run() { this.getAndInvoke(this.callback) } getAndInvoke(cb) { const value = this.get() if (value !== this.value || typeof value == 'object') { const oldValue = this.value this.value = value cb.call(this.target, value, oldValue) } } } // parsePath函數(shù) function parsePath(str) { var segments = str.split('.') // 返回一個函數(shù) return (obj) => { for (let i = 0; i < segments.length; i++) { obj = obj[segments[i]] } return obj } } var fb = parsePath('a.b.c.d.e') var v =fb({a:{b:{c:{d:{e:12}}}}}) console.log(v) // 12 //
dep和watcher實(shí)例化的位置
dep: defineReactive的閉包中(創(chuàng)建響應(yīng)式中、閉包內(nèi)可以得到)、observer構(gòu)造器內(nèi)
//defineReactive function defineReactive(data, key, val) { const dep = new Dep() console.log(data, key, val) if (arguments.length == 2) { val = data[key] } // 子元素要進(jìn)行遞歸 let childOb = observe(val) Object.defineProperty(data, key, { get() { // 如果處于依賴收集階段 if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() } } return val }, set(newValue) { // 如果你要設(shè)置的值和原值相等,直接返回 if(newValue === val) return val = newValue // 設(shè)置了新值,也要被observe childOb = observe(newValue) // 發(fā)布訂閱模式,通知dep } }) } // Observer class Observer { constructor(value) { // 每一個observer實(shí)例身上,都有一個dep this.dep = new Dep() // 給實(shí)例添加__ob__屬性,數(shù)值是這次new的實(shí)例 def(value, '__ob__', this, false) // 如果是數(shù)組,需要把數(shù)組的原型,指向arrayMethods if(Array.isArray(value)) { Object.setPrototypeOf(value, arrayMethods) // 讓數(shù)組可以響應(yīng)式、既observe this.observeArray(value) } else { this.walk(value) } } // 遍歷 walk(value) { for (let k in value) { defineReactive(value, k) } } // 數(shù)組的特殊遍歷 observeArray(arr) { for(let i = 0, l = arr.length; i < l; i++) { // 逐項(xiàng)進(jìn)行observe observe(arr[i]) } } } methodsNeedChange.foreach(methodName => { // 備份原來的方法, 因?yàn)閿?shù)組的功能不能被剝奪 const original = arrayPrototype[methodName] // 定義新的方法 def(arrayMethods, methodName, functio(this){ // 恢復(fù)原來的功能 const result = original.apply(this, arguments) // 把類數(shù)組對象變?yōu)閿?shù)組 const args = [...arguments] // 把數(shù)組身上的__ob__取出來,__ob__已經(jīng)被添加了,因?yàn)閿?shù)組不是最高層,在遍歷obj對象第一層的時(shí)候,已經(jīng)添加了__ob__屬性 const ob = this.__ob__ //push, unshift, splice可以插入新項(xiàng),因此也要把插入的新項(xiàng)也變?yōu)閛bserve let inserted = [] switch(methodName){ case 'push': case 'unshift': inserted = arguments; break; case 'splice': // splice格式是splice(下標(biāo)、數(shù)量、插入的新項(xiàng)) inserted = args.slice(2); break; } // 判斷有沒有要插入的新項(xiàng), 如果有讓新項(xiàng)也變?yōu)轫憫?yīng)的 if (inserted) { ob.observeArray(inserted) } ob.dep.notify() return result }, false) })
dep類和watcher類 沒太懂,后續(xù)研究后詳細(xì)說明下,更多關(guān)于vue修改數(shù)據(jù)視圖更新的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一文詳細(xì)了解Vue?3.0中的onMounted和onUnmounted鉤子函數(shù)
Vue3.0引入了新的組件生命周期鉤子函數(shù)onMounted和onUnmounted,分別用于組件掛載后和卸載前的操作,這些鉤子函數(shù)為開發(fā)者提供了更多靈活性,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-10-10在 Linux/Unix 中不重啟 Vim 而重新加載 .vimrc 文件的流程
這篇文章主要介紹了在 Linux/Unix 中不重啟 Vim 而重新加載 .vimrc 文件的流程,需要的朋友可以參考下2018-03-03vue組件傳遞對象中實(shí)現(xiàn)單向綁定的示例
下面小編就為大家分享一篇vue組件傳遞對象中實(shí)現(xiàn)單向綁定的示例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-02-02vue.nextTick()與setTimeout的區(qū)別及說明
這篇文章主要介紹了vue.nextTick()與setTimeout的區(qū)別及說明,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03Vue3+Element?Plus的項(xiàng)目搭建過程詳解
這篇文章主要為大家介紹了Vue3+Element?Plus的項(xiàng)目搭建過程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08