小程序里的自定义组件里是有数据监听器的,可以监听对应数据的变化来执行callBack,但是页面Page里没有对应的api就显的很生硬,比如某个数据变了(如切换城市)需要重新刷页面,如果不做监听,每次都要在数据变化的地方手动去调一次函数。

那么如何像vue那样在Page里实现 watch 和 computed 呢 ?如果这时候你脑子里能想到 Obejct.defineProperty 或者 Proxy 那么接下来就慢慢实现吧。

先晒出是这样调用的,请牢记这个调用,后面会反复提到 test2 test3 currentCity:

  this.$computed(this, {
test2: function() {
return this.data.currentCity.cityID + '2222222'
},
test3: function() {
return this.data.currentCity.cityID + '3333333'
}
})
this.$watch(this, {
currentCity(city) {
console.log('回调传值',city)
if (city.cityID) {
this.getHotSpotList()
}
}
})

第一步,先定义一个函数来检测对应属性的变化,每当setter,getter的时候会触发。

function defineReactive(data, key, val, fn) {
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get: function() {
     // 2020.01.06补充 针对对象数组等复杂类型的数据,需要做深拷贝处理,deepClone可自行构造
     // 不做深拷贝就切不断关联,在取this.data.xx的时候会触发get属性,
return deepClone(val)
},
set: function(newVal) {
if (val == newVal) return
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)
})
})
}

上面的方法defineReactive需要稍微改造一下,在set改变值的时候,执行回调函数 fn,且set里新旧值的对比要考虑复杂类型的对比,直接引入lodash的isEqual 方法来对比

function defineReactive(data, key, val, fn) {
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get: function() {
    // 2020.01.06补充  针对对象数组等复杂类型的数据,需要做深拷贝处理,deepClone可自行构造
   // 不做深拷贝就切不断关联,在取this.data.xx的时候会触发get属性,
return deepClone(val)
 }, set: function(newVal) { if (_.isEqual(val,newVal)) return fn && fn(newVal) val = newVal }, }) }

接下来实现 computed,这个会比较麻烦点,有几个注意的地方,1:需要把computed初始的时候传进来的属性算出值并放在this.data里,跟vue是一样的原理。2:每个传进来的属性值都要进行遍历监听变化。

 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])
})
}

基于上面的,我们要补充实现刚才提到的第一点,算出computed对应属性的初始值并设在this.data里

  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) => {
prev[next] = obj[next].call(ctx)
return prev
}, {})
ctx.setData(firstComputedObj)
}

但是现在有个问题,test2 test3 的初始值都算出来了,但后续如果this.data.currentCity变化的时候,test2,test3对应的也要计算出新的值的,这样才是实现了所谓的computed。

那么该如何去处理呢?我们就需要抓住一个时机,当currentCity变化的时候会触发 set,这个时候应该触发一些机制去更新test2,test3.

请注意上面的这行代码:prev[next] = obj[next].call(ctx)

请看obj[next].call(ctx) 调的就是test2,test3对应的function并执行函数,这个时候函数内部的this.data.currentCity 会触发到 get ,就是这个时机,我们能完美的把所有跟currentCity属性相关的其他属性关联到一起。

这个时候触发了get,我们何不把对应的函数记下来,在set的时候去调用,这样就能做到currentCity变化的时候 test2 test3也同步变化。思路大致有了,接下来看代码:

computed 大致如下:

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) })
}
// obj[next].call(ctx) 执行的时候会触发该函数执行,函数内部的this.data相关属性的调用会触发defineReactive.get
prev[next] = obj[next].call(ctx)
ctx.data.$target = null
return prev
}, {})
ctx.setData(firstComputedObj)
}

defineReactive 函数,上面说过在触发currentCity get的时候要记下 test2 test3对应的函数,到了set的时候再去执行,起到cuerrentCity变化的时候,test2,test3 也能同步变化。

上面的  ctx.data.$target 稍微 funtion 后立马再经过 prev[next] = obj[next].call(ctx) 这一句之后,又恢复为null,可能会有点疑惑,上面提过的,你需要注意 prev[next] = obj[next].call(ctx) 中 obj[next] 会触发 test2 test3的 函数,函数里的 this.data.currentCity 会触发自己的get,这个时候我们来把 test2 test3 和 currentCity 关联,在currentcity set的时候,去跟新 test2 test3的值。

defineReactive 的代码需要加个处理,记下test2 test3的处理函数

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 (_.isEqual(newVal,val)) return
fn && fn(newVal)
if (subs.length) {
subs.forEach(sub => sub())
}
val = newVal
},
})
}

这样处理下来,大致基本实现了,接下来需要处理几个坑点,如果fn函数里有取this.data,可能currentCity仍旧是旧的值,明明set里的是新的值,这个涉及到了this.setData异步的问题,咱们需要加个处理。

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 (_.isEqual(newVal,val)) return
// 经过试验,这里的触发要早于setData的回调
// fn && fn(newVal) // 可能setData异步 还没及时完成,newVal 是新的,但是this.data里还是旧的
//这样watch 里去调用对应的方法,可能取的this.data就不是新的
// 如果fn取的是函数形参,那么可以不用setTimeout,但如果是函数里取得this.data就需要
setTimeout(() => {
// 这时候已经完成了setData,fn里取this.data就是最新的
fn && fn(newVal)
}, 0)
if (subs.length) {
// 用 setTimeout 因为此时 this.data 还没更新
// 涉及到微任务,宏任务
setTimeout(() => {
subs.forEach(sub => sub())
}, 0)
// 跟上面那个setTimeout一样,如果函数里用到了this.data,就需要加setTimeout
}
val = newVal
},
})
}

解决完异步的问题,还需要再注意一点:我们在Page里先写了 computed 然后写了 个 watch ,由于 computed初始化完成之后,如上面的 test2 test3 已经添加到 this.data里了,那么在 watch里咱们可以直接对 test2 test3 进行 监听,看上去是挺完美的,但是看 defineReactive 的代码 咱们应该注意,如果由于每次 执行defineReactive subs都是会置空的,那么 computed 就会失效, this.data.currentCity 变化的时候,对应的 test2 test3 的值就得不到更新,因为 subs 都被清空了,currentCity 触发set的时候,subs是空的,很尴尬。。。

那么如何保证 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
}
//val 形成局部作用域保存在函数内部,set的时候会改变该值,所以一直能返回对应的属性值
return val
},
set: function(newVal) { // === 不适用判断复杂类型,所以这里引用lodash中的 isEqual 方法
if (_.isEqual(newVal,val)) return
// console.log('触发set',newVal, new Date().getTime())
// 经过试验,这里的触发要早于setData的回调
// fn && fn(newVal) // 可能setData异步 还没及时完成,newVal 是新的,但是this.data里还是旧的
//这样watch 里去调用对应的方法,可能取的this.data就不是新的
// 如果fn取的是函数形参,那么可以不用setTimeout,但如果是函数里取得this.data就需要
setTimeout(() => {
// 这时候已经完成了setData,fn里取this.data就是最新的
fn && fn(newVal)
}, 0)
if (subs.length) {
// 用 setTimeout 因为此时 this.data 还没更新
// 涉及到微任务,宏任务
setTimeout(() => {
subs.forEach(sub => sub())
}, 0)
// 跟上面那个setTimeout一样,如果函数里用到了this.data,就需要加setTimeout
}
val = newVal
},
})
}
到这里,我们算是完成了 computed 和 watch 的实现了。最好把这两个方法绑定到每个page ,这个过程只要进行mixin就好了,大致思路是对小程序的 Page 对象和 mixin 进行 assign

后续有时间会写一下小程序整个Page的封装改造!!!

小程序里实现 watch 和 computed的更多相关文章

  1. 在微信小程序里使用 watch 和 computed

    在开发 vue 的时候,我们可以使用 watch 和 computed 很方便的检测数据的变化,从而做出相应的改变,但是在小程序里,只能在数据改变时手动触发 this.setData(),那么如何给小 ...

  2. 微信小程序里使用 Redux 状态管理

    微信小程序里使用 Redux 状态管理 前言 前阵子一直在做小程序开发,采用的是官方给的框架 wepy , 如果还不了解的同学可以去他的官网查阅相关资料学习:不得不说的是,这个框架确相比于传统小程序开 ...

  3. 微信小程序里如何用阿里云上传视频,图片。。

    纯手写,踩了半天多的坑干出来了... 网上也有对于阿里云如何在微信小程序里使用,但是很不全,包括阿里云文档的最佳实践里. 话不多说上代码了. upvideo(){ var aliOssParams = ...

  4. 微信小程序里实现跑马灯效果

    在微信小程序 里实现跑马灯效果,类似滚动字幕或者滚动广告之类的,使用简单的CSS样式控制,没用到JS wxml: <!-- 复制的跑马灯效果 --> <view class=&quo ...

  5. 微信小程序里碰到的坑和小知识

    本文作者:dongtao   来自:授权地址 本人低级程序员,以下bug不能确保在其它地方可以以相同的原因复现.同时, 出现很多bug的原因是小程序的基本知识还有编码的基本功不到位造成 路还很长,共勉 ...

  6. 微信小程序里的bug---video 的play()

    微信小程序hidden转换后执行play()用真机测试不会播放.在调试器里可以. 解决方法,把hidden换成wx:if. 我刚开始以为网速问题,其实不是, 具体我也不知道为什,换上wxif解决了.

  7. 微信小程序里解决app.js onLaunch事件与小程序页面的onLoad加载前后异常问题

    使用 Promise 解决小程序页面因为需要app.js onLaunch 参数导致的请求失败 app.js onLaunch 的代码 "use strict"; Object.d ...

  8. 微信小程序里使用阿里巴巴矢量图标

    登录 阿里巴巴矢量图标 (https://www.iconfont.cn) 选中图标,加入购物车图标 下载源代码 解析出来如下文件结构 有两种使用方式: 1)不转换成base64的文件 找到 icon ...

  9. 小程序里打开app的实现过程

    之前开发过类似得需求,也踩了一些小坑,在这里和大家分享下,毕竟这样的需求也不在少数,基本上产品后期都会有这样的需求: 官方说明 因为需要用户主动触发才能打开 APP,所以该功能不由 API 来调用,需 ...

随机推荐

  1. npropress进度条插件的使用

    官网下载地址:http://ricostacruz.com/nprogress/ npropress.css /* Make clicks pass-through */ #nprogress { p ...

  2. Quartz.NET浅谈一 : 简单Job使用(定时发送QQ邮件)

    Quartz.NET是一个全功能的开源作业调度系统,可用于从最小的应用程序到大型企业系统. 直接上代码吧... 一.新建一个控制台项目 略过 二.安装Nuget包 三.创建发送邮箱辅助工具类 stat ...

  3. Redis恢复数据

    对于单点或者集群,都可以用 cat data.txt | redis-cli --pipe方式进行冷恢复. 对于大数据量会很慢,但不会出错.

  4. 利用JDK方式和GuavaAPI方式实现观察者模式

    1.JDK方法实现案例需求: 去餐厅吃饭有时候需要排队,进行排队叫号.假如所有等待的人都是观察者,叫号系统就是一个被监听的主题对象.当叫号系统发出叫号通知后,所有等待的人,都会收到通知,然后检查自己的 ...

  5. mac使用crawlab

    这里采用docker-compose的 第一步拉镜像 docker pull tikazyq/crawlab:latest 第二步修改compose文件 docker-compose.yml文件 ve ...

  6. Python中单引号和双引号的作用

    一.单引号和双引号 在Python中我们都知道单引号和双引号都可以用来表示一个字符串,比如 str1 = 'python' str2 = "python" str1和str2是没有 ...

  7. WINDOWS远程控制LINUX终端XSHELL

    WINDOWS远程控制LINUX终端XSHELL 笔者购买的腾讯云CENTOS7,通过腾讯云的控制台登录,每次都要打开相关网页.输入密码,感觉操作非常不方便. 使用XSHELL远程控制LINUX终端, ...

  8. 两个Double类型相减出现精度丢失问题

    两个Double类型相减出现精度丢失问题 720.50-279.5=440.099999999 而不是440.1 解决方法,将数据库中的类型改为decimal类型,小数精确到2位

  9. ftp配置 Laravel上传文件到ftp服务器

    listen=YES anonymous_enable=NO local_enable=YES write_enable=YES local_umask= dirmessage_enable=YES ...

  10. Understanding glibc malloc

    https://wooyun.js.org/drops/深入理解%20glibc%20malloc.html https://sploitfun.wordpress.com/2015/02/10/un ...