詳解Vue 如何監(jiān)聽Array的變化
回憶
在上一篇Vue響應(yīng)式原理-理解Observer、Dep、Watcher簡單講解了Observer、Dep、Watcher三者的關(guān)系。
在Observer的偽代碼中我們模擬了如下代碼:
class Observer { constructor() { // 響應(yīng)式綁定數(shù)據(jù)通過方法 observe(this.data); } } export function observe (data) { const keys = Object.keys(data); for (let i = 0; i < keys.length; i++) { // 將data中我們定義的每個(gè)屬性進(jìn)行響應(yīng)式綁定 defineReactive(obj, keys[i]); } } export function defineReactive () { // ...省略 Object.defineProperty get-set }
今天我們就進(jìn)一步了解Observer里還做了什么事。
Array的變化如何監(jiān)聽?
data 中的數(shù)據(jù)如果是一個(gè)數(shù)組怎么辦?我們發(fā)現(xiàn)Object.defineProperty對數(shù)組進(jìn)行響應(yīng)式化是有缺陷的。
雖然我們可以監(jiān)聽到索引的改變。
function defineReactive (obj, key, val) { Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: () => { console.log('我被讀了,我要不要做點(diǎn)什么好?'); return val; }, set: newVal => { if (val === newVal) { return; } val = newVal; console.log("數(shù)據(jù)被改變了,我要渲染到頁面上去!"); } }) } let data = [1]; // 對數(shù)組key進(jìn)行監(jiān)聽 defineReactive(data, 0, 1); console.log(data[0]); // 我被讀了,我要不要做點(diǎn)什么好? data[0] = 2; // 數(shù)據(jù)被改變了,我要渲染到頁面上去!
但是defineProperty不能檢測到數(shù)組長度的變化,準(zhǔn)確的說是通過改變length而增加的長度不能監(jiān)測到。這種情況無法觸發(fā)任何改變。
data.length = 0; // 控制臺沒有任何輸出
而且監(jiān)聽數(shù)組所有索引的的代價(jià)也比較高,綜合一些其他因素,Vue用了另一個(gè)方案來處理。
首先我們的observe需要改造一下,單獨(dú)加一個(gè)數(shù)組的處理。
// 將data中我們定義的每個(gè)屬性進(jìn)行響應(yīng)式綁定 export function observe (data) { const keys = Object.keys(data); for (let i = 0; i < keys.length; i++) { // 如果是數(shù)組 if (Array.isArray(keys[i])) { observeArray(keys[i]); } else { // 如果是對象 defineReactive(obj, keys[i]); } } } // 數(shù)組的處理 export function observeArray () { // ...省略 }
那接下來我們就應(yīng)該考慮下Array變化如何監(jiān)聽?
Vue 中對這個(gè)數(shù)組問題的解決方案非常的簡單粗暴,就是對能夠改變數(shù)組的方法做了一些手腳。
我們知道,改變數(shù)組的方法有很多,舉個(gè)例子比如說push方法吧。push存在Array.prototype上的,如果我們能
能攔截到原型上的push方法,是不是就可以做一些事情呢?
Object.defineProperty
對象里目前存在的屬性描述符有兩種主要形式:數(shù)據(jù)描述符和存取描述符。存取描述符是由getter-setter函數(shù)對描述的屬性,也就是我們用來給對象做響應(yīng)式綁定的。Object.defineProperty-MDN
雖然我們無法使用Object.defineProperty將數(shù)組進(jìn)行響應(yīng)式的處理,也就是getter-setter,但是還有其他的功能可以供我們使用。就是數(shù)據(jù)描述符,數(shù)據(jù)描述符是一個(gè)具有值的屬性,該值可能是可寫的,也可能不是可寫的。
value
該屬性對應(yīng)的值??梢允侨魏斡行У?JavaScript 值(數(shù)值,對象,函數(shù)等)。默認(rèn)為 undefined。
writable
當(dāng)且僅當(dāng)該屬性的writable為true時(shí),value才能被賦值運(yùn)算符改變。默認(rèn)為 false。
因此我們只要把原型上的方法,進(jìn)行value的重新賦值。
如下代碼,在重新賦值的過程中,我們可以獲取到方法名和所有參數(shù)。
function def (obj, key) { Object.defineProperty(obj, key, { writable: true, enumerable: true, configurable: true, value: function(...args) { console.log('key', key); console.log('args', args); } }); } // 重寫的數(shù)組方法 let obj = { push() {} } // 數(shù)組方法的綁定 def(obj, 'push'); obj.push([1, 2], 7, 'hello!'); // 控制臺輸出 key push // 控制臺輸出 args [Array(2), 7, "hello!"]
通過如上代碼我們就可以知道,用戶使用了數(shù)組上原型的方法以及參數(shù)我們都可以攔截到,這個(gè)攔截的過程就可以做一些變化的通知。
Vue監(jiān)聽Array三步曲
接下來,就看看Vue是如何實(shí)現(xiàn)的吧~
第一步:先獲取原生 Array 的原型方法,因?yàn)閿r截后還是需要原生的方法幫我們實(shí)現(xiàn)數(shù)組的變化。
第二步:對 Array 的原型方法使用 Object.defineProperty 做一些攔截操作。
第三步:把需要被攔截的 Array 類型的數(shù)據(jù)原型指向改造后原型。
我們將代碼進(jìn)行下改造,攔截的過程中還是要將開發(fā)者的參數(shù)傳給原生的方法,保證數(shù)組按照開發(fā)者的想法被改變,然后我們再去做視圖的更新等操作。
const arrayProto = Array.prototype // 獲取Array的原型 function def (obj, key) { Object.defineProperty(obj, key, { enumerable: true, configurable: true, value: function(...args) { console.log(key); // 控制臺輸出 push console.log(args); // 控制臺輸出 [Array(2), 7, "hello!"] // 獲取原生的方法 let original = arrayProto[key]; // 將開發(fā)者的參數(shù)傳給原生的方法,保證數(shù)組按照開發(fā)者的想法被改變 const result = original.apply(this, args); // do something 比如通知Vue視圖進(jìn)行更新 console.log('我的數(shù)據(jù)被改變了,視圖該更新啦'); this.text = 'hello Vue'; return result; } }); } // 新的原型 let obj = { push() {} } // 重寫賦值 def(obj, 'push'); let arr = [0]; // 原型的指向重寫 arr.__proto__ = obj; // 執(zhí)行push arr.push([1, 2], 7, 'hello!'); console.log(arr);
被改變后的arr。
Vue源碼解析
array.js
Vue在array.js中重寫了methodsToPatch中七個(gè)方法,并將重寫后的原型暴露出去。
// Object.defineProperty的封裝 import { def } from '../util/index' // 獲得原型上的方法 const arrayProto = Array.prototype // Vue攔截的方法 const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]; // 將上面的方法重寫 methodsToPatch.forEach(function (method) { def(arrayMethods, method, function mutator (...args) { console.log('method', method); // 獲取方法 console.log('args', args); // 獲取參數(shù) // ...功能如上述,監(jiān)聽到某個(gè)方法執(zhí)行后,做一些對應(yīng)的操作 // 1、將開發(fā)者的參數(shù)傳給原生的方法,保證數(shù)組按照開發(fā)者的想法被改變 // 2、視圖更新等 }) }) export const arrayMethods = Object.create(arrayProto);
observer
在進(jìn)行數(shù)據(jù)observer綁定的時(shí)候,我們先判斷是否hasProto,如果存在__proto__,就直接將value 的 __proto__指向重寫過后的原型。如果不能使用 __proto__,貌似有些瀏覽器廠商沒有實(shí)現(xiàn)。那就直接循環(huán) arrayMethods把它身上的這些方法直接裝到 value 身上好了。畢竟調(diào)用某個(gè)方法是先去自身查找,當(dāng)自身找不到這關(guān)方法的時(shí)候,才去原型上查找。
// 判斷是否有__proto__,因?yàn)椴糠譃g覽器是沒有__proto__ const hasProto = '__proto__' in {} // 重寫后的原型 import { arrayMethods } from './array' // 方法名 const arrayKeys = Object.getOwnPropertyNames(arrayMethods); // 數(shù)組的處理 export function observeArray (value) { // 如果有__proto__,直接覆蓋 if (hasProto) { protoAugment(value, arrayMethods); } else { // 沒有__proto__就把方法加到屬性自身上 copyAugment(value, arrayMethods, ) } } // 原型的賦值 function protoAugment (target, src) { target.__proto__ = src; } // 復(fù)制 function copyAugment (target, src, keys) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]); } }
通過上面的代碼我們發(fā)現(xiàn),沒有直接修改 Array.prototype,而是直接把 arrayMenthods 賦值給 value 的 __proto__ 。因?yàn)檫@樣不會污染全局的Array, arrayMenthods 只對 data中的Array 生效。
總結(jié)
因?yàn)楸O(jiān)聽的數(shù)組帶來的代價(jià)和一些問題,Vue使用了重寫原型的方案代替。攔截了數(shù)組的一些方法,在這個(gè)過程中再去做通知變化等操作。
本文的一些代碼均是Vue源碼簡化后的,為了方便大家理解。思想理解了,源碼就容易看懂了。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
avue實(shí)現(xiàn)自定義搜索欄及清空搜索事件的實(shí)踐
本文主要介紹了avue實(shí)現(xiàn)自定義搜索欄及清空搜索事件的實(shí)踐,主要包括對搜索欄進(jìn)行自定義,并通過按鈕實(shí)現(xiàn)折疊搜索欄效果,具有一定的參考價(jià)值,感興趣的可以了解一下2021-12-12在Vue中使用deep深度選擇器修改element UI組件的樣式
這篇文章主要介紹了在Vue中使用deep深度選擇器修改element UI組件的樣式,本文分為兩種方法給大家介紹,在這小編比較推薦使用第二種使用 deep 深度選擇器,感興趣的朋友跟隨小編一起看看吧2022-12-12Vue默認(rèn)插槽,具名插槽,作用域插槽定義及使用方法
這篇文章主要介紹了Vue默認(rèn)插槽,具名插槽,作用域插槽定義及使用方法,插槽的作用是在子組件中某個(gè)位置插入父組件的自定義html結(jié)構(gòu)和data數(shù)據(jù),下面詳細(xì)內(nèi)容需要的小伙伴可以參考一下2022-03-03vue+vue-meta-info動(dòng)態(tài)設(shè)置meta標(biāo)簽教程
這篇文章主要介紹了vue+vue-meta-info動(dòng)態(tài)設(shè)置meta標(biāo)簽教程,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04Vue ElementUI this.$confirm async await封
這篇文章主要介紹了Vue ElementUI this.$confirm async await封裝方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09