Vue计算和记忆函数
1的初衷假设如下一段代码,render方法的参数c经常变化,而a和b不经常变化。分析这段代码,我们可以得出一个结论:每次调用render函数,都会触发calc函数的调用。如果a和b参数不变,就会增加不必要的计算。functioncalc(a,b){console.log('calc',a,b)//复杂计算返回a**b}functionrender(a,b,c){console.log('render',a,b,c)return{status:c%10,result:calc(a,b)}}解决这类问题的方法有很多种。我相信你足够聪明,想出如何使用缓存来解决这个问题;这里给大家介绍一下Memoize功能,一种解决问题的方法。2内存函数的原理是将函数参数和计算结果缓存起来,再次调用进行比较:如果参数发生变化,则需要重新计算;如果参数没有变化,则返回上次的计算结果:targetFunc(...args)}lastArgs=argsreturnlastResult}}functionargumentsShallowlyEqual(prev,next){if(prev===null||next===null||prev.length!==next.length){returnfalse}constlength=prev.lengthfor(leti=0;ioption.value,render)memoizedRender()//calcmemoizedRender()memoizedRender()然后修改createMemo方法:functioncreateMemo(...funcs){consttargetFunc=funcs.pop()constdependencies=[...funcs]constmemoizedTargetFunc=defaultMemoize(targetFunc)constselector=defaultMemoize(function(...args){constparams=[]constlength=dependencies.lengthfor(leti=0;i{{b}}
',data(){return{a:1}},computed:{b(){returna+1}}})constDemo2=newVue({template:'
{{b}}{{c}}
',data(){return{a:1}},computed:{b(){console.log('b')returna+1},c(){console.log('c')returnb+a}}})如果a此时改变,那么c和b计算属性中分别打印了多少次?3.2原理实例化一个Vue组件大致有以下过程。从下面的简化代码可以看出,计算属性computed主要是在initComputed方法中初始化的https://github.com/vuejs/vue/blob/dev/src/core/instance/index.js#L8functionVue(选项){this._init(选项)}Vue.prototype._init=function(选项){//...//https://github.com/vuejs/vue/blob/dev/src/core/instance/init.js#L52initLifecycle(vm)initEvents(vm)initRender(vm)callHook(vm,'beforeCreate')initInjections(vm)//解决数据/道具之前的注入initState(vm)}//https://github.com/vuejs/vue/blob/dev/src/core/instance/state.js#L48functioninitState(vm){vm._watchers=[]constopts=vm.$optionsif(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)}}在computed初始化阶段,需要注意的是Vue会遍历computed对象,为每个属性实例化一个lazyWatcher,然后定义每个att恭敬如计算。constcomputedWatcherOptions={lazy:true}functioninitComputed(vm:Component,computed:Object){//$flow-disable-lineconstwatchers=vm._computedWatchers=Object.create(null)//计算属性只是SSR期间的getterconstisSSR=isServerRendering()for(constkeyincomputed){constuserDef=computed[key]constgetter=typeofuserDef==='function'?userDef:userDef.getif(process.env.NODE_ENV!=='production'&&getter==null){warn(`Getterismissingforcomputedproperty"${key}".`,vm)}if(!isSSR){//为计算属性创建内部观察器。watchers[key]=newWatcher(vm,getter||noop,noop,computedWatcherOptions)}//组件定义的计算属性已经定义在//组件原型上。我们只需要在这里定义实例化时定义的计算属性。如果(!(keyinvm)){defineComputed(vm,key,userDef)}elseif(process.env.NODE_ENV!=='production'){if(keyinvm.$data){warn(`计算属性"${key}"已在数据中定义。`,vm)}elseif(vm.$options.props&&keyinvm.$options.props){warn(`Thecomputedproperty"${key}"is已经定义为一个prop.`,vm)}}}}先总结一下,在实例化计算属性的时候,我们主要做的是:遍历每个属性,为每个属性实例化一个lazyWatcher,先为每个属性定义Computed,再往下看是什么defineComputed做了3.2.1defineComputed的过程很简单,类似于定义一个对象defineReactiveconstsharedPropertyDefinition={enumerable:true,configurable:true,get:noop,set:noop}exportfunctiondefineComputed(target:any,key:string,userDef:Object|Function){constshouldCache=!isServerRendering()if(typeofuserDef==='函数'){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}Object.defineProperty(target,key,sharedPropertyDefinition)}functioncreateComputedGetter(key){返回函数computedGetter(){constwatcher=this._computedWatchers&&this._computedWatchers[key]if(watcher){if(watcher.dirty){观察者.evaluate()}if(Dep.target){watcher.depend()}returnwatcher.value}}}这里主要关注computedGetter方法。在此之前,先了解一下watcher的几个属性方法,后面会详细讲:watcher.dirty是标记watcher是否需要重新求值。当依赖发生变化时,dirty会被赋值为true,因为watcher.evaluate需要重新求值。watcher.evaluate所做的是评估。评估完成后,将dirty分配给falsewatcher.depend依赖于当前的Dep.target。比如当前在渲染进程中,Target.target是一个渲染观察者。然后当前计算属性的观察者将被渲染观察者收集。当渲染观察器收集到计算属性的值时,将触发computedGetter方法。第一次调用会触发watcher.evalute计算,中间会有一个依赖收集的过程;计算完成后,会将值缓存起来,之后再次调用计算属性时,不会触发计算属性的计算。你可能在这里有点困惑。其实Watcher就是计算。属性实现的关键,和理解计算属性一样,一定要深入到Watcher的实现。3.2.2lazyWatcher简化后,计算属性相关的代码如下://https://github.com/vuejs/vue/blob/dev/src/core/observer/watcher.js#L26classWatcher{constructor(vm:Component,expOrFn:string|Function,cb:Function,options?:?Object,isRenderWatcher?:boolean){this.vm=vmthis.lazy=!!options.lazythis.dirty=this.lazythis.getter=expOrFn//lazywatcher不会立即计算this.value=this.lazy?undefined:this.get()}get(){pushTarget(this)让值value=this.getter.call(vm,vm)if(this.deep){traverse(value)}popTarget()this.cleanupDeps()returnvalue}update(){/*istanbulignoreelse*///当计算属性的依赖更新时if(this.lazy){this.dirty=true}elseif(this.sync){this.run()}else{queueWatcher(this)}}//计算属性值时触发evaluate()(){this.value=this.get()this.dirty=false}}这部分代码可以分为三个过程来解释:Watcher实例化过程、计算属性值过程、依赖更新过程Watcher实例计算属性的lazy在转换过程中会被复制为false,也就是说如果实例化了一个lazyWatcher,如果当前是一个lazyWatcher,那么它不会立即被求值。当计算属性的计算getter值函数被触发时,会调用watcher.evaluate方法。该方法实际上是调用getter函数(即开发者定义的computed函数)计算结果并将结果缓存到watcher.value中。当watcher.get被调用时,Dep.target会成为当前计算属性的watcher,所以当this.getter被调用时,函数内部的所有依赖都会被当前watcher收集。如果你不知道这里依赖收集的过程,推荐你看一下vue的observer的过程。或者看看我的其他相关文章,了解更多vue响应式的原理。当watcher.evaluate被调用时,dirty会立即被设置为false,所以被计算属性的valuefunction如果后面再触发就不会重新计算了。这样就达到了缓存的效果。依赖更新的过程当计算属性的依赖更新时,会触发计算属性的watcher.update方法。这里不做求值,只是给当前dirty赋值false,表示当前watcher的依赖。已经改变,那么下次调用计算属性时,它会触发重新计算。这里说明一下,当一个计算属性的依赖更新时,这个计算属性不会立即重新计算,而是在调用时重新计算。4综上所述,本文详细分析了内存功能和Vue的计算特性。Vue的计算特性与Vue自身的响应式特性巧妙结合。Redux还可以通过简单的内存函数实现性能优化。原帖:Vuecomputedandmemorizefunctions