前言

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

正文

前情回顾

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

computed:{total(){return this.a + this.b}
}

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

initComputed

// initComputed(vm, opts.computed)
function initComputed (vm: Component, computed: Object) {// 定义计算属性相关的watchers.const watchers = vm._computedWatchers = Object.create(null)// 是否是服务端渲染,这里赞不考虑。const isSSR = isServerRendering()for (const key in computed) {// 获得用户定义的计算属性中的item,通常是一个方法// 在示例程序中,仅有一个key为total的计算a+b的方法。const userDef = computed[key]const getter = typeof userDef === 'function' ? userDef : userDef.getif (process.env.NODE_ENV !== 'production' && getter == null) {warn(`Getter is missing for computed property "${key}".`,vm)}if (!isSSR) {// create internal watcher for the computed property.// 为计算属性创建一个内部的watcher。// 其中computedWatcherOptions的值为lazy,意味着这个wacther内部的value,先不用计算。// 只有在需要的情况下才计算,这里主要是在后期页面渲染中,生成虚拟dom的时候才会计算。// 这时候new Watcher只是走一遍watcher的构造函数,其内部value由于// lazy为true,先设置为了undefined.同时内部的dirty = lazy;watchers[key] = new Watcher(vm,getter || noop,noop,computedWatcherOptions // 上文定义过,值为{lazy: true})}// component-defined computed properties are already defined on the// component prototype. We only need to define computed properties defined// at instantiation here.// 组件定义的属性只是定义在了组件上,这里只是把它翻译到实例中。即当前的vm对象。if (!(key in vm)) {// 将计算属性定义到实例中。defineComputed(vm, key, userDef)} else if (process.env.NODE_ENV !== 'production') {if (key in vm.$data) {warn(`The computed property "${key}" is already defined in data.`, vm)} else if (vm.$options.props && key in vm.$options.props) {warn(`The computed property "${key}" is already defined as a prop.`, vm)}}}
}

defineComputed

const sharedPropertyDefinition = {enumerable: true,configurable: true,get: noop,set: noop
}
// defineComputed(vm, key, userDef)
export function defineComputed (target: any,key: string,userDef: Object | Function
) {// 是否需要缓存。即非服务端渲染需要缓存。// 由于本案例用的demo非服务端渲染,这里结果是trueconst shouldCache = !isServerRendering()if (typeof userDef === 'function') {// userDef = total() {...}sharedPropertyDefinition.get = shouldCache// 根据key创建计算属性的getter? createComputedGetter(key): userDef// 计算属性是只读的,所以设置setter为noop.sharedPropertyDefinition.set = noop} else {sharedPropertyDefinition.get = userDef.get? shouldCache && userDef.cache !== false? createComputedGetter(key): userDef.get: noopsharedPropertyDefinition.set = userDef.set? userDef.set: noop}// 计算属性是只读的,所以设置值得时候需要报错提示if (process.env.NODE_ENV !== 'production' &&sharedPropertyDefinition.set === noop) {sharedPropertyDefinition.set = function () {warn(`Computed property "${key}" was assigned to but it has no setter.`,this)}}// 将组件属性-》实例属性,关键的一句,设置属性描述符Object.defineProperty(target, key, sharedPropertyDefinition)
}

createComputedGetter

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

initComputed小结

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

接下来的问题就是:

  1. 这个计算属性什么时候会计算,前文{lazy:true}预示着当时new Watcher得到的值是undefined。还没开始计算。
  2. 计算属性是怎么知道它本身依赖于哪些属性的。以便知道其什么时候更新。
  3. vue官方文档的缓存计算结果怎么理解。

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

用来生成vnode的render函数

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

(function anonymous() {with (this) {return _c('div', {attrs: {"id": "demo"}}, [_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', {on: {"click": addA}}, [_v("a+1")])])])}
}
)

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

<div id="demo"><div><p>a:{{a}}</p><p>b: {{b}}</p><p>a+b: {{total}}</p><button @click="addA">a+1</button></div>
</div>

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

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

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

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

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

watcher细说

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

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

// class Watcher内部
/*** Evaluate the value of the watcher.* This only gets called for lazy watchers.*/evaluate () {this.value = this.get()this.dirty = false}

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

// Dep相关逻辑,Dep Class用来收集依赖某个值的watcher
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()
}// Watcher class 相关逻辑
get () {// 将当前的watcher推到Dep.target中pushTarget(this)let valueconst vm = this.vmtry {// 这里的getter实际上就是对应total的函数体,// 而这个函数体内藏有很大的猫腻,接下来我们仔细分析这一段。value = this.getter.call(vm, vm)} catch (e) {if (this.user) {handleError(e, vm, `getter for watcher "${this.expression}"`)} else {throw e}} finally {// "touch" every property so they are all tracked as// dependencies for deep watchingif (this.deep) {traverse(value)}popTarget()this.cleanupDeps()}return value}

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

 这里我们以访问this.a为例
export function defineReactive (obj: Object,  // {a:1,b:1}key: string,  // 'a'val: any,     // 1customSetter?: ?Function,shallow?: boolean
) {const dep = new Dep()const property = Object.getOwnPropertyDescriptor(obj, key)if (property && property.configurable === false) {return}// cater for pre-defined getter/settersconst getter = property && property.getconst setter = property && property.setlet childOb = !shallow && observe(val)Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter () {const value = getter ? getter.call(obj) : val// this.a会触发这里的代码。首先获得value,// 由于watcher内部this.get执行total计算属性时候,已经将// total的watcher设置为Dep.targetif (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()}})
}

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

class Dep {//省略代码...depend () {// 由于这里的Dep.target此时对应的是total的watcher。// 而这里的this.是指定义this.a时,生成的dep。// 所以这里是告诉total依赖于this.aif (Dep.target) {// 通过调用addDep.让total的watcher知道total依赖this.aDep.target.addDep(this)}}
}class Watcher {// ...省略代码addDep (dep: Dep) {// 此时的this是total的watcherconst id = dep.id// 防止重复收集if (!this.newDepIds.has(id)) {// 将依赖的可观察对象记录。this.newDepIds.add(id)this.newDeps.push(dep)// 如果这个可观察对象没有记录当前watcher,if (!this.depIds.has(id)) {// 则将当前的watcher加入到可观察对象中// (方便后续a变化后,告知total)dep.addSub(this)}}}
}

至此,上述的第二个问题,计算属性是怎么知道它本身依赖于哪些属性的?也有了答案。就是当生成虚拟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中所做的事情。

set: function reactiveSetter (newVal) {const value = getter ? getter.call(obj) : val// 如果旧值与新值相当,什么都不做。直接返回。if (newVal === value || (newVal !== newVal && value !== value)) {return}// 无关代码,passif (process.env.NODE_ENV !== 'production' && customSetter) {customSetter()}// 有定义过setter的话通过setter设置新值if (setter) {setter.call(obj, newVal)} else {// 否则的话直接设置新值val = newVal}// 考虑新值是对象的情况。childOb = !shallow && observe(newVal)// 通知观察了this.a的观察者。// 这里实际上是有两个观察a的观察者// 一个是上一篇讲的updateComponent。// 一个是这节讲的total。dep.notify()
}

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

class Dep {// **** 其他代码notify () {// 这里的subs其实就是上述的两个watcher。// 分别执行watcher的updateconst subs = this.subs.slice()for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}}
}class Watcher{update () {// 第一个watcher,即关于updateComponent的。// 会执行queueWatcher。也就是会将处理放到等待队列里// 等待队列中,而第二个watcher由于lazy为true,// 所以只是将watcher标记为dirty。// 由于队列这个比较复杂,所以单开话题去讲// 这里我们只需要知道它是一个异步的队列,最后结果就是// 挨个执行队列中watcher的run方法。if (this.lazy) {this.dirty = true} else if (this.sync) {this.run()} else {queueWatcher(this)}}run () {if (this.active) {const value = this.get()if (value !== this.value ||// Deep watchers and watchers on Object/Arrays should fire even// when the value is the same, because the value may// have mutated.isObject(value) ||this.deep) {// set new valueconst oldValue = this.valuethis.value = valueif (this.user) {try {this.cb.call(this.vm, value, oldValue)} catch (e) {handleError(e, this.vm, `callback for watcher "${this.expression}"`)}} else {this.cb.call(this.vm, value, oldValue)}}}}
}

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

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

至此,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源码分析系列之响应式数据(二)

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

  1. vue 源码自问自答-响应式原理

    vue 源码自问自答-响应式原理 最近看了 Vue 源码和源码分析类的文章,感觉明白了很多,但是仔细想想却说不出个所以然. 所以打算把自己掌握的知识,试着组织成自己的语言表达出来 不打算平铺直叙的写清 ...

  2. vue源码分析系列二:$mount()和new Watcher()的执行过程

    续vue源码分析系列一:new Vue的初始化过程 在initMixin()里面调用了$mount() if (vm.$options.el) {vm.$mount(vm.$options.el);/ ...

  3. Spring源码分析系列——bean创建过程分析(三)——工厂方法创建bean

    前言 spring创建bean的方式 测试代码准备 createBeanInstance()方法分析 instantiateUsingFactoryMethod()方法分析 总结 spring创建be ...

  4. vue源码分析系列三:render的执行过程和Virtual DOM的产生

    render 手写 render 函数,仔细观察下面这段代码,试想一下这里的 createElement 参数是什么 . new Vue({el: '#application',render(crea ...

  5. vue源码分析系列一:new Vue的初始化过程

    import Vue from 'vue'(作者用的vue-cli一键生成) node环境下import Vue from 'vue'的作用是什么意思? 在 NPM 包的 dist/ 目录你将会找到很 ...

  6. Vue源码分析系列四:Virtual DOM

    前言 当我们操作Dom其实是一件非常耗性能的事,每个元素都涵盖了许多的属性,因为浏览器的标准就把 DOM 设计的非常复杂.而Virtual Dom就是用一个原生的JS对象去描述一个DOM节点,即VNo ...

  7. Vue源码解析系列——数据驱动篇:patch的执行过程

    准备 vue版本号2.6.12,为方便分析,选择了runtime+compiler版本. 回顾 如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:<Vue源码分析系列:目录> ...

  8. vue 计算属性_lt;Vue 源码笔记系列6gt;计算属性 computed 的实现

    1. 前言 原文发布在语雀: <Vue 源码笔记系列6>计算属性 computed 的实现 · 语雀​www.yuque.com 上一章我们已经学习过 watch,这一章就来看一下计算属性 ...

  9. Spring IOC 容器源码分析系列文章导读 1

    1. 简介 Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本.经过十几年的迭代,现在的 Spring 框架已经非常成熟了.Spring ...

  10. 菜鸟读jQuery 2.0.3 源码分析系列(1)

    原文链接在这里,作为一个菜鸟,我就一边读一边写 jQuery 2.0.3 源码分析系列 前面看着差不多了,看到下面一条(我是真菜鸟),推荐木有入门或者刚刚JS入门摸不着边的看看,大大们手下留情,想一起 ...

最新文章

  1. docker-compose 命令
  2. spring boot四:探究hello world
  3. 安装erlang没有bin文件夹_Centos7安装RabbitMQ(Centos6 此方案同样可行)
  4. 【LeetCode笔记】20.有效的括号(Java、栈) 21. 合并两个有序链表(Java)
  5. 开源运维管理软件排名_企业运维监控平台架构设计与实现(ganglia篇)
  6. 2008年信息安全服务市场发展报告
  7. java取负数_阿里巴巴 Java 开发手册之MySQL 规约
  8. excel通过转成xml格式模板,下载成excel文件
  9. 第二章:Improving On User Commands--14.格式化长句
  10. 顺序容器和关联容器添加新元素方法详解
  11. DDA算法、中点Bresenam算法,圆或椭圆的绘制
  12. 微波射频学习笔记5--------同轴线与射频接头
  13. 川崎机器人f控制柜接线图_Kawasaki川崎机器人控制柜维修
  14. 在纯Win10环境下部署DzzOffice+OnlyOffice协同办公系统
  15. 副本全攻略之哀号洞穴(超详细)
  16. 微信强制使用本机浏览器打开指定链接是怎么做的
  17. discuz数据字典
  18. 筛选状态下进行复制粘贴为数值
  19. 项目管理的五个典型工具
  20. 传统文本分类和基于深度学习文本分类

热门文章

  1. 判断数组是否为某二叉搜索树的后序遍历
  2. 利用数组构造MaxTree
  3. c:forEach无法显示信息的可能原因以及需要注意的地方
  4. 写“博客”页面踩过的坑
  5. 【生信进阶练习1000days】day3-Bioconductor annotation resources
  6. 【0x50 动态规划】Mobile Service【线性DP】
  7. Raki的读paper小记:Model Zoo: A Growing “Brain” That Learns Continually
  8. 使用threading+queue队列,发送get请求,输出状态码
  9. Python——Pycharm基本设置
  10. LeetCode Sparse Matrix Multiplication