虽然目前的技术栈已由 Vue 转到了 React,但从之前使用 Vue 开发的多个项目实际经历来看还是非常愉悦的,Vue 文档清晰规范,api 设计简洁高效,对前端开发人员友好,上手快,甚至个人认为在很多场景使用 Vue 比 React 开发效率更高,之前也有断断续续研读过 Vue 的源码,但一直没有梳理总结,所以在此做一些技术归纳同时也加深自己对 Vue 的理解,那么今天要写的便是 Vue 中最常用到的 API 之一 computed
的实现原理。
话不多说,一个最基本的例子如下:
<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
的设计初衷也正是用于解决此类问题。
watch
当然很多时候我们使用 computed
时往往会与 Vue 中另一个 API 也就是侦听器 watch
相比较,因为在某些方面它们是一致的,都是以 Vue 的依赖追踪机制为基础,当某个依赖数据发生变化时,所有依赖这个数据的相关数据或函数都会自动发生变化或调用。
watch
选项提供了一个更通用的方法来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。从 Vue 官方文档对 watch
的解释我们可以了解到,使用 watch
选项允许我们执行异步操作(访问一个 API)或高消耗性能的操作,限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态,而这些都是计算属性无法做到的。
下面还另外总结了几点关于 computed
和 watch
的差异:
computed
是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而 watch
是监听已经存在且已挂载到 vm
上的数据,所以用 watch
同样可以监听 computed
计算属性的变化(其它还有 data
、props
)
computed
本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed
属性,才会计算新的值,而 watch
则是当数据发生变化便会调用执行函数
从使用场景上说,computed
适用一个数据被多个数据影响,而 watch
适用一个数据影响多个数据;
以上我们了解了 computed
和 watch
之间的一些差异和使用场景的区别,当然某些时候两者并没有那么明确严格的限制,最后还是要具体到不同的业务进行分析。
言归正传,回到文章的主题 computed
身上,为了更深层次地了解计算属性的内在机制,接下来就让我们一步步探索 Vue 源码中关于它的实现原理吧。
在分析 computed
源码之前我们先得对 Vue 的响应式系统有一个基本的了解,Vue 称其为非侵入性的响应式系统,数据模型仅仅是普通的 JavaScript 对象,而当你修改它们时,视图便会进行自动更新。
当你把一个普通的 JavaScript 对象传给 Vue 实例的
data
选项时,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty
把这些属性全部转为 getter/setter
,这些 getter/setter
对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化,每个组件实例都有相应的 watcher
实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter
被调用时,会通知 watcher
重新计算,从而致使它关联的组件得以更新。Vue 响应系统,其核心有三点:observe
、watcher
、dep
:
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
然后执行 wather.depend()
收集依赖和 watcher.evaluate()
计算求值。
当组件初始化的时候,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
方法,完成响应更新。
相关推荐:
thinkphp 控制器中 display()步骤实现的原理
Atas ialah kandungan terperinci Vue中computed的实现原理是什么?. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!