爲何lodash的remove在vuejs中不是響應式的?

問題引出

當咱們開發中但願從數組中按照某種篩選條件移除數組的一個元素時,很容易想到使用splice或者filter來操做javascript

/* 從數組arr中移除值爲val的元素 */
let index = arr.indexOf(val)
index !== -1 && arr.splice(index, 1)	

/* 從數組arr中移除知足predicate條件的元素 */
arr = arr.filter(predicate)
複製代碼

能夠看到,splice方法的可讀性並很差,並且還須要考慮val不是arr的元素的狀況;filter可讀性還不錯,但實際上獲得了一個新的數組。比較好的辦法是循環使用splice,但那樣寫就太麻煩了。vue

因此就有了lodash這種原生js庫來幫助咱們。lodash庫中的remove方法語義明確,它使用一個循環的splice操做實現元素移除(在後面能夠看到源碼)。java

/* 從數組arr中移除知足predicate條件的元素 */
_.remove(arr, predicate)
複製代碼

但美中不足的是,若是在vuejs的開發中使用這個方法,你會發現使用這個方法並不能觸發vuejs的DOM更新響應。算法

這是爲何呢?本篇文章就所以簡單探討一下。首先咱們能夠簡單地認爲數組操做後會觸發一種機制進行DOM更新。那麼題目問題就轉化成了兩個問題:typescript

  1. 數組操做是怎麼觸發vuejs響應機制的?
  2. lodash的remove實現和普通的數組操做有什麼區別?

數組操做是怎麼觸發vuejs響應機制的?

簡單來講,vuejs用修改後的方法替換了觀察的數組自己的原型方法,實現了攔截,增長了觸發響應的部分。替換原型方法的代碼以下:npm

// 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
  })
})
複製代碼

結合兩份代碼,能夠看到以下過程:app

  1. vuejs在數組原型的基礎上,建立了新的原型對象,
  2. 修改新的原型對象中的'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'方法。
  3. 將觀察的數組的原型替換成新的原型對象。

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算法大概可讓這個效率影響下降,而若是在攔截方法中實現對原數組元素是否變動的須要可能比較影響性能


以上,一點拙見,歡迎指出問題。

相關文章
相關標籤/搜索