computed
计算属性的初始化是发生在Vue实例初始化阶段的initState函数中,执行了
if (opts.computed) initComputed(vm, opts.computed)
initComputed定义在src/core/instance/state.js中
const computedWatcherOptions = { lazy: true }function initComputed (vm: Component, computed: Object) {// $flow-disable-line// watchers、vm._computedWatchers 空对象const watchers = vm._computedWatchers = Object.create(null)// computed properties are just getters during SSRconst isSSR = isServerRendering()// 对computed对象做遍历for (const key in computed) {// 拿到计算属性的每一个userDefconst userDef = computed[key]// 获取userDef对应的getter函数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)}// 给每个getter创建一个watcherif (!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.// 判断key是不是vm属性if (!(key in vm)) { // 不是的话defineComputed(vm, key, userDef)} else if (process.env.NODE_ENV !== 'production') {// 否则判断计算属性key是否已经被data或者prop所占用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)} else if (vm.$options.methods && key in vm.$options.methods) {warn(`The computed property "${key}" is already defined as a method.`, vm)}}}}
watcher 和渲染 watcher 有一点很大的不同,它是一个 computed watcher,因为
const computedWatcherOptions = { lazy: true }
defineComputed方法
export function defineComputed (target: any,key: string,userDef: Object | Function) {const shouldCache = !isServerRendering()if (typeof userDef === 'function') {sharedPropertyDefinition.get = shouldCache? createComputedGetter(key): createGetterInvoker(userDef)sharedPropertyDefinition.set = noop} else {sharedPropertyDefinition.get = userDef.get? shouldCache && userDef.cache !== false? createComputedGetter(key): createGetterInvoker(userDef.get): noopsharedPropertyDefinition.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给计算属性对应的key值添加getter和setterObject.defineProperty(target, key, sharedPropertyDefinition)}
setter 通常是计算属性是一个对象,并且拥有 set 方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有 setter 的情况比较少
getter 对应的是 createComputedGetter(key) 的返回值
function createComputedGetter (key) {// computedGetter 计算属性对应的getterreturn function computedGetter () {const watcher = this._computedWatchers && this._computedWatchers[key]if (watcher) {if (watcher.dirty) {watcher.evaluate()}if (Dep.target) {watcher.depend()}return watcher.value}}}
分析 computed watcher 的实现
计算属性是一个computed watcher,它和普通的watcher有什么区别呢
例子
var vm = new Vue({data: {firstName: 'Foo',lastName: 'Bar'},computed: {fullName: function () {return this.firstName + ' ' + this.lastName}}})
初始化这个computed watcher实例时构造函数部分逻辑略有不同
export default class Watcher {// ...constructor (vm: Component,expOrFn: string | Function,cb: Function,options?: ?Object,isRenderWatcher?: boolean) {// ...// 对于computed watcher this.value值为undefined 并不会立刻求值this.value = this.lazy? undefined: this.get()}// ...}
然后当render函数执行访问到this.fullname时就触发了计算属性的getter,它会拿到计算属性对应的watcher,然后执行watcher.depend()
/*** Depend on all deps collected by this watcher.*/depend () {let i = this.deps.lengthwhile (i--) {this.deps[i].depend()}}
然后再执行watcher.evaluate()去求值
/*** Evaluate the value of the watcher.* This only gets called for lazy watchers.*/evaluate () {this.value = this.get()this.dirty = false}
在求值过程中会执行 value = this.getter.call(vm, vm) 实际上就是执行了计算属性定义的getter函数
依赖的数据变化后的逻辑
一旦对计算属性依赖的数据修改则会触发setter过程,通知所有订阅它变化的watcher更新,执行watcher.update()方法
/*** Subscriber interface.* Will be called when a dependency changes.*/update () {/* istanbul ignore else */if (this.lazy) {this.dirty = true} else if (this.sync) {this.run()} else {queueWatcher(this)}}
计算属性本质上就是一个 computed watcher,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher 重新渲染,本质上是一种优化
watch
监听属性的初始化也是发生在Vue的实例初始化阶段的initState函数中,在computed初始化之后,执行了
if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch)}
initWatch定义在src/core/instance/state.js中
function initWatch (vm: Component, watch: Object) {for (const key in watch) {const handler = watch[key]// Vue是支持watch的同一个key对应多个handlerif (Array.isArray(handler)) {for (let i = 0; i < handler.length; i++) {createWatcher(vm, key, handler[i])}} else {createWatcher(vm, key, handler)}}}
createWatcher方法
function createWatcher (vm: Component,expOrFn: string | Function,handler: any,options?: Object) {// 对hanlder类型做判断 拿到它最终的回调函数if (isPlainObject(handler)) {options = handlerhandler = handler.handler}if (typeof handler === 'string') {handler = vm[handler]}// $watch是Vue原型上的方法,它是在执行stateMixin时定义的return vm.$watch(expOrFn, handler, options)}
vm.$watch定义在src/core/instance/state.js中
Vue.prototype.$watch = function (expOrFn: string | Function,cb: any,options?: Object): Function {const vm: Component = this// 判断是否是对象 $watch方法是用户可以直接调用的,它可以传递一个对象也可以传递函数if (isPlainObject(cb)) {return createWatcher(vm, expOrFn, cb, options)}options = options || {}options.user = true// 实例化一个watcher(user watcher) 因为options.user = trueconst watcher = new Watcher(vm, expOrFn, cb, options)if (options.immediate) {const info = `callback for immediate watcher "${watcher.expression}"`pushTarget()invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)popTarget()}return function unwatchFn () {watcher.teardown()}}
通过实例化 watcher 的方式,一旦 watch 的数据发送变化,它最终会执行 watcher 的 run 方法,执行回调函数 cb,并且如果设置了 immediate 为 true,则直接会执行回调函数 cb。最后返回了一个 unwatchFn 方法,它会调用 teardown 方法去移除这个 watcher
本质上侦听属性也是基于 Watcher 实现的,它是一个 user watcher
Watcher options
Watcher 支持了不同的类型,梳理一下它有哪些类型以及它们的作用
Watcher的构造函数对options做了处理
// optionsif (options) {this.deep = !!options.deepthis.user = !!options.userthis.lazy = !!options.lazythis.sync = !!options.syncthis.before = options.before} else {this.deep = this.user = this.lazy = this.sync = false}
deep watcher
需要对对象做深度观测时设置这个属性为true
var vm = new Vue({data() {a: {b: 1}},watch: {a: {handler(newVal) {console.log(newVal)}}}})vm.a.b = 2
这个时候是不会 log 任何数据的,因为 watch 了 a 对象,只触发了 a 的 getter,并没有触发 a.b 的 getter,所以并没有订阅它的变化,导致对 vm.a.b = 2 赋值的时候,虽然触发了 setter,但没有可通知的对象,所以也并不会触发 watch 的回调函数了
只需要对代码做稍稍修改,就可以观测到这个变化了
watch: {a: {deep: true,handler(newVal) {console.log(newVal)}}}
这样就创建了一个deep watcher了,在watcher执行get求值的过程中有判断这部分的代码
// "touch" every property so they are all tracked as// dependencies for deep watchingif (this.deep) {traverse(value)}
在对watch的表达式或者函数求值后会调用traverse函数
定义在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) {// 对一个对象做深层递归遍历// 遍历过程就是对一个子对象的访问,会触发它们的getter过程,这样就可以收集到依赖,也就是订阅它们变化的watcher_traverse(val, seenObjects)seenObjects.clear()}function _traverse (val: any, seen: SimpleSet) {let i, keysconst isA = Array.isArray(val)if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {return}if (val.__ob__) {// 把子响应式对象通过它们的depId记录到seenObjects, 避免以后重复访问const depId = val.__ob__.dep.idif (seen.has(depId)) {return}seen.add(depId)}if (isA) {i = val.lengthwhile (i--) _traverse(val[i], seen)} else {keys = Object.keys(val)i = keys.lengthwhile (i--) _traverse(val[keys[i]], seen)}}
在执行了traverse后再对watch的对象内部任何一个值做修改也会调用watcher的回调函数
如果观测了一个复杂对象,并且会改变对象内部深层某个值的时候也希望触发回调,一定要设置 deep 为 true,但是因为设置了 deep 后会执行 traverse 函数,会有一定的性能开销,所以一定要根据应用场景权衡是否要开启这个配置
user watcher
通过vm.$watch创建的watcher是一个user watcher,在对watcher求值以及在执行回调函数时会处理一下错误
computed watcher (lazy watcher)
sync watcher
当响应式数据发送变化后,触发了 watcher.update(),只是把这个 watcher 推送到一个队列中,在 nextTick 后才会真正执行 watcher 的回调函数。而一旦设置了 sync,就可以在当前 Tick 中同步执行 watcher 的回调函数
update () {/* istanbul ignore else */if (this.lazy) {this.dirty = true} else if (this.sync) {this.run()} else {queueWatcher(this)}}
只有需要 watch 的值的变化 到 执行 watcher 的回调函数 是一个同步过程的时候才会去设置该属性为 true
了解了 watcher 的 4 个 options,通常会在创建 user watcher 的时候配置 deep 和 sync,可以根据不同的场景做相应的配置
