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 &amp; DHTML 实例编程(教程)基础知识
Jun 02 Javascript
JavaScript 放大镜 移动镜片效果代码
May 09 Javascript
jquery mobile事件多次绑定示例代码
Sep 13 Javascript
重写document.write实现无阻塞加载js广告(补充)
Dec 12 Javascript
jquery zTree异步加载简单实例讲解
Feb 25 Javascript
jquery实现页面常用的返回顶部效果
Mar 04 Javascript
全面解析JavaScript中“&amp;&amp;”和“||”操作符(总结篇)
Jul 18 Javascript
iscroll实现下拉刷新功能
Jul 18 Javascript
微信小程序登录对接Django后端实现JWT方式验证登录详解
Jul 29 Javascript
layui radio点击事件实现input显示和隐藏的例子
Sep 02 Javascript
小程序自定义导航栏兼容适配所有机型(附完整案例)
Apr 26 Javascript
JavaScript 实现页面滚动动画
Apr 24 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数据库配置文件一般做法分享
2012/07/07 PHP
php中socket通信机制实例详解
2015/01/03 PHP
php微信公众平台开发类实例
2015/04/01 PHP
php下载远程大文件(获取远程文件大小)的实例
2017/06/17 PHP
Laravel配合jwt使用的方法实例
2020/10/25 PHP
javascript字典探测用户名工具
2006/10/05 Javascript
JQuery扩展插件Validate 1 基本使用方法并打包下载
2011/09/05 Javascript
jQuery图片轮播的具体实现
2013/09/11 Javascript
关于页面嵌入swf覆盖div层的问题的解决方法
2014/02/11 Javascript
jQuery向后台传入json格式数据的方法
2015/02/13 Javascript
jQuery原型属性和原型方法详解
2015/07/07 Javascript
ion content 滚动到底部会遮住一部分视图的快速解决方法
2016/09/06 Javascript
js定时器实例分享
2016/12/20 Javascript
Angular.js中控制器之间的传值详解
2017/04/24 Javascript
Vue2.0表单校验组件vee-validate的使用详解
2017/05/02 Javascript
详解如何将angular-ui的图片轮播组件封装成一个指令
2017/05/09 Javascript
vue中封装axios并实现api接口的统一管理
2020/12/25 Vue.js
Python实现的文本简单可逆加密算法示例
2017/05/18 Python
基于Python3 逗号代码 和 字符图网格(详谈)
2017/06/22 Python
python放大图片和画方格实现算法
2018/03/30 Python
Python实现随机漫步功能
2018/07/09 Python
python实现全盘扫描搜索功能的方法
2019/02/14 Python
python实现的接收邮件功能示例【基于网易POP3服务器】
2019/09/11 Python
python hmac模块验证客户端的合法性
2020/11/07 Python
洲际酒店集团英国官网:IHG英国
2019/07/10 全球购物
美国健康和保健平台:healtop
2020/07/02 全球购物
群胜软件Java笔试题
2012/09/29 面试题
元旦晚会邀请函
2014/01/27 职场文书
服装电子商务创业计划书
2014/01/30 职场文书
草船借箭教学反思
2014/02/03 职场文书
心理学专业大学生职业生涯规划范文
2014/02/19 职场文书
家庭贫困证明书(3篇)
2014/09/15 职场文书
拾金不昧表扬信
2015/01/16 职场文书
2015年幼儿园保育员工作总结
2015/04/23 职场文书
MySQL数据库10秒内插入百万条数据的实现
2021/11/01 MySQL
MySQL创建管理RANGE分区
2022/04/13 MySQL