学习代码仓库

今天来看Vue的数据响应式原理,也就是watch一个值,改变了这个数据,能够得到通知并且在回调里获得新值和旧值从而进行操作,这一个功能的实现。本篇暂不包含与模板层的双向绑定。

# 数据初始化

// demo/index.js
function Vue3(options) {
  this._init(options)
}
// 每次执行new Vue()操作都会执行的构造函数
Vue3.prototype._init = function(options) {
  const vm = this
  vm.$options = options || {}
  vm._watchers = []
  if (vm.$options.data) initData(vm)
}
function initData(vm) {
  let data = vm.$options.data
  // 将配置项data的值挂载在vm._data上,如果data是个函数,则调用`getData`方法获取返回值
  data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}

  // ...省略data的校验步骤
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    const key = keys[i]
    // ...省略data的key值校验步骤
    // 遍历 data 的 key,把 data 上的属性代理到 vm 实例上
    proxy(vm, '_data', key)
  }
  // observe(data, true /* asRootData */)
}

function getData(data, vm) {
  try {
    return data.call(vm, vm)
  } catch (e) {
    console.error(e)
    return {}
  }
}

function noop() {}

function proxy(target, sourceKey, key) {
  const sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
  }
  // 获取 vm[key]值的时候,返回vm._data[key]
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key]
  }
  // 设置 vm[key]值的时候,vm._data[key] = val
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val
  }
  // 将[key]挂载在vm上
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

window.Vue3 = Vue3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

通过以上的初始化,成功让配置项里data的每个值都挂到了vm实例上。并且vm[key]的操作会同步到vm._data[key]

var a = new Vue3({
  data(){
    return {
      name: 'cky',
      age: 18
    }
  }
})
1
2
3
4
5
6
7
8

# Observer、Watcher、Dep

接下来要说的三个类ObserverWatcherDep,我们先提前搞清楚他们三个的作用。 Observer类是用来把一个值变得可观测的工具,它会循环传入的值,然后改写它们的getter/setter。它的实例方法有observeArraywalk,分别把数组和对象类型变得可观测。

Watcher可以设定多个观察对象,然后对于它们的改变做出相应行为。它的实例方法有以下:

  • get()。根据传入的表达式或函数,计算出值
  • addDep(dep)。如果没有将自身传入过该dep记录依赖,则将自身传入,调用dep.addSub(this)
  • cleanupDeps()。清理不再依赖的dep, 同时在dep的依赖列表中移除自身
  • update()。订阅者接口,当订阅的值改变时会触发
  • run()。对比前后值,如果前后值发生了改变,或者deeptrue,或者该值为对象,则触发cb回调。
  • evaluate()。触发this.get() 只会在lazy watcher里被触发
  • depend()。循环触发this.deps的项的depend()方法
  • teardown()。销毁watcher,从所有的this.deps里移除自身,并且将active设为false

Dep类是针对某个值的依赖收集器,比如id为1的Dep实例对象是负责收集_data.name的依赖的,那以后所有对这个值的进行观测的watcher都会被统计到这个实例里。 它的实例方法有如下:

  • addSub(sub: Watcher)。 增加依赖
  • removeSub (sub: Watcher)。 移除依赖
  • depend()。如果有Watcher在Dep.target, 则将这个Watcher加入依赖
  • notify()。调用所有的subsupdate()方法。

上面在initData()函数中注释了一句

observe(data, true /* asRootData */)
1

现在来看看这个函数是干什么的。

// /demo/observer.js
import { isObject } from './util'

export function observe(value, asRootData) {
  // 如果不是对象,直接返回
  if (!isObject(value)) {
    return
  }
  let ob
  // 如果value对象上有__ob__属性,而且这个属性是Observer类的一个实例
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else {
    // 否则ob为一个新的Oserver实例
    ob = new Observer(value)
  }
  // 如果是vm.$data, 那么 ob.vmCount++
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn(obj, key) {
  return hasOwnProperty.call(obj, key)
}

export function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

总结一下上面干了什么,observe函数返回的是一个Observer类的实例,如果传入的value有__ob__属性,直接返回,如果没有 则传入value值去构造一个Observer的实例并返回。

我们再来看看Observer类的定义

export class Observer {
  // value
  // dep
  // vmCount // number of vms that has this object as root $data
  constructor(value) {
    this.value = value
    this.vmCount = 0
    // 给value添加'__ob__'属性,就是这个实例
    def(value, '__ob__', this)
    this.walk(value)
  }
  /**
   * 循环每个属性,并转换它们的getter/setters,这个方法只能用于Object类型的值
   */
  walk(obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // 第一次初始化时, obj: vm._data  keys[i]: key, obj[keys[i]]: value
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
}
/**
 * 在一个对象上定义一个响应式的属性
 */
export function defineReactive(obj, key, val, customSetter, shallow) {
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  const getter = property && property.get
  const setter = property && property.set

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // 如果之前这个字段已经定义过getter了就用之前的getter
      const value = getter ? getter.call(obj) : val
      return value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        // 如果值没有改变 或者类似 NaN !== NaN这种情况
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      console.log(`我被改变了,新值:${newVal}, 旧值:${value}`)
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

总结一下上面的操作,Observer的构造函数给传入的value值增加__ob__属性,也就是这个构造出的实例,并且如果value是对象类型,会循环它的每个属性,调用defineReactive方法,而这个方法就是改写每个属性的getter/setter,从而可以在进行值获取和赋值的时候进行某些操作,也就能监听到值的改变。

看看执行上面代码的结果:

var a = new Vue3({
  data(){
    return {
      name: 'cky',
      age: 18,
      mom: {
      name: 'zj',
        age: 28
      },
      friends: ['aa', 'bbb', 'ccc']
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13

之前对于vm.agevm.name也有gettersetter的重写,和这里的vm._data.agegetter/setter是不一样的,在vm.agegetter是去读vm._data.age,从而触发vm._data.agegettersetter同理。其实真正对于数据改变的监听是在_data属性上的gettersetter上完成的。

# 数组更新检测

上面的操作在数组执行push这类的会改变数组的方法的时候,却没有任何作用,因为这个并不会触发setter。那对于数组Vue又是怎么处理的呢。

// /demo/observer.js
import { arrayMethods } from './array'
export class Observer {
  constructor(value) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      value.__proto__ = arrayMethods
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  // walk()...
  observeArray (items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

// /demo/array.js
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
    console.log(`我是数组,被${method}方法改变了`)
    return result
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

这个array.js文件export出了一个 arrayMethodsarrayMethods 继承了 Array.prototype,并在自身定义了那些变异方法来拦截原始数组的那些方法调用。 我们知道,当我们访问对象上的一个属性的时候,假如对象自身不存在这个属性,则会延续到它的 __proto__ 上去找,找不到就继续。所以上面只需要把数组的 __proto__ 指向 vue 自己的 ArrayMethods 就实现了拦截部分属性并继承原始 Array 的其他原型方法,十分巧妙。

官方文档说,不支持直接对数组this.xx[n] = xyz这样的赋值监听,提供了Vue.setthis.$set 方法,其实这个方法内部在前一篇也讲了,就是调用了splice这个变异方法从而实现监听。

# 依赖收集

 上面的操作还是仅仅是观察者能够监听到了数组的变化,观察者看到发生变化后,就要去通知那些订阅者(watcher)。那这个订阅依赖是怎么统计起来的呢。首先,我们需要定义一个Dep类,这是观察者和订阅者的桥梁,它统计了所有的watcher,然后统一发出通知。

// /demo/dep.js
export default class Dep {
  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub () {...}  // 添加订阅者(依赖)
  removeSub () {...}  // 删除订阅者(依赖)
  depend () {...}  // 检查当前Dep.target是否存在以及判断这个watcher已经被添加到了相应的依赖当中,如果没有则添加订阅者(依赖),如果已经被添加了那么就不做处理
  notify () {...}  // 通知订阅者(依赖)更新
}
1
2
3
4
5
6
7
8
9
10
11

而什么时候该去添加依赖呢,其实就是你在获取这个值,而且说明自己是个订阅者的时候,就可以把你作为这个值的依赖了。 这一步就在defineReactive方法里实现,这一步重写了gettersetter,所以只需要在getter里记录依赖,在setter里通知改变就行了。

// /demo/observer.js
export function defineReactive(obj, key, val, customSetter, shallow) {
  const dep = new Dep() // 该值的依赖收集器
  //...
  let childOb = !shallow && observe(val) // 返回的是一个Observer实例

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) { // 当订阅者存在的时候,才进行依赖收集
        dep.depend() // 依赖收集,
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter(newVal) {
      // ...
      dep.notify()
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

Vue的订阅者实现是一个Watcher类,在Vue的生命周期里,有四个地方会实例化这个类。

  • Vue实例化的过程中有watch选项
  • Vue实例化的过程中有computed计算属性选项
  • Vue原型上有挂载$watch方法: Vue.prototype.$watch,可以直接通过实例调用this.$watch方法
  • Vue生成了render函数,更新视图时
// demo/watcher.js
export default class Watcher {
  constructor (vm, expOrFn, cb, options) {
    // 缓存这个实例vm
    this.vm = vm
    // vm实例中的_watchers中添加这个watcher
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } 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
    ....
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // eg: 'a.b.c',parsePath方法返回了一个函数,接收obj参数,然后返回obj[a][b][c]的值
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
      }
    }
    // 通过get方法去获取最新的值
    // 如果lazy为true, 初始化的时候为undefined
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  get () {...}
  addDep () {...}
  update () {...}
  run () {...}
  evaluate () {...}
  run () {...}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

在除了computed选项外,其他几种实例化watcher的方式都是在实例化过程中完成求值及依赖的收集工作:this.value = this.lazy ? undefined : this.get().在Watcherget方法中:

get () {
  // pushTarget即设置当前的需要被执行的watcher
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // $watch(function () {})
    // 调用this.getter的时候,触发了属性的getter函数
    // 在getter中进行了依赖的管理
    value = this.getter.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `getter for watcher "${this.expression}"`)
  }
  // traverse方法其实就是遍历读取了value的值,从而遍历触发了下面的`getter`从而进行了依赖收集
  if (this.deep) {
    traverse(value)
  }
  // 完成了依赖收集
  popTarget()
  // 清理和删除老旧依赖
  this.cleanupDeps()
  return value
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

get()方法中触发了getter,调用dep.depend()

// demo/dep.js
  depend () {
    // 检查当前Dep.target是否存在以及判断这个watcher已经被添加到了相应的依赖当中,如果没有则添加订阅者(依赖),如果已经被添加了那么就不做处理
    if (Dep.target) {
      // Dep.target为一个watcher
      Dep.target.addDep(this)
    }
  }
1
2
3
4
5
6
7
8
// demo/watcher.js
  // 添加依赖
  addDep(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)
      }
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13

其他的细节代码就不细说了,现在完善代码, 并执行以下代码后

var a = new Vue3({
  data(){
    return {
       name: 'cky',
       age: 18,
       mom: {
	     name: 'zj',
         age: 28
       },
       friends: ['aa', 'bbb', 'ccc'],
       classmate: ['dd', 'ee', 'gg']
    }
  }
})
a.$watch('name', function(newValue, oldValue) {
  console.log(newValue, oldValue)
})
a.$watch('age', function(newValue, oldValue) {
  console.log(newValue, oldValue)
})
a.$watch('age', function(newValue, oldValue) {
  console.log(newValue, oldValue)
})
a.$watch('name', function(newValue, oldValue) {
  console.log(newValue, oldValue)
})
a.$watch('age', function(newValue, oldValue) {
  console.log(newValue, oldValue)
})
a.$watch(function(){
  return this.name + this.age
}, function(newValue, oldValue) {
  console.log(newValue, oldValue)
})
a.$watch('age', function(newValue, oldValue) {
  console.log(newValue, oldValue)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

其中Dep.id为1的是对name值的依赖收集器,它下面有id为1、4、6的watcherDep.id为2的是对age的依赖收集器,它的收集的订阅者有id为2、3、5、6、7的watcher id为6的watcher因为用到了nameage2个值,所以上面都有他,而它的deps字段也能看出来。

# 参考

Vue的数据依赖实现原理简析