如果你之前看过我的这一篇文章 Vue1.0源码解析系列:实现数据响应化 ,那么你可以很轻松看懂 Vue2.x版本中的响应化,因为基本思路以及大部分代码其实都没有变化。当然没看过也没关系,不用去看,因为这里我会讲的非常详细。

数据响应我会分两章来讲,本章讲 Observer 相关,下一章讲 Watcher

从data开始

state 的初始化是从 initState 函数开始的,下面是 initState 的完整代码:

core/instance/state.js

  1. export function initState (vm: Component) {
  2. vm._watchers = []
  3. const opts = vm.$options
  4. if (opts.props) initProps(vm, opts.props)
  5. if (opts.methods) initMethods(vm, opts.methods)
  6. if (opts.data) {
  7. initData(vm)
  8. } else {
  9. observe(vm._data = {}, true /* asRootData */)
  10. }
  11. if (opts.computed) initComputed(vm, opts.computed)
  12. if (opts.watch && opts.watch !== nativeWatch) {
  13. initWatch(vm, opts.watch)
  14. }
  15. }

这里包括了四个部分:props, methods, datawatch,为了方便起见,让我们从最简单的,但是也能完整揭示数据响应化原理的 data 作为切入点。为什么选它呢,因为 props 还涉及到如何从模板中解析,而另外两个其实是函数。

让我们先看一下 initData 的完整代码:

  1. function initData (vm: Component) {
  2. let data = vm.$options.data
  3. data = vm._data = typeof data === 'function'
  4. ? getData(data, vm)
  5. : data || {}
  6. if (!isPlainObject(data)) {
  7. data = {}
  8. process.env.NODE_ENV !== 'production' && warn(
  9. 'data functions should return an object:\n' +
  10. 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
  11. vm
  12. )
  13. }
  14. // proxy data on instance
  15. const keys = Object.keys(data)
  16. const props = vm.$options.props
  17. const methods = vm.$options.methods
  18. let i = keys.length
  19. while (i--) {
  20. const key = keys[i]
  21. if (process.env.NODE_ENV !== 'production') {
  22. if (methods && hasOwn(methods, key)) {
  23. warn(
  24. `Method "${key}" has already been defined as a data property.`,
  25. vm
  26. )
  27. }
  28. }
  29. if (props && hasOwn(props, key)) {
  30. process.env.NODE_ENV !== 'production' && warn(
  31. `The data property "${key}" is already declared as a prop. ` +
  32. `Use prop default value instead.`,
  33. vm
  34. )
  35. } else if (!isReserved(key)) {
  36. proxy(vm, `_data`, key)
  37. }
  38. }
  39. // observe data
  40. observe(data, true /* asRootData */)
  41. }

看起来并不算短,不过我们可以先把开发模式下的一些友好警告给忽略掉,毕竟对我们分析源码来说这些警告不是很重要,其中有三段警告,让我们分别看看:

  1. if (!isPlainObject(data)) {
  2. data = {}
  3. process.env.NODE_ENV !== 'production' && warn(
  4. 'data functions should return an object:\n' +
  5. 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
  6. vm
  7. )
  8. }

上面这段的意思是,如果发现 data 竟然不是一个平凡对象,那么就打印一段警告,告诉你必须应该返回一个对象。

  1. if (process.env.NODE_ENV !== 'production') {
  2. if (methods && hasOwn(methods, key)) {
  3. warn(
  4. `Method "${key}" has already been defined as a data property.`,
  5. vm
  6. )
  7. }
  8. }

大的循环体都是在循环 data 上的 key,上面这一段是说,如果发现 methods 中有和 data 上定义重复的key,那么就打印一个警告。

  1. if (props && hasOwn(props, key)) {
  2. process.env.NODE_ENV !== 'production' && warn(
  3. `The data property "${key}" is already declared as a prop. ` +
  4. `Use prop default value instead.`,
  5. vm
  6. )
  7. }

上面这一段是说,如果发现 props 中发现了重复的 key,那么也会打印一段警告。当然上述两种警告都只有在开发模式下才有的。弄懂了这两段警告的意思,让我们把它删了,然后在看看代码变成这样了:

  1. function initData (vm: Component) {
  2. let data = vm.$options.data
  3. data = vm._data = typeof data === 'function'
  4. ? getData(data, vm)
  5. : data || {}
  6. if (!isPlainObject(data)) {
  7. data = {}
  8. }
  9. // proxy data on instance
  10. const keys = Object.keys(data)
  11. let i = keys.length
  12. while (i--) {
  13. const key = keys[i]
  14. if (!isReserved(key)) {
  15. proxy(vm, `_data`, key)
  16. }
  17. }
  18. // observe data
  19. observe(data, true /* asRootData */)
  20. }

是不是简单了很多,我们把上面这段代码拆成三段来分别看看。其中最上面的一段代码是:

  1. let data = vm.$options.data
  2. data = vm._data = typeof data === 'function'
  3. ? getData(data, vm)
  4. : data || {}
  5. if (!isPlainObject(data)) {
  6. data = {}
  7. }

首先把 vm.$options.data 取个别名,免得后面这样写太长了,然后判断了它的类型,如果是函数,就通过 getData 获取函数的返回值。然后还有一个操作就是把 data 放到了 this._data 上,至于为什么这么做,下一段代码我们就会明白。

这里大家会有另一个疑问了,为什么不是直接调用函数获得返回值,而是需要一个 getData 呢,它除了调用函数肯定还做了别的事,让我们看看 getData 的源码:

  1. export function getData (data: Function, vm: Component): any {
  2. // #7573 disable dep collection when invoking data getters
  3. pushTarget()
  4. try {
  5. return data.call(vm, vm)
  6. } catch (e) {
  7. handleError(e, vm, `data()`)
  8. return {}
  9. } finally {
  10. popTarget()
  11. }
  12. }

其实它确实是调用了函数,并获得了返回值,除了一段异常处理代码外,他在调用我们的 data 函数前进行了一个 pushTarget 操作,而在结束后调用了一个 popTarget 操作。我们继续来看这两个函数,他们在 **core/observer/dep.js`中有定义,而且异常简单。

  1. Dep.target = null
  2. const targetStack = []
  3. export function pushTarget (_target: ?Watcher) {
  4. if (Dep.target) targetStack.push(Dep.target)
  5. Dep.target = _target
  6. }
  7. export function popTarget () {
  8. Dep.target = targetStack.pop()
  9. }

虽然看起来代码很简单,就是在一个全局的 Dep.target 中把自己记录了一下,也就是在 data 函数调用前记录了一下,然后调用后又恢复了之前的值。这里暂时理解起来会比较困难,因为我们要结合本文后面讲到的内容才能理解。简单的说,在 getData 的时候,我们调用 pushTarget 却没有传参数,目的是把 Dep.target 给清空,这样不会在获取 data 初始值的过程中意外的把依赖记录下来。

我们再回到 initState 的第二段代码:

  1. const keys = Object.keys(data)
  2. let i = keys.length
  3. while (i--) {
  4. const key = keys[i]
  5. if (!isReserved(key)) {
  6. proxy(vm, `_data`, key)
  7. }
  8. }

就是遍历了 data 的key,然后做了一个 proxy,我们来看 proxy 的代码:

  1. function proxy (target, sourceKey, key) {
  2. sharedPropertyDefinition.get = function proxyGetter () {
  3. return this[sourceKey][key]
  4. };
  5. sharedPropertyDefinition.set = function proxySetter (val) {
  6. this[sourceKey][key] = val;
  7. };
  8. Object.defineProperty(target, key, sharedPropertyDefinition);
  9. }

这里target是就是我们的 vm 也就是我们的组件自身,sourceKey 就是 _data,也就是我们的 data,这段代码会把对 vm 上的数据读写代理到 _data 上去。哈哈,我们这样就明白了一个问题,为什么我们是通过 data.msg 定义的数据,却可以通过 this.msg 访问呢?原来是这里做了一个代理。

到目前为止虽然说了这么多,但是做的事情很简单,除了一些异常处理之外,我们主要做了三件事:

  1. 通过 getData 把options中传入的data取出来,这期间做了一些 依赖 的处理
  2. this._data = data
  3. 对于每一个 data 上的key,都在 vm 上做一个代理,实际操作的是 this._data

这样结束之后,其实vm会变成这样:

observer1

弄懂了这个之后我们再看最后一段代码:

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

observe 是如何工作的?我们来看看他的代码,这是响应式的核心代码。

深入 Observer

observer 的定义在 core/observer/index.js 中,我们看看 代码:

  1. export function observe (value: any, asRootData: ?boolean): Observer | void {
  2. if (!isObject(value) || value instanceof VNode) {
  3. return
  4. }
  5. let ob: Observer | void
  6. if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
  7. ob = value.__ob__
  8. } else if (
  9. shouldObserve &&
  10. !isServerRendering() &&
  11. (Array.isArray(value) || isPlainObject(value)) &&
  12. Object.isExtensible(value) &&
  13. !value._isVue
  14. ) {
  15. ob = new Observer(value)
  16. }
  17. if (asRootData && ob) {
  18. ob.vmCount++
  19. }
  20. return ob
  21. }

其中有一些很多if的判断,包括对类型的判断,是否之前已经做过监听等。我们暂且抛开这些,把代码精简一下,就只剩下两行了:

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

可以看到主要逻辑就是创建了一个 Observer 实例,那么我们再看看 Observer 的代码:

  1. export class Observer {
  2. value: any;
  3. dep: Dep;
  4. vmCount: number; // number of vms that has this object as root $data
  5. constructor (value: any) {
  6. this.value = value
  7. this.dep = new Dep()
  8. this.vmCount = 0
  9. def(value, '__ob__', this)
  10. if (Array.isArray(value)) {
  11. const augment = hasProto
  12. ? protoAugment
  13. : copyAugment
  14. augment(value, arrayMethods, arrayKeys)
  15. this.observeArray(value)
  16. } else {
  17. this.walk(value)
  18. }
  19. }
  20. /**
  21. * Walk through each property and convert them into
  22. * getter/setters. This method should only be called when
  23. * value type is Object.
  24. */
  25. walk (obj: Object) {
  26. const keys = Object.keys(obj)
  27. for (let i = 0; i < keys.length; i++) {
  28. defineReactive(obj, keys[i])
  29. }
  30. }
  31. /**
  32. * Observe a list of Array items.
  33. */
  34. observeArray (items: Array<any>) {
  35. for (let i = 0, l = items.length; i < l; i++) {
  36. observe(items[i])
  37. }
  38. }
  39. }

这个类包括构造函数在内,总共有三个函数。

  1. constructor (value: any) {
  2. this.value = value
  3. this.dep = new Dep()
  4. this.vmCount = 0
  5. def(value, '__ob__', this)
  6. if (Array.isArray(value)) {
  7. const augment = hasProto
  8. ? protoAugment
  9. : copyAugment
  10. augment(value, arrayMethods, arrayKeys)
  11. this.observeArray(value)
  12. } else {
  13. this.walk(value)
  14. }
  15. }

构造函数代码如上,主要做了这么几件事:

  1. this.value = value
  2. this.dep = new Dep()
  3. this.vmCount = 0
  4. def(value, '__ob__', this)

这里记录了 value, depvmCount, 和 __ob__四个值,其中值得注意的是这两个:

  • this.dep 是 明显是记录依赖的,记录的是对这个value 的依赖,我们在下面马上就能看到怎么记录和使用的
  • __ob__ 其实是把自己记录一下,避免重复创建
  1. if (Array.isArray(value)) {
  2. const augment = hasProto
  3. ? protoAugment
  4. : copyAugment
  5. augment(value, arrayMethods, arrayKeys)
  6. this.observeArray(value)
  7. } else {
  8. this.walk(value)
  9. }

这一段代码会判断 value 的类型,进行递归的 observe,对数组来说,就是对其中每一项都进行递归 observe:

  1. observeArray (items: Array<any>) {
  2. for (let i = 0, l = items.length; i < l; i++) {
  3. observe(items[i])
  4. }
  5. }

显然,直到碰到数组中非数组部分后,最终就会进入 walk 函数,在看 walk 函数之前,我们先看看这一段代码:

  1. const augment = hasProto
  2. ? protoAugment
  3. : copyAugment
  4. augment(value, arrayMethods, arrayKeys)

这里我不打算详细讲解每一行,如果你看源码其实很容易看懂。这里的作用就是把 数组上的原生方法进行了一次劫持,因此你调用比如 push 方法的时候,其实调用的是被 劫持 一个方法,而在这个方法内部,Vue会进行 notify 操作,因此就知道了你对数组的修改了。不过这个做法没法劫持直接通过下标对数组的修改。

好,让我们回到 walk 函数:

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

walk 函数会对每一个 key 进行 defineReactive 操作,在这个函数内部其实就会调用 getter/setter 拦截读写操作,实现响应化。那么这时候可能有人会有一个疑问了,如果某个 key 的值也是一个对象呢?难道不能进行深度的依赖么?当然可以的,不过对对象嵌套的递归操作不是在这里进行的,而是在 defineReactive 中进行了递归。让我们看看 defineReactive 函数:

  1. export function defineReactive (
  2. obj: Object,
  3. key: string,
  4. val: any,
  5. customSetter?: ?Function,
  6. shallow?: boolean
  7. ) {
  8. const dep = new Dep()
  9. const property = Object.getOwnPropertyDescriptor(obj, key)
  10. if (property && property.configurable === false) {
  11. return
  12. }
  13. // cater for pre-defined getter/setters
  14. const getter = property && property.get
  15. if (!getter && arguments.length === 2) {
  16. val = obj[key]
  17. }
  18. const setter = property && property.set
  19. let childOb = !shallow && observe(val)
  20. Object.defineProperty(obj, key, {
  21. enumerable: true,
  22. configurable: true,
  23. get: function reactiveGetter () {
  24. const value = getter ? getter.call(obj) : val
  25. if (Dep.target) {
  26. dep.depend()
  27. if (childOb) {
  28. childOb.dep.depend()
  29. if (Array.isArray(value)) {
  30. dependArray(value)
  31. }
  32. }
  33. }
  34. return value
  35. },
  36. set: function reactiveSetter (newVal) {
  37. const value = getter ? getter.call(obj) : val
  38. /* eslint-disable no-self-compare */
  39. if (newVal === value || (newVal !== newVal && value !== value)) {
  40. return
  41. }
  42. /* eslint-enable no-self-compare */
  43. if (process.env.NODE_ENV !== 'production' && customSetter) {
  44. customSetter()
  45. }
  46. if (setter) {
  47. setter.call(obj, newVal)
  48. } else {
  49. val = newVal
  50. }
  51. childOb = !shallow && observe(newVal)
  52. dep.notify()
  53. }
  54. })
  55. }

终于看到了传说中的 getter/setter,上面是完整的代码,有些长,按照惯例我们分别进行讲解。

  1. const dep = new Dep()
  2. const property = Object.getOwnPropertyDescriptor(obj, key)
  3. if (property && property.configurable === false) {
  4. return
  5. }
  6. // cater for pre-defined getter/setters
  7. const getter = property && property.get
  8. if (!getter && arguments.length === 2) {
  9. val = obj[key]
  10. }
  11. const setter = property && property.set

这段代码中,第一步是创建了一个 dep 来收集对当前 obj.key 的依赖,这里可能大家又会问:之前 new Observer 的时候不是已经创建了吗,这里怎么又创建一次?这是一个深度依赖的问题,为了回答这个问题我们还得先往下看代码。

dep 之后是获取了getter/setter ,比较简单,我们再往下看:

  1. let childOb = !shallow && observe(val)

这一段代码非常重要,如果 val 是一个对象,那么我们要递归进行监听。也就是又回到了 new Observer 中,可以知道,childOb 返回的是一个 observer 实例。有了这个对孩子的监听器之后,当孩子改变的时候我们就能知道了。让我们继续往下看最重要的一段代码getter

  1. Object.defineProperty(obj, key, {
  2. enumerable: true,
  3. configurable: true,
  4. get: function reactiveGetter () {
  5. const value = getter ? getter.call(obj) : val
  6. if (Dep.target) {
  7. dep.depend()
  8. if (childOb) {
  9. childOb.dep.depend()
  10. if (Array.isArray(value)) {
  11. dependArray(value)
  12. }
  13. }
  14. }
  15. return value
  16. },

首先,我们自定义的 getter 中,会把需要取出的值拿出来,通过原来的 getter。然后会判断 Dep.target 存在就进行一个 dep.depend() 操作,并且如果有孩子,也会对孩子进行 dep.depend() 操作。

dep.depend() 的代码如下:

  1. depend () {
  2. if (Dep.target) {
  3. Dep.target.addDep(this)
  4. }
  5. }

也就是把当前这个 dep 加入到 target 中。

那么这个 target 就非常重要了,他到底是什么呢?我们在 getData 的时候设置过 Dep.target ,但当时我们目的是清空,而不是设置一个值。所以这里我们依然不知道 target 是什么。代码看到当前位置其实是肯定无法理解 target 的作用的,没关系,我们可以带着这个疑问继续往下看。

但是这里我简单说明一下,这个target其实是一个 watcher,我们在获取一个数据的时候,比如 this.msg 并是不直接去 this._data.msg 上取,而是先创建一个watcher,然后通过 watcher.value来取,而watcher.value === msg.getter 所以在取值的时候,我们就知道 watcher 是依赖于当前的 dep 的,而 dep.depend() 相当于 watcher.deps.push(dep)

如果你面试的时候被问到 Vue的原理,那么有一个常见的考点是问你 Vue 是怎么收集依赖的,比如 computed 中有如下代码:

  1. info () {
  2. return this.name + ',' + this.age
  3. }

Vue 是如何知道 info 依赖 nameage 呢?是因为在第一次获取 info 的值的时候,会取 nameage 的值,因此就可以在他们的 getter 中记录依赖。当然由于我们现在还没有看 Watcher 的代码,所以这一块并不能理解的很透彻,没关系,让我们暂且继续往下看。这里只要记住** Vue 在第一次取值的时候收集依赖 就行了**。

再看看 setter 函数,我删除了部分不影响整体逻辑的代码:

  1. set: function reactiveSetter (newVal) {
  2. const value = getter ? getter.call(obj) : val
  3. if (setter) {
  4. setter.call(obj, newVal)
  5. } else {
  6. val = newVal
  7. }
  8. childOb = !shallow && observe(newVal)
  9. dep.notify()
  10. }

抛开一些异常情况的处理,主要代码其实做了两件事,第一件事是设置值,不过这里的 setter 是什么呢?其实是我们自定义的 setter,如果我们有自定义,那么就调用我们的 setter,否则就直接设置。

然后如果发现我们设置的新值是一个对象,那么就递归监听这个对象。

最后,通过 dep.notify 来通知响应的 target 们,我更新啦。

还记得上面我们留了一个深度依赖的问题吗?我们举个栗子说明,假设我们的 data 是这样的:

  1. data: {
  2. people: {
  3. name: '123'
  4. }
  5. }

我们对 people 进行 defineReactive 的时候,我们当然可以处理 this.people={} 的操作。但是如果我进行了 this.people.name='xx' 的操作的时候要怎么办呢?显然我们此时是无法检测到这个更新的。所以我们会创建对 {name:123} 再创建一个 childObj ,然后我们的 target 也依赖于这个孩子,就能检测到他的更新了。

到这里我们就讲完 Observer 了,总结一下,Observer就是通过 getter/setter 监听数据读写,在 getter 中记录依赖, 在 setter 中通知哪些依赖们。让我们把之前的一张图完善下,变成这样:

observer 2

下一章 我们看看 什么是 Watcher

下一章:Vue2.x源码解析系列五:数据响应之Watcher