Vue2.x源码解析系列四:数据响应之Observer

写在前面的话:关于做者言川

笔名言川, 前端工程师,精通 Vue/Webpack/Git等,熟悉Node/React等,涉猎普遍,对算法/后端/人工智能/linux等都有必定研究。开源爱好者,github上目前总计5000+ Starhtml

  • 个人github主页:https://github.com/lihongxun945
  • 个人博客地址:https://github.com/lihongxun945/myblog
  • 个人掘金主页:https://juejin.im/user/5756771b1532bc0064a2b024/posts
  • 个人知乎专栏:https://zhuanlan.zhihu.com/c_1007281871281090560

此博客原地址:https://github.com/lihongxun945/myblog/issues/25前端

若是你以前看过个人这一篇文章 Vue1.0源码解析系列:实现数据响应化 ,那么你能够很轻松看懂 Vue2.x版本中的响应化,由于基本思路以及大部分代码其实都没有变化。固然没看过也不要紧,不用去看,由于这里我会讲的很是详细。vue

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

从data开始

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

core/instance/state.jsgit

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
复制代码

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

让咱们先看一下 initData 的完整代码:面试

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}
复制代码

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

if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
复制代码

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

if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
复制代码

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

if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    }
复制代码

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

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
  }
  // proxy data on instance
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}
复制代码

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

let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
  }
复制代码

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

这里你们会有另外一个疑问了,为何不是直接调用函数得到返回值,而是须要一个 getData 呢,它除了调用函数确定还作了别的事,让咱们看看 getData 的源码:

export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}
复制代码

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

Dep.target = null
const targetStack = []

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

export function popTarget () {
  Dep.target = targetStack.pop()
}
复制代码

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

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

const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
复制代码

就是遍历了 data 的key,而后作了一个 proxy,咱们来看 proxy 的代码:

function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  };
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}
复制代码

这里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

弄懂了这个以后咱们再看最后一段代码:

observe(data, true /* asRootData */)
复制代码

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

深刻 Observer

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

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
复制代码

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

export function observe (value: any, asRootData: ?boolean): Observer | void {
  ob = new Observer(value)
  return ob
}
复制代码

能够看到主要逻辑就是建立了一个 Observer 实例,那么咱们再看看 Observer 的代码:

export class Observer {
  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
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      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)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /** * Observe a list of Array items. */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
复制代码

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

constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
复制代码

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

this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
复制代码

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

  • this.dep 是 明显是记录依赖的,记录的是对这个value 的依赖,咱们在下面立刻就能看到怎么记录和使用的
  • __ob__ 实际上是把本身记录一下,避免重复建立
if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
复制代码

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

observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
}
复制代码

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

const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
复制代码

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

好,让咱们回到 walk 函数:

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

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

export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) {
  const dep = new Dep()

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

  // cater for pre-defined getter/setters
  const getter = property && property.get
  if (!getter && arguments.length === 2) {
    val = obj[key]
  }
  const setter = property && property.set

  let childOb = !shallow && observe(val)
  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) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
复制代码

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

const dep = new Dep()

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

  // cater for pre-defined getter/setters
  const getter = property && property.get
  if (!getter && arguments.length === 2) {
    val = obj[key]
  }
  const setter = property && property.set
复制代码

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

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

let childOb = !shallow && observe(val)
复制代码

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

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
    },
复制代码

首先,咱们自定义的 getter 中,会把须要取出的值拿出来,经过原来的 getter。而后会判断 Dep.target 存在就进行一个 dep.depend() 操做,而且若是有孩子,也会对孩子进行 dep.depend() 操做。

dep.depend() 的代码以下:

depend () { 
  if (Dep.target) { 
    Dep.target.addDep(this) 
  } 
} 
复制代码

也就是把当前这个 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 中有以下代码:

info () {
  return this.name + ',' + this.age
}
复制代码

Vue 是如何知道 info 依赖 nameage 呢?是由于在第一次获取 info 的值的时候,会取 nameage 的值,所以就能够在他们的 getter 中记录依赖。固然因为咱们如今尚未看 Watcher 的代码,因此这一块并不能理解的很透彻,不要紧,让咱们暂且继续往下看。这里只要记住** Vue 在第一次取值的时候收集依赖 就好了**。

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

set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
复制代码

抛开一些异常状况的处理,主要代码其实作了两件事,第一件事是设置值,不过这里的 setter 是什么呢?实际上是咱们自定义的 setter,若是咱们有自定义,那么就调用咱们的 setter,不然就直接设置。

而后若是发现咱们设置的新值是一个对象,那么就递归监听这个对象。

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

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

data: {
  people: {
    name: '123'
  }
}
复制代码

咱们对 people 进行 defineReactive 的时候,咱们固然能够处理 this.people={} 的操做。可是若是我进行了 this.people.name='xx' 的操做的时候要怎么办呢?显然咱们此时是没法检测到这个更新的。因此咱们会建立对 {name:123} 再建立一个 childObj ,而后咱们的 target 也依赖于这个孩子,就能检测到他的更新了。

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

observer 2

下一章 咱们看看 什么是 Watcher

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

相关文章
相关标签/搜索