Vue2源码学习笔记 - 14.响应式原理—核心本质

经过前面几节的学习,我们已经了解了响应式原理中的几个重要知识,其中特别是 Observer、Dep 及 Watcher 类等。这一节我们整体串联起来描述响应式的整个核心过程和原理,所以强烈推荐先学习前几节的内容。文章有点长,但是配了大量图来辅助解说,不用紧张:)。

<<< 前置知识

Observer 类详解Dep 类详解Watcher 类详解等,响应式原理的系列文章(非常重要!)

Vue 响应式应用

经过前面的学习,我们知道除了每个组件实例都对应一个 watcher 实例外,计算属性(computed)和侦听属性(watch)本质也是依赖 Watcher 实例实现的。为便于描述,我们分别给这三类 Watcher 实例命名为渲染 watcherrenderWatcher),计算属性 watchercomputedWatcher)和侦听属性 watcherwatchWatcher)。

通常我们编写的使用数据、计算属性和侦听属性的代码类似如下:

new Vue({
    el: '#app',
    // 数据对象
    data: {
        id: 999,
        name: 'java',
        subObj: {
            code: 'js',
            rank: 3
        }
    },
    // 计算属性
    computed: {
        tipMsg: function() {
            return 'hi, '+this.name+', your Uid:'+this.id;
        },
        fullname: function() {
            return 'dev '+this.name;
        }
    },
    // 侦听属性
    watch: {
        id: function(val, oldVal) {
            console.log('Id is changed: '+val)
        }
    }
})

数据对象(data)的响应式

通过前面 “Dep 类详解” 中我们知道,数据在初始化时经过 observe 和 defineReactive 函数等处理后关联了一些 Observer 对象和 Dep 对象,并且设置了响应式的 getter\setter 方法(如下图)。

vue 响应式数据处理

其中每个对象关联了 Observer 对象(__ob__),每个对象属性关联了 Dep 对象(dep)。

  • 引用时

这里我们主要研究页面渲染时的数据引用,它是典型的使用场景。我们先简单回顾下 renderWatcher,它在 mountComponent 函数中被实例化:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

updateComponent 为最终的渲染函数,在实例化对象时它被当作 expOrFn 传入 Watcher 的构造函数,那么它会被保存在 renderWatcher 的 getter 属性上,并在 renderWatcher.get 成员方法中被调用。在这个 get 方法中,会通过 pushTarget 设置 Dep.target 为本 renderWatcher 对象,然后执行 renderWatcher.getter 属性指向的方法,即执行渲染函数 updateComponent,渲染过程中会引用响应式对象的属性。

get () {![在这里插入图片描述](https://img-blog.csdnimg.cn/cd1514ef6956455db1da1335c6d1cc51.png)

  pushTarget(this)
  ...
    // 渲染watcher 中,this.getter 就是 updateComponent
    // 计算属性watcher 中,this.getter 就是用户定义的求值函数
    value = this.getter.call(vm, vm)
    ...
    popTarget()
    this.cleanupDeps()
  }
  return value
}

vue Dep与Watcher

页面在被渲染时,会引用对象属性,调用属性的 reactiveGetter 方法,代码如下。在这个场景下 Dep.target 为真,然后执行属性关联的 dep 对象 depend 方法,此时它就等价于执行了 renderWatcher.addDep(dep),这样就把 renderWatcher 依赖的数据属性的 dep 对象保存到了 Watcher.newDeps 里,同时 Dep.subs 里面也保存有该 renderWatcher,这就完成了依赖收集。

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) {
      // 等价于 Dep.target.addDep(dep)
      // 或 等价于 renderWatcher.addDep(dep)
      dep.depend()
      if (childOb) {
        // 属性值为对象,收集值对象的 childOb.dep
        childOb.dep.depend()
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
    }
    return value
  },
  set: function reactiveSetter (newVal) {...}
})

vue 数据属性data 响应式流程图

  • 更新时

在更新对象属性时(this.name = xxx),触发属性的响应式 setter 方法,代码如下,在设置新的值后,调用关联的 dep 对象的 notify 方法派发通知,在这个方法中遍历所有订阅的 Watcher 对象(dep.subs数组变量中)并调用其 update 方法。

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {...},
  set: function reactiveSetter (newVal) {
    const value = getter ? getter.call(obj) : val
    ...
    // #7981: for accessor properties without setter
    if (getter && !setter) return
    if (setter) {
      setter.call(obj, newVal)
    } else {
      val = newVal
    }
    // 对属性值附加 Observer对象,并处理为响应式数据对象
    childOb = !shallow && observe(newVal)
    // 派发更新通知
    dep.notify()
  }
})

update 方法根据不同情况做不同响应,renderWatcher 中会在下一个任务调度中执行页面重新渲染;computedWatcher 则把对象状态属性 dirty 设为真,在被引用时再求值;watchWatcher 则会在下一个任务调度中执行用户定义的回调函数,关于具体的任务调度,我们后面会用一个小节来研究学习。

计算属性(computed)的响应式

在前面 “计算属性与侦听属性初始化浅析” 和 “Watcher 类详解” 中我们学习了计算属性响应式的处理,用户定义的返回值的求值函数最终赋值给了 computedWatcher.getter 成员变量,最后的本质同样是调用 Object.defineProperty 定制了 getter\setter 方法,我们通过图片简单回顾下:

vue 计算属性computed 初始化

我们看到计算属性是没有对应的 Dep 对象与之关联的,这与数据对象的处理很大不同,而且它还没有 setter 方法,每个计算属性只有一个 computedWatcher 与之关联。

  • 引用时

为了便于描述我们仍然以页面渲染为引用场景,前面章节我们学习了计算属性的初始化流程,这里我们直接上代码,来看看计算属性的 getter 方法:

function computedGetter () {
  const watcher = this._computedWatchers && this._computedWatchers[key]
  if (watcher) {
    if (watcher.dirty) {
      // 调用 watcher.get 求值
      watcher.evaluate()
    }
    if (Dep.target) {
       // Dep.target 收集 watcher.deps 为依赖
      watcher.depend()
    }
    return watcher.value
  }
}

在 getter 方法中,获取与计算属性关联的 computedWatcher,如果首次引用或者是依赖的数据有更新时,computedWatcher.dirty 都为 true,这就会执行 computedWatcher,它内部会执行 computedWatcher.get 方法。在这个 get 方法内,会把 computedWatcher 赋值给 Dep.target,然后执行 computedWatcher.getter 求值,这个过程中就会收集到计算属性依赖的变量的 Dep 对象,这就完成了 computedWatcher 的依赖收集。

在计算属性的依赖收集结束后,出栈恢复 Dep.target 为 renderWatcher,然后调用 computedWatcher.depend,它的作用就是让 renderWatcher 收集所有 computedWatcher.deps 为依赖,因为计算属性没有与之关联的 Dep 对象,这个我们在上一节 Watcher 类详解中有详细讲解,通过图片我们简单回顾下。

vue 计算属性依赖收集

可以看到,计算属性的依赖收集有两轮,第一轮是 computedWatcher 自身对于数据 Dep 的收集,第二轮才是使用计算属性的其他 Dep.target(比如 renderWatcher)对数据 Dep 的收集。

vue 计算属性computed 响应式流程图

  • 更新时

计算属性因为没有 setter,我们不能主动修改它,它的值只有在它依赖的数据被改变时随之改变。当依赖的数据更新时,数据的 Dep 会 notify 派发通知到订阅的 computedWatcher,在 computedWatcher.update 中设置 computedWatcher.dirty 为 true 即完成了对 computedWatcher 的通知。

在收集依赖环节我们知道 renderWatcher 也会收集对数据的依赖,就是第二轮的收集操作,那么在派发通知的时候肯定也要通知 renderWatcher.update,这个方法中它会把自身放入调度队列,在下一轮任务调度时重新渲染页面,这个流程如上图所示。

侦听属性(watch)的响应式

在前面 “计算属性与侦听属性初始化浅析” 中我们同样学习了侦听属性的处理,它没有被动的依赖收集环节,在初始化时通过调用 Vue.prototype.$watch 主动收集依赖,我们简单看看它的源码:

  • 依赖收集

Vue.prototype.$watch = function (expOrFn: string | Function,cb: any,
  options?: Object): Function {
  ...
  options = options || {}
  options.user = true
  const watcher = new Watcher(vm, expOrFn, cb, options)
  ...
}

在实例化 Watcher 时,侦听属性的键作为 expOrFn 传入构造函数,cb 为用户定义的回调函数,在Watcher 构造函数中把字符串的键通过调用函数 parsePath 转化为 getter 函数,它只是一个简单的访问属性值的函数。经过处理后的侦听属性每个元素都对应一个 watchWatcher,如下图所示:

vue 侦听属性watch 初始化

因为 watchWatcher.lazy 为 false,所以此刻立即执行 watchWatcher.get 方法求值,这样就把依赖的属性的 Dep 对象存入 watchWatcher.deps 中完成依赖收集。这个过程相较于前两种情况来说简单很多。

vue 侦听属性watch 响应式流程图

  • 触发回调

与前面相同,数据有更新时同样会主动调用 watchWatcher.update 派发通知,它会把自身放入调度队列,在下一轮任务调度中执行 watchWatcher.run 方法:

run () {
  if (this.active) {
    const value = this.get() // <--------
    if (value !== this.value || isObject(value) ||this.deep) {
      // set new value
      const oldValue = this.value
      this.value = value
      if (this.user) {
        const info = `callback for watcher "${this.expression}"`
        invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
      } else {
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

因为 watchWatcher.user 在实例化时设置为 true,所以在这个方法中会主动执行用户定义的 cb 回调函数,这样就完成了侦听的响应过程。

总结

这节内容主要从多个不同类型的 Watcher 为切入点来研究学习 Vue 的响应式原理,虽然类型不同,但是它们的核心还是不离前面提到的那几个类和函数,比如 Dep\Watcher\Observer 类,Object.defineProperty\observe 等函数,这些是响应式的核心。因为前面有章节已经详细阐述过,且配有大量形象的图片说明,这节内容有些环节简要带过。如果有什么不明白的地方,请先阅读前面几节内容吧~

Vue2源码学习笔记 - 11.响应式原理—Observer 类详解
Vue2源码学习笔记 - 12.响应式原理—Dep 类详解
Vue2源码学习笔记 - 13.响应式原理—Watcher 类详解

或者从本专栏 第七节 的 响应式原来-基础 开始阅读。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值