您好,登錄后才能下訂單哦!
這篇文章主要介紹“Vue數據的響應過程”,在日常操作中,相信很多人在Vue數據的響應過程問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Vue數據的響應過程”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
Vue 中可以用 $watch 實例方法觀察一個字段,當該字段的值發生變化時,會執行指定的回調函數(即觀察者),實際上和 watch 選項作用相同。如下:
vm.$watch('box', () => { console.log('box變了') }) vm.box = 'newValue' // 'box變了'
以上例切入,我想實現一個功能類似的方法 myWatch。
如何知道我觀察的屬性被修改了?
—— Object.defineProperty 方法
該方法可以為指定對象的指定屬性設置 getter-setter 函數對,通過這對 getter-setter 可以捕獲到對屬性的讀取和修改操作。示例如下:
const data = { box: 1 } Object.defineProperty(data, 'box', { set () { console.log('修改了 box') }, get () { console.log('讀取了 box') } }) console.log(data.box) // '讀取了 box' // undefined data.box = 2 // '修改了 box' console.log(data.box) // '讀取了 box' // undefined
如此,便攔截到了對 box 屬性的修改和讀取操作。
但 res 為 undefined,data.box = 2 的修改操作也無效。
get 與 set 函數功能不健全
故修改如下:
const data = { box: 1 } let value = data.box Object.defineProperty(data, 'box', { set (newVal) { if (newVal === value) return value = newVal console.log('修改了 box') }, get () { console.log('讀取了 box') return value } }) console.log(data.box) // '讀取了 box' // 1 data.box = 2 // '修改了 box' console.log(data.box) // '讀取了 box' // 2
有了這些, myWatch 方法便可實現如下:
const data = { box: 1 } function myWatch(key, fn) { let value = data[key] Object.defineProperty(data, key, { set (newVal) { if (newVal === value) return value = newVal fn() }, get () { return value } }) } myWatch('box', () => { console.log('box變了') }) data.box = 2 // 'box變了'
但存在一個問題,不能給同一屬性添加多個依賴(觀察者):
myWatch('box', () => { console.log('我是觀察者') }) myWatch('box', () => { console.log('我是另一個觀察者') }) data.box = 2 // '我是另一個觀察者'
后面的依賴(觀察者)會將前者覆蓋掉。
如何能夠添加多個依賴(觀察者)?
—— 定義一個數組,作為依賴收集器:
const data = { box: 1 } const dep = [] function myWatch(key, fn) { dep.push(fn) let value = data[key] Object.defineProperty(data, key, { set (newVal) { if (newVal === value) return value = newVal dep.forEach((f) => { f() }) }, get () { return value } }) } myWatch('box', () => { console.log('我是觀察者') }) myWatch('box', () => { console.log('我是另一個觀察者') }) data.box = 2 // '我是觀察者' // '我是另一個觀察者'
修改 data.box 后,兩個依賴(觀察者)都執行了。
若上例 data 對象需新增兩個能夠響應數據變化的屬性 foo bar:
const data = { box: 1, foo: 1, bar: 1 }
只需執行以下代碼即可:
myWatch('foo', () => { console.log('我是foo的觀察者') }) myWatch('bar', () => { console.log('我是bar的觀察者') })
但問題是,不同屬性的依賴(觀察者)都被收集進了同一個 dep,修改任何一個屬性,都會觸發所有的依賴(觀察者):
data.box = 2 // '我是觀察者' // '我是另一個觀察者' // '我是foo的觀察者' // '我是bar的觀察者'
我想可以這樣解決:
const data = { box: 1, foo: 1, bar: 1 } const dep = {} function myWatch(key, fn) { if (!dep[key]) { dep[key] = [fn] } else { dep[key].push(fn) } let value = data[key] Object.defineProperty(data, key, { set (newVal) { if (newVal === value) return value = newVal dep[key].forEach((f) => { f() }) }, get () { return value } }) } myWatch('box', () => { console.log('我是box的觀察者') }) myWatch('box', () => { console.log('我是box的另一個觀察者') }) myWatch('foo', () => { console.log('我是foo的觀察者') }) myWatch('bar', () => { console.log('我是bar的觀察者') }) data.box = 2 // '我是box的觀察者' // '我是box的另一個觀察者' data.foo = 2 // '我是foo的觀察者' data.bar = 2 // '我是bar的觀察者'
但實際上這樣更好些:
const data = { box: 1, foo: 1, bar: 1 } let target = null for (let key in data) { const dep = [] let value = data[key] Object.defineProperty(data, key, { set (newVal) { if (newVal === value) return value = newVal dep.forEach(f => { f() }) }, get () { dep.push(target) return value } }) } function myWatch(key, fn) { target = fn data[key] } myWatch('box', () => { console.log('我是box的觀察者') }) myWatch('box', () => { console.log('我是box的另一個觀察者') }) myWatch('foo', () => { console.log('我是foo的觀察者') }) myWatch('bar', () => { console.log('我是bar的觀察者') }) data.box = 2 // '我是box的觀察者' // '我是box的另一個觀察者' data.foo = 2 // '我是foo的觀察者' data.bar = 2 // '我是bar的觀察者'
聲明 target 全局變量作為依賴(觀察者)的中轉站,myWatch 函數執行時用 target 緩存依賴,然后調用 data[key] 觸發對應的 get 函數以收集依賴,set 函數被觸發時會將 dep 里的依賴(觀察者)都執行一遍。這里的 get set 函數形成閉包引用了上面的 dep 常量,這樣一來,data 對象的每個屬性都有了對應的依賴收集器。
且這一實現方式不需要通過 myWatch 函數顯式地將 data 里的屬性一一轉為訪問器屬性。
但運行以下代碼,會發現仍有問題:
console.log(data.box) data.box = 2 // '我是box的觀察者' // '我是box的另一個觀察者' // '我是bar的觀察者'
四個 myWatch 執行完之后 target 緩存的值變成了最后一個 myWatch 方法調用時所傳遞的依賴(觀察者),故執行 console.log(data.box) 讀取 box 屬性的值時,會將最后緩存的依賴存入 box 屬性所對應的依賴收集器,故而再修改 box 的值時,會打印出 '我是bar的觀察者'。
我想可以在每次收集完依賴之后,將全局變量 target 設置為空函數來解決這問題:
const data = { box: 1, foo: 1, bar: 1 } let target = null for (let key in data) { const dep = [] let value = data[key] Object.defineProperty(data, key, { set (newVal) { if (newVal === value) return value = newVal dep.forEach(f => { f() }) }, get () { dep.push(target) target = () => {} return value } }) } function myWatch(key, fn) { target = fn data[key] } myWatch('box', () => { console.log('我是box的觀察者') }) myWatch('box', () => { console.log('我是box的另一個觀察者') }) myWatch('foo', () => { console.log('我是foo的觀察者') }) myWatch('bar', () => { console.log('我是bar的觀察者') })
經測無誤。
但開發過程中,還常碰到需觀測嵌套對象的情形:
const data = { box: { gift: 'book' } }
這時,上述實現未能觀測到 gift 的修改,顯出不足。
如何進行深度觀測?
——遞歸
通過遞歸將各級屬性均轉為響應式屬性即可:
const data = { box: { gift: 'book' } } let target = null function walk(data) { for (let key in data) { const dep = [] let value = data[key] if (Object.prototype.toString.call(value) === '[object Object]') { walk(value) } Object.defineProperty(data, key, { set (newVal) { if (newVal === value) return value = newVal dep.forEach(f => { f() }) }, get () { dep.push(target) target = () => {} return value } }) } } walk(data) function myWatch(key, fn) { target = fn data[key] } myWatch('box', () => { console.log('我是box的觀察者') }) myWatch('box.gift', () => { console.log('我是gift的觀察者') }) data.box = {gift: 'basketball'} // '我是box的觀察者' data.box.gift = 'guitar'
這時 gift 雖已是訪問器屬性,但 myWatch 方法執行時 data[box.gift] 未能觸發相應 getter 以收集依賴, data[box.gift] 訪問不到 gift 屬性,data[box][gift] 才可以,故 myWatch 須改寫如下:
function myWatch(exp, fn) { target = fn let pathArr, obj = data if (/\./.test(exp)) { pathArr = exp.split('.') pathArr.forEach(p => { obj = obj[p] }) return } data[exp] }
如果要讀取的字段包括 . ,那么按照 . 將其分為數組,然后使用循環讀取嵌套對象的屬性值。
這時執行代碼后發現,data.box.gift = 'guitar' 還是未能觸發相應的依賴,即打印出 '我是gift的觀察者' 這句信息。調試之后找到問題:
myWatch('box.gift', () => { console.log('我是gift的觀察者') })
執行以上代碼時,pathArr 即 ['box', 'gift'],循環內 obj = obj[p] 實際上就是 obj = data[box],讀取了一次 box,觸發了 box 對應的 getter,收集了依賴:
() => { console.log('我是gift的觀察者') }
收集完將全局變量 target 置為空函數,而后,循環繼續執行,又讀取了 gift 的值,但這時,target 已是空函數,導致屬性 gift 對應的 getter 收集了一個“空依賴”,故,data.box.gift = 'guitar' 的操作不能觸發期望的依賴。
以上代碼有兩個問題:
修改 box 會觸發“我是gift的觀察者”這一依賴
修改 gift 未能觸發“我是gift的觀察者”的依賴
第一個問題,讀取 gift 時,必然經歷讀取 box 的過程,故觸發 box 對應的 getter 無可避免,那么,box 對應 getter 收集 gift 的依賴也就無可避免。但想想也算合理,因為 box 修改時,隸屬于 box 的 gift 也算作修改,從這一點看,問題一也不算作問題,劃去。
第二個問題,我想可以這樣解決:
function myWatch(exp, fn) { let pathArr, obj = data if (/\./.test(exp)) { pathArr = exp.split('.') pathArr.forEach(p => { target = fn obj = obj[p] }) return } target = fn data[exp] } data.box.gift = 'guitar' // '我是gift的觀察者' data.box = {gift: 'basketball'} // '我是box的觀察者' // '我是gift的觀察者'
保證屬性讀取時 target = fn 即可。
那么:
const data = { box: { gift: 'book' } } let target = null function walk(data) { for (let key in data) { const dep = [] let value = data[key] if (Object.prototype.toString.call(value) === '[object Object]') { walk(value) } Object.defineProperty(data, key, { set (newVal) { if (newVal === value) return value = newVal dep.forEach(f => { f() }) }, get () { dep.push(target) target = () => {} return value } }) } } walk(data) function myWatch(exp, fn) { let pathArr, obj = data if (/\./.test(exp)) { pathArr = exp.split('.') pathArr.forEach(p => { target = fn obj = obj[p] }) return } target = fn data[exp] } myWatch('box', () => { console.log('我是box的觀察者') }) myWatch('box.gift', () => { console.log('我是gift的觀察者') })
現在我想,假如我有以下數據:
const data = { player: 'James Harden', team: 'Houston Rockets' }
執行以下代碼:
function render() { document.body.innerText = `The last season's MVP is ${data.player}, he's from ${data.team}` } render() myWatch('player', render) myWatch('team', render) data.player = 'Kobe Bryant' data.team = 'Los Angeles Lakers'
是不是就可以將數據映射到頁面,并響應數據的變化?
執行代碼發現,data.player = 'Kobe Bryant' 報錯,究其原因,render 方法執行時,會去獲取 data.player 和 data.team 的值,但此時,target 為 null,那么讀取 player 時對應的依賴收集器 dep 便收集了 null,導致 player 的 setter 調用依賴時報錯。
那么我想,在 render 執行時便主動去收集依賴,就不會導致 dep 里收集了 null。
細看 myWatch,這方法做的事情其實就是幫助 getter 收集依賴,它的第一個參數就是要訪問的屬性,要觸發誰的 getter,第二個參數是相應要收集的依賴。
這么看來,render 方法既可以幫助 getter 收集依賴(render 執行時會讀取 player team),而且它本身就是要收集的依賴。那么,我能不能修改一下 myWatch 的實現,以支持這樣的寫法:
myWatch(render, render)
第一個參數作為函數執行一下便有了之前第一個參數的作用,第二個參數還是需要被收集的依賴,嗯,想來合理。
那么,myWatch 改寫如下:
function myWatch(exp, fn) { target = fn if (typeof exp === 'function') { exp() return } let pathArr, obj = data if (/\./.test(exp)) { pathArr = exp.split('.') pathArr.forEach(p => { target = fn obj = obj[p] }) return } data[exp] }
但,對 team 的修改未能觸發頁面更新,想來因為 render 執行讀取 player 收集依賴后 target 變為空函數,導致讀取 team 收集依賴時收集到了空函數。這里大家的依賴都是 render,故可將 target = () => {} 這句刪去。
myWatch 這樣實現還有個好處,假如 data 中有許多屬性都需要通過 render 渲染至頁面,一句 myWatch(render, render) 便可,無須如此這般繁復:
myWatch('player', render) myWatch('team', render) myWatch('number', render) myWatch('height', render) ...
那么最終:
const data = { player: 'James Harden', team: 'Houston Rockets' } let target = null function walk(data) { for (let key in data) { const dep = [] let value = data[key] if (Object.prototype.toString.call(value) === '[object Object]') { walk(value) } Object.defineProperty(data, key, { set (newVal) { if (newVal === value) return value = newVal dep.forEach(f => { f() }) }, get () { dep.push(target) return value } }) } } walk(data) function myWatch(exp, fn) { target = fn if (typeof exp === 'function') { exp() return } let pathArr, obj = data if (/\./.test(exp)) { pathArr = exp.split('.') pathArr.forEach(p => { target = fn obj = obj[p] }) return } data[exp] } function render() { document.body.innerText = `The last season's MVP is ${data.player}, he's from ${data.team}` } myWatch(render, render)
到此,關于“Vue數據的響應過程”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。