从需求出发阅读 Vue 源码之响应式基础(Vue 2)

tao
发布于2024-04-03 | 更新于2024-04-03

今天这篇笔记以响应式基础为主题继续从需求出发阅读 Vue 源码,有了之前两篇笔记从需求出发阅读 Vue 源码之声明式渲染(Vue 2)从需求出发阅读 Vue 源码之声明式渲染(Vue 3)
的经验,我们在比较 Vue 2 和 Vue 3 在响应式基础上的不同就变得比较容易了。

初始数据被渲染到了页面上,那么接下来自然能想到如何在修改数据的时候自动更新页面渲染呢,这就产生了响应式的需求。

首先,我们先把之前的测试代码更改一下让其在完成挂载后延迟两秒更改 data 选项属性值,打开浏览器执行以下经过更改的代码,页面会在两秒之后渲染新的属性值。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue Reactivity</title>
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <div>{{ message }}</div>
    </div>
  </body>

  <script>
    new Vue({
      el: '#app',
      data: {
        message: 'hello vue!'
      },
      mounted() {
        setTimeout(() => {
          this.message += ' hello world!'
        }, 2000)
      }
    })
  </script>
</html>

打开 src/core/instance/state.js,在第 113 行 initData 函数中拿到 data 选项值,在函数最后一行执行了 observe 函数。

......
function initData (vm: Component) {
  ......
  observe(data, true /* asRootData */)
}
......

打开 src/core/observer/index.js,第 110 行定义了函数 observe,第 124 行函数内新建了一个 Observer 实例。

export function observe (value: any, asRootData: ?boolean): Observer | void {
  ......
  let ob: Observer | void
  ......
    ob = new Observer(value)
  ......
  return ob
}

src/core/observer/index.js 第 37 行定义了 Observer 类,在类构造方法中,创建了 Dep 实例并赋值给了类属性 dep,执行了类方法 walk 去遍历 data 对象的每个属性,将 data 对象和对象中的每个属性名(key)作为参数传入 defineReactive 方法并执行。

......
export class Observer {
  value: any;
  dep: Dep;
  ......

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    ......
    def(value, '__ob__', this)
    ......
      this.walk(value)
    ......
  }

  ......

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}

src/core/observer/index.js 第 135 行定义了 defineReactive 函数,第 142 行创建了 Dep 实例并赋值给函数内部变量 dep,第 156 行会继续执行 observe 方法进行一个递归操作对深层嵌套的对象值设置响应式,第 157 行使用 Object.defineProperty 方法为对象属性修改描述符,修改完成后当访问该属性时会执行 160 行的 reactiveGetter 取值函数 get,当为属性设置新值时会执行 173 行的 reactiveSetter 存值函数 set

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  ......
) {
  const dep = new Dep()

  ......

  let childOb = ...... observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = ...... val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          ......
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = ...... val

      // 此处用自身进行比较是为了排除值为 NaN 的情况
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
   
      ......
        val = newVal
      ......

      childOb = ...... observe(newVal)
      dep.notify()
    }
  })
}

Observer 类构造函数和 defineReactive 函数中都创建了 Dep 类的实例,Dep 类定义在 src/core/observer/dep.js 第 13 行,其中类静态属性 target 标识了当前目标(当前目标为 Watcher 实例),并有对应的方法 pushTargetpopTarget 在目标栈 targetStack 中添加或移除目标同时设置当前目标,类属性 idDep 类实例的唯一标识,类属性 subs 是订阅数组并有相对应的 addSubremoveSub 方法为数组添加和删除订阅(sub 订阅为 Watcher 实例),另外还有 depend 方法为当前目标添加 Dep 实例,notify 方法执行 subs 数组中每项订阅的 update 方法。

import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

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

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

通过上述可以发现 data 对象每个属性对应一个 Dep 实例,包括 data 对象自身也对应一个 Dep 实例,但是没有被 Watcher 实例收集。

src/core/instance/lifecycle.js 中第 197 行创建了 Watcher 实例,传入参数主要是 Vue 实例和 updateComponent 函数,从这里也可以发现一个 Vue 实例对应一个 Watcher 实例updateComponent 函数就是在从需求出发阅读 Vue 源码之声明式渲染(Vue 2)提到的生成虚拟节点并完成挂载的函数。第 167 行调用了 beforeMount 生命周期函数,第 210 行调用了 mounted 生命周期函数。

export function mountComponent (
  vm: Component,
  el: ?Element,
  ......
): Component {
  ......
  callHook(vm, 'beforeMount')

  let updateComponent

  ......

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

  ......

  new Watcher(vm, updateComponent, noop, {
    ......
  }, true /* isRenderWatcher */)

  ......
    callHook(vm, 'mounted')
  ......

  return vm
}

src/core/observer/watcher.js 第 27 行定义了 Watcher 类,在从需求出发阅读 Vue 源码之声明式渲染(Vue 2)中曾提过在类中将构造函数入参 updateComponent 赋值给 this.getter 并通过 this.get 方法让函数得以执行以此完成页面渲染,现在我们还需关注 this.getter 方法执行之前的 pushTarget 操作。

......
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  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
  }
  ......
}
......

pushTarget 方法会将当前的 Watcher 实例添加到 targetStack 数组中,并将当前目标设置为此 Watcher 实例,随后在执行 this.getter 函数时会读取 data 对象中已经设置了取值函数的属性,触发 dep.depend 收集依赖,执行 Watcher 实例中的 addDep 方法并将当前 Dep 实例添加到 Watcher 实例的 newDeps 数组中,同时在 newDepIds 集合中添加 Dep 实例的 id 属性,最后在 Dep 实例中添加订阅也就是将当前 Watcher 实例添加到 Dep 实例的 subs 数组中,当数据发生变更时发布通知 dep.notify 执行订阅,这里就用到了经典的设计模式发布订阅模式

// 1. src/core/observer/watcher.js
pushTarget(this)

// 2. src/core/observer/dep.js
pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

// 3. src/core/observer/watcher.js
this.getter()

// 4. reactiveGetter

// 5. src/core/observer/index.js
dep.depend()

// 6. src/core/observer/dep.js
depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}

// 7. src/core/observer/watcher.js
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)
    }
  }
}

// 8. src/core/observer/dep.js
addSub (sub: Watcher) {
  this.subs.push(sub)
}

// 9. reactiveSetter

// 10. src/core/observer/dep.js
notify () {
  // stabilize the subscriber list first
  const subs = this.subs.slice()
  ......
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

src/core/observer/dep.js 第 47 行实际上执行的就是 Watcher 实例的 update 方法去更新页面显示,到这里就完成了响应式,至于在更新过程中的异步更新队列、diff 算法、nextTick 等我们在之后的笔记中再去记录。

刚才只是在 data 选项对象中设置了一个字符串类型的 message 属性,接下来继续设置一些复杂类型来试试。将测试代码改成如下所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue</title>
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <div>{{ message }}</div>
      <div>{{ this.car.name }}</div>
    </div>
  </body>

  <script>
    new Vue({
      el: '#app',
      data: {
        message: 'hello vue!',
        car: {
          name: 'xiaomi'
        }
      },
      mounted() {
        setTimeout(() => {
          this.message += ' hello world!'
          this.car.name = 'tesla'
        }, 2000)
      }
    })
  </script>
</html>

执行上述代码能观察到 2 秒后更改 data 选项对象中复杂类型 car 对象的 name 属性值,页面上也能反应出变化,这是由于在 src/core/observer/index.js 第 156 行执行函数 observe 递归处理对象的深层次嵌套属性,因此即使我们把测试代码改成上述那样也依然能保持响应性。

但是当把测试代码改成如下所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue</title>
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <div>{{ message }}</div>
      <!-- <div>{{ this.car.name }}</div> -->
      <div>{{ this.car.color }}</div>
    </div>
  </body>

  <script>
    new Vue({
      el: '#app',
      data: {
        message: 'hello vue!',
        car: {
          name: 'xiaomi'
        }
      },
      mounted() {
        setTimeout(() => {
          // this.message += ' hello world!'
          // this.car.name = 'tesla'
          this.car.color = 'red'
        }, 2000)
      }
    })
  </script>
</html>

这样的话 2 秒后更改 data 选项对象中复杂类型 car 对象的 color 属性值,页面上就不能反应出变化了,这是因为由于没有在 data 选项对象中定义复杂类型 car 对象的 color 属性,而响应性是依赖于 Object.defineProperty 所定义的存取值函数的,因此其不具备响应性的。这也是我们常常碰到的数据更新了,但是页面没有更新的情况之一,常用的解决办法是使用全局 Vue.set 方法为新属性设置响应性。

但是如果把代码改成如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue</title>
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <div>{{ message }}</div>
      <!-- <div>{{ this.car.name }}</div> -->
      <div>{{ this.car.color }}</div>
    </div>
  </body>

  <script>
    new Vue({
      el: '#app',
      data: {
        message: 'hello vue!',
        car: {
          name: 'xiaomi'
        }
      },
      mounted() {
        setTimeout(() => {
          this.message += ' hello world!'
          // this.car.name = 'tesla'
          this.car.color = 'red'
        }, 2000)
      }
    })
  </script>
</html>

在 2 秒后同时更新 messagecar.color 的值,页面上可以反应出 car.color 的更改,这是因为 message 是响应性的,当 message 变化出发页面更新时,渲染函数执行时会读取新的 car.color 值,渲染到页面时也就体现了变更。

接下来我们测试当复杂类型属性为数组时的处理,在 src/core/observer/index.js 第 37 行定义的类 Observer 的构造函数中,并没有把数组的每个成员变成响应式,也就是说通过数组下标或者改变数组 length 属性是不能触发页面更新的,而是对数组做了特殊处理,这个原因 Vue 2 的作者尤雨溪亲自回应过就是性能代价和获得的用户体验收益不成正比

import { ...... def ...... hasProto ......} from '../util/index'
// export const hasProto = '__proto__' in {}

// export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
//   Object.defineProperty(obj, key, {
//     value: val,
//     enumerable: !!enumerable,
//     writable: true,
//     configurable: true
//   })
// }

import { arrayMethods } from './array'
// const arrayProto = Array.prototype
// export const arrayMethods = Object.create(arrayProto)

const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

/**
 * Augment a target Object or Array by intercepting
 * the prototype chain using __proto__
 */
function protoAugment (target, src: Object) {
  target.__proto__ = src
}

/**
 * Augment a target Object or Array by defining
 * hidden properties.
 */
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

......
export class Observer {
  ......
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  ......
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
  ......
}

......

根据上述代码可以看出程序对数组原型链做了变动,改变后不会直接访问数组原型链上的方法,而是访问经过处理的原型链方法。同时,程序执行了 observeArray 函数遍历数组,递归地调用 observe 函数为数组成员设置响应性。

原型链方法在 src/core/observer/array.js 中做出的具体变动如下,针对 methodsToPatch 中这几种方法,会先执行原始方法拿到结果,对会产生新数据的几种方法比如 push,程序拿到传入的新数据后执行 observeArray,和上面一样为可能新增的对象属性设置响应性,最后发布通知执行订阅返回结果。也就是说执行这几种数组的执行方法会触发页面更新

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
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
  })
})

至此 Vue 2 的响应式基础完成。