為什麼lodash的remove在vuejs中不是響應式的?
當我們開發中希望從陣列中按照某種篩選條件移除陣列的一個元素時,很容易想到使用splice或者filter來操作
/* 從陣列arr中移除值為val的元素 */ let index = arr.indexOf(val) index !== -1 && arr.splice(index, 1) /* 從陣列arr中移除滿足predicate條件的元素 */ arr = arr.filter(predicate) 複製程式碼
可以看到,splice方法的可讀性並不好,而且還需要考慮val不是arr的元素的情況;filter可讀性還不錯,但實際上得到了一個新的陣列。比較好的辦法是迴圈使用splice,但那樣寫就太麻煩了。
所以就有了lodash這種原生js庫來幫助我們。lodash庫中的remove方法語義明確,它使用一個迴圈的splice操作實現元素移除(在後面可以看到原始碼)。
/* 從陣列arr中移除滿足predicate條件的元素 */ _.remove(arr, predicate) 複製程式碼
但美中不足的是,如果在vuejs的開發中使用這個方法,你會發現使用這個方法並不能觸發vuejs的DOM更新響應。
這是為什麼呢?本篇文章就因此簡單探討一下。首先我們可以簡單地認為陣列操作後會觸發一種機制進行DOM更新。那麼題目問題就轉化成了兩個問題:
- 陣列操作是怎麼觸發vuejs響應機制的?
- lodash的remove實現和普通的陣列操作有什麼區別?
陣列操作是怎麼觸發vuejs響應機制的?
簡單來說,vuejs用修改後的方法替換了觀察的陣列本身的原型方法,實現了攔截,增加了觸發響應的部分。替換原型方法的程式碼如下:
// project: vue // version: 2.5.6 // file: src/core/observer/index.js if (Array.isArray(value)) {// value: 觀察的物件 const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) // 用arrayMethods替換掉value的原型 this.observeArray(value) } 複製程式碼
其中arrayMethods就是vuejs修改後的原型物件,它的實現程式碼如下:
// project: vue // version: 2.5.6 // file: src/core/observer/array.js const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] /** * 修改會改變陣列元素的方法,實現攔截 */ methodsToPatch.forEach(function (method) { const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { // def類似Object.defineProperty const result = original.apply(this, args)// 先執行原方法 // 省略部分程式碼 ob.dep.notify() // 觸發響應更新事件 return result }) }) 複製程式碼
結合兩份程式碼,可以看到如下過程:
- vuejs在陣列原型的基礎上,建立了新的原型物件,
- 修改新的原型物件中的'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'方法。
- 將觀察的陣列的原型替換成新的原型物件。
lodash的remove方法的實現
lodash的原始碼非常易讀,其remove的關鍵實現就是通過篩選條件找到對應值在陣列中的index,然後使用basePullAt
方法將array中對應序號的元素剔除,其中basePullAt
的方法實現如下
// project: lodash // version: 4.17.10-npm // file: _basePullAt.js var baseUnset = require('./_baseUnset'), isIndex = require('./_isIndex'); var arrayProto = Array.prototype; var splice = arrayProto.splice;// 關鍵:使用Array.prototype中的splice方法 function basePullAt(array, indexes) { var length = array ? indexes.length : 0, lastIndex = length - 1; while (length--) { var index = indexes[length]; if (length == lastIndex || index !== previous) { var previous = index; if (isIndex(index)) { splice.call(array, index, 1);// splice操作 } else { baseUnset(array, index); } } } return array; } 複製程式碼
結論
看完lodash中remove方法的實現程式碼,題目問題的答案就很明朗了:
vue通過改造觀察陣列的原型方法使它操作對應方法時會觸發更新響應,而lodash的remove方法使用Array原型中的splice方法對陣列進行操作,因此不會觸發響應更新。
我們也得到了一些更多的問題。
更多的問題
1. 為什麼lodash要使用Array原型中的splice方法,而不是直接使用陣列物件上的splice?
可能是為了相容一些類陣列物件。但還有一個奇怪的地方,lodash的開發分支上早在17年四月就已經修改basePullAt的實現為直接使用splice,而不是用Array的原型。而npm即使是最新的4.17.11-npm,也還是沿用Array原型中的splice方法。可能是因為npm包需要儘量向前相容吧。
2. 那有什麼辦法方便地實現響應式地從陣列移除元素呢?
建議自己開發工具包方法。
3. 如果使用者在vue中使用arr.splice(0, 0)
操作,並不會對原陣列產生修改,而同樣會觸發響應更新,那麼是不是會影響效率?
猜測: 使用了key屬性與vue的diff演算法大概可以讓這個效率影響降低,而如果在攔截方法中實現對原陣列元素是否變更的需要可能比較影響效能
以上,一點拙見,歡迎指出問題。