Vue中computed的实现原理是什么?
虽然目前的技术栈已由 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()步骤实现的原理
以上就是Vue中computed的实现原理是什么?的详细内容,更多请关注其它相关文章!
上一篇: 安装MySQL-python有关问题解决