在开发 vue 的时候,咱们可使用 watch 和 computed 很方便的检测数据的变化,从而作出相应的改变,可是在小程序里,只能在数据改变时手动触发 this.setData()
,那么如何给小程序也加上这两个功能呢?html
咱们知道在 vue 里是经过 Object.defineProperty
来实现数据变化检测的,给该变量的 setter 里注入全部的绑定操做,就能够在该变量变化时带动其它数据的变化。那么是否是能够把这种方法运用在小程序上呢?vue
实际上,在小程序里实现要比 vue 里简单,应为对于 data 里对象来讲,vue 要递归的绑定对象里的每个变量,使之响应式化。可是在微信小程序里,无论是对于对象仍是基本类型,只能经过 this.setData()
来改变,这样咱们只需检测 data 里面的 key 值的变化,而不用检测 key 值里面的 key 。git
先上测试代码github
<view>{{ test.a }}</view>
<view>{{ test1 }}</view>
<view>{{ test2 }}</view>
<view>{{ test3 }}</view>
<button bindtap="changeTest">change</button>
复制代码
const { watch, computed } = require('./vuefy.js')
Page({
data: {
test: { a: 123 },
test1: 'test1',
},
onLoad() {
computed(this, {
test2: function() {
return this.data.test.a + '2222222'
},
test3: function() {
return this.data.test.a + '3333333'
}
})
watch(this, {
test: function(newVal) {
console.log('invoke watch')
this.setData({ test1: newVal.a + '11111111' })
}
})
},
changeTest() {
this.setData({ test: { a: Math.random().toFixed(5) } })
},
})
复制代码
如今咱们要实现 watch 和 computed 方法,使得 test 变化时,test一、test二、test3 也变化,为此,咱们增长了一个按钮,当点击这个按钮时,test 会改变。小程序
watch 方法相对简单点,首先咱们定义一个函数来检测变化:微信小程序
function defineReactive(data, key, val, fn) {
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get: function() {
return val
},
set: function(newVal) {
if (newVal === val) return
fn && fn(newVal)
val = newVal
},
})
}
复制代码
而后遍历 watch 函数传入的对象,给每一个键调用该方法数组
function watch(ctx, obj) {
Object.keys(obj).forEach(key => {
defineReactive(ctx.data, key, ctx.data[key], function(value) {
obj[key].call(ctx, value)
})
})
}
复制代码
这里有参数是 fn ,即上面 watch 方法里 test 的值,这里把该方法包一层,绑定 context。微信
接着来看 computed,这个稍微复杂,由于咱们没法得知 computed 里依赖的是 data 里面的哪一个变量,所以只能遍历 data 里的每个变量。dom
function computed(ctx, obj) {
let keys = Object.keys(obj)
let dataKeys = Object.keys(ctx.data)
dataKeys.forEach(dataKey => {
defineReactive(ctx.data, dataKey, ctx.data[dataKey])
})
let firstComputedObj = keys.reduce((prev, next) => {
ctx.data.$target = function() {
ctx.setData({ [next]: obj[next].call(ctx) })
}
prev[next] = obj[next].call(ctx)
ctx.data.$target = null
return prev
}, {})
ctx.setData(firstComputedObj)
}
复制代码
详细解释下这段代码,首先给 data 里的每一个属性调用 defineReactive
方法。接着计算 computed 里面每一个属性第一次的值,也就是上例中的 test二、test3。函数
computed(this, {
test2: function() {
return this.data.test.a + '2222222'
},
test3: function() {
return this.data.test.a + '3333333'
}
})
复制代码
这里分别调用 test2 和 test3 的值,将返回值与对应的 key 值组合成一个对象,而后再调用 setData()
,这样就会第一次计算这两个值,这里使用了 reduce
方法。可是你可能会发现其中这两行代码,它们好像都没有被提到是干吗用的。
ctx.data.$target = function() {
ctx.setData({ [next]: obj[next].call(ctx) })
}
ctx.data.$target = null
复制代码
能够看到,test2 和 test3 都是依赖 test 的,这样必须在 test 改变的时候在其的 setter 函数中调用 test2 和 test3 中对应的函数,并经过 setData 来设置这两个变量。为此,须要将 defineReactive
改动一下。
function defineReactive(data, key, val, fn) {
let subs = [] // 新增
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get: function() {
// 新增
if (data.$target) {
subs.push(data.$target)
}
return val
},
set: function(newVal) {
if (newVal === val) return
fn && fn(newVal)
// 新增
if (subs.length) {
// 用 setTimeout 由于此时 this.data 还没更新
setTimeout(() => {
subs.forEach(sub => sub())
}, 0)
}
val = newVal
},
})
}
复制代码
相较于以前,增长了几行代码,咱们声明了一个变量来保存全部在变化时须要执行的函数,在 set 时执行每个函数,由于此时 this.data.test
的值还未改变,使用 setTimeout 在下一轮再执行。如今就有一个问题,怎么将函数添加到 subs 中。不知道各位仍是否记得上面咱们说到的在 reduce 里的那两行代码。由于在执行计算 test1 和 test2 第一次 computed 值的时候,会调用 test 的 getter 方法,此刻就是一个好机会将函数注入到 subs 中,在 data 上声明一个 $target 变量,并将须要执行的函数赋值给该变量,这样在 getter 中就能够判断 data 上有无 target 值,从而就能够 push 进 subs,要注意的是须要立刻将 target 设为 null,这就是第二句的用途,这样就达到了一石二鸟的做用。固然,这其实就是 vue 里的原理,只不过这里没那么复杂。
到此为止已经实现了 watch 和 computed,可是还没完,有个问题。当同时使用这二者的时候,watch 里的对象的键也同时存在于 data 中,这样就会重复在该变量上调用 Object.defineProperty
,后面会覆盖前面。由于这里不像 vue 里能够决定二者的调用顺序,所以咱们推荐先写 computed 再写 watch,这样能够 watch computed 里的值。这样就有一个问题,computed 会因覆盖而无效。
思考一下为何?
很明显,这时由于以前的 subs 被从新声明为空数组了。这时,咱们想一个简单的方法就是把以前 computed 里的 subs 存在一个地方,下一次调用 defineReactive
的时候看对应的 key 是否已经有了 subs,这样就能够解决问题。修改一下代码。
function defineReactive(data, key, val, fn) {
let subs = data['$' + key] || [] // 新增
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get: function() {
if (data.$target) {
subs.push(data.$target)
data['$' + key] = subs // 新增
}
return val
},
set: function(newVal) {
if (newVal === val) return
fn && fn(newVal)
if (subs.length) {
// 用 setTimeout 由于此时 this.data 还没更新
setTimeout(() => {
subs.forEach(sub => sub())
}, 0)
}
val = newVal
},
})
}
复制代码
这样,咱们就一步一步的实现了所需的功能。完整的代码和例子请戳。
虽然通过了一些测试,但不保证没有其它未知错误,欢迎提出问题。