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图表动画插件Highcharts Examples
Apr 16 Javascript
js导出txt示例代码
Jan 14 Javascript
node.js中的fs.readdirSync方法使用说明
Dec 17 Javascript
JavaScript返回网页中锚点数目的方法
Apr 03 Javascript
js+html5绘制图片到canvas的方法
Jun 05 Javascript
最简单的tab切换实例代码
May 13 Javascript
AngularJS实现数据列表的增加、删除和上移下移等功能实例
Sep 05 Javascript
D3.js实现雷达图的方法详解
Sep 22 Javascript
Javarscript中模块(module)、加载(load)与捆绑(bundle)详解
May 28 Javascript
Node.js 的模块知识汇总
Aug 16 Javascript
vue .sync修饰符的使用详解
Jun 15 Javascript
React更新渲染原理深入分析
Dec 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 preg_replace替换实例讲解
2013/11/04 PHP
php读取csv文件并输出的方法
2015/03/14 PHP
PHP面向对象程序设计实例分析
2016/01/26 PHP
PHP中加速、缓存扩展的区别和作用详解(eAccelerator、memcached、xcache、APC )
2016/07/09 PHP
javascript[js]获取url参数的代码
2007/10/17 Javascript
jQuery bxCarousel实现图片滚动切换效果示例代码
2013/05/15 Javascript
JS获取select-option-text_value的方法
2013/12/26 Javascript
JS弹出层单纯的绝对定位居中示例代码
2014/02/18 Javascript
深入理解JavaScript系列(33):设计模式之策略模式详解
2015/03/03 Javascript
JavaScript中的关联数组问题
2015/03/04 Javascript
Bootstrap每天必学之媒体对象
2015/11/30 Javascript
nodejs入门教程一:概念与用法简介
2017/04/24 NodeJs
Javascript ES6中对象类型Sets的介绍与使用详解
2017/07/17 Javascript
echarts学习笔记之图表自适应问题详解
2017/11/22 Javascript
jQuery实现表格隔行换色
2018/09/01 jQuery
Vue如何获取数据列表展示
2019/12/11 Javascript
Angular 多模块项目构建过程
2020/02/13 Javascript
[01:10:16]DOTA2上海特级锦标赛B组资格赛#2 Fnatic VS Spirit第一局
2016/02/27 DOTA
解决python写的windows服务不能启动的问题
2014/04/15 Python
Python实现从百度API获取天气的方法
2015/03/11 Python
python使用multiprocessing模块实现带回调函数的异步调用方法
2015/04/18 Python
python的mysqldb安装步骤详解
2017/08/14 Python
浅谈Python实现贪心算法与活动安排问题
2017/12/19 Python
Python pyautogui模块实现鼠标键盘自动化方法详解
2020/02/17 Python
CSS3 简单又实用的5个属性
2010/03/04 HTML / CSS
利用Canvas模仿百度贴吧客户端loading小球的方法示例
2017/08/13 HTML / CSS
寻找迷宫的一条出路,o通路;X:障碍
2016/07/10 面试题
人力资源主管岗位职责
2014/01/29 职场文书
学生会感恩节活动方案
2014/10/11 职场文书
单位租房协议范本
2014/12/03 职场文书
2014年中职班主任工作总结
2014/12/16 职场文书
小学生作文评语集锦
2014/12/25 职场文书
幸福来敲门观后感
2015/06/04 职场文书
三八红旗手先进事迹材料(2016推荐版)
2016/02/25 职场文书
Memcached介绍及php-memcache扩展安装
2021/04/01 PHP
Python序列化模块JSON与Pickle
2022/06/05 Python