【4-2】响应式原理 之 数据劫持

响应式原理 = 数据劫持 + 观察者模式。另,我们知道可以用数据劫持实现数据代理。即:响应式原理不是全包含了数据劫持。

  • 先把几个关键的逻辑搞通,就很容明白后面的内容:
    • 响应式原理 = 数据劫持 + 观察者模式
    • observe函数:处理各种情况,核心还是defineReactive
      • defineReactive 做了什么工作? – 除了执行本分的gettter和setter的工作之余,主要就是触发依赖收集派发更新
        • 是怎么联系到Dep.target的? – dep.depend()
        • Dep.target若不存在,就谈不上响应式,访问data的操作本身就很多,不可能每个都搞响应式
    • 通常都是在获取目标的值的时候,目标同时会依赖收集。
    • vue里面主要有3种watcher,结合若有Dep.target则构成响应式原理。也就是三种watcher的响应式原理都是:数据劫持+响应式原理

observe函数,尝试调用ob = new Observer(obj),如果发现已经调用过了则直接返回ob

new Observer(obj)主要接收一系列数据劫持所属的根对象,还有作为入口兼容一些特殊情况。其中每一个属性交由 defineReactive 来具体实现数据劫持,所以 defineReactive(obj, key, val) 的函数参数签名会有key。另外单独开辟一章节来讲observer函数的细节和如何处理特殊情况,在本章节主要讲 defineReactive,以支持理解整个宏观的流程。

defineReactive ()

Dep.target:因为访问 this.data的事务有很多,所以Dep.target的作用在于标志是由哪一个 watcher 发起的访问data,这时候才有必要“依赖收集”。

先看看 defineReactive 做了什么工作:

  • 对应每一对key value都有闭包dep对象
  • getter,若存在Dep.target(即某Watcher实例),则额外去“依赖收集”
  • setter,首先拿到value,多数情况会触发getter。比对新旧value,如果跟一样则不用任何操作。否则新旧value不同则更新该value,并且额外去“派发更新”
// #摘取自vue #基本能用:
export function defineReactive (
  obj, key, val, customSetter?: ?Function, shallow?: boolean
) {
  const dep = new Dep() // 对应每一个key都有闭包dep对象

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // 如果之前该对象已经预设了getter/setter则将其缓存,新定义的getter/setter中会将其执行
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) { val = obj[key] }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val // 如果原本对象拥有getter方法则执行,不论如何拿到value
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val // 如果原本对象拥有getter方法则执行,不论如何拿到value
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return // 如果跟原来值一样则不用任何操作
      }

      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

从高维度来看流程

Watcher就3种使用方式: (尤其是在判断区分expOrFn转成watcher.getter的时候注意一下)

  • 侦听器 watcher:this.$watch('include', cb) ==> new Watcher(vm, 'include', cb, options)
  • 计算属性 watcher:new Watcher(vm, computed[key] || noop, noop, options)
  • 渲染 watcher:new Watcher(vm, updateComponent || noop, noop, options)

最简单的还是侦听器 watcher。而渲染 watcher 的流程,还包含了对计算属性的 获取。

应用场景的出发点是不同的: 计算属性适合用在模板渲染中,某个值是依赖了其它的响应式data甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。

它们3者的原理都是 数据劫持+响应式原理

侦听器 watcher:用户更新数据,data的setter触发,“派发更新”,触发用户设置的回调即可。 (它还支持deep、sync、immediate等配置)

计算属性 watcher

  • 首次获取该计算属性的时候,触发该计算属性的getter,“依赖收集”比如渲染watcher订阅了这个计算属性,
  • 另需要触发所依赖数据的 getter,更新了一次“依赖收集”比如该计算属性订阅某些data的变化。
  • 数据更新的时候,触发所依赖data的setter,导致拿“用户定义的computed函数”重新生成一个computed值,并且重新订阅所依赖的data。
  • 另外,如果computed计算结果前后不一样,就也派发更新视图更新updateComponent回调执行。此后渲染watcher也会重新依赖收集该computed。

渲染 watcher

因为vue在调用每个vm.$mount都会执行new Watcher,意味着每个视图组件实例都有渲染 watcher。

传入的 expOrFnupdateComponent = () => { vm._update(vm._render(), hydrating) }

首次构建,需要构建vnode触发数据的getter,有时候也需要获取computed,再最后才传给vm._update去patch。在这个过程其中,渲染watcher更新了一次“依赖收集”。

数据更新的时候,由setter派发更新,触发watcher.update,也就是在未来触发 watcher.run。其中触发 watcher.get,对于渲染 watcher而言,也就是触发updateComponentupdateComponent,这里又要vm._render()生成vnode,所以又访问vm上的数据,所以触发数据的getter(为下一次重新渲染视图更新“依赖收集”),再最后才传给vm._update去patch。

也就是已考虑到了v-if false导致离线一大堆视图的情景。

文章目录
  1. defineReactive ()
  2. 从高维度来看流程