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方法添加的函数设置立即执行