Life and freedom Ge Lin ——— Draw by Razzh

Vue set 实现

Feb 17 · 8 min

由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此还是有一些办法来回避这些限制并保证它们的响应性,那就是 Vue.set

实际上 Vue 也可以使用 Object.defineProperty 来做到对数组的监听,但是出于性能考虑 ,Vue 没有选择这个做法。

#实际应用

var vm = new Vue({
  el: '#app',
  template: '<div @click="addItem">{{list}}</div>',
  data: {
    list: [1, 2, 3]
  },
  methods: {
    addItem() {
      this.list[0] = 'a'
    }
  }
})

上述代码点击 div 之后会将 list 中的第一位索引换成 a 字符,打印一下 vm._data

image

已经可以看到这个时候 list 中的第一项已经变成了 a,但页面视图却没有更新,可见通过数组索引修改内容并不能触发页面更新,所以这个时候 Vue.set 就该登场了:

var vm = new Vue({
  el: '#app',
  template: '<div @click="addItem">{{list}}</div>',
  data: {
    list: [1, 2, 3]
  },
  methods: {
    addItem() {
      this.$set(this.list, 0, 'a')
    }
  }
})

这个时候点击 div 页面视图也随之更新为 ['a', 2, 3]

#思考

顺着这个思路,我想到在生命周期 created 中通过下标修改数组的值,那么此时视图会变吗?

var vm = new Vue({
  el: '#app',
  template: '<div>{{list}}</div>',
  data: {
    list: [1, 2, 3]
  },
  created() {
    this.list[0] = 'a'
  }
})

这段代码执行完毕时,这个时候视图竟然跟着更新了!😅 奇怪了,不是说 Vue 不能检测到数组通过索引来修改 data 中值的情况吗?为什么这时候视图却更新了?

解释:由于 created 阶段 data 数据已经初始化完成,可以修改其数据,以至于到后面的渲染函数拿到的也是修改后的 list:['a', 2 ,3],所以渲染的时候并不是视图发生了更新变化,而是第一次 render 拿到的 list 已经是修改后的数据,这样也就解释得通了。

#set 源码

function set(target, key, val) {
  if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target))) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${target}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = target.__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' &&
      warn(
        'Avoid adding reactive properties to a Vue instance or its root $data ' +
          'at runtime - declare it upfront in the data option.'
      )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

我们可以看到,除去开发环境下的报错提示也就 10 多行代码,其核心逻辑就是触发 dep.notify

#数组处理

function set(target, key, val) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  // ... ignore
}

通过 Array.isArray 来判断 target 是否是数组,并验证 key 是否是数组下标,可能你会对 splice 之前对 target.length 赋值的操作感到困惑,这里举一个小案例

const key = 5
const arr1 = [1, 2, 3]
arr1.splice(key, 1, 666)
console.log(arr1) // [1, 2, 3, 666]
 
const arr2 = [1, 2, 3]
arr2.length = key
arr2.splice(key, 1, 666)
console.log(arr2) // [1, 2, 3, empty x 2, 666]

🧐 各位是不是看出什么"端倪"了?

这里的 key 代表将要改变的数组下标,当 key 大于数组的 length 时,splice 只会向数组的末尾新增值(也可理解为向数组 push 了一个值)那么这样跟我们的预期也就不同了,所以要对 keylength 取最大值。

Vue 的响应式系统中,已经对 data 选项中 Array 类型的原型上进行了一层代理,之后执行 splice 时,会触发 dep.notify 方法去通知 Watcher 进行视图更新。

#对象处理

function set(target, key, val) {
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = target.__ob__
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

首先检测 key 是否已经存在在 target 上了,那么直接赋值就会触发 setter 函数来对视图进行更新。

之后获取了 ob,这是一个响应式数据的的标志,若目标对象不是响应式数据,那么直接赋值返回,这段逻辑在官网对 Vue.set 中的介绍中可以找到
最后,如果上述情况都不符合,说明:

  • target 不是数组
  • key 不在 target 上
  • target 是一个响应式数据

那么这个时候就需要通过 defineReactivetargetkey 值转换成响应式数据,并通过 ob.dep.notify 通知 Watcher 进行视图更新。

#总结

Vue.set 对数组和对象分别坐了不同处理:

  • 对于数组,通过使用 splice 方法触发视图更新
  • 通过对象,通过检测目标对象是否存在 key 、 是否是响应式数据和调用 ob.dep.notify 来触发视图更新
浙ICP备2024129591号-1
春秋(Live) - 张敬轩
--:-- / --:--
  1. 1春秋(Live)张敬轩
  2. 2不吐不快(live)张敬轩
  3. 3男孩最痛(live)张敬轩
  4. 4粤语残片(live)陈奕迅
  5. 5几分之几(live)卢广仲
  6. 6地球很危险古巨基
  7. 7樱花树下(live)张敬轩

春秋 (Live) - 张敬轩 (Hins Cheung)

词:林夕

曲:Edmond Tsang

那夜谁将酒喝掉

因此我讲得多了

然后你摇着我手拒绝我

动人像友情深了

我没权终止见面

只因你友善依然

仍用接近甜蜜那种字眼通电

没人应该 怨地怨天

得到这结局

难道怪罪神没有更伪善的祝福

我没有为你伤春悲秋不配有憾事

你没有共我踏过万里

不够剧情延续故事

头发未染霜 着凉亦错在我幼稚

应快活像个天使

有没有运气再扮弱者 玩失意

有没有道理为你落发

必须得到世人同意

心灰得极可耻 心伤得无新意

那一线眼泪 欠大志

爱若能堪称伟大 再难挨照样开怀

如令你发现为你而活到失败

令人不安 我品性坏

我没有为你伤春悲秋不配有憾事

你没有共我踏过万里

不够剧情延续故事

头发未染霜 着凉亦错在我幼稚

应快活像个天使

有没有运气再扮弱者玩失意

有没有道理为你落发

必须得到世人同意

心灰得极可耻 心伤得无新意

那一线眼泪 欠大志 太没意思

若自觉这叫痛苦未免过份容易

我没有被你改写一生怎配有心事

我没有被你害过恨过

写成情史 变废纸

春秋只转载要事

如果爱你欠意义

这眼泪 无从安置

我没有运气放大自私的失意

更没有道理在这日

你得到真爱制造恨意

想心酸 还可以 想心底 留根刺

至少要见面上万次