在开发 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 &amp;&amp; fn(newVal)
val = newVal
},
})
}

然后遍历 watch 函数传入的对象,给每个键调用该方法


function watch(ctx, obj) {
Object.keys(obj).forEach(key =&gt; {
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 =&gt; {
defineReactive(ctx.data, dataKey, ctx.data[dataKey])
})
let firstComputedObj = keys.reduce((prev, next) =&gt; {
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 &amp;&amp; fn(newVal)
// 新增
if (subs.length) {
// 用 setTimeout 因为此时 this.data 还没更新
setTimeout(() =&gt; {
subs.forEach(sub =&gt; 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 &amp;&amp; fn(newVal)
if (subs.length) {
// 用 setTimeout 因为此时 this.data 还没更新
setTimeout(() =&gt; {
subs.forEach(sub =&gt; sub())
}, 0)
}
val = newVal
},
})
}

这样,我们就一步一步的实现了所需的功能。完整的代码和例子请戳

虽然经过了一些测试,但不保证没有其它未知错误,欢迎提出问题。

原文地址:https://segmentfault.com/a/1190000014109601

在微信小程序里使用 watch 和 computed的更多相关文章

  1. 微信小程序里如何用阿里云上传视频,图片。。

    纯手写,踩了半天多的坑干出来了... 网上也有对于阿里云如何在微信小程序里使用,但是很不全,包括阿里云文档的最佳实践里. 话不多说上代码了. upvideo(){ var aliOssParams = ...

  2. 微信小程序里使用 Redux 状态管理

    微信小程序里使用 Redux 状态管理 前言 前阵子一直在做小程序开发,采用的是官方给的框架 wepy , 如果还不了解的同学可以去他的官网查阅相关资料学习:不得不说的是,这个框架确相比于传统小程序开 ...

  3. 微信小程序里实现跑马灯效果

    在微信小程序 里实现跑马灯效果,类似滚动字幕或者滚动广告之类的,使用简单的CSS样式控制,没用到JS wxml: <!-- 复制的跑马灯效果 --> <view class=&quo ...

  4. 微信小程序里碰到的坑和小知识

    本文作者:dongtao   来自:授权地址 本人低级程序员,以下bug不能确保在其它地方可以以相同的原因复现.同时, 出现很多bug的原因是小程序的基本知识还有编码的基本功不到位造成 路还很长,共勉 ...

  5. 微信小程序里的bug---video 的play()

    微信小程序hidden转换后执行play()用真机测试不会播放.在调试器里可以. 解决方法,把hidden换成wx:if. 我刚开始以为网速问题,其实不是, 具体我也不知道为什,换上wxif解决了.

  6. 微信小程序里使用阿里巴巴矢量图标

    登录 阿里巴巴矢量图标 (https://www.iconfont.cn) 选中图标,加入购物车图标 下载源代码 解析出来如下文件结构 有两种使用方式: 1)不转换成base64的文件 找到 icon ...

  7. 在微信小程序里自动获得当前手机所在的经纬度并转换成地址

    效果:我在手机上打开微信小程序,自动显示出我当前所在的地理位置: 具体步骤: 1. 使用微信jssdk提供的getLocation API拿到经纬度: 2. 调用高德地图的api使用经纬度去换取地址的 ...

  8. 记录微信小程序里自带 时间格式 工具

    微信小程序里面自己给了一个时间工具,是用来记录log日志,感觉可以记录下来,所以拿来自己用,以此记录: 直接传入 日期对象 进入 formatTime //得到下面格式的时间格式2017/07/22 ...

  9. 微信小程序里多出来的奇怪宽度

    最近在看微信小程序相关的东西,写页面的时候,因为一直以Iphone6作为标准调试(微信官方建议以Iphone6调试),下面以实际的案例讲解以rpx会引发的问题 wxml: <view class ...

随机推荐

  1. LeetCode OJ 之 Valid Anagram

    题目: Given two strings s and t, write a function to determine if t is an anagram of s. For example, s ...

  2. swift 拼图小游戏

    依据这位朋友的拼图小游戏改编 http://tangchaolizi.blog.51cto.com/3126463/1571616 改编主要地方是: 原本着我仁兄的代码时支持拖动小图块来移动的,我參照 ...

  3. hive学习路线

    hive学习路线图:

  4. Android updater-scripts(Edify Script)各函数详细说明【转】

    本文转载自:http://blog.csdn.net/kwuwei/article/details/40616909 这是Android系统来运行updater-scripts的Edify语言的基本介 ...

  5. PCB Windows远程桌面一键登录

    开发时会经常需远程操作服务器,每次运行再也熟悉不过的命令 mstsc 或 mstsc -v  120.79.36.65 远程到目标服务器, 每次需输入密码,弹出烦人的 如何免密码一键登录呢,其实微软已 ...

  6. css3 选择器 权重问题 (第一部分)

    其实我现在写的这些博文笔记都是我很早之前学习的时候所写的笔记,只不过之前是没有写博客的习惯,所以都是写在word文档中个人需要的时候看而已.最近刚刚开了博客,所以想将自己的笔记贴到博客. 但是现在看来 ...

  7. golang单点推送

    package main import ( "encoding/json" "flag" "fmt" "log" &qu ...

  8. SQL之LEFT JOIN,EIGHT JOIN,INSERT JOIN的区别

    left join(左联接) 返回包括左表中的所有记录和右表中联结字段相等的记录 right join(右联接) 返回包括右表中的所有记录和左表中联结字段相等的记录inner join(等值连接) 只 ...

  9. HDU 4474 Yet Another Multiple Problem BFS

    题意:求m的倍数中不包含一些数码的最小倍数数码是多少.比如15 ,不包含0  1 3,答案是45. BFS过程:用b[]记录可用的数码.设一棵树,树根为-1.树根的孩子是所有可用的数码,孩子的孩子也是 ...

  10. Java中last_insert_id的使用

    last_insert_id的作用是:在当前表中的主键是自增时,插入一条新记录的同时使用last_insert_id可以获取当前的新记录的主键id. 下面是一个例子: import java.sql. ...