响应式set方法

10/25/2021 vue

set方法主要是为响应式对象中动态的添加响应式属性,该方法在全局方法Vue.set和实例方法vm.$set内被调用,可分别在src/core/global-api/index.jssrc/core/instance/state.js文件中可查看相关定义

该方法会返回设置的响应式属性的值

// src/core/global-api/index.js
Vue.set = set

// src/core/instance/state.js
export function stateMixin (Vue: Class<Component>) {
  
  // ......
  Vue.prototype.$set = set
  // ......
	
}
1
2
3
4
5
6
7
8
9
10
11

src/core/observer/index.js中可查看对set方法的定义

export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  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: any).__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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

该方法有三个参数,target为需要动态添加响应式属性的响应式对象,keyval则分别为属性和值,方法内部首先会判断target,如果为undefined或为简单类型的值,则会给出warn提示,接着会判断target是否为数组并且key是一个有效的索引

# 数组

src/shared/util.js中可查看对isValidArrayIndex方法的定义

export function isValidArrayIndex (val: any): boolean {
  const n = parseFloat(String(val))
  return n >= 0 && Math.floor(n) === n && isFinite(val)
}
1
2
3
4

isValidArrayIndex逻辑很简单,主要判断val是一个有限数值,大于等于0,且向下取证和原数值相等(确保val不是小数)

如果target为数组且key是有效索引,先利用target.length调整数组的长度,再利用splice方法添加新增项

这里可能大家注意到了,为什么需要使用target.length,而不是直接使用target.splice动态添加?为什么target.splice的第二个参数为1

索引值key有两种情况

  • 超出target的长度范围
  • target的长度索引范围内

如果索引值key超出了target长度范围,直接使用splice添加会直接在数组的末尾进行添加,这样target的长度会不准确,比如:

var a = [1, 2]
a.splice(3, 1, 3) // 直接在数组的末尾添加
a // [1, 2, 3]
1
2
3

按正确的逻辑,数组的长度为数组的最后一项的索引值加1,所以target.length应该为4,结合target.length = Math.max(target.length, key),可以保证target的长度准确性

如果索引值keytarget的长度范围内,target.splice的第二个参数1,会先删除再添加

响应式对象 (opens new window)小节说过,会对数组的原型进行重写,重写原型的逻辑在一个单独的文件中,可在src/core/observer/array.js中查看

methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

重写的方法中,会对响应式数组新增的数据循环调用Observe方法,然后再根据响应式数组对应的dep.notify触发更新

# 对象

继续回到set方法中,后续则是对对象的分析处理

export function set (target: Array<any> | Object, key: any, val: any): any {
  
  // ......
  
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

如果key是响应式对象target的自身属性,则会直接修改值,如果targetVue的实例化对象,或者为根data对象,则会报warn提示,如果target不是响应式对象则会直接做赋值处理,接着会直接调用defineReactive方法为响应式对象target动态添加响应式属性,并且调用响应式对象target对应的dep.notify方法派发更新

最后更新时间: 12/4/2022, 1:44:46 PM