虽然目前的技术栈已由Vue转到了React,但从以前使用Vue开发的多个项目实际经从来看仍是很是愉悦的,Vue文档清晰规范,api设计简洁高效,对前端开发人员友好,上手快,甚至我的认为在不少场景使用Vue比React开发效率更高,以前也有断断续续研读过Vue的源码,但一直没有梳理总结,因此在此作一些技术概括同时也加深本身对Vue的理解,那么今天要写的即是Vue中最经常使用到的API之一computed
的实现原理。前端
话很少说,一个最基本的例子以下:vue
<div id="app"> <p>{{fullName}}</p> </div>
new Vue({ data: { firstName: 'Xiao', lastName: 'Ming' }, computed: { fullName: function () { return this.firstName + ' ' + this.lastName } } })
Vue中咱们不须要在template里面直接计算{{this.firstName + ' ' + this.lastName}}
,由于在模版中放入太多声明式的逻辑会让模板自己太重,尤为当在页面中使用大量复杂的逻辑表达式处理数据时,会对页面的可维护性形成很大的影响,而computed
的设计初衷也正是用于解决此类问题。api
watch
固然不少时候咱们使用computed
时每每会与Vue中另外一个API也就是侦听器watch
相比较,由于在某些方面它们是一致的,都是以Vue的依赖追踪机制为基础,当某个依赖数据发生变化时,全部依赖这个数据的相关数据或函数都会自动发生变化或调用。数组
虽然计算属性在大多数状况下更合适,但有时也须要一个自定义的侦听器。这就是为何 Vue 经过
watch
选项提供了一个更通用的方法来响应数据的变化。当须要在数据变化时执行异步或开销较大的操做时,这个方式是最有用的。
从vue官方文档对watch
的解释咱们能够了解到,使用 watch
选项容许咱们执行异步操做 (访问一个API)或高消耗性能的操做,限制咱们执行该操做的频率,并在咱们获得最终结果前,设置中间状态,而这些都是计算属性没法作到的。缓存
computed
和watch
的差别:computed
是计算一个新的属性,并将该属性挂载到vm(Vue实例)上,而watch
是监听已经存在且已挂载到vm
上的数据,因此用watch
一样能够监听computed
计算属性的变化(其它还有data
、props
)computed
本质是一个惰性求值的观察者,具备缓存性,只有当依赖变化后,第一次访问 computed
属性,才会计算新的值,而watch
则是当数据发生变化便会调用执行函数computed
适用一个数据被多个数据影响,而watch
适用一个数据影响多个数据;以上咱们了解了computed
和watch
之间的一些差别和使用场景的区别,固然某些时候二者并无那么明确严格的限制,最后仍是要具体到不一样的业务进行分析。app
言归正传,回到文章的主题computed
身上,为了更深层次地了解计算属性的内在机制,接下来就让咱们一步步探索Vue源码中关于它的实现原理吧。异步
在分析computed
源码以前咱们先得对Vue的响应式系统有一个基本的了解,Vue称其为非侵入性的响应式系统,数据模型仅仅是普通的JavaScript对象,而当你修改它们时,视图便会进行自动更新。函数
当你把一个普通的 JavaScript 对象传给 Vue 实例的data
选项时,Vue 将遍历此对象全部的属性,并使用Object.defineProperty
把这些属性所有转为getter/setter
,这些getter/setter
对用户来讲是不可见的,可是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化,每一个组件实例都有相应的watcher
实例对象,它会在组件渲染的过程当中把属性记录为依赖,以后当依赖项的setter
被调用时,会通知watcher
从新计算,从而导致它关联的组件得以更新。
Vue响应系统,其核心有三点:observe
、watcher
、dep
:oop
observe
:遍历data
中的属性,使用 Object.defineProperty 的get/set
方法对其进行数据劫持dep
:每一个属性拥有本身的消息订阅器dep
,用于存放全部订阅了该属性的观察者对象watcher
:观察者(对象),经过dep
实现对响应属性的监听,监听到结果后,主动触发本身的回调进行响应对响应式系统有一个初步了解后,咱们再来分析计算属性。
首先咱们找到计算属性的初始化是在src/core/instance/state.js
文件中的initState
函数中完成的性能
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 */) } // computed初始化 if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }
调用了initComputed
函数(其先后也分别初始化了initData
和initWatch
)并传入两个参数vm
实例和opt.computed
开发者定义的computed
选项,转到initComputed
函数:
const computedWatcherOptions = { computed: true } function initComputed (vm: Component, computed: Object) { // $flow-disable-line const watchers = vm._computedWatchers = Object.create(null) // computed properties are just getters during SSR const isSSR = isServerRendering() for (const key in computed) { const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get if (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. watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) } // component-defined computed properties are already defined on the // component prototype. We only need to define computed properties defined // at instantiation here. 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) } } } }
从这段代码开始咱们观察这几部分:
获取计算属性的定义userDef
和getter
求值函数
const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get
定义一个计算属性有两种写法,一种是直接跟一个函数,另外一种是添加set
和get
方法的对象形式,因此这里首先获取计算属性的定义userDef
,再根据userDef
的类型获取相应的getter
求值函数。
计算属性的观察者watcher
和消息订阅器dep
watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions )
这里的watchers
也就是vm._computedWatchers
对象的引用,存放了每一个计算属性的观察者watcher
实例(注:后文中提到的“计算属性的观察者”、“订阅者”和watcher
均指代同一个意思但注意和Watcher
构造函数区分),Watcher
构造函数在实例化时传入了4个参数:vm
实例、getter
求值函数、noop
空函数、computedWatcherOptions
常量对象(在这里提供给Watcher
一个标识{computed:true}
项,代表这是一个计算属性而不是非计算属性的观察者,咱们来到Watcher
构造函数的定义:
class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { if (options) { this.computed = !!options.computed } if (this.computed) { this.value = undefined this.dep = new Dep() } else { this.value = this.get() } } get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { } finally { popTarget() } return value } update () { if (this.computed) { if (this.dep.subs.length === 0) { this.dirty = true } else { this.getAndInvoke(() => { this.dep.notify() }) } } else if (this.sync) { this.run() } else { queueWatcher(this) } } evaluate () { if (this.dirty) { this.value = this.get() this.dirty = false } return this.value } depend () { if (this.dep && Dep.target) { this.dep.depend() } } }
为了简洁突出重点,这里我手动去掉了咱们暂时不须要关心的代码片断。
观察Watcher
的constructor
,结合刚才讲到的new Watcher
传入的第四个参数{computed:true}
知道,对于计算属性而言watcher
会执行if
条件成立的代码this.dep = new Dep(),
而dep
也就是建立了该属性的消息订阅器。
export default class Dep { static target: ?Watcher; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } Dep.target = null
dep
一样精简了部分代码,咱们观察Watcher
和dep
的关系,用一句话总结
watcher
中实例化了dep
并向dep.subs
中添加了订阅者,dep
经过notify
遍历了dep.subs
通知每一个watcher
更新。
defineComputed
定义计算属性
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) } }
由于computed
属性是直接挂载到实例对象中的,因此在定义以前须要判断对象中是否已经存在重名的属性,defineComputed
传入了三个参数:vm
实例、计算属性的key
以及userDef
计算属性的定义(对象或函数)。
而后继续找到defineComputed
定义处:
export function defineComputed ( target: any, key: string, userDef: Object | Function ) { const shouldCache = !isServerRendering() if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : userDef sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : userDef.get : noop sharedPropertyDefinition.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) }
在这段代码的最后调用了原生Object.defineProperty
方法,其中传入的第三个参数是属性描述符sharedPropertyDefinition
,初始化为:
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }
随后根据Object.defineProperty
前面的代码能够看到sharedPropertyDefinition
的get/set
方法在通过userDef
和 shouldCache
等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinition
的get
函数也就是createComputedGetter(key)
的结果,咱们找到createComputedGetter
函数调用结果并最终改写sharedPropertyDefinition
大体呈现以下:
sharedPropertyDefinition = { enumerable: true, configurable: true, get: function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { watcher.depend() return watcher.evaluate() } }, set: userDef.set || noop }
当计算属性被调用时便会执行get
访问函数,从而关联上观察者对象watcher
。
分析完以上步骤,咱们再来梳理下整个流程:
computed
和data
会分别创建各自的响应系统,Observer
遍历data
中每一个属性设置get/set
数据拦截初始化computed
会调用initComputed
函数
watcher
实例,并在内实例化一个Dep
消息订阅器用做后续收集依赖(好比渲染函数的watcher
或者其余观察该计算属性变化的watcher
)Object.defineProperty
的get
访问器函数watcher.depend()
方法向自身的消息订阅器dep
的subs
中添加其余属性的watcher
watcher
的evaluate
方法(进而调用watcher
的get
方法)让自身成为其余watcher
的消息订阅器的订阅者,首先将watcher
赋给Dep.target
,而后执行getter
求值函数,当访问求值函数里面的属性(好比来自data
、props
或其余computed
)时,会一样触发它们的get
访问器函数从而将该计算属性的watcher
添加到求值函数中属性的watcher
的消息订阅器dep
中,当这些操做完成,最后关闭Dep.target
赋为null
并返回求值函数结果。set
拦截函数,而后调用自身消息订阅器dep
的notify
方法,遍历当前dep
中保存着全部订阅者wathcer
的subs
数组,并逐个调用watcher
的 update
方法,完成响应更新。