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 相关文章推荐
javascript multibox 全选
Mar 22 Javascript
javascript中window.event事件用法详解
Dec 11 Javascript
使用CSS和jQuery模拟select并附提交后取得数据的代码
Oct 18 Javascript
Js 导出table内容到Excel的简单实例
Nov 19 Javascript
整理的比较全的event对像在ie与firefox浏览器中的区别
Nov 25 Javascript
JSuggest自动匹配下拉框使用方法(示例代码)
Dec 27 Javascript
JQuery中操作Css样式的方法
Feb 12 Javascript
Node.js 异步编程之 Callback介绍(一)
Mar 30 Javascript
举例详解JavaScript中Promise的使用
Jun 24 Javascript
jquery中each循环的简单回滚操作
May 05 jQuery
Node.js连接Sql Server 2008及数据层封装详解
Aug 27 Javascript
Electron vue的使用教程图文详解
Jul 05 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
Smarty安装配置方法
2008/04/10 PHP
PHP的Yii框架中YiiBase入口类的扩展写法示例
2016/03/17 PHP
调用WordPress函数统计文章访问量及PHP原生计数器的实现
2016/03/21 PHP
php使用ftp实现文件上传与下载功能
2017/07/21 PHP
关于锚点跳转及jQuery下相关操作与插件
2012/10/01 Javascript
JQuery中操作Css样式的方法
2014/02/12 Javascript
js对象继承之原型链继承实例
2015/01/10 Javascript
JavaScript截取、切割字符串的技巧
2016/01/07 Javascript
jquery淡入淡出效果简单实例
2016/01/14 Javascript
使用JQuery实现智能表单验证功能
2016/03/08 Javascript
AngularJS 作用域详解及示例代码
2016/08/17 Javascript
解决Window10系统下Node安装报错的问题分析
2016/12/13 Javascript
js实现数组去重方法及效率?Ρ? target=
2017/02/14 Javascript
详解Axios统一错误处理与后置
2018/09/26 Javascript
jQuery实现获取当前鼠标位置并输出功能示例
2019/01/05 jQuery
Vue js with语句原理及用法解析
2020/09/03 Javascript
详解vite2.0配置学习(typescript版本)
2021/02/25 Javascript
[02:30]联想杯DOTA2完美世界全国高校联赛—北京站现场
2015/11/16 DOTA
[02:55]2018DOTA2国际邀请赛勇士令状不朽珍藏Ⅲ饰品一览
2018/08/01 DOTA
python关闭windows进程的方法
2015/04/18 Python
利用Python如何生成随机密码
2016/04/20 Python
Django后台admin的使用详解
2019/07/08 Python
git查看、创建、删除、本地、远程分支方法详解
2020/02/18 Python
Python实现AI自动抠图实例解析
2020/03/05 Python
python 安装impala包步骤
2020/03/28 Python
浅谈JupyterNotebook导出pdf解决中文的问题
2020/04/22 Python
Win10用vscode打开anaconda环境中的python出错问题的解决
2020/05/25 Python
Python 按比例获取样本数据或执行任务的实现代码
2020/12/03 Python
幼师岗位求职简历的自荐信格式
2013/09/21 职场文书
银行实习自我鉴定
2013/10/12 职场文书
高中毕业生登记表自我鉴定范文
2014/03/18 职场文书
生日宴会主持词
2014/03/20 职场文书
党员承诺书格式
2014/05/21 职场文书
音乐节策划方案
2014/06/09 职场文书
严以用权专题学习研讨会发言材料
2015/11/09 职场文书
2016年万圣节活动总结
2016/04/05 职场文书