深入理解Vue Computed计算属性原理
computed 计算属性是 vue 中常用的一个功能,但你理解它是怎么工作的吗?
拿官网简单的例子来看一下:
<div id="example"> <p>original message: "{{ message }}"</p> <p>computed reversed message: "{{ reversedmessage }}"</p> </div>
var vm = new vue({ el: '#example', data: { message: 'hello' }, computed: { // a computed getter reversedmessage: function () { // `this` points to the vm instance return this.message.split('').reverse().join('') } } })
situation
vue 里的 computed 属性非常频繁的被使用到,但并不是很清楚它的实现原理。比如:计算属性如何与属性建立依赖关系?属性发生变化又如何通知到计算属性重新计算?
关于如何建立依赖关系,我的第一个想到的就是语法解析,但这样太浪费性能,因此排除,第二个想到的就是利用 javascript 单线程的原理和 vue 的 getter 设计,通过一个简单的发布订阅,就可以在一次计算属性求值的过程中收集到相关依赖。
因此接下来的任务就是从 vue 源码一步步分析 computed 的实现原理。
task
分析依赖收集实现原理,分析动态计算实现原理。
action
data 属性初始化 getter setter:
// src/observer/index.js // 这里开始转换 data 的 getter setter,原始值已存入到 __ob__ 属性中 object.defineproperty(obj, key, { enumerable: true, configurable: true, get: function reactivegetter () { const value = getter ? getter.call(obj) : val // 判断是否处于依赖收集状态 if (dep.target) { // 建立依赖关系 dep.depend() ... } return value }, set: function reactivesetter (newval) { ... // 依赖发生变化,通知到计算属性重新计算 dep.notify() } })
computed 计算属性初始化
// src/core/instance/state.js // 初始化计算属性 function initcomputed (vm: component, computed: object) { ... // 遍历 computed 计算属性 for (const key in computed) { ... // 创建 watcher 实例 // create internal watcher for the computed property. watchers[key] = new watcher(vm, getter || noop, noop, computedwatcheroptions) // 创建属性 vm.reversedmessage,并将提供的函数将用作属性 vm.reversedmessage 的 getter, // 最终 computed 与 data 会一起混合到 vm 下,所以当 computed 与 data 存在重名属性时会抛出警告 definecomputed(vm, key, userdef) ... } } export function definecomputed (target: any, key: string, userdef: object | function) { ... // 创建 get set 方法 sharedpropertydefinition.get = createcomputedgetter(key) sharedpropertydefinition.set = noop ... // 创建属性 vm.reversedmessage,并初始化 getter setter object.defineproperty(target, key, sharedpropertydefinition) } function createcomputedgetter (key) { return function computedgetter () { const watcher = this._computedwatchers && this._computedwatchers[key] if (watcher) { if (watcher.dirty) { // watcher 暴露 evaluate 方法用于取值操作 watcher.evaluate() } // 同第1步,判断是否处于依赖收集状态 if (dep.target) { watcher.depend() } return watcher.value } } }
无论是属性还是计算属性,都会生成一个对应的 watcher 实例。
// src/core/observer/watcher.js // 当通过 vm.reversedmessage 获取计算属性时,就会进到这个 getter 方法 get () { // this 指的是 watcher 实例 // 将当前 watcher 实例暂存到 dep.target,这就表示开启了依赖收集任务 pushtarget(this) let value const vm = this.vm try { // 在执行 vm.reversedmessage 的函调函数时,会触发属性(步骤1)和计算属性(步骤2)的 getter // 在这个执行过程中,就可以收集到 vm.reversedmessage 的依赖了 value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleerror(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { if (this.deep) { traverse(value) } // 结束依赖收集任务 poptarget() this.cleanupdeps() } return value }
上面多出提到了 dep.depend, dep.notify, dep.target,那么 dep 究竟是什么呢?
dep 的代码短小精悍,但却承担着非常重要的依赖收集环节。
// src/core/observer/dep.js export default class dep { static target: ?watcher; id: number; subs: array<watcher>; constructor () { this.id = uid++ this.subs = [] } addsub (sub: watcher) { this.subs.push(sub) } removesub (sub: watcher) { remove(this.subs, 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++) { // 更新 watcher 的值,与 watcher.evaluate() 类似, // 但 update 是给依赖变化时使用的,包含对 watch 的处理 subs[i].update() } } } // 当首次计算 computed 属性的值时,dep 将会在计算期间对依赖进行收集 dep.target = null const targetstack = [] export function pushtarget (_target: watcher) { // 在一次依赖收集期间,如果有其他依赖收集任务开始(比如:当前 computed 计算属性嵌套其他 computed 计算属性), // 那么将会把当前 target 暂存到 targetstack,先进行其他 target 的依赖收集, if (dep.target) targetstack.push(dep.target) dep.target = _target } export function poptarget () { // 当嵌套的依赖收集任务完成后,将 target 恢复为上一层的 watcher,并继续做依赖收集 dep.target = targetstack.pop() }
result
总结一下依赖收集、动态计算的流程:
1. data 属性初始化 getter setter
2. computed 计算属性初始化,提供的函数将用作属性 vm.reversedmessage 的 getter
3. 当首次获取 reversedmessage 计算属性的值时,dep 开始依赖收集
4. 在执行 message getter 方法时,如果 dep 处于依赖收集状态,则判定 message 为 reversedmessage 的依赖,并建立依赖关系
5. 当 message 发生变化时,根据依赖关系,触发 reversemessage 的重新计算
到此,整个 computed 的工作流程就理清楚了。
vue 是一个设计非常优美的框架,使用 getter setter 设计使依赖关系实现的非常顺其自然,使用计算与渲染分离的设计(优先使用 mutationobserver,降级使用 settimeout)也非常贴合浏览器计算引擎与排版引擎分离的的设计原理。
如果你想成为一名架构师,不能只停留在框架的 api 使用层面。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。