文档教程前端【免费下载链接】vue-analysis:thumbsup: Vue.js 源码分析项目地址https://gitcode.com/gh_mirrors/vu/vue-analysis点击查看免费下载Vue 的组件对象同时提供了computed计算属性和watch侦听属性两个选项不少开发者会困惑什么时候该用 computed、什么时候该用 watch。本文以 Vue 2 源码仓库 vue/src/core/instance/state.js 与 vue/src/core/observer/watcher.js为第一手依据从初始化入口、Watcher 实例化、依赖收集、更新触发四个层面拆解两者的实现差异帮助你理解计算属性本质上是computed watcher侦听属性本质上是user watcher以及deep、user、computed、sync四种 watcher 选项分别作用于什么场景。读完本文你将能依据源码逻辑而非经验直觉正确选择 computed 与 watch。初始化入口initState 中的执行顺序计算属性与侦听属性的初始化都发生在 Vue 实例初始化阶段的initState函数中其定义位于 vue/src/core/instance/state.jsexport 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 */) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch opts.watch ! nativeWatch) { initWatch(vm, opts.watch) } }注意两点细节顺序computed的初始化在watch之前且都在data之后。这也是为什么 computed 与 watch 都能直接访问到已经代理到实例上的data属性。nativeWatch判断if (opts.watch opts.watch ! nativeWatch)排除了用户恰好把$options.watch赋值为浏览器原生Object.prototype.watchFirefox 遗留 API的边界情况避免 Vue 把原生 watch 当作侦听配置处理。computed计算属性的初始化initState中执行initComputed(vm, opts.computed)其完整实现如下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) } } } }这段初始化逻辑可以拆成四步创建容器vm._computedWatchers Object.create(null)用一个无原型对象保存计算属性名 → computed watcher的映射。提取 getter对每个计算属性拿到userDef若是函数则直接作为 getter若是对象则取其userDef.getgetter 缺失时在开发环境报警告Getter is missing for computed property key.。仓库测试 vue/test/unit/features/options/computed.spec.js 的warn with setter and no getter用例验证了只写set不写get时的警告行为。创建 computed watchernew Watcher(vm, getter || noop, noop, computedWatcherOptions)其中computedWatcherOptions { computed: true }——这是它与渲染 watcher、user watcher 最本质的差别。SSR 特例服务端渲染时不会创建 watcher计算属性退化为纯 getter源码注释computed properties are just getters during SSR因为 SSR 不需要缓存与响应式追踪。定义实例属性并做冲突检查若key尚未存在于vm则调用defineComputed(vm, key, userDef)把它定义为实例属性若已存在且是$data或props中的键则在开发环境警告测试用例warn conflict with data与warn conflict with props分别验证了两条警告文案。defineComputed用 Object.defineProperty 定义访问器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给计算属性对应的key添加 getter 和 setter。getter 的来源分三种情况userDef是函数简写形式getter 为createComputedGetter(key)的返回值浏览器端setter 为noopuserDef是对象且get存在默认仍然走createComputedGetter但cache: false时跳过缓存包装直接用userDef.get仓库测试cache: false用例验证了每次访问都会重新执行 getteruserDef是对象但无getgetter 为noop。setter 仅在对象写法提供set方法时存在否则在开发环境中被替换为一个专门的警告函数一旦有人对无 setter 的计算属性赋值就会提示Computed property key was assigned to but it has no setter.对应测试用例warn assigning to computed with no setter。日常开发中计算属性带 setter 的情况较少重点关注 getter 部分即可。createComputedGetter访问时触发求值function createComputedGetter (key) { return function computedGetter () { const watcher this._computedWatchers this._computedWatchers[key] if (watcher) { watcher.depend() return watcher.evaluate() } } }createComputedGetter返回computedGetter函数它就是计算属性最终的 getter每当模板渲染或业务代码访问this.fullName都会拿到该 key 对应的 computed watcher先执行watcher.depend()再执行watcher.evaluate()取回缓存或重新计算的值。computed watcher 的工作机制一次完整求值链路为了直观理解用仓库文档中的经典示例docs/v2/reactive/computed-watcher.mdvar vm new Vue({ data: { firstName: Foo, lastName: Bar }, computed: { fullName: function () { return this.firstName this.lastName } } })构造computed watcher 不立即求值initComputed里创建 watcher 时Watcher构造函数vue/src/core/observer/watcher.js对 computed 走了一条与普通 watcher 不同的分支if (this.computed) { this.value undefined this.dep new Dep() } else { this.value this.get() }普通 watcher如渲染 watcher、user watcher构造时立即执行get()求值并收集依赖而computed watcher 不立刻求值初始value为undefined并额外持有一个自己的Dep实例。这一点也被构造函数前面的this.dirty this.computed印证computed watcher 初始dirty为true等待第一次被访问时才真正求值。第一次访问depend evaluate当render函数执行并访问this.fullName时触发上文computedGetter依次执行两步第一步watcher.depend()——让当前正在收集依赖的 watcher 订阅该 computed watcher/** * Depend on this watcher. Only for computed property watchers. */ depend () { if (this.dep Dep.target) { this.dep.depend() } }此刻Dep.target是渲染 watcherthis.dep.depend()相当于渲染 watcher 订阅了这个 computed watcher 的变化dep 的depend最终调用Dep.target.addDep(this)见 vue/src/core/observer/dep.js。第二步watcher.evaluate()——按需求值/** * Evaluate and return the value of the watcher. * This only gets called for computed property watchers. */ evaluate () { if (this.dirty) { this.value this.get() this.dirty false } return this.value }evaluate逻辑简单dirty为true才通过this.get()重新求值随后置dirty false并返回值从而实现缓存——在依赖不变的前提下多次访问计算属性只会求值一次。仓库测试用例caching精确验证了这一点getter 内 spy 计数在两次访问vm.b后仍为 1。求值过程中value this.getter.call(vm, vm)执行了计算属性定义的 getter 函数本例即return this.firstName this.lastName。这里有一个关键点由于this.firstName和this.lastName都是响应式对象访问它们会触发各自的 getter把自身持有的 dep 添加到当前正在计算的 watcher中——此时Dep.target就是该 computed watcherget()开头调用pushTarget(this)见 vue/src/core/observer/dep.js 的pushTarget/popTarget栈式管理。于是 computed watcher 与firstName、lastName两个响应式数据的 dep 建立订阅关系。依赖变化lazy 与 activated 两种模式一旦修改了计算属性依赖的数据如vm.firstName Hello会触发 setter 过程调用dep.notify()进而执行订阅者的watcher.update()。computed watcher 的update分支与普通 watcher 完全不同update () { /* istanbul ignore else */ if (this.computed) { // A computed property watcher has two modes: lazy and activated. // It initializes as lazy by default, and only becomes activated when // it is depended on by at least one subscriber, which is typically // another computed property or a components render function. if (this.dep.subs.length 0) { // In lazy mode, we dont want to perform computations until necessary, // so we simply mark the watcher as dirty. The actual computation is // performed just-in-time in this.evaluate() when the computed property // is accessed. this.dirty true } else { // In activated mode, we want to proactively perform the computation // but only notify our subscribers when the value has indeed changed. this.getAndInvoke(() { this.dep.notify() }) } } else if (this.sync) { this.run() } else { queueWatcher(this) } }lazy 模式当this.dep.subs.length 0即没有任何 watcher 订阅该 computed watcher 的变化时只把this.dirty true标记为脏不立即重新计算。等到下次访问该计算属性时evaluate()会恰好及时just-in-time地重新求值。activated 模式当渲染 watcher 订阅了它本例场景执行getAndInvoke主动重算但只有当最终计算值确实变化时才通知订阅者。getAndInvoke仅当最终值变化才通知getAndInvoke (cb: Function) { 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 value const oldValue this.value this.value value this.dirty false if (this.user) { try { cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, callback for watcher ${this.expression}) } } else { cb.call(this.vm, value, oldValue) } } }getAndInvoke重新计算新值并比较value ! this.value时才更新内部值与dirty状态并执行回调——本例中回调是this.dep.notify()即触发订阅了该 computed watcher 的渲染 watcher 重新渲染。这揭示了当前 computed 实现的精髓Vue 想确保不是计算属性依赖的值一变就重渲染而是计算属性最终计算出的值发生变化才触发渲染 watcher 重新渲染本质上是一种优化。仓库测试should avoid unnecessary re-renders对应 issue #7767正是验证这一行为当msg从bar变为baz而计算属性结果布尔值不变时getter 被重新执行了spy 计数 1但组件的updated钩子没有触发——避免了一次无意义的重新渲染。watch侦听属性的初始化侦听属性的初始化同样在initState中位于 computed 之后if (opts.watch opts.watch ! nativeWatch) { initWatch(vm, opts.watch) }initWatch 与 createWatcher三种 handler 形式function initWatch (vm: Component, watch: Object) { for (const key in watch) { const handler watch[key] if (Array.isArray(handler)) { for (let i 0; i handler.length; i) { createWatcher(vm, key, handler[i]) } } else { createWatcher(vm, key, handler) } } }initWatch遍历watch对象。Vue 支持同一个 key 对应多个 handler因此当handler是数组时逐个调用createWatcher否则直接调用。function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) { if (isPlainObject(handler)) { options handler handler handler.handler } if (typeof handler string) { handler vm[handler] } return vm.$watch(expOrFn, handler, options) }createWatcher对 handler 做归一化支持三种写法对象写法最常见handler是普通对象则把整个对象作为options取handler.handler作为真正的回调于是deep、immediate、sync等配置都来自该对象字符串写法handler是字符串时视为组件实例上的方法名取vm[handler]函数写法直接作为回调。最终统一调用vm.$watch(expOrFn, handler, options)。$watchuser watcher 的诞生$watch是 Vue 原型上的方法在stateMixin同样位于 vue/src/core/instance/state.js中定义Vue.prototype.$watch function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component this if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options options || {} options.user true const watcher new Watcher(vm, expOrFn, cb, options) if (options.immediate) { cb.call(vm, watcher.value) } return function unwatchFn () { watcher.teardown() } }要点依次是cb为对象时复用createWatcher因为$watch是用户可直接调用的 API第二个参数既可以是回调函数也可以是{ handler, deep, immediate, sync }形式的配置对象强制options.user true通过$watch创建的一定是user watcher与 Vue 内部的渲染 watcher 相区分实例化Watcher立即求值new Watcher(...)会立刻执行一次 getterthis.value this.get()收集依赖并得到当前值这是后续比较新旧值的基础immediate处理设置了immediate: true时在实例化后立即调用一次cb.call(vm, watcher.value)回调的第一个参数就是当前值、第二个参数为 undefined仓库测试with option: immediate验证了这一点返回取消侦听函数unwatchFn调用watcher.teardown()移除该 watcher从vm._watchers与各 dep 的订阅列表中移除见 vue/src/core/observer/watcher.js 的teardown。Watcher内部对expOrFn的处理值得一提如果它是字符串getter parsePath(expOrFn)vue/src/core/util/lang.js只支持形如a.b.c的点分隔路径bailRE /[^\w.$]/会拒绝含运算符、括号等的复杂表达式此时开发环境会警告并建议改用函数因此需要侦听复杂表达式时应写成函数形式。Watcher 的四种类型options 决定的运行差异Watcher构造函数开头对options做解析if (options) { this.deep !!options.deep this.user !!options.user this.computed !!options.computed this.sync !!options.sync this.before options.before } else { this.deep this.user this.computed this.sync false }于是 watcher 共有四种开关下面逐一分析不同组合下的行为差异。deep watchertraverse 深度遍历如果只 watch 对象本身而不开启deep对象内部属性的变化不会被感知。考虑仓库文档给出的场景var vm new Vue({ data() { a: { b: 1 } }, watch: { a: { handler(newVal) { console.log(newVal) } } } }) vm.a.b 2此时不会 log 任何数据watcher 求值时只访问了a本身触发了a的 getter 却未触发a.b的 getter因此没有订阅a.b的变化vm.a.b 2虽触发了 setter但没有任何 watcher 订阅它回调自然不会被调用。只需给 handler 配置deep: true即可观测到内部变化watch: { a: { deep: true, handler(newVal) { console.log(newVal) } } }deep watcher的原理藏在Watcher.get()中——求值后追加一次深度遍历get () { pushTarget(this) let value const vm this.vm try { 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 watching if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value }traverse定义在 vue/src/core/observer/traverse.jsexport function traverse (val: any) { _traverse(val, seenObjects) seenObjects.clear() } function _traverse (val: any, seen: SimpleSet) { let i, keys const isA Array.isArray(val) if ((!isA !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { return } if (val.__ob__) { const depId val.__ob__.dep.id if (seen.has(depId)) { return } seen.add(depId) } if (isA) { i val.length while (i--) _traverse(val[i], seen) } else { keys Object.keys(val) i keys.length while (i--) _traverse(val[keys[i]], seen) } }traverse的本质是对对象做深层递归遍历——遍历即访问访问即触发子对象的 getter从而把每一层嵌套属性都收集为依赖。其中有两个细节优化用val.__ob__.dep.id去重把已访问过的响应式对象记入seenObjects避免循环引用如对象自引用导致死循环遇到Object.isFrozen(val)或VNode实例直接返回跳过无需追踪的对象。开启deep后对 watch 对象内部任何值的修改都会调用 watcher 回调。仓库测试with option: deepvue/test/unit/features/options/watch.spec.js验证了该行为。实践提醒观测复杂对象且希望内部深层值变化也触发回调时务必设置deep: true但deep会执行traverse递归存在一定的性能开销尤其对象很大或层级很深时应根据场景权衡是否开启。user watcher错误处理与 immediate前文已述通过vm.$watch创建的 watcher 是user watcher。它的功能核心在于错误处理在 getter 求值与回调执行两个环节捕获异常并交给handleErrorget() { // ... try { value this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, getter for watcher ${this.expression}) } else { throw e } } }以及getAndInvoke中if (this.user) { try { cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, callback for watcher ${this.expression}) } } else { cb.call(this.vm, value, oldValue) }handleError定义在 vue/src/core/util/error.js是 Vue 错误捕获机制的门户它沿着vm.$parent链逐层寻找组件上的errorCaptured钩子并依次调用若某个钩子返回false则停止向上传递最终如果没有被捕获会交给config.errorHandler全局配置处理否则在开发环境warn并console.error。这意味着一句watch 回调里的异常会进入全局错误处理流程而不是直接中断渲染的实现基础。computed watchercomputed watcher几乎是为计算属性量身定制lazy 求值、自带dep、depend()/evaluate()双方法、update()的 lazy/activated 双模式。前文已完整分析此处不再赘述。sync watcher同步执行 vs 异步队列回顾响应式 setter 的常规流程数据变化后触发watcher.update()对于普通 watcher它并不立即执行回调而是被queueWatcher推入异步队列等到nextTick后由调度器统一flushSchedulerQueue执行见 vue/src/core/observer/scheduler.js队列按 watcher id 排序、对重复 id 去重、并在开发环境检测超过MAX_UPDATE_COUNT 100的无限更新循环。而设置了sync: true后update () { if (this.computed) { // ... } else if (this.sync) { this.run() } else { queueWatcher(this) } }syncwatcher 在update()中直接调用this.run()内部getAndInvoke(this.cb)重新求值并执行回调即在当前 Tick 内同步执行回调跳过异步队列。只有当你确实需要值变化 → 回调执行是严格同步过程时才应开启否则默认的异步批处理是更优选择合并同一 Tick 内的多次变更、减少重复计算。总结何时用 computed何时用 watch通过源码剖析可以得出两个本质结论计算属性本质上是computed watcherlazy 求值 结果缓存 自带dep依赖变化时先重算、仅在最终值变化时通知订阅者渲染 watcher天然避免无意义的重新渲染。侦听属性本质上是user watcher初始化即求值收集依赖值变化后默认异步执行用户回调且全程经过handleError错误处理。由此对应到应用场景的取舍计算属性适合模板渲染场景某个值依赖其它响应式对象、甚至是其它计算属性推导而来如fullName由firstName、lastName组合它自带缓存与结果变化才触发渲染的优化应优选用 computed侦听属性适合观测值的变化以完成一段复杂业务逻辑如值变化后发请求、写存储、联动其它组件状态等副作用操作用 watch配合deep、immediate、sync选项按需配置。在创建user watcher时最常配置的是deep和sync前者解决深层对象观测后者将异步回调改为同步执行而computed与user两个标志通常由 Vue 内部initComputed与$watch自动设置无需手动干预。理解这四种 watcher 选项的运行差异就能在写业务时做出更合理、更省性能的选择。深入阅读指引初始化入口与两个选项的完整实现vue/src/core/instance/state.jsinitState、initComputed、defineComputed、createComputedGetter、initWatch、createWatcher、stateMixinWatcher 核心类vue/src/core/observer/watcher.js构造、get、update、run、getAndInvoke、evaluate、depend、teardown依赖收集与通知vue/src/core/observer/dep.jsDep、depend、notify、pushTarget/popTarget深度遍历vue/src/core/observer/traverse.js异步调度队列vue/src/core/observer/scheduler.jsqueueWatcher、flushSchedulerQueue表达式解析与错误处理vue/src/core/util/lang.jsparsePath、vue/src/core/util/error.jshandleError行为验证测试vue/test/unit/features/options/computed.spec.js缓存、cache: false、避免不必要重渲染等、vue/test/unit/features/options/watch.spec.jsimmediate、deep、多回调等赞分享文档教程前端【免费下载链接】vue-analysis:thumbsup: Vue.js 源码分析项目地址https://gitcode.com/gh_mirrors/vu/vue-analysis点击查看免费下载相关推荐Vue.js 源码分析计算属性与侦听器的实现差异Vue.js 源码分析计算属性与侦听器的实现差异 在Vue.js开发中计算属性Computed和侦听器Watcher是处理响应式数据的两种重要机制。文档教程前端JavaScript Signals API深度解析State、Computed与Watcher详解JavaScript Signals API深度解析State、Computed与Watcher详解 JavaScript Signals API 正在Vue.js 2.x 计算属性与侦听器提升应用性能的10个最佳实践Vue.js 2.x 计算属性与侦听器提升应用性能的10个最佳实践 Vue.js 2.x中的计算属性和侦听器是优化应用性能的核心工具。计算属性提供了高效的数据AI 技能AI 插件多模态上一篇NVIDIA Profile Inspector解锁显卡隐藏性能的三大秘籍下一篇AutoFigure-Edit完全指南免费AI科研插图工具从论文方法文本自动生成可编辑SVG创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考