重学Vue【计算属性和监听属性】

网友投稿 231 2022-11-02

重学Vue【计算属性和监听属性】

正文

计算属性

计算属性的初始化是在Vue初始化的 ​​initState​​ 方法中:

// ...initState(vm)// ...

具体实现是在 ​​src/core/instance/state.js​​ 里面:

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 */) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) }}

可以看到在里面初始化了 ​​props​​​、​​methods​​​、​​data​​​,另外也初始化了 ​​computed​​​ 和 ​​watch​​​,这里就看看下 ​​initComputed​​ 的具体实现:

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) } } }}

先定义了一个 ​​watchers​​​ 和 ​​_computedWatchers​​​ 为空对象,然后判断是不是服务端渲染,这里肯定是false,后面遍历 ​​computed​​​,这个 ​​computed​​​ 其实就是我们自定义写的 ​​computed​​​ 属性,然后拿到每一个定义的值,它可以是函数也可以是对象,一般我们都会写一个函数,如果定义成对象的话,就必须定义一个 ​​get​​​ 属性,相当于拿到 ​​getter​​​,接着判断 ​​getter​​​ 有没有,如果没有就报错,接着实例化一个 ​​Watcher​​​,对应的值就是 ​​watchers[key]​​​,这里就可以看出来 ​​computed​​​ 里面的定义其实就是通过 ​​watcher​​​ 来实现的,传入 ​​watcher​​​ 的值分别是vm实例、getter(也就是定义的get)、noop回调函数和一个computedWatcherOptions(定义了一个对象: {computed: true})。接着判断 ​​key​​​ 在不在 ​​vm​​​ 中,如果不在就走一个 ​​defineComputed​​​ 方法,如果在,那肯定在 ​​data​​​ 或者 ​​props​​​ 定义过,就报错,所以在 ​​computed​​​ 里面定义的键值不可以在 ​​data​​​ 或者 ​​props​​ 里出现。

来看下这个 ​​defineComputed​​ 方法:

const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop}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)}

如果定义的每个计算属性是一个函数,就把对象的 ​​get​​​ 和 ​​set​​​ 赋值,也就是说当访问我们自定义的 ​​computed​​​ 属性的时候,就会执行对应的重新赋值的 ​​get​​​ 方法,这里 ​​shouldCache​​​ 是true,所以会走 ​​createComputedGetter​​​ 方法,如果我们写的是一个对象,就走下面的 ​​else​​​ 逻辑,也会走到 ​​createComputedGetter​​​,因为这个 ​​computed​​ 属性一般是计算而来的,所以一般我们不会用对象的方式去写。

计算属性的依赖收集

接着看下这个 ​​createComputedGetter​​ 做了什么:

function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { watcher.depend() return watcher.evaluate() } }}

单从名字来看,就是定义了在我们访问 ​​computed​​​ 属性的时候的 ​​getter​​​,可以看到每次访问的时候就会执行 ​​computedGetter​​​,拿到刚才存起来的 ​​_computedWatchers​​​,走一个 ​​depend​​​ 方法,返回执行 ​​watcher.evaluate()​​ 方法。

到这里计算属性的初始化流程就结束了,回到上面说到的访问 ​​computed​​​ 的时候会 new 一个 ​​watcher​​​,它和之前说过的创建渲染watcher有哪些不一样,我们来重新看下 ​​watcher​​​ 的构造函数,在 ​​src/core/observer/watcher.js​​ 里,这里只看关键点:

if (this.computed) { this.value = undefined this.dep = new Dep()} else { this.value = this.get()}

前面传进来的对象 ​​{computed: true}​​​ 在这里用到了,作为 ​​computed watcher​​​,会在当前实例定义一个 ​​value​​​,并且定义一个 ​​dep​​​ 为 ​​new Dep()​​,然后就结束了,并不会像以前一样去求值。举个例子:

computed: { sum(){ return this.a + this.b }}

当 ​​render​​​ 函数执行访问到 ​​this.sum​​​ 的时候,就触发了计算属性的 ​​getter​​​,它就会拿到计算属性的 ​​watcher​​​,执行上面提到的 ​​watcher.depend()​​:

/** * Depend on this watcher. Only for computed property watchers. */depend(){ if(this.dep && Dep.target){ this.dep.depend(); }}

这个时候的 ​​Dep.target​​​ 不是当前的 ​​computed watcher​​​ ,而是 渲染watcher,所以​​this.dep.depend()​​​ 就相当于 渲染 watcher 订阅了这个 ​​computed watcher​​​ 的变化(有关的订阅可以看​​这篇​​​),接着按照上面 ​​computedGetter​​​ 的逻辑就会执行 ​​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​​​ 里判断 ​​this.dirty​​​(dirty 就是传入的 computed 布尔值),如果是true,就通过 ​​this.get()​​​ 求值,然后把 ​​dirty​​​ 设置为false,而在执行 ​​get​​​ 的时候,里面有一个 ​​value = this.getter.call(vm, vm)​​​,也就是执行了计算属性定义的 ​​getter​​​ 函数,在上面的例子中就是执行了我们自定义的 ​​return this.a + this.b​​​,由于 ​​a​​​ 和 ​​b​​​ 都是响应式对象,所以在执行它们的 ​​getter​​​ 的时候,会将自身的 ​​dep​​​ 添加到当前正在计算的 ​​watcher​​​ 中,此时的 ​​Dep.target​​​ 就是 ​​computed watcher​​ 了,到此求值结束。

计算属性的派发更新

一旦对计算属性依赖的数据做了修改,就会触发 ​​setter​​​ 过程,然后通知所有订阅它变化的 ​​watcher​​​ 进行派发更新,最终执行 ​​watcher.update()​​​ 方法(有关派发更新可以看​​这篇​​):

/** * Subscriber interface. * Will be called when a dependency changes. */ 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 component's render function. if (this.dep.subs.length === 0) { // In lazy mode, we don't 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) } }

通过注释可以看出来 ​​computed watcher​​​ 有两种模式:​​lazy​​​ 和 ​​activated​​​。如果 ​​this.dep.subs.length === 0​​​ 为true,就说明没有人订阅这个 ​​computed watcher​​​ 的变化,就只把 ​​this.dirty​​ 改成true,然后在下次再访问这个计算属性的时候才会去重新求值;否则如果订阅了,就执行:

this.getAndInvoke(() => { this.dep.notify()})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​​​ 函数也比较简单,先通过 ​​get​​​ 方法得到一个新值 ​​value​​​,然后让新值和之前的值做对比,如果值不相等,或者新值 ​​value​​​ 是一个对象,或者它是一个 deep watcher 的话,就执行回调(user watcher 比 渲染watcher 多了一个报错提示而已),注意回调函数 ​​cb​​​ 执行的时候会把第一个和第二个参数传入新值 ​​value​​​ 和旧值 ​​oldValue​​,这就是当我们自定义 ​​watcher​​ 的时候可以拿到新值和旧值的原因。

​​getAndInvoke​​​ 会重新计算,对比新旧值,如果变化了就执行回调函数,这里的回调函数就是 ​​this.dep.notify()​​​,在求值 ​​sum​​​ 的情况下,就是触发了 ​​watcher​​​ 的重新渲染,对于这个 ​​getAndInvoke​​​ 里面的 ​​value !== this.value​​ 其实算是做了一个优化,如果新值和旧值相等的话,就不做什么处理,也不会触发回调,这样就少走了一层渲染。

计算属性小总结

计算属性本质上就是一个 ​​computed watcher​​​,它在创建的过程中会实例一个 ​​watcher​​​,访问它的时候走定义的 ​​computedGetter​​ 进行依赖收集,在计算的时候触发依赖的更新,并且当计算属性最终计算的值更新发生变化才会触发 渲染 watcher 的重新渲染。

监听属性

最上面提到,在Vue实例初始化的时候会执行监听属性的初始化,它是在 ​​computed​​ 初始化之后的:

if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch)}

看下 ​​initWatcher​​​ 的具体实现,它定义在 ​​src/core/instance/state.js​​:

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) } }}

可以看到对 ​​watcher​​​ 做了遍历,拿到每一个 ​​handler​​​,因为Vue是支持 ​​watcher​​​ 的同一个 ​​key​​​ 有多个 ​​handler​​​ 的,所以如果 ​​handler​​​ 是一个数组,就遍历数组并调用 ​​createWatcher​​​ 方法,否则直接调用 ​​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​​​ 的类型做了判断,然后拿到它的回调函数,也就是我们自定义的函数,最后调用 ​​vm.$watch​​​ 函数,这个 ​​vm.$watch​​​ 是一个Vue原型上扩展的方法,它是在执行 ​​stateMixin​​ 的时候被加进去的:

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() }}

换句话说,监听属性最终执行的其实是 ​​$watch​​​,首先判断了 ​​cb​​​ 如果是一个对象,就直接调用 ​​createWatcher​​​,因为 ​​$watch​​​ 是可以直接外部调用的,它可以传一个对象,也可以传一个函数。接着实例化一个 ​​watcher​​​,注意这个 ​​watcher​​​ 是一个 ​​user watcher​​​,因为是用户直接调用的,而且 ​​options.user = true​​​。这样通过实例化一个 ​​watcher​​​ 的方式,一旦我们自定义的 ​​watcher​​​ 发生了变化,最终会执行 ​​watcher​​​ 的 ​​run​​​ 方法(watcher 里的 update 调用的 run),从而调用我们的回调函数,也就是 ​​cb​​​,如果 ​​options​​​ 上的 ​​immediate​​​ 也是true,就直接执行回调函数,最终返回了一个 ​​unwatchFn​​​ 方法,调用一个 ​​teardown​​​ 来移除这个 ​​watcher​​:

/** * Scheduler job interface. * Will be called by the scheduler. */run () { if (this.active) { this.getAndInvoke(this.cb) }}/** * Remove self from all dependencies' subscriber list. */teardown () { if (this.active) { // remove self from vm's watcher list // this is a somewhat expensive operation so we skip it // if the vm is being destroyed. if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false }}

到底有几种 watcher

在 ​​Watcher​​​ 的构造函数里对 ​​options​​ 做了处理,代码如下:

if (options) { this.deep = !!options.deep this.user = !!options.user this.computed = !!options.computed this.sync = !!options.sync // ...} else { this.deep = this.user = this.computed = this.sync = false}

可以看出来一共有四种 ​​watcher​​。

deep watcher

这个一般在深度监听的时候会用到,我们一般会这样写:

var vm = new Vue({ data() { a: { b: 1 } }, watch: { a: { handler(newVal) { console.log(newVal) } } }})vm.a.b = 2

这种情况下 ​​handler​​​ 是不会监听到b的变化的,因为此时 watch 的是 ​​a​​​ 对象,只触发了 ​​a​​​ 的 ​​getter​​​,并没有触发 ​​a.b​​​ 的 ​​getter​​​,所以没有订阅它,所以在修改 ​​a.b​​​ 的时候,虽然触发了 ​​setter​​, 但是没有可通知的对象,所以也不会触发 watch 的回调函数。

此时如果想监听到 ​​a.b​​,就得这样使用:

watch: { a: { deep: true, handler(newVal) { console.log(newVal) } }}

这样就创建了一个 ​​deep watcher​​​,在 ​​watcher​​​ 执行 ​​get​​ 求值的过程中有这么一段:

try { value = this.getter.call(vm, vm)} catch (e) { //...} finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } //...}

在对 ​​value​​​ 求值后,如果 ​​this.deep​​​ 为true,就执行 ​​traverse(value)​​​ 方法,它的定义在 ​​src/core/observer/traverse.js​​:

import { _Set as Set, isObject } from '../util/index'import type { SimpleSet } from '../util/index'import VNode from '../vdom/vnode'const seenObjects = new Set()/** * Recursively traverse an object to evoke all converted * getters, so that every nested property inside the object * is collected as a "deep" dependency. */export 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​​​ 访问,这样就可以收集到依赖,也就是订阅它们的 ​​watcher​​​,在遍历过程中还会把子响应式对象通过它们的 ​​dep id​​​ 记录到 ​​seenObjects​​,避免以后重复访问。

在执行 ​​traverse​​​ 之后,再对 watch 对象的内部任何一个属性进行监听就都可以调用 ​​watcher​​ 的回调函数。

user watcher

这个在上面有分析到,通过 ​​vm.$watch​​​ 创建的 ​​watcher​​​ 是一个 ​​user watcher​​​,其实源码涉及到它的地方就是 ​​get​​​ 求值方法和 ​​getAndInvoke​​ 调用回调函数了:

get() { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e }},getAndInvoke() { // ... if (this.user) { try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) }}

​​handleError​​ 在 Vue 中是一个错误捕获并且暴露给用户的一个利器。

computed watcher

​​computed watcher​​ 几乎就是为计算属性量身定制的,上面已经对它做了详细的分析,这里就不再赘述了。

sync watcher

这个在 ​​watch​​​ 的 ​​update​​ 方法里:

update () { if (this.computed) { // ... } else if (this.sync) { this.run() } else { queueWatcher(this) }}

当响应式数据发生变化之后,就会触发 ​​watch.update()​​​,然后把这个 ​​watcher​​​ 推到一个队列中,等下一个tick再进行 ​​watcher​​​ 的回调函数,如果设置了 ​​sync​​​,就直接在当前tick中同步执行 ​​watcher​​ 回调函数了。

只有当我们需要 watch 的值的变化到执行 ​​watcher​​ 的回调函数是一个同步过程的时候才会去设置该属性为 true。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:MyBatis ofType和javaType的区别说明
下一篇:如何根据应用场合选择功率分析仪的通讯接口
相关文章

 发表评论

暂时没有评论,来抢沙发吧~