Vue源码学习 响应式原理

450 阅读5分钟

Vue是一个MVVM框架,最吸引人的地方就是它的响应式。

官方解释

首先我们看一下官方文档中对响应式原理的解释。

如何追踪变化

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setterObject.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 getter/setter 的格式化并不同,所以建议安装 vue-devtools 来获取对检查数据更加友好的用户界面。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

上图的过程大致上是这样的,当一个组件实例化的时候,会首先为所有data中的数据定义getter和setter,然后触发render函数,在渲染成virtual dom过程中,会触发渲染过程中使用到的那些数据的getter,而getter中会将这些数据加入到Watcher中,这一步叫做依赖收集,等到某个依赖的数据改变时会通知Watcher去重新触发render函数来重新渲染页面。

检测变化的注意事项

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

Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。例如:

var vm = new Vue({
  data:{
    a:1
  }
})

// `vm.a` 是响应式的

vm.b = 2
// `vm.b` 是非响应式的

对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。例如,对于:

Vue.set(vm.someObject, 'b', 2)

您还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:

this.$set(this.someObject,'b',2)

有时你可能需要为已有对象赋值多个新 property,比如使用 Object.assign()_.extend()。但是,这样添加到对象上的新 property 不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象。

// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

对于数组

Vue 不能检测以下数组的变动:

  1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

举个例子:

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的

为了解决第一类问题,以下两种方式都可以实现和 vm.items[indexOfItem] = newValue 相同的效果,同时也将在响应式系统内触发状态更新:

// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)

你也可以使用 vm.$set 实例方法,该方法是全局方法 Vue.set 的一个别名:

vm.$set(vm.items, indexOfItem, newValue)

为了解决第二类问题,你可以使用 splice

vm.items.splice(newLength)

源码解析

new Vue({
  el: '#example',
  data(){
      return{
          obj:{
              a:1
          }
      }
  },
})

当我们写下这行代码时,vue将我们在data内定义的obj对象进行依赖追踪.

具体做法为执行new Observer(obj)

//经过上面的代码,我们的obj对象会变为以下的样子
{
  obj:{
    a:1,
    __ob__:{ //Observer 实例
        dep:{Dep 实例
            subs:[ //存放 Watcher 实例
              new Watcher(),
              new Watcher(),
              new Watcher(),
              new Watcher(),
            ]
        }
    }
  }
}

Observe 实现

defineReactive

function defineReactive(obj, key, val) {
  observe(val)

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get');
      const ob = this.__ob__
      ob.dep.depend();
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('set');
      if (newVal === val) return
      val = newVal
      observe(newVal)
      const ob = this.__ob__
      ob.dep.notify();
    },

  })
}

observe函数

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
 /*
 尝试创建一个Observer实例(__ob__),如果成功创建Observer实例则返回新的Observer实例,如果已有Observer实例则返回现有的Observer实例。
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  /*判断是否是一个对象*/
  if (!isObject(value)) {
    return
  }
  let ob: Observer | void

  /*这里用__ob__这个属性来判断是否已经有Observer实例,如果没有Observer实例则会新建一个Observer实例并赋值给__ob__这个属性,如果已有Observer实例则直接返回该Observer实例*/
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (

    /*这里的判断是为了确保value是单纯的对象,而不是函数或者是Regexp等情况。*/
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {

    /*如果是根数据则计数,后面Observer中的observe的asRootData非true*/
    ob.vmCount++
  }
  return ob
}

Observer类

/**
 * Observer class that are attached to each observed
 * object. Once attached, the observer converts target
 * object's property keys into getter/setters that
 * collect dependencies and dispatches updates.
 */
export class  {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0

    /*
    将Observer实例绑定到data的__ob__属性上面去,之前说过observe的时候会先检测是否已经有__ob__对象存放Observer实例了,def方法定义可以参考https://github.com/vuejs/vue/blob/dev/src/core/util/lang.js#L16
    */
    def(value, '__ob__', this)
    if (Array.isArray(value)) {

      /*
          如果是数组,将修改后可以截获响应的数组方法替换掉该数组的原型中的原生方法,达到监听数组数据变化响应的效果。
          这里如果当前浏览器支持__proto__属性,则直接覆盖当前数组对象原型上的原生数组方法,如果不支持该属性,则直接覆盖数组对象的原型。
      */
      const augment = hasProto
        ? protoAugment  /*直接覆盖原型的方法来修改目标对象*/
        : copyAugment   /*定义(覆盖)目标对象或数组的某一个方法*/
      augment(value, arrayMethods, arrayKeys)
      /*Github:https://github.com/answershuto*/
      /*如果是数组则需要遍历数组的每一个成员进行observe*/
      this.observeArray(value)
    } else {

      /*如果是对象则直接walk进行绑定*/
      this.walk(value)
    }
  }

  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)

    /*walk方法会遍历对象的每一个属性进行defineReactive绑定*/
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {

    /*数组需要遍历每一个成员进行observe*/
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

这里面牵扯到了 Dep,我们也把Dep实现下,

Dep

class Dep {
  constructor() {
    this.subs = []
  }

  addSub(sub) {
    this.subs.push(sub)
  }

  depend() {
    this.subs.push(Dep.target)
  }

  notify() {
    for (let i = 0; i < this.subs.length; i++) {
      this.subs[i].fn()
    }
  }
}

Dep.target = null

Observer 类 主要做了以下事情

  1. 遍历 data 下的每一个属性,若是对象,则 执行 new Observer() ,在对象上新增__ob__属性,该属性的值为 Observer 的实例
  2. 劫持对象属性的变化,在 getter 的时候,拿到 Observer 实例的dep实例,执行dep.depend(),代码如下
  const ob = this.__ob__
  ob.dep.depend();

看下 dep.depend()做了些啥

this.subs.push(Dep.target)

Dep.target添加到 订阅数组内(this.subs)

也就是说,只要我们 Dep.target 赋值了,再执行 dep.depend(),那么该值就会被添加到 dep 的 subs 数组内,比如

Dep.target =function test(){}
dep.depend()
// test 函数就算 Dep 的订阅者了

Watcher 实现

这个watcher的实现是简化版,甚至将一些定义在其他函数中的逻辑直接放在了这里,只是为了更好理解。

具体的Watcher解释可以看我的另一篇博客Vue的数据驱动原理

const Dep = require('./Dep')

class Watcher {
  constructor(vm, exp, fn) {
    this.vm = vm
    this.exp = exp
    this.fn = fn
    Dep.target = this//将自己挂载到 Dep.target,调用 Dep.depend时会读取该变量
    this.vm[exp]  // 这里源码其实是调用了mount函数,间接调用render函数,渲染时去使用数据。
  }
}

module.exports = Watcher

根据一个小例子来理解 Watcher

const obj = {
  a: 1,
  b: {
    c: 2
  }
}

new Observer(obj)
new Watcher(obj, 'a', () => {
  console.log('Watcher 回调执行')
})
obj.a='222'


流程如下:

  1. 先观测 obj 对象(new Observer(obj)
  2. 实例化Watcher时,会执行Dep.target = this,然后执行this.vm[exp],也就是取一次值,那么会触发 getter,将自身(Watcher实例)添加到dep的订阅者数组内
 get: function reactiveGetter() {
      const ob = this.__ob__
      ob.dep.depend();
      return val
    },

最后,改变数据时候,触发setter

 set: function reactiveSetter(newVal) {
      if (newVal === val) return
      val = newVal
      observe(newVal)
      const ob = this.__ob__
      ob.dep.notify();
    },

执行ob.dep.notify()

 notify() {
    for (let i = 0; i < this.subs.length; i++) {
      this.subs[i].fn()
    }

遍历 订阅者(subs)执行回调函数,整个流程结束

依赖收集过程

  • 通过observer函数,将data中的每个属性都添加getter和setter
  • 添加新的Watcher,Watcher构造过程中,会首先将Dep.target赋值为自己,然后手动调用一遍getter,这样就能触发getter中将Dep.target加入到subs中的逻辑
  • 一旦修改某个属性,会触发setter中的notify,通知所有添加进来的subs(也就是所有的Watcher实例)调用更新函数。

依赖注销过程

不知道大家有没有想过,上面说的过程都是讲的vue如何收集依赖,如何在依赖变化时触发渲染watcher的更新,但是我们也知道,由于v-if之类的存在,当条件为true的时候,我们可能会收集一些依赖,但是当这个条件为false时,这些依赖的变化就不应该触发重新渲染,那么就应该把这些依赖给注销。

那么vue是怎么做的呢?

我们已经知道,当数据发生变化的时候,会触发dep.notify(),从而触发所有订阅该dep的watcher的update

update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

这里就会重新调用get 方法

get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

其中最后一步cleanupDeps其实就是用来清除无效依赖的,他其实是和addDep配合使用的

addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

每次收集依赖的时候都会把dep的id保存到newDepIds数组中,等到触发更新后,会查找上次更新时所有的依赖(this.deps),如果不再newDepIds中,就会将这个dep移除

Vue3响应式升级

Vue3中重新利用ES6的Proxy语法对响应式进行了更新,能够更好地处理上面说的数组更新。

sunra.top/2020/07/08/…

参考文章:

cn.vuejs.org/v2/guide/re…

github.com/answershuto…

github.com/answershuto…

juejin.cn/post/684490…