虽然目前的技术栈已由 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 的设计初衷也正是用于解决此类问题。
当然很多时候我们使用 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 之间的一些差异和使用场景的区别,当然某些时候两者并没有那么明确严格的限制,最后还是要具体到不同的业务进行分析。
言归正传,回到文章的主题 computed 身上,为了更深层次地了解计算属性的内在机制,接下来就让我们一步步探索 Vue 源码中关于它的实现原理吧。
在分析 computed 源码之前我们先得对 Vue 的响应式系统有一个基本的了解,Vue 称其为非侵入性的响应式系统,数据模型仅仅是普通的 JavaScript 对象,而当你修改它们时,视图便会进行自动更新。
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的实现原理是什么?的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号