在微信小程序里使用watch和computed的方法
在开发vue的时候,我们可以使用watch和computed很方便的检测数据的变化,从而做出相应的改变,但是在小程序里,只能在数据改变时手动触发this.setData(),那么如何给小程序也加上这两个功能呢?
我们知道在vue里是通过Object.defineProperty来实现数据变化检测的,给该变量的setter里注入所有的绑定操作,就可以在该变量变化时带动其它数据的变化。那么是不是可以把这种方法运用在小程序上呢?
实际上,在小程序里实现要比vue里简单,应为对于data里对象来说,vue要递归的绑定对象里的每一个变量,使之响应式化。但是在微信小程序里,不管是对于对象还是基本类型,只能通过this.setData()来改变,这样我们只需检测data里面的key值的变化,而不用检测key值里面的key。
先上测试代码
{{test.a}} {{test1}} {{test2}} {{test3}} change
const{watch,computed}=require('./vuefy.js') Page({ data:{ test:{a:123}, test1:'test1', }, onLoad(){ computed(this,{ test2:function(){ returnthis.data.test.a+'2222222' }, test3:function(){ returnthis.data.test.a+'3333333' } }) watch(this,{ test:function(newVal){ console.log('invokewatch') this.setData({test1:newVal.a+'11111111'}) } }) }, changeTest(){ this.setData({test:{a:Math.random().toFixed(5)}}) }, })
现在我们要实现watch和computed方法,使得test变化时,test1、test2、test3也变化,为此,我们增加了一个按钮,当点击这个按钮时,test会改变。
watch方法相对简单点,首先我们定义一个函数来检测变化:
functiondefineReactive(data,key,val,fn){ Object.defineProperty(data,key,{ configurable:true, enumerable:true, get:function(){ returnval }, set:function(newVal){ if(newVal===val)return fn&&fn(newVal) val=newVal }, }) }
然后遍历watch函数传入的对象,给每个键调用该方法
functionwatch(ctx,obj){ Object.keys(obj).forEach(key=>{ defineReactive(ctx.data,key,ctx.data[key],function(value){ obj[key].call(ctx,value) }) }) }
这里有参数是fn,即上面watch方法里test的值,这里把该方法包一层,绑定context。
接着来看computed,这个稍微复杂,因为我们无法得知computed里依赖的是data里面的哪个变量,因此只能遍历data里的每一个变量。
functioncomputed(ctx,obj){ letkeys=Object.keys(obj) letdataKeys=Object.keys(ctx.data) dataKeys.forEach(dataKey=>{ defineReactive(ctx.data,dataKey,ctx.data[dataKey]) }) letfirstComputedObj=keys.reduce((prev,next)=>{ ctx.data.$target=function(){ ctx.setData({[next]:obj[next].call(ctx)}) } prev[next]=obj[next].call(ctx) ctx.data.$target=null returnprev },{}) ctx.setData(firstComputedObj) }
详细解释下这段代码,首先给data里的每个属性调用defineReactive方法。接着计算computed里面每个属性第一次的值,也就是上例中的test2、test3。
computed(this,{ test2:function(){ returnthis.data.test.a+'2222222' }, test3:function(){ returnthis.data.test.a+'3333333' } })
这里分别调用test2和test3的值,将返回值与对应的key值组合成一个对象,然后再调用setData(),这样就会第一次计算这两个值,这里使用了reduce方法。但是你可能会发现其中这两行代码,它们好像都没有被提到是干嘛用的。
ctx.data.$target=function(){ ctx.setData({[next]:obj[next].call(ctx)}) } ctx.data.$target=null
可以看到,test2和test3都是依赖test的,这样必须在test改变的时候在其的setter函数中调用test2和test3中对应的函数,并通过setData来设置这两个变量。为此,需要将defineReactive改动一下。
functiondefineReactive(data,key,val,fn){ letsubs=[]//新增 Object.defineProperty(data,key,{ configurable:true, enumerable:true, get:function(){ //新增 if(data.$target){ subs.push(data.$target) } returnval }, set:function(newVal){ if(newVal===val)return fn&&fn(newVal) //新增 if(subs.length){ //用setTimeout因为此时this.data还没更新 setTimeout(()=>{ subs.forEach(sub=>sub()) },0) } val=newVal }, }) }
相较于之前,增加了几行代码,我们声明了一个变量来保存所有在变化时需要执行的函数,在set时执行每一个函数,因为此时this.data.test的值还未改变,使用setTimeout在下一轮再执行。现在就有一个问题,怎么将函数添加到subs中。不知道各位还是否记得上面我们说到的在reduce里的那两行代码。因为在执行计算test1和test2第一次computed值的时候,会调用test的getter方法,此刻就是一个好机会将函数注入到subs中,在data上声明一个$target变量,并将需要执行的函数赋值给该变量,这样在getter中就可以判断data上有无target值,从而就可以push进subs,要注意的是需要马上将target设为null,这就是第二句的用途,这样就达到了一石二鸟的作用。当然,这其实就是vue里的原理,只不过这里没那么复杂。
到此为止已经实现了watch和computed,但是还没完,有个问题。当同时使用这两者的时候,watch里的对象的键也同时存在于data中,这样就会重复在该变量上调用Object.defineProperty,后面会覆盖前面。因为这里不像vue里可以决定两者的调用顺序,因此我们推荐先写computed再写watch,这样可以watchcomputed里的值。这样就有一个问题,computed会因覆盖而无效。
思考一下为什么?
很明显,这时因为之前的subs被重新声明为空数组了。这时,我们想一个简单的方法就是把之前computed里的subs存在一个地方,下一次调用defineReactive的时候看对应的key是否已经有了subs,这样就可以解决问题。修改一下代码。
functiondefineReactive(data,key,val,fn){ letsubs=data['$'+key]||[]//新增 Object.defineProperty(data,key,{ configurable:true, enumerable:true, get:function(){ if(data.$target){ subs.push(data.$target) data['$'+key]=subs//新增 } returnval }, set:function(newVal){ if(newVal===val)return fn&&fn(newVal) if(subs.length){ //用setTimeout因为此时this.data还没更新 setTimeout(()=>{ subs.forEach(sub=>sub()) },0) } val=newVal }, }) }
这样,我们就一步一步的实现了所需的功能。完整的代码和例子请戳。
虽然经过了一些测试,但不保证没有其它未知错误,欢迎提出问题。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。