Vue源码详解之数据的响应化

# Vue源码详解之数据的响应化

vue源码构造实例的过程就记住一行:

this._init(options)
1

执行_init初始化函数,带参数对象

_init函数中先进行了大量的参数初始化操作 this.xxx=xxx

this._data = {}

// call init hook
this._callHook('init')

// initialize data observation and scope inheritance.
this._initState()

// setup event system and option events.
this._initEvents()

// call created hook
this._callHook('created')

// if `el` option is passed, start compilation.
if (options.el) {
	this.$mount(options.el)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

基本就是触发init钩子,初始化一些状态,初始化event,然后触发created钩子,最后挂载到具体的元素上面去。_initState()方法中包含了数据的初始化操作,也就是让数据变成响应式的,让Vue能够监听到数据的变动。而this.$mount()方法则承载了绝大部分的代码量,负责模板的嵌入、编译、link、指令和watcher的生成、批处理的执行等等。

# 数据的响应化

Vue是如何解决"getter/setter无法监听属性的添加和删除"的。

对于data对象的几乎任何更改我们都能够监听到。这是MVVM的基础,基本思路就是遍历每一个属性,然后使用Object.defineProperty将这个属性设置为响应式的

遍历一个对象

function touch (obj) {
  if (typeof obj === 'object')
    if (Array.isArray(obj)) {
      for (let i = 0,l = obj.length; i < l; i++) {
        touch(obj[i])
      }
    } else {
      let keys = Object.keys(obj)
      for (let key of keys) touch(obj[key])
    }
  console.log(obj)
}
1
2
3
4
5
6
7
8
9
10
11
12

遇到普通数据属性,直接处理,遇到对象,遍历属性之后递归进去处理属性,遇到数组,递归进去处理数组元素

遍历完就到处理了,也就是Object.defineProperty部分了,对于一个对象,我们可以用这个来改写它属性的getter/setter,这样,当你改属性的值我就有办法监听到。但是对于数组就有问题了。

Vue.prototype._initData = function() {
 // 初始化数据,其实一方面把data的内容代理到vm实例上
 // 另一方面改造data,变成reactive
 // get时 触发依赖收集,将订阅者加入dep实例的subs数组中,set时notify订阅者
 var dataFn = this.$options.data

 var data = this._data = dataFn ? dataFn() : {}

 var props = this._props

 // proxy data on instance

 var keys = Object.keys(data)

 var i, key
 i = keys.length

 while (i--) {
  key = keys[i]
  // 将data属性的内容代理到vm上面去, 使得vm访问指定属性即可拿到_data内的同名属性
		// 实现 vm.prop === vm._data.prop,
		// 这样当前vm的后代实例就能直接通过原型链查找到父代的属性
		// 比如v-for指令会为数组的每一个元素创建一个scope,这个scope就继承自vm或上级数组元素的scope,
		// 这样就可以在v-for的作用域中访问父级的数据
  this._proxy(key)
 }
 // observe data
 observe(data, this)
}
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

开始observe这个data:

export function observe(value, vm) {
 if(!value || typeof value !== 'object') {
  // 保证只有对象会进入到这个函数
  return
 }
 var ob
 if (
  // 如果这个数据身上已经有了ob实例了,那observe过了,就直接返回那个ob实例
  hasOwn(value, '__ob__') && value.__ob__ instanceof Observer
 ) {
  ob = value.__ob__
 } else if (
  shouldConvert &&
  (isArray(value) || _.isPlainObject(value)) && Object.isExtensible(value) && 
  !value._isVue
 ) {
  // 是对象(包括数组)的话就深入进去遍历属性,observe每个属性
  ob = new Observer(value)
 }
 if (ob && vm) {
  // 把vm加入到ob的vms数组当中,因为有的时候我们会对数据手动执行$set/$delete操作,
  // 那么就要提示vm实例这个行为的发生(让vm代理这个新$set的数据,和更新界面)
 }
 return ob
}
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

代码的执行过程一般都是进入到那个else if里,执行new Observer(value),至于shouldConvert和后续的几个判断则是为了防止value不是单纯的对象而是Regexp或者函数之类的,或者是vm实例再或者是不可扩展的,shouldConvert则是某些特殊情况下为false

现在就进入到拿当前的data对象去new Observer(value),现在你可能会疑惑,递归遍历的过程不是应该是纯命令式的、面向过程的吗?怎么代码跑着跑着跑出来一句new一个对象了,嗯先不用管,我们先理清代码执行过程,先带着这个疑问。同时,我们注意到代码最后return了ob,结合代码,我们可以理解为如果return的是undifned,那么说明传进来的value不是对象,反之return除了一个ob,则说明这个value是对象或数组,他可以添加或删除属性

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.
 *
 * @param {Array|Object} value
 * @constructor
 */

function Observer (value) {
  this.value = value
  this.dep = new Dep()
  def(value, '__ob__', this) //value的__ob__属性指向这个Ob实例
  if (isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
  } else {
    // 如果是对象则使用walk遍历每个属性
    this.walk(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

# observe一个数组

如果遇到数组data中的数组实例增加了一些“变异”的push、pop等方法,这些方法会在数组原本的push、pop方法执行后发出消息,表明发生了改动。听起来这好像可以用继承的方式实现: 继承数组然后在这个子类的原型上附加上变异的方法。

但是你需要知道的是在es5及更低版本的js里,无法完美继承数组,主要原因是Array.call(this)时,Array根本不是像一般的构造函数那样对你传进去this进行改造,而是直接返回一个新的数组。所以一般的继承方式就没法实现了。

但是如果当前浏览器里存在__proto__这个非标准属性的话(大部分都有),那又可以有方法继承,就是创建一个继承自Array.prototype的Object: Object.create(Array.prototype),在这个继承了数组原生方法的对象上添加方法或者覆盖原有方法,然后创建一个数组,把这个数组的__proto__指向这个对象,这样这个数组的响应式的length属性又得以保留,又获得了新的方法,而且无侵入,不会改变本来的数组原型。

Vue就是基于这个思想,先判断__proto__能不能用(hasProto),如果能用,则把那个一个继承自Array.prototype的并且添加了变异方法的Object (arrayMethods),设置为当前数组的__proto__,完成改造,如果__proto__不能用,那么就只能遍历arrayMethods就一个个的把变异方法def到数组实例上面去,这种方法效率不高,所以优先使用改造__proto__的那个方法。

源码里后面那句this.observeArray非常简单,for遍历传进去的value,然后对每个元素执行observe,处理之前说的数组的元素为对象或者数组的情况。

# observe 对象

defineReactive函数

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

Dep.prototype.depend = function () {
  Dep.target.addDep(this)
}

Dep.prototype.notify = function () {
  // stablize the subscriber list first
  var subs = toArray(this.subs)
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

function defineReactive (obj, key, val) {
  // 生成一个新的Dep实例,这个实例会被闭包到getter和setter中
  var dep = new Dep()

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

  // cater for pre-defined getter/setters
  var getter = property && property.get
  var setter = property && property.set
  // 对属性的值继续执行observe,如果属性的值是一个对象,那么则又递归进去对他的属性执行defineReactive
  // 保证遍历到所有层次的属性
  var childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val
      // 只有在有Dep.target时才说明是Vue内部依赖收集过程触发的getter
      // 那么这个时候就需要执行dep.depend(),将watcher(Dep.target的实际值)添加到dep的subs数组中
      // 对于其他时候,比如dom事件回调函数中访问这个变量导致触发的getter并不需要执行依赖收集,直接返回value即可
      if (Dep.target) {
        dep.depend()
        if (childOb) {
	     //如果value是对象,那就让生成的Observer实例当中的dep也收集依赖
          childOb.dep.depend()
        }
        if (isArray(value)) {
          for (var e, i = 0, l = value.length; i < l; i++) {
            e = value[i]
			//如果数组元素也是对象,那么他们observe过程也生成了ob实例,那么就让ob的dep也收集依赖
            e && e.__ob__ && e.__ob__.dep.depend()
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val
      if (newVal === value) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // observe这个新set的值
      childOb = observe(newVal)
      // 通知订阅我这个dep的watcher们:我更新了
      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
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
61
62
63
64
65
66
67
68
69
70
71
72
73

我们来说说这个Dep,Dep类的定义极其简单,一个id,一个数组,他就是一个很基本的发布者-观察者模式的实现,作为一个发布者,他的subs属性用来存放了订阅他的观察者,也就是后面我们会说到的watcher。

defineProperty是用来将对象的属性转化为响应式的getter/setter的,defineProperty函数执行过程中新建了一个Dep,闭包在了属性的getter和setter中,因此每个属性都有一个唯一的Dep与其对应,我们暂且可以把属性和他对应的Dep理解为一体的。

Dep其实是dependence依赖的缩写,我之前一直没能理解依赖、依赖收集是什么,其实对于我们的一个模板NaN,我们会说他的依赖有a和b,其实就是依赖了data的a和b属性,更精确的说是依赖了a属性中闭包的dep实例和b属性中闭包的那个dep实例。

详细来说:我们的这个NaN在dom里最终会被"a+b"表达式的真实值所取代,所以存在一个求出这个“a+b”的表达式的过程,求值的过程就会自然的分别触发a和b的getter,而在getter中,我们看到执行了dep.depend(),这个函数实际上回做dep.addSub(Dep.target),即在dep的订阅者数组中存放了Dep.target,让Dep.target订阅dep。

那Dep.target是什么?他就是我们后面介绍的Watcher实例,为什么要放在Dep.target里呢?是因为getter函数并不能传参,dep可以通过闭包的形式放进去,那watcher可就不行了,watcher内部存放了a+b这个表达式,也是由watcher计算a+b的值,在计算前他会把自己放在一个公开的地方(Dep.target),然后计算a+b,从而触发表达式中所有遇到的依赖的getter,这些getter执行过程中会把Dep.target加到自己的订阅列表中。等整个表达式计算成功,Dep.target又恢复为null.这样就成功的让watcher分发到了对应的依赖的订阅者列表中,订阅到了自己的所有依赖。

我们可以看到这是极其精妙的一笔!在一个表达式的求值过程中隐式的完成依赖订阅。

上面完成的是订阅的过程,而上面setter代码里的dep.notify就负责完成数据变动时通知订阅者的功能。而且数据变化时,后文会说明只有依赖他的那些dom会精确更新,不会出现一些介绍mvvm的文章里虽然实现了订阅更新但是重新计算整个视图的情况。

于是一整个对象订阅、notify的过程就结束了。

# Observer类

getter和setter存在的缺陷:只能监听到属性的更改,不能监听到属性的删除与添加。

上次更新: 2022/7/6 上午11:51:19