在微信小程序里使用 watch 和 computed
在开发 vue 的时候,我们可以使用 watch 和 computed 很方便的检测数据的变化,从而做出相应的改变,但是在小程序里,只能在数据改变时手动触发 this.setData()
,那么如何给小程序也加上这两个功能呢?
我们知道在 vue 里是通过 Object.defineProperty
来实现数据变化检测的,给该变量的 setter 里注入所有的绑定操作,就可以在该变量变化时带动其它数据的变化。那么是不是可以把这种方法运用在小程序上呢?
实际上,在小程序里实现要比 vue 里简单,应为对于 data 里对象来说,vue 要递归的绑定对象里的每一个变量,使之响应式化。但是在微信小程序里,不管是对于对象还是基本类型,只能通过 this.setData()
来改变,这样我们只需检测 data 里面的 key 值的变化,而不用检测 key 值里面的 key 。
先上测试代码
```<view>{{ test.a }}</view>
<view>{{ test1 }}</view>
<view>{{ test2 }}</view>
<view>{{ test3 }}</view>
<button bindtap="changeTest">change</button>
```
const { watch, computed } = require('./vuefy.js')
Page({
data: {
test: { a: 123 },
test1: 'test1',
},
onLoad() {
computed(this, {
test2: function() {
return this.data.test.a + '2222222'
},
test3: function() {
return this.data.test.a + '3333333'
}
})
watch(this, {
test: function(newVal) {
console.log('invoke watch')
this.setData({ test1: newVal.a + '11111111' })
}
})
},
changeTest() {
this.setData({ test: { a: Math.random().toFixed(5) } })
},
})
现在我们要实现 watch 和 computed 方法,使得 test 变化时,test1、test2、test3 也变化,为此,我们增加了一个按钮,当点击这个按钮时,test 会改变。
watch 方法相对简单点,首先我们定义一个函数来检测变化:
function defineReactive(data, key, val, fn) {
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get: function() {
return val
},
set: function(newVal) {
if (newVal === val) return
fn && fn(newVal)
val = newVal
},
})
}
然后遍历 watch 函数传入的对象,给每个键调用该方法
function watch(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 里的每一个变量。
function computed(ctx, obj) {
let keys = Object.keys(obj)
let dataKeys = Object.keys(ctx.data)
dataKeys.forEach(dataKey => {
defineReactive(ctx.data, dataKey, ctx.data[dataKey])
})
let firstComputedObj = 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
return prev
}, {})
ctx.setData(firstComputedObj)
}
详细解释下这段代码,首先给 data 里的每个属性调用 defineReactive
方法。接着计算 computed 里面每个属性第一次的值,也就是上例中的 test2、test3。
computed(this, {
test2: function() {
return this.data.test.a + '2222222'
},
test3: function() {
return this.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
改动一下。
function defineReactive(data, key, val, fn) {
let subs = [] // 新增
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get: function() {
// 新增
if (data.$target) {
subs.push(data.$target)
}
return val
},
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,这样可以 watch computed 里的值。这样就有一个问题,computed 会因覆盖而无效。
思考一下为什么?
很明显,这时因为之前的 subs 被重新声明为空数组了。这时,我们想一个简单的方法就是把之前 computed 里的 subs 存在一个地方,下一次调用 defineReactive
的时候看对应的 key 是否已经有了 subs,这样就可以解决问题。修改一下代码。
function defineReactive(data, key, val, fn) {
let subs = data['$' + key] || [] // 新增
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get: function() {
if (data.$target) {
subs.push(data.$target)
data['$' + key] = subs // 新增
}
return val
},
set: function(newVal) {
if (newVal === val) return
fn && fn(newVal)
if (subs.length) {
// 用 setTimeout 因为此时 this.data 还没更新
setTimeout(() => {
subs.forEach(sub => sub())
}, 0)
}
val = newVal
},
})
}
这样,我们就一步一步的实现了所需的功能。完整的代码和例子请戳。
虽然经过了一些测试,但不保证没有其它未知错误,欢迎提出问题。
原文地址:https://segmentfault.com/a/1190000014109601
在微信小程序里使用 watch 和 computed的更多相关文章
- 微信小程序里如何用阿里云上传视频,图片。。
纯手写,踩了半天多的坑干出来了... 网上也有对于阿里云如何在微信小程序里使用,但是很不全,包括阿里云文档的最佳实践里. 话不多说上代码了. upvideo(){ var aliOssParams = ...
- 微信小程序里使用 Redux 状态管理
微信小程序里使用 Redux 状态管理 前言 前阵子一直在做小程序开发,采用的是官方给的框架 wepy , 如果还不了解的同学可以去他的官网查阅相关资料学习:不得不说的是,这个框架确相比于传统小程序开 ...
- 微信小程序里实现跑马灯效果
在微信小程序 里实现跑马灯效果,类似滚动字幕或者滚动广告之类的,使用简单的CSS样式控制,没用到JS wxml: <!-- 复制的跑马灯效果 --> <view class=&quo ...
- 微信小程序里碰到的坑和小知识
本文作者:dongtao 来自:授权地址 本人低级程序员,以下bug不能确保在其它地方可以以相同的原因复现.同时, 出现很多bug的原因是小程序的基本知识还有编码的基本功不到位造成 路还很长,共勉 ...
- 微信小程序里的bug---video 的play()
微信小程序hidden转换后执行play()用真机测试不会播放.在调试器里可以. 解决方法,把hidden换成wx:if. 我刚开始以为网速问题,其实不是, 具体我也不知道为什,换上wxif解决了.
- 微信小程序里使用阿里巴巴矢量图标
登录 阿里巴巴矢量图标 (https://www.iconfont.cn) 选中图标,加入购物车图标 下载源代码 解析出来如下文件结构 有两种使用方式: 1)不转换成base64的文件 找到 icon ...
- 在微信小程序里自动获得当前手机所在的经纬度并转换成地址
效果:我在手机上打开微信小程序,自动显示出我当前所在的地理位置: 具体步骤: 1. 使用微信jssdk提供的getLocation API拿到经纬度: 2. 调用高德地图的api使用经纬度去换取地址的 ...
- 记录微信小程序里自带 时间格式 工具
微信小程序里面自己给了一个时间工具,是用来记录log日志,感觉可以记录下来,所以拿来自己用,以此记录: 直接传入 日期对象 进入 formatTime //得到下面格式的时间格式2017/07/22 ...
- 微信小程序里多出来的奇怪宽度
最近在看微信小程序相关的东西,写页面的时候,因为一直以Iphone6作为标准调试(微信官方建议以Iphone6调试),下面以实际的案例讲解以rpx会引发的问题 wxml: <view class ...
随机推荐
- 幻世(OurDream)TM 2D图形引擎开通捐赠渠道
为了支持幻世(OurDream)TM 2D图形引擎更好的发展,同一时候也是为了给希望支持引擎发展的朋友一个安全快捷的渠道,Lizcst Software Lab于今日正式在官方旗舰店增设了一个捐赠捐款 ...
- ICM Technex 2017 and Codeforces Round #400 (Div. 1 + Div. 2, combined) C. Molly's Chemicals
感觉自己做有关区间的题目方面的思维异常的差...有时简单题都搞半天还完全没思路,,然后别人提示下立马就明白了...=_= 题意:给一个含有n个元素的数组和k,问存在多少个区间的和值为k的次方数. 题解 ...
- HDU1010-奇偶剪枝(DFS)
题目链接:Tempter of the Bone 第一次做剪枝的题目,剪枝,说实话研究的时间不短.好像没什么实质性的进展,遇到题目.绝对有会无从下手的感觉,剪枝越来越神奇了. .. . HDU1010 ...
- oc55--ARC单个对象的内存管理
// Person.h #import <Foundation/Foundation.h> @interface Person : NSObject @end // Person.m #i ...
- Android WiFi/WiFi热点开发总结
首先看一下WiFi的自我介绍: Wi-Fi是一种允许电子设备连接到一个无线局域网(WLAN)的技术,通常使用2.4G UHF或5G SHF ISM 射频频段.连接到无线局域网通常是有密码保护的:但也可 ...
- C# 文件的一些基本操作(转)//用C#读写ini配置文件
C# 文件的一些基本操作 2009-07-19 来自:博客园 字体大小:[大 中 小] 摘要:介绍C#对文件的一些基本操作,读写等. using System;using System.IO;us ...
- UESTC--1251--谕神的密码(贪心)
谕神的密码 Time Limit: 1000MS Memory Limit: 65535KB 64bit IO Format: %lld & %llu Submit Status ...
- hdu1150——最小点覆盖
As we all know, machine scheduling is a very classical problem in computer science and has been stud ...
- 如何在ashx处理页中获取Session值
本文章摘自:http://www.cnblogs.com/vihone/archive/2010/06/04/1751490.html 在一般事务处理页面,可以轻松的得到 Request,Respon ...
- simpleOS 1.0
做了一个so simple的OS,本不好意思多说的....不过还是说下吧. 首先,买不起开发板的我没有办完完成一件事,那就是保存任务上下文,因为这个过程实际上是将寄存器的值存放到任务堆栈中去的. 而要 ...