150行代码带你实现微信小程序中的数据侦听


Posted in Javascript onMay 17, 2019

在小程序项目中, 我们的通常会使用到使用到一个全局对象作为各个页面通用的数据存储容器, 将它绑定到app对象后, 就能在每一个页面都自由的操纵这个对象. 然而在实践中, 由于这个对象及其属性不具备响应式条件, 它不能直接参与业务逻辑的编写, 能力仅仅局限于数据储存. 若是在VueJS项目中, 我们可能经常使用到 Vue.$watch 去侦听某个数据是否发生变化, 小程序却缺乏这种能力.

在这篇文章中, 我将用150行代码, 手把手带你打造一个小程序也可以使用的侦听器(下简称VX):

// 一个快速赋值的语法糖函数, 可以创建结构为 { value: a { b: { val: ''} } } 的对象
vx.set('value.a.d', { val: '' })
// 对某个属性进行侦听, 如果发生改变, 则执行相应函数(可多次watch以执行多个函数)
vx.watch('value.a.d.val', newVal => {
 console.log(`val改变为 : `, newVal)
})
value.a.d.val = 3 // val改编为 : 3

使用VX侦听器, 我们可以更加方便的管理各个页面的状态. 同时, 我们凭借 watch 语法, 可以更优雅地编写业务逻辑.

坐稳了, 三轮车准备启动了~ 各位评论见~ :yum:

稍微理一理思路

在全局对象中, 我们不一定要对每一个属性都进行侦听, 所以VX主要的功能就是通过set去设置某个具体属性的setter/getter, 同时通过watch向添加该属性添加需要订阅的回调函数.

依赖对象的实现

首先我们需要造一个通用的 依赖对象 , 依赖对象携带一个订阅数组用于存放一组回调函数, 同时它还应该包括一些操作订阅数组能力(如添加订阅, 清空订阅)的函数

class Dep {
 constructor () {
 this.subs = []
 }
 // 将回调添加到数组中
 addSub (fn) { /*...*/ }
 delSub (fn) { /*...*/ }
 // 执行数组中每一项函数
 notify (newVal, oldVal) {
 this.subs.forEach(func => func(newVal, oldVal))
 }
}

全局对象中每一个响应式属性(及其每一个子属性), 都应该和一个新的Dep实例保持一一对应的关系, 这样我们进行侦听变化, 执行订阅的回调函数时, 只需要找到对应的实例执行 notify 通知更新即可.

设置响应式属性

defineProperty

可能是因为接触DefineProperty要比接触Proxy早一些的缘故, 代码使用了前者进行响应式的实现, Object.defineProperty方法会直接在一个对象上定义一个新属性, 这里快速过一遍 defineProperty 具体配置:

// @param obj 要在其上定义属性的对象
// @param key 要定义或修改的属性的名称
Object.defineProperty(obj, key, {
 // 该属性是否能被枚举
 enumerable: true,
 // 该属性能否被删除
 configurable: true,
 // 访问该属性则会执行此方法
 get: () => {
 return val
 },
 // 修改该属性时会执行此方法
 set: newVal => {
 val = newVal
 },
 // value & writeble 不能和 getter/setter 同时出现
})

通过对defineProperty进行上层封装, 我们可以轻松的实现在全局对象上设置响应式属性功能, 在此, 我们结合刚才定义的Dep对象, 将一个新的dep实例绑定到新增属性的setter中:

set (key, val, options = {}, obj = this.store) {
 const dep = new Dep()
 Object.defineProperty(obj, key, {
 enumerable: true,
 configurable: true,
 get: () => {
  return val
 },
 set: newVal => {
  if (newVal === val) {
  return
  }
  dep.notify(newVal, val)
  val = newVal
 }
 })
}

每当对应属性被赋值, 就会执行依赖数组中的回调函数.

不过这样还不够, 我们还得想办法获取到这个dep实例, 才能给它的依赖数组填充函数.

这边提供一个很简单的思路, 并不推荐实践中这么做:

set (key, val, options = {}, obj = this.store) {
 const dep = new Dep()
 Object.defineProperty(obj, key, {})
+ return dep
}
const valueDep = set('value', b, {})
valueDep.addSub(() => { console.log('value changed!') })

虽然代码能使用了, 就是是看起来怪怪的~ :yum: 我们的三轮车开进了岔路~

通过watch添加订阅

喝口水我们继续

<黑客与画家>一书中曾经提到这样一个观点, 我深有体会:

如果你觉得你的代码奇怪, 那么往往它是错的

上面的那一串代码仅仅是能跑通的水平, 我们需要加入更多的细节和思考, 有时候只需要坐下来稍微看一下代码, 就会有各种想法蹦出来:

构思这种东西有一个特点,那就是它会导致更多的构思。你有没有注意过,坐下来写东西的时候,一半的构思是写作时产生的?

隐藏Dep

这些内容应和外部是解耦的. 首先一点, 我们创建一个侦听器类, 用于封装我们侦听所用到的所有方法, 它包含了我们想要的全局对象以及操作它的方法(如watch,set):

class VX {
 constructor () {
 this.store = Object.create(null)
 }
 watch (key, fn, obj = this.store) {}
 set (key, val, options = {}, obj = this.store) {}
}
const vx = new VX()

我们可以在watch中给对象某个属性添加回调, 就不用去直接操作Dep依赖数组了. 只是, 我们在业务代码中调用watch, 要怎么去获取obj.key对应的dep呢?

我们设置一个全局的depHandler, 在obj.key的getter中主动将depHandler设置为当前obj.key的dep实例, 那么我们在watch函数里, 只要用任意操作触发obj.key的getter, 就能通过depHandler得到它的dep实例了, 代码形如:

+ // 一开始没有持有dep实例
+ let handleDep = null
 class VX {
 watch (key, fn, obj = this.store) {
+  console.log(obj.key) // 使用任意操作触发obj.key的getter, 那么handleDep将自动引用obj.key的dep实例
+  handleDep.addSub(fn)
 }
 set (key, val, options = {}, obj = this.store) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: () => {
+   handleDep = dep
   return val
  },
  set: newVal => {}
  })
 }
 }

主动收集依赖

我们增加 handleDep.addSub(fn) 添加回调函数的逻辑, 其实可以直接放到getter中, 首先在Dep类中封装一个'主动'收集依赖的 collect 方法, 他会将全局 handleFn 存放到订阅数组中, 这样一来, 在watch函数中, 我们只要触发obj.key的getter, 就可以主动收集依赖了:

let handleFn = null
class Dep {
 addSub (fn) {}
 delSub (fn) {}
 clear () {}
 collect (fn = handleFn) {
 if (fn && !this.subs.find(x => x === fn)) {
  this.addSub(fn)
 }
 }
 notify (newVal, oldVal) {}
}

let handleDep = null
class VX {
 watch (key, fn, obj = this.store) {
 handleFn = fn
 console.log(obj.key)
 }
 set (key, val, options = {}, obj = this.store) {
 const dep = new Dep()
 Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: () => {
  handleDep = dep
  handleDep.collect()
  return val
  },
  set: newVal => {}
 })
 }
}

处理key值为对象链的情况

在先前的watch函数中, 我们使用console.log(obj.key)去触发对应属性的getter, 如果我们调用方式是 watch('a.b.c') 就无能为力了. 这里我们封装一个通用方法, 用于处理对象链字符串的形式:

// 通过将字符串'a.b'分割为['a', 'b'], 再使用一个while循环就可以走完这个对象链
function walkChains (key, obj) {
 const segments = key.split('.')
 let deepObj = obj
 while (segments.length) {
 deepObj = deepObj[segments.shift()]
 }
}
class VX {
 watch (key, fn, obj = this.store) {
 handleFn = fn
 walkChains(key, obj)
 }
}

在set方法中处理对象链字符串稍微有些不同, 因为如果 set('a.b') 时, 没有在我们全局对象中找到a属性, 这里应该抛错.

实际的处理中, 需要推断'obj.a'以及'obj.a.b'是否存在. 假设没有'obj.a', 那么我们应该创建一个新的对象, 并且给新的对象添加属性'b', 所以代码类似 walkChains 函数, 只是稍作一层判断:

set (key, val, obj) {
 const segments = key.split('.')
 // 这里需要注意, 我们只处理到倒数第二个属性
 while (segments.length > 1) {
 const handleKey = segments.shift()
 const handleVal = obj[handleKey]
 // 存在'obj.a'的情况
 if (typeof handleVal === 'object') {
  obj = handleVal
 // 不存在'obj.a'则给a属性赋一个非响应式的对象
 } else if (!handleVal) {
  obj = (
  key = handleKey,
  obj[handleKey] = {},
  obj[handleKey]
  )
 } else {
  console.trace('already has val')
 }
 }
 // 最后一个属性要手动赋值
 key = segments[0]
}

业务场景应用

小程序跨页面刷新数据

我们经常碰到在小程序中由A页面跳转到B页面, 如果B页面进行了一些操作, 希望A页面自动刷新数据的情况. 但是由于A页面跳转到B页面不同(也许是redirect,也许是navigate), 处理方法也不尽相同.

使用navigate方式跳转后, A页面不会被注销, 所以我们一般会通过全局对象去贮存A页面实例(也就是A页面的this对象), 然后在B页面直接调用相应的方法(如A.refreshList())进行刷新操作.

引入VX后, 我们可以在 onload 生命周期直接调用watch方法添加订阅:

// app.js
import VX from '@/utils/suites/vx'
const vx = new VX()
app.vx = vx
app.store = vx.store
app.vx.set('userType', '商户')

// page a
onLoad () {
 app.vx.watch('userType', userType => {
 if (userType === '商户') {
  // ...
 } else if (userType === '管理员') {
  // ...
 }
 }, {
 immediate: true
 })
}

// page b
switchUserType () {
 app.store.userType = '管理员'
}

可能遇到的问题

给watch方法添加的函数设置立即执行

有的时候我们希望通过watch添加函数的同时还立即执行该函数一次, 这个时候我们需要再定义额外的参数传递到watch中. 问题是这个函数不一定是同步函数.

简单处理如下:

class VX {
 async watch (key, fn, options = { immediately: false }, obj = this.store) {
 handleDep = fn
 walkChains(key, obj)
 options.immediately && await fn(options.defaultParams)
 }
}

this绑定丢失问题

在我在对VX进行删除属性方法的扩展时, 我往walkChain函数中添加了一个执行回调函数的机制, 并且在删除属性这个方法直接调用了walkChain:

+ function walkChains (key, obj, fn) {
 const segments = key.split('.')
 let deepObj = obj
 while (segments.length) {
  deepObj = deepObj[segments.shift()]
+  fn && fn()
 }
 }
del (key, obj = this.store) {
 walkChains(key, obj, handleDep.clear)
 delete obj[key]
}

因为handleDep.clear当成参数传递进walkChains中会 丢失this绑定 , 所以上面那段代码其实是有问题的, 不过稍作修改就好了:

del (key, obj = this.store) {
+ walkChains(key, obj, () => handleDep.clear())
 delete obj[key]
 }

总结

以上所述是小编给大家介绍的150行代码带你实现微信小程序中的数据侦听,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

Javascript 相关文章推荐
HTML TO JavaScript 转换
Jun 26 Javascript
jquery ajax例子返回值详解
Sep 11 Javascript
javascript实现依次输入input自动定焦
Dec 23 Javascript
基于Bootstrap实现Material Design风格表单插件 附源码下载
Apr 18 Javascript
浅谈jQuery 选择器和dom操作
Jun 07 Javascript
js 获取元素所有兄弟节点的实现方法
Sep 06 Javascript
JS取模、取商及取整运算方法示例
Oct 13 Javascript
使用Curl命令查看请求响应时间方法
Nov 04 Javascript
VueJS如何引入css或者less文件的一些坑
Apr 25 Javascript
vue-cli常用设置总结
Feb 24 Javascript
angularJs自定义过滤器实现手机号信息隐藏的方法
Oct 08 Javascript
Vue项目配置跨域访问和代理proxy设置方式
Sep 08 Javascript
angular4应用中输入的最小值和最大值的方法
May 17 #Javascript
jQuery实现的点击显示隐藏下拉菜单功能完整示例
May 17 #jQuery
详解 微信小程序开发框架(MINA)
May 17 #Javascript
Vue模板语法中数据绑定的实例代码
May 17 #Javascript
jQuery控制input只能输入数字和两位小数的方法
May 16 #jQuery
微信小程序云开发详细教程
May 16 #Javascript
atom-design(Vue.js移动端组件库)手势组件使用教程
May 16 #Javascript
You might like
PHP exif扩展方法开启详解
2014/07/28 PHP
详解PHP导入导出CSV文件
2014/11/03 PHP
Yii框架参数化查询中IN查询只能查询一个的解决方法
2017/05/20 PHP
php的常量和变量实例详解
2017/06/27 PHP
IOS 开发之NSDictionary转换成JSON字符串
2017/08/14 PHP
让任务管理器中的CPU跳舞的js代码
2008/11/01 Javascript
dropdownlist之间的互相联动实现(显示与隐藏)
2009/11/24 Javascript
javascript 兼容所有浏览器的DOM扩展功能
2012/08/01 Javascript
web的各种前端打印方法之jquery打印插件PrintArea实现网页打印
2013/01/09 Javascript
jQuery图片旋转插件jQueryRotate.js用法实例(附demo下载)
2016/01/21 Javascript
基于jQuery实现带动画效果超炫酷的弹出对话框(附源码下载)
2016/02/22 Javascript
JS正则表达式之非捕获分组用法实例分析
2016/12/28 Javascript
js处理包含中文的字符串实例
2017/10/11 Javascript
在vue.js中使用JSZip实现在前端解压文件的方法
2018/09/05 Javascript
你了解vue3.0响应式数据怎么实现吗
2019/06/07 Javascript
vue-router 控制路由权限的实现
2020/09/24 Javascript
[54:10]完美世界DOTA2联赛PWL S2 Magma vs FTD 第二场 11.29
2020/12/03 DOTA
编写Python小程序来统计测试脚本的关键字
2016/03/12 Python
Python实现的生产者、消费者问题完整实例
2018/05/30 Python
Python绘制频率分布直方图的示例
2019/07/08 Python
纯python进行矩阵的相乘运算的方法示例
2019/07/17 Python
python能开发游戏吗
2020/06/11 Python
如何利用input事件来监听移动端的输入
2016/04/15 HTML / CSS
Skyscanner香港:机票比价, 平机票和廉价航空机票预订
2020/02/07 全球购物
实习单位接收函模板
2014/01/10 职场文书
年度考核自我评价
2014/01/25 职场文书
户外拓展活动方案
2014/02/11 职场文书
学习党代会心得体会
2014/09/05 职场文书
完整版商业计划书
2014/09/15 职场文书
机关党总支领导班子整改方案
2014/09/20 职场文书
2014年置业顾问工作总结
2014/11/17 职场文书
四年级小学生评语
2014/12/26 职场文书
财务出纳岗位职责
2015/03/31 职场文书
安全温馨提示语大全
2015/07/14 职场文书
学习商务礼仪心得体会
2016/01/22 职场文书
我们认为中短波广播场强仪的最佳组合
2022/04/05 无线电