资讯专栏INFORMATION COLUMN

vue源码分析系列之响应式数据(三)

shenhualong / 1690人阅读

摘要:并在内执行了函数,在函数内部,访问了。至此知道了它依赖于。需要根据最新的计算。本例中收集到了依赖并且也被告知观察了他们。文章链接源码分析系列源码分析系列之环境搭建源码分析系列之入口文件分析源码分析系列之响应式数据一源码分析系列之响应式数据二

前言

上一节着重讲述了initData中的代码,以及数据是如何从data中到视图层的,以及data修改后如何作用于视图。这一节主要记录initComputed中的内容。

正文 前情回顾

在demo示例中,我们定义了一个计算属性。

</>复制代码

  1. computed:{
  2. total(){
  3. return this.a + this.b
  4. }
  5. }

本章节我们继续探究这个计算属性的相关流程。

initComputed

</>复制代码

  1. // initComputed(vm, opts.computed)
  2. function initComputed (vm: Component, computed: Object) {
  3. // 定义计算属性相关的watchers.
  4. const watchers = vm._computedWatchers = Object.create(null)
  5. // 是否是服务端渲染,这里赞不考虑。
  6. const isSSR = isServerRendering()
  7. for (const key in computed) {
  8. // 获得用户定义的计算属性中的item,通常是一个方法
  9. // 在示例程序中,仅有一个key为total的计算a+b的方法。
  10. const userDef = computed[key]
  11. const getter = typeof userDef === "function" ? userDef : userDef.get
  12. if (process.env.NODE_ENV !== "production" && getter == null) {
  13. warn(
  14. `Getter is missing for computed property "${key}".`,
  15. vm
  16. )
  17. }
  18. if (!isSSR) {
  19. // create internal watcher for the computed property.
  20. // 为计算属性创建一个内部的watcher。
  21. // 其中computedWatcherOptions的值为lazy,意味着这个wacther内部的value,先不用计算。
  22. // 只有在需要的情况下才计算,这里主要是在后期页面渲染中,生成虚拟dom的时候才会计算。
  23. // 这时候new Watcher只是走一遍watcher的构造函数,其内部value由于
  24. // lazy为true,先设置为了undefined.同时内部的dirty = lazy;
  25. watchers[key] = new Watcher(
  26. vm,
  27. getter || noop,
  28. noop,
  29. computedWatcherOptions // 上文定义过,值为{lazy: true}
  30. )
  31. }
  32. // component-defined computed properties are already defined on the
  33. // component prototype. We only need to define computed properties defined
  34. // at instantiation here.
  35. // 组件定义的属性只是定义在了组件上,这里只是把它翻译到实例中。即当前的vm对象。
  36. if (!(key in vm)) {
  37. // 将计算属性定义到实例中。
  38. defineComputed(vm, key, userDef)
  39. } else if (process.env.NODE_ENV !== "production") {
  40. if (key in vm.$data) {
  41. warn(`The computed property "${key}" is already defined in data.`, vm)
  42. } else if (vm.$options.props && key in vm.$options.props) {
  43. warn(`The computed property "${key}" is already defined as a prop.`, vm)
  44. }
  45. }
  46. }
  47. }
defineComputed

</>复制代码

  1. const sharedPropertyDefinition = {
  2. enumerable: true,
  3. configurable: true,
  4. get: noop,
  5. set: noop
  6. }
  7. // defineComputed(vm, key, userDef)
  8. export function defineComputed (
  9. target: any,
  10. key: string,
  11. userDef: Object | Function
  12. ) {
  13. // 是否需要缓存。即非服务端渲染需要缓存。
  14. // 由于本案例用的demo非服务端渲染,这里结果是true
  15. const shouldCache = !isServerRendering()
  16. if (typeof userDef === "function") {
  17. // userDef = total() {...}
  18. sharedPropertyDefinition.get = shouldCache
  19. // 根据key创建计算属性的getter
  20. ? createComputedGetter(key)
  21. : userDef
  22. // 计算属性是只读的,所以设置setter为noop.
  23. sharedPropertyDefinition.set = noop
  24. } else {
  25. sharedPropertyDefinition.get = userDef.get
  26. ? shouldCache && userDef.cache !== false
  27. ? createComputedGetter(key)
  28. : userDef.get
  29. : noop
  30. sharedPropertyDefinition.set = userDef.set
  31. ? userDef.set
  32. : noop
  33. }
  34. // 计算属性是只读的,所以设置值得时候需要报错提示
  35. if (process.env.NODE_ENV !== "production" &&
  36. sharedPropertyDefinition.set === noop) {
  37. sharedPropertyDefinition.set = function () {
  38. warn(
  39. `Computed property "${key}" was assigned to but it has no setter.`,
  40. this
  41. )
  42. }
  43. }
  44. // 将组件属性-》实例属性,关键的一句,设置属性描述符
  45. Object.defineProperty(target, key, sharedPropertyDefinition)
  46. }
createComputedGetter

</>复制代码

  1. // 根据key创建计算属性的getter
  2. // createComputedGetter(key)
  3. function createComputedGetter (key) {
  4. return function computedGetter () {
  5. // 非服务端渲染的时候,在上述的initComputed中定义了vm._computedWatchers = {},并根据组件中的设定watchers[key] = new Watcher(..),这里只是根据key取出了当时new的watcher
  6. const watcher = this._computedWatchers && this._computedWatchers[key]
  7. if (watcher) {
  8. // watcher.dirty表示这个值是脏值,过期了。所以需要重新计算。
  9. // new Watcher的时候,这个total的watcher中,内部的dirty已经被置为
  10. // dirty = lazy = true;
  11. // 那么这个值什么时候会过期,会脏呢。就是内部的依赖更新时候,
  12. // 比如我们的total依赖于this.a,this.b,当着两个值任意一个变化时候
  13. // 我们的total就已经脏了。需要根据最新的a,b计算。
  14. if (watcher.dirty) {
  15. // 计算watcher中的值,即value属性.
  16. watcher.evaluate()
  17. }
  18. // 将依赖添加到watcher中。
  19. if (Dep.target) {
  20. watcher.depend()
  21. }
  22. // getter的结果就是返回getter中的值。
  23. return watcher.value
  24. }
  25. }
  26. }
initComputed小结

继initComputed之后,所有组件中的computed都被赋值到了vm实例的属性上,并设置好了getter和setter。在非服务端渲染的情况下,getter会缓存计算结果。并在需要的时候,才计算。setter则是一个什么都不做的函数,预示着计算属性只能被get,不能被set。即只读的。

接下来的问题就是:

这个计算属性什么时候会计算,前文{lazy:true}预示着当时new Watcher得到的值是undefined。还没开始计算。

计算属性是怎么知道它本身依赖于哪些属性的。以便知道其什么时候更新。

vue官方文档的缓存计算结果怎么理解。

接下来我们继续剖析后面的代码。解决这里提到的三个问题。

用来生成vnode的render函数

下次再见到这个计算属性total的时候,已是在根据el选项或者template模板中,生成的render函数,render函数上一小节也提到过。长这个样子。

</>复制代码

  1. (function anonymous() {
  2. with (this) {
  3. return _c("div", {
  4. attrs: {
  5. "id": "demo"
  6. }
  7. }, [_c("div", [_c("p", [_v("a:" + _s(a))]), _v(" "), _c("p", [_v("b: " + _s(b))]), _v(" "), _c("p", [_v("a+b: " + _s(total))]), _v(" "), _c("button", {
  8. on: {
  9. "click": addA
  10. }
  11. }, [_v("a+1")])])])
  12. }
  13. }
  14. )

这里可以结合一下我们的html,看出一些特点。

</>复制代码

  1. a:{{a}}

  2. b: {{b}}

  3. a+b: {{total}}

这里使用到计算属性的主要是这一句

</>复制代码

  1. _v("a+b: " + _s(total))

那么对于我们来说的关键就是_s(total)。由于这个函数的with(this)中,this被设置为vm实例,所以这里就可以理解为_s(vm.total)。那么这里就会触发之前定义的sharedPropertyDefinition.get

</>复制代码

  1. -> initComputed()
  2. -> defineComputed()
  3. -> Object.defineProperty(target, key, sharedPropertyDefinition)

也就是createComputedGetter返回的函数中的内容,也就是:

watcher细说

</>复制代码

  1. const watcher = this._computedWatchers && this._computedWatchers[key]
  2. if (watcher) {
  3. // 由于初始化的时候这个dirty为true,所以会进行watcher.evaluate()的计算。
  4. if (watcher.dirty) {
  5. watcher.evaluate()
  6. }
  7. if (Dep.target) {
  8. watcher.depend()
  9. }
  10. // getter的结果就是返回getter中的值。
  11. return watcher.value
  12. }

这里我们看下watcher.evaluate的部分。

</>复制代码

  1. // class Watcher内部
  2. /**
  3. * Evaluate the value of the watcher.
  4. * This only gets called for lazy watchers.
  5. */
  6. evaluate () {
  7. this.value = this.get()
  8. this.dirty = false
  9. }

这里this.get即得到了value的值,这就是第一个问题的答案。
1.计算属性何时会计算。
即用到的时候会计算,精确的说,就是在计算vnode的时候会用到它,从而计算它。
对于第二个问题,计算属性是怎么知道它本身依赖于哪些属性的?则是在这个
this.get内。

</>复制代码

  1. // Dep相关逻辑,Dep Class用来收集依赖某个值的watcher
  2. Dep.target = null
  3. const targetStack = []
  4. export function pushTarget (_target: Watcher) {
  5. if (Dep.target) targetStack.push(Dep.target)
  6. Dep.target = _target
  7. }
  8. export function popTarget () {
  9. Dep.target = targetStack.pop()
  10. }
  11. // Watcher class 相关逻辑
  12. get () {
  13. // 将当前的watcher推到Dep.target中
  14. pushTarget(this)
  15. let value
  16. const vm = this.vm
  17. try {
  18. // 这里的getter实际上就是对应total的函数体,
  19. // 而这个函数体内藏有很大的猫腻,接下来我们仔细分析这一段。
  20. value = this.getter.call(vm, vm)
  21. } catch (e) {
  22. if (this.user) {
  23. handleError(e, vm, `getter for watcher "${this.expression}"`)
  24. } else {
  25. throw e
  26. }
  27. } finally {
  28. // "touch" every property so they are all tracked as
  29. // dependencies for deep watching
  30. if (this.deep) {
  31. traverse(value)
  32. }
  33. popTarget()
  34. this.cleanupDeps()
  35. }
  36. return value
  37. }

当代码执行到this.getter.call,实际上执行的是计算属性的函数,也就是
total() { return this.a + this.b};当代码执行到this.a时候。就会触发上一节我们所讲的defineReactive内部的代码。

</>复制代码

  1. //// 这里我们以访问this.a为例
  2. export function defineReactive (
  3. obj: Object, // {a:1,b:1}
  4. key: string, // "a"
  5. val: any, // 1
  6. customSetter?: ?Function,
  7. shallow?: boolean
  8. ) {
  9. const dep = new Dep()
  10. const property = Object.getOwnPropertyDescriptor(obj, key)
  11. if (property && property.configurable === false) {
  12. return
  13. }
  14. // cater for pre-defined getter/setters
  15. const getter = property && property.get
  16. const setter = property && property.set
  17. let childOb = !shallow && observe(val)
  18. Object.defineProperty(obj, key, {
  19. enumerable: true,
  20. configurable: true,
  21. get: function reactiveGetter () {
  22. const value = getter ? getter.call(obj) : val
  23. // this.a会触发这里的代码。首先获得value,
  24. // 由于watcher内部this.get执行total计算属性时候,已经将
  25. // total的watcher设置为Dep.target
  26. if (Dep.target) {
  27. // 所以这里开始收集依赖。
  28. dep.depend()
  29. if (childOb) {
  30. childOb.dep.depend()
  31. if (Array.isArray(value)) {
  32. dependArray(value)
  33. }
  34. }
  35. }
  36. return value
  37. },
  38. set: function reactiveSetter (newVal) {
  39. const value = getter ? getter.call(obj) : val
  40. /* eslint-disable no-self-compare */
  41. if (newVal === value || (newVal !== newVal && value !== value)) {
  42. return
  43. }
  44. /* eslint-enable no-self-compare */
  45. if (process.env.NODE_ENV !== "production" && customSetter) {
  46. customSetter()
  47. }
  48. if (setter) {
  49. setter.call(obj, newVal)
  50. } else {
  51. val = newVal
  52. }
  53. childOb = !shallow && observe(newVal)
  54. dep.notify()
  55. }
  56. })
  57. }

上述代码中,this.a触发了dep.depend()。我们细看这里的代码。

</>复制代码

  1. class Dep {
  2. //省略代码...
  3. depend () {
  4. // 由于这里的Dep.target此时对应的是total的watcher。
  5. // 而这里的this.是指定义this.a时,生成的dep。
  6. // 所以这里是告诉total依赖于this.a
  7. if (Dep.target) {
  8. // 通过调用addDep.让total的watcher知道total依赖this.a
  9. Dep.target.addDep(this)
  10. }
  11. }
  12. }
  13. class Watcher {
  14. // ...省略代码
  15. addDep (dep: Dep) {
  16. // 此时的this是total的watcher
  17. const id = dep.id
  18. // 防止重复收集
  19. if (!this.newDepIds.has(id)) {
  20. // 将依赖的可观察对象记录。
  21. this.newDepIds.add(id)
  22. this.newDeps.push(dep)
  23. // 如果这个可观察对象没有记录当前watcher,
  24. if (!this.depIds.has(id)) {
  25. // 则将当前的watcher加入到可观察对象中
  26. // (方便后续a变化后,告知total)
  27. dep.addSub(this)
  28. }
  29. }
  30. }
  31. }

至此,上述的第二个问题,计算属性是怎么知道它本身依赖于哪些属性的?也有了答案。就是当生成虚拟dom的时候,用到了total,由于得到total值的watcher是脏的,需要计算一次,然后就将Dep.target的watcher设为total相关的watcher。并在watcher内执行了total函数,在函数内部,访问了this.a。this.a的getter中,通过dep.depend(),将this.a的getter上方的dep,加入到total的watcher.dep中,再通过watcher中的dep.addSub(this),将total的watcher加入到了this.a的getter上方中的dep中。至此total知道了它依赖于this.a。this.a也知道了,total需要this.a。

当计算属性的依赖变更时发生了什么

当点击页面按钮的时候,会执行我们案例中绑定的this.a += 1的代码。此时会走
this.a的setter函数。我们看看setter中所做的事情。

</>复制代码

  1. set: function reactiveSetter (newVal) {
  2. const value = getter ? getter.call(obj) : val
  3. // 如果旧值与新值相当,什么都不做。直接返回。
  4. if (newVal === value || (newVal !== newVal && value !== value)) {
  5. return
  6. }
  7. // 无关代码,pass
  8. if (process.env.NODE_ENV !== "production" && customSetter) {
  9. customSetter()
  10. }
  11. // 有定义过setter的话通过setter设置新值
  12. if (setter) {
  13. setter.call(obj, newVal)
  14. } else {
  15. // 否则的话直接设置新值
  16. val = newVal
  17. }
  18. // 考虑新值是对象的情况。
  19. childOb = !shallow && observe(newVal)
  20. // 通知观察了this.a的观察者。
  21. // 这里实际上是有两个观察a的观察者
  22. // 一个是上一篇讲的updateComponent。
  23. // 一个是这节讲的total。
  24. dep.notify()
  25. }

这里我们看看dep.notify干了什么

</>复制代码

  1. class Dep {
  2. // **** 其他代码
  3. notify () {
  4. // 这里的subs其实就是上述的两个watcher。
  5. // 分别执行watcher的update
  6. const subs = this.subs.slice()
  7. for (let i = 0, l = subs.length; i < l; i++) {
  8. subs[i].update()
  9. }
  10. }
  11. }
  12. class Watcher{
  13. update () {
  14. // 第一个watcher,即关于updateComponent的。
  15. // 会执行queueWatcher。也就是会将处理放到等待队列里
  16. // 等待队列中,而第二个watcher由于lazy为true
  17. // 所以只是将watcher标记为dirty。
  18. // 由于队列这个比较复杂,所以单开话题去讲
  19. // 这里我们只需要知道它是一个异步的队列,最后结果就是
  20. // 挨个执行队列中watcher的run方法。
  21. if (this.lazy) {
  22. this.dirty = true
  23. } else if (this.sync) {
  24. this.run()
  25. } else {
  26. queueWatcher(this)
  27. }
  28. }
  29. run () {
  30. if (this.active) {
  31. const value = this.get()
  32. if (
  33. value !== this.value ||
  34. // Deep watchers and watchers on Object/Arrays should fire even
  35. // when the value is the same, because the value may
  36. // have mutated.
  37. isObject(value) ||
  38. this.deep
  39. ) {
  40. // set new value
  41. const oldValue = this.value
  42. this.value = value
  43. if (this.user) {
  44. try {
  45. this.cb.call(this.vm, value, oldValue)
  46. } catch (e) {
  47. handleError(e, this.vm, `callback for watcher "${this.expression}"`)
  48. }
  49. } else {
  50. this.cb.call(this.vm, value, oldValue)
  51. }
  52. }
  53. }
  54. }
  55. }

当触发了依赖更新时候,第一个watcher(关于total的)会将自己的dirty标记为true,第二个则会执行run方法,在其中运行this.get导致updateComponent执行,进而再次计算vnode,这时会再次计算this.total。则会再次触发total的getter,这时候我们再复习一下之前讲过的这个computed的getter:

</>复制代码

  1. const watcher = this._computedWatchers && this._computedWatchers[key]
  2. if (watcher) {
  3. // watcher.dirty表示这个值是脏值,过期了。所以需要重新计算。
  4. // new Watcher的时候,这个total的watcher中,内部的dirty已经被置为
  5. // dirty = lazy = true;
  6. // 那么这个值什么时候会过期,会脏呢。就是内部的依赖更新时候,
  7. // 比如我们的total依赖于this.a,this.b,当着两个值任意一个变化时候
  8. // 我们的total就已经脏了。需要根据最新的a,b计算。
  9. if (watcher.dirty) {
  10. // 计算watcher中的值,即value属性.
  11. watcher.evaluate()
  12. }
  13. // 将依赖添加到watcher中。
  14. if (Dep.target) {
  15. watcher.depend()
  16. }
  17. // getter的结果就是返回getter中的值。
  18. return watcher.value
  19. }

至此,computed中total的更新流程也结束了。
所以我们的第3个问题,vue官方文档的缓存计算结果怎么理解?也就有了答案。也就是说计算属性只有其依赖变更的时候才会去计算,依赖不更新的时候,是不会计算的。正文这一小节提到的,total的更新是由于this.a的更新导致其setter被触发,因此通知了其依赖,即total这个watcher。如果total的不依赖于this.a,则total相关的watcher的dirty就不会变为true,也就不会再次计算了。

总结

本章节我们以示例程序探究了计算属性,从initComputed中,计算属性的初始化到计算属性的变更,对着代码做了进一步的解释。整体流程可以归纳为:

initComputed定义了相关的计算属性相关的watcher,以及watcher的getter。
在第一次计算vnode的时候顺便执行了计算属性的计算逻辑,顺便收集了依赖。本例中total收集到了依赖a,b;并且a,b也被告知total观察了他们。当a,b任何一个改变时的时候,就会将total相关的watcher.dirty设置为true,下次需要更新界面时,计算属性就会被重新计算。当然,如果没有依赖于total的地方。那么total是不会计算的,例如total根本没被界面或者js代码用到,就不会计算total;如果total所有的依赖没有变更,其dirty为false,则也是无需计算的。

文章链接

vue源码分析系列

vue源码分析系列之debug环境搭建

vue源码分析系列之入口文件分析

vue源码分析系列之响应式数据(一)

vue源码分析系列之响应式数据(二)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101737.html

相关文章

  • vue源码分析系列响应数据(四)

    摘要:执行当时传入的回调,并将新值与旧值一并传入。文章链接源码分析系列源码分析系列之环境搭建源码分析系列之入口文件分析源码分析系列之响应式数据一源码分析系列之响应式数据二源码分析系列之响应式数据三 前言 上一节着重讲述了initComputed中的代码,以及数据是如何从computed中到视图层的,以及data修改后如何作用于computed。这一节主要记录initWatcher中的内容。 ...

    GHOST_349178 评论0 收藏0
  • vue源码分析系列响应数据(一)

    摘要:代码初始化部分一个的时候做了什么当我们一个时,实际上执行了的构造函数,这个构造函数内部挂载了很多方法,可以在我的上一篇文章中看到。合并构造函数上挂载的与当前传入的非生产环境,包装实例本身,在后期渲染时候,做一些校验提示输出。 概述 在使用vue的时候,data,computed,watch是一些经常用到的概念,那么他们是怎么实现的呢,让我们从一个小demo开始分析一下它的流程。 dem...

    liujs 评论0 收藏0
  • vue源码分析系列入口文件分析

    摘要:中引入了中的中引入了中的中,定义了的构造函数中的原型上挂载了方法,用来做初始化原型上挂载的属性描述符,返回原型上挂载的属性描述符返回原型上挂载与方法,用来为对象新增删除响应式属性原型上挂载方法原型上挂载事件相关的方法。 入口寻找 入口platforms/web/entry-runtime-with-compiler中import了./runtime/index导出的vue。 ./r...

    kgbook 评论0 收藏0
  • vue源码分析系列入debug环境搭建

    摘要:目标是为了可以调试版本的,也就是下的源码,所以主要是的开启。结语至此就可以开心的研究源码啦。文章链接源码分析系列源码分析系列之入口文件分析源码分析系列之响应式数据一源码分析系列之响应式数据二 概述 为了探究vue的本质,所以想debug一下源码,但是怎么开始是个问题,于是有了这样一篇记录。目标是为了可以调试es6版本的,也就是src下的源码,所以主要是sourceMap的开启。原文来自...

    nihao 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<