【4-3】响应式原理 之 数据劫持之observe的细节

《数据劫持之observe的细节》:

之前一个章节讲数据劫持,主要是讲 defineReactive,以支持理解整个宏观的流程。

这里特别讲 observe 和其所调用的 Observer构造函数,就是想探讨一下细节和各种特殊情况的。

  • 大概总结一些这个章节:
    • 数组是响应式的:通过重写数组的原型对象上的数组方法(所以同时还有一些没有用响应式覆盖的地方)
      • 在数组中通过数组索引对项进行修改时,是不会触发更新的
    • 包括数组,还有一些响应式没有覆盖到的地方怎么处理? – 通过Vue.$set(包括类似的Vue.$del)和“数组变异”来补足。他们都是内部手动做了依赖更新的派发。

observe函数,尝试调用ob = new Observer(obj),如果发现已经调用过了则直接返回ob__ob__属性保存的就是 observer实例。

new Observer(obj)主要接收一系列数据劫持所属的根对象,还有作为入口兼容一些特殊情况。其中每一个属性交由 defineReactive 来具体实现数据劫持,所以 defineReactive(obj, key, val) 的函数参数签名会有key。

observe 详解

  • 如果data不是 object 类型则无效果
  • __ob__属性复用返回 observer
  • ob = new Observer(value)

new Observer

  • this.dep = new Dep()
  • def(value, '__ob__', observer)
    • def 函数是对 Object.defineProperty 进行一定的封装
  • 如果value是数组,尝试“通过改变原型对象”重写数组方法,不行就“遍历要重写的属性”来重写数组方法; 然后每个数组元素也递归observe
  • 如果value是普通对象则用walk实例方法,即遍历并observe每一对key value
    • 若先前已 Object.defineProperty 一次,则在walk的时候就遍历不到它
  • defineReactive上一章已经说了,就是defineProperty getter和setter,实现响应式原理
    • getter
      • childOb.dep.depend() 针对child更新依赖收集
      • dependArray(value) 兼容针对value是数组依赖收集
    • setter
      • 【重要】新设置的值是 newVal,也observe(newVal) :childOb = !shallow && observe(newVal)

剩下的observer文件的一些闭包变量和方法 #了解一下即可

还有文件内的3个函数(在该文件都没有调用过),直接把源码的函数注释抄下来:

  • set:Adds the new property and triggers change notification if the property doesn’t already exist. 应该就是对应Vue.set
  • del:Delete a property and trigger change if necessary. 应该就是对应Vue.delete
  • function toggleObserving (value) { shouldObserve = value }

响应式所没有覆盖到的地方 #包括数组 #面试会问

  • 没有事先声明的data属性,不是响应式的,因为没有被数据劫持。
  • 其他的没覆盖到的地方:通过Vue.$set(包括类似的Vue.$del)和“数组变异”来补足。他们都是内部手动做了依赖更新的派发。

数组变异的原理 :重写操作数组的方法

export class Observer {
  constructor (value) {
    // 【一些初始化】
    if (Array.isArray(value)) {
      const augment = hasProto // 对于多数浏览器 hasProto为true
        ? protoAugment // 单纯设置__proto__属性
        : copyAugment // 复制数组的元素
      augment(value, arrayMethods, arrayKeys) // arrayMethods
      this.observeArray(value)
    } else {
      // ....
    }
  }

拦截并且重写了数组的方法,做了些增强,并且会调用 ob.dep.notify。也是notify手动通知。(之所以能通知到,也是因为先前 defineReactive 的时候,对child做了一次依赖收集childOb.dep.depend。)

Vue.$set

Vue.$set:(因为defineProperty是不支持=的,所以必须暴露$set来包装)

【1】针对数组, 更新数组的length并且用splice来插入。 【2】针对object,也是手动“依赖收集”和“派发更新”

function set (target: Array<any> | Object, key, val) {
  // 校验
  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)) { // 有vmCount就是root $data
    // 这种情况警告
    return val
  }
  if (!ob) { target[key] = val; return val } // 普通对象
  defineReactive(ob.value, key, val) // 里面调用childOb.dep.depend ,做一个渲染watcher的依赖收集
  ob.dep.notify() // 触发重新渲染
  return val
}
思考数组为啥不同 #可略

对于数组类型观测,会调用 observeArray 方法:

observeArray(data) {
  data.forEach(item => {
    observe(item)
  })
}

与对象不同,它执行 observe 对数组内的对象类型进行观测,并没有对数组的每一项进行 Object.defineProperty 的定义,也就是说数组内的项是没有 dep 的。

所以,我们通过数组索引对项进行修改时,是不会触发更新的【看代码演示】。但可以通过 this.$set 来修改触发更新。那么问题来了,为什么 Vue 要这样设计?

结合实际场景:

  • 【1】数组中通常会存放多项数据,比如列表数据。这样观测起来会消耗性能。
  • 【2】一般修改数组元素很少会直接通过索引将整个元素替换掉。例如:
export default {
    data() {
        return {
            list: [
                {id: 1, name: 'Jack'},
                {id: 2, name: 'Mike'}
            ]
        }
    },
    created() {
        // 如果想要修改 name 的值,一般是这样使用
        this.list[0].name = 'JOJO'
        // 而不是以下这样
        // this.list[0] = {id:1, name: 'JOJO'}
        // 当然你可以这样更新
        // this.$set(this.list, '0', {id:1, name: 'JOJO'})
    }
}
文章目录
  1. observe 详解
  2. new Observer
  3. 剩下的observer文件的一些闭包变量和方法 #了解一下即可
  4. 响应式所没有覆盖到的地方 #包括数组 #面试会问
    1. 数组变异的原理 :重写操作数组的方法
    2. Vue.$set
      1. 思考数组为啥不同 #可略