Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
lihongxun945 opened this issue Jul 27, 2018 · 7 comments
Open

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

lihongxun945 opened this issue Jul 27, 2018 · 7 comments

Comments

@lihongxun945
Copy link
Owner

lihongxun945 commented Jul 27, 2018

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

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

从data开始

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

core/instance/state.js

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 还涉及到如何从模板中解析,而另外两个其实是函数。

让我们先看一下 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

@eltonchan
Copy link

我们在获取一个数据的时候,比如 this.msg 并是不直接去 this._data.msg 上取,而是先创建一个watcher,然后通过 watcher.value来取 ------ 这个是针对computed来说的。 data 上面声明的属性并不是取watcher.value, 事实上所有data 声明的属性都会关联初始化声明的那个watcher 用来渲染视图的, watcher.value 是undefined

@lihongxun945
Copy link
Owner Author

@eltonchan 你的说法非常正确,在 computed 中因为存在依赖关系,所以需要通过 watcher 来监听数据的更新。如果是在模板中通过 {{msg}} 来使用,其实编译出来的render 函数就是直接用了 msg 并不需要创建 watcher, 数据的响应是通过一个统一的 watcher 来更新VDOM实现的。

@xiake-1024
Copy link

const getter = property && property.get
const setter = property && property.set
//有getter 无setter val=undefined 不是太理解??????
//无getter 有setter
//无getter 无setter
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}

let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {

大佬 看源码 有疑问 请问 if ((!getter || setter) && arguments.length === 2) { 这个怎么比较细致的理解的?有点困惑 不胜感激!!谢谢

@cw84973570
Copy link

const getter = property && property.get
const setter = property && property.set
//有getter 无setter val=undefined 不是太理解??????
//无getter 有setter
//无getter 无setter
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}

let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {

大佬 看源码 有疑问 请问 if ((!getter || setter) && arguments.length === 2) { 这个怎么比较细致的理解的?有点困惑 不胜感激!!谢谢

有getter没setter说明是只读属性,在get方法中通过getter获取value的值,set中也是一样。至于依赖收集之类的功能也不需要了。具体看这句:if (getter && !setter) return,后面的dep.notify()不会触发,也就是说只读属性不是响应式的。val只在这两个地方有用到,所以只读属性就不需要获取val的值了。

以上是个人的理解。

相关的issue:

vuejs/vue#9203

vuejs/vue#7981

@cw84973570
Copy link

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

this.msg取的就是data上的值吧?

  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

这个val就是存的属性的值,至于watch里面的value,只是回调的时候传参的时候用到。

watch: {
  msg (newVal, oldVal) {
    // oldVal就是watch上保存的value
    console.log(newVal, oldVal)
  }

@YuJianghao
Copy link

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

最后一行的链接失效了。可能是手滑删了?应该是下面这个吧,博主修复一下?

https://github.com/lihongxun945/myblog/issues/27

@lihongxun945
Copy link
Owner Author

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

最后一行的链接失效了。可能是手滑删了?应该是下面这个吧,博主修复一下?

https://github.com/lihongxun945/myblog/issues/27

感谢指正,已更新

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants