vue中methods/watch/computed对比分析,watch及computed原理挖掘
Author:zhoulujun Date:
理解Vue中Watch的实现原理和方式之前,你需要深入的理解MVVM的实现原理
vue computed用法
直接跟函数
computed: { fullName: function () { return this.firstName + ' ' + this.lastName } }
需要注意的是,就算在data中没有直接声明出要计算的变量,也可以直接在computed中写入。
也可跟对象
计算属性默认只有getter,可以在需要的时候自己设定setter:
fullName: { // getter get: function () { return this.firstName + ' ' + this.lastName }, // setter set: function (newValue) { var names = newValue.split(' ') this.firstName = names[0] this.lastName = names[names.length - 1] } } }
适用场景:
computed原理分析
Vue响应系统,其核心有三点:observe、watcher、dep:
observe:遍历data中的属性,使用 Object.defineProperty 的get/set方法对其进行数据劫持
dep:每个属性拥有自己的消息订阅器dep,用于存放所有订阅了该属性的观察者对象
watcher:观察者(对象),通过dep实现对响应属性的监听,监听到结果后,主动触发自己的回调进行响应
对响应式系统有一个初步了解后,我们再来分析计算属性。
首先我们找到计算属性的初始化是在src/core/instance/state.js文件中的initState函数中完成的
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 */) } // computed初始化 if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }
调用了initComputed函数(其前后也分别初始化了initData和initWatch)并传入两个参数vm实例和opt.computed开发者定义的computed选项,转到initComputed函数:
// 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) } } } }
从这段代码开始我们观察这几部分:
1、获取计算属性的定义userDef和getter求值函数
const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get
定义一个计算属性有两种写法,一种是直接跟一个函数,另一种是添加set和get方法的对象形式,所以这里首先获取计算属性的定义userDef,再根据userDef的类型获取相应的getter求值函数。
2、计算属性的观察者watcher和消息订阅器dep
watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions )
这里的watchers也就是vm._computedWatchers对象的引用,存放了每个计算属性的观察者watcher实例(注:后文中提到的“计算属性的观察者”、“订阅者”和watcher均指代同一个意思但注意和Watcher构造函数区分),Watcher构造函数在实例化时传入了4个参数:vm实例、getter求值函数、noop空函数、computedWatcherOptions常量对象(在这里提供给Watcher一个标识{computed:true}项,表明这是一个计算属性而不是非计算属性的观察者,我们来到Watcher构造函数的定义:
class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { if (options) { this.computed = !!options.computed } if (this.computed) { this.value = undefined this.dep = new Dep() } else { this.value = this.get() } } get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { } finally { popTarget() } return value } update () { if (this.computed) { if (this.dep.subs.length === 0) { this.dirty = true } else { this.getAndInvoke(() => { this.dep.notify() }) } } else if (this.sync) { this.run() } else { queueWatcher(this) } } evaluate () { if (this.dirty) { this.value = this.get() this.dirty = false } return this.value } depend () { if (this.dep && Dep.target) { this.dep.depend() } } }
为了简洁突出重点,这里我手动去掉了我们暂时不需要关心的代码片段。
观察Watcher的constructor,结合刚才讲到的new Watcher传入的第四个参数{computed:true}知道,对于计算属性而言watcher会执行if条件成立的代码this.dep = new Dep(),而dep也就是创建了该属性的消息订阅器。
export default class Dep { static target: ?Watcher; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(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++) { subs[i].update() } } } Dep.target = null
dep同样精简了部分代码,我们观察Watcher和dep的关系,用一句话总结
watcher中实例化了dep并向dep.subs中添加了订阅者,dep通过notify遍历了dep.subs通知每个watcher更新。
3、defineComputed定义计算属性
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) } }
因为computed属性是直接挂载到实例对象中的,所以在定义之前需要判断对象中是否已经存在重名的属性,defineComputed传入了三个参数:vm实例、计算属性的key以及userDef计算属性的定义(对象或函数)。
然后继续找到defineComputed定义处:
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方法,其中传入的第三个参数是属性描述符sharedPropertyDefinition,初始化为:
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }
随后根据Object.defineProperty前面的代码可以看到sharedPropertyDefinition的get/set方法在经过userDef和 shouldCache等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinition的get函数也就是createComputedGetter(key)的结果,我们找到createComputedGetter函数调用结果并最终改写sharedPropertyDefinition大致呈现如下:
sharedPropertyDefinition = { enumerable: true, configurable: true, get: function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { watcher.depend() return watcher.evaluate() } }, set: userDef.set || noop }
当计算属性被调用时便会执行get访问函数,从而关联上观察者对象watcher。
computed实现整个流程:
当组件初始化的时候,
computed
和data
会分别建立各自的响应系统,Observer
遍历data
中每个属性设置get/set
数据拦截初始化
computed
会调用initComputed
函数注册一个
watcher
实例,并在内实例化一个Dep
消息订阅器用作后续收集依赖(比如渲染函数的watcher
或者其他观察该计算属性变化的watcher
)调用计算属性时会触发其
Object.defineProperty
的get
访问器函数调用
watcher.depend()
方法向自身的消息订阅器dep
的subs
中添加其他属性的watcher
调用
watcher
的evaluate
方法(进而调用watcher
的get
方法)让自身成为其他watcher
的消息订阅器的订阅者,首先将watcher
赋给Dep.target
,然后执行getter
求值函数,当访问求值函数里面的属性(比如来自data
、props
或其他computed
)时,会同样触发它们的get
访问器函数从而将该计算属性的watcher
添加到求值函数中属性的watcher
的消息订阅器dep
中,当这些操作完成,最后关闭Dep.target
赋为null
并返回求值函数结果。当某个属性发生变化,触发
set
拦截函数,然后调用自身消息订阅器dep
的notify
方法,遍历当前dep
中保存着所有订阅者wathcer
的subs
数组,并逐个调用watcher
的update
方法,完成响应更新。
vue watch 用法
watch和computed很相似,watch用于观察和监听页面上的vue实例,当然在大部分情况下我们都会使用computed,但如果要在数据变化的同时进行异步操作或者是比较大的开销,那么watch为最佳选择。watch为一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。
之前注解过《vue watch对象键值说明,immediate属性详解》,computed也是我们常用的监听函数
如果在data中没有相应的属性的话,是不能watch的,这点和computed不一样。
适用场景:
watch实现原理
具体阅读《这一次 彻底理解Vue的watch实现原理及其实现方式》
methods
在某些时候methods和computed看不出来具体的差别,但是一旦在运算量比较复杂的页面中,就会体现出不一样。
computed是具有缓存的,这就意味着只要计算属性的依赖没有进行相应的数据更新,那么computed会直接从缓存中获取值,多次访问都会返回之前的计算结果。
methond每次调用都重新计算。
computed和watch比较
一个是计算,一个是观察,在语义上是有区别的。
计算是通过变量计算来得出数据。而观察是观察一个特定的值,根据被观察者的变动进行相应的变化,在特定的场景下不能相互混用,所以还是需要注意api运用的合理性和语义性。
computed其实只是纯数据操作,需要返回数据结果。
watch就可以监测某个数据发生了变更进行一系列的回调操作
从vue官方文档对watch的解释我们可以了解到,使用 watch 选项允许我们执行异步操作 (访问一个API)或高消耗性能的操作,限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态,而这些都是计算属性无法做到的。
computed是计算一个新的属性,并将该属性挂载到vm(Vue实例)上,而watch是监听已经存在且已挂载到vm上的数据,所以用watch同样可以监听computed计算属性的变化(其它还有data、props)
computed本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值,而watch则是当数据发生变化便会调用执行函数
从使用场景上说,computed适用一个数据被多个数据影响,而watch适用一个数据影响多个数据;
参考文章:
Vue中watch、computed和methods 之间的对比 https://my.oschina.net/rlqmy/blog/1930090
浅谈Vue中计算属性computed的实现原理 https://segmentfault.com/a/1190000016368913?utm_source=tag-newest
转载本站文章《vue中methods/watch/computed对比分析,watch及computed原理挖掘》,
请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/vue/8447.html