在微信小程序里使用watch和computed的方法


Posted in Javascript onAugust 02, 2018

在开发 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
  },
 })
}

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

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
怎么用javascript进行拖拽
Jul 20 Javascript
javascript css styleFloat和cssFloat
Mar 15 Javascript
JavaScript从数组中删除指定值元素的方法
Mar 18 Javascript
jQuery封装的屏幕居中提示信息代码
Jun 08 Javascript
AngularJS入门教程之模块化操作用法示例
Nov 02 Javascript
浅谈jquery的html方法里包含特殊字符的处理
Nov 30 Javascript
JavaScript之浏览器对象_动力节点Java学院整理
Jul 03 Javascript
JavaScript30 一个月纯 JS 挑战中文指南(英文全集)
Jul 23 Javascript
Vue2.0基于vue-cli+webpack父子组件通信(实例讲解)
Sep 14 Javascript
JS/HTML5游戏常用算法之碰撞检测 包围盒检测算法详解【圆形情况】
Dec 13 Javascript
vuex管理状态 刷新页面保持不被清空的解决方案
Nov 11 Javascript
解决vue项目,npm run build后,报路径错的问题
Aug 13 Javascript
在小程序中使用Echart图表的示例代码
Aug 02 #Javascript
node.js读取Excel数据(下载图片)的方法示例
Aug 02 #Javascript
Vue-cli配置打包文件本地使用的教程图解
Aug 02 #Javascript
详解使用VueJS开发项目中的兼容问题
Aug 02 #Javascript
重新认识vue之事件阻止冒泡的实现
Aug 02 #Javascript
深入浅出理解JavaScript高级定时器原理与用法
Aug 02 #Javascript
解决vue router组件状态刷新消失的问题
Aug 01 #Javascript
You might like
第十四节 命名空间 [14]
2006/10/09 PHP
php curl 登录163邮箱并抓取邮箱好友列表的代码(经测试)
2011/04/07 PHP
比较strtr, str_replace和preg_replace三个函数的效率
2013/06/26 PHP
PHP中使用Memache作为进程锁的操作类分享
2015/03/30 PHP
php使用curl通过代理获取数据的实现方法
2016/05/16 PHP
随机显示经典句子或诗歌的javascript脚本
2007/08/04 Javascript
toString()一个会自动调用的方法
2010/02/08 Javascript
jquery 缓存问题的几个解决方法
2013/11/11 Javascript
浅析XMLHttpRequest的缓存问题
2013/12/13 Javascript
fastclick插件导致日期(input[type=&quot;date&quot;])控件无法被触发该如何解决
2015/11/09 Javascript
checkbox批量选中,获取选中项的值的简单实例
2016/06/28 Javascript
js阻止移动端页面滚动的两种方法
2017/01/25 Javascript
bootstrap实现的自适应页面简单应用示例
2017/03/09 Javascript
Javascript中this关键字指向问题的测试与详解
2017/08/11 Javascript
浅谈开发eslint规则
2018/10/01 Javascript
简述pm2常用命令集合及配置文件说明
2019/05/30 Javascript
弱类型语言javascript中 a,b 的运算实例小结
2019/08/07 Javascript
vue+Element-ui实现登录注册表单
2020/11/17 Javascript
[01:41]DOTA2超级联赛专访YYF 称一辈子难忘TI2
2013/05/28 DOTA
Python3爬虫学习之应对网站反爬虫机制的方法分析
2018/12/12 Python
对python 读取线的shp文件实例详解
2018/12/22 Python
Python使用Beautiful Soup爬取豆瓣音乐排行榜过程解析
2019/08/15 Python
浅谈ROC曲线的最佳阈值如何选取
2020/02/28 Python
python 利用matplotlib在3D空间中绘制平面的案例
2021/02/06 Python
连卡佛中国官网:Lane Crawford中文站
2018/01/27 全球购物
英国HYPE双肩包官网:英国本土时尚潮牌
2018/09/26 全球购物
公司董事长职责
2013/12/12 职场文书
五心教育心得体会
2014/09/04 职场文书
酒店餐厅2014重阳节活动策划方案
2014/09/16 职场文书
党支部书记岗位职责
2015/02/15 职场文书
甲午大海战观后感
2015/06/02 职场文书
女性健康讲座主持词
2015/07/04 职场文书
2015年学校教研室主任工作总结
2015/07/20 职场文书
Nginx的反向代理实例详解
2021/03/31 Servers
Python 中的Sympy详细使用
2021/08/07 Python
一小时迅速入门Mybatis之bind与多数据源支持 Java API
2021/09/15 Javascript