c语言sscanf函数的用法是什么
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小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~