小程序里实现 watch 和 computed
小程序里的自定义组件里是有数据监听器的,可以监听对应数据的变化来执行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的更多相关文章
- 在微信小程序里使用 watch 和 computed
在开发 vue 的时候,我们可以使用 watch 和 computed 很方便的检测数据的变化,从而做出相应的改变,但是在小程序里,只能在数据改变时手动触发 this.setData(),那么如何给小 ...
- 微信小程序里使用 Redux 状态管理
微信小程序里使用 Redux 状态管理 前言 前阵子一直在做小程序开发,采用的是官方给的框架 wepy , 如果还不了解的同学可以去他的官网查阅相关资料学习:不得不说的是,这个框架确相比于传统小程序开 ...
- 微信小程序里如何用阿里云上传视频,图片。。
纯手写,踩了半天多的坑干出来了... 网上也有对于阿里云如何在微信小程序里使用,但是很不全,包括阿里云文档的最佳实践里. 话不多说上代码了. upvideo(){ var aliOssParams = ...
- 微信小程序里实现跑马灯效果
在微信小程序 里实现跑马灯效果,类似滚动字幕或者滚动广告之类的,使用简单的CSS样式控制,没用到JS wxml: <!-- 复制的跑马灯效果 --> <view class=&quo ...
- 微信小程序里碰到的坑和小知识
本文作者:dongtao 来自:授权地址 本人低级程序员,以下bug不能确保在其它地方可以以相同的原因复现.同时, 出现很多bug的原因是小程序的基本知识还有编码的基本功不到位造成 路还很长,共勉 ...
- 微信小程序里的bug---video 的play()
微信小程序hidden转换后执行play()用真机测试不会播放.在调试器里可以. 解决方法,把hidden换成wx:if. 我刚开始以为网速问题,其实不是, 具体我也不知道为什,换上wxif解决了.
- 微信小程序里解决app.js onLaunch事件与小程序页面的onLoad加载前后异常问题
使用 Promise 解决小程序页面因为需要app.js onLaunch 参数导致的请求失败 app.js onLaunch 的代码 "use strict"; Object.d ...
- 微信小程序里使用阿里巴巴矢量图标
登录 阿里巴巴矢量图标 (https://www.iconfont.cn) 选中图标,加入购物车图标 下载源代码 解析出来如下文件结构 有两种使用方式: 1)不转换成base64的文件 找到 icon ...
- 小程序里打开app的实现过程
之前开发过类似得需求,也踩了一些小坑,在这里和大家分享下,毕竟这样的需求也不在少数,基本上产品后期都会有这样的需求: 官方说明 因为需要用户主动触发才能打开 APP,所以该功能不由 API 来调用,需 ...
随机推荐
- [golang]Go net.lookup包
DNS (Domain Name System 的缩写)的作用非常简单,就是根据域名查出IP地址. 域名系统(通常被称为“DNS”)是一个网络系统,允许我们把对人类友好的名称解析为唯一的地址. Int ...
- [提权]sudo提权复现(CVE-2019-14287)
2019年10月14日, sudo 官方在发布了 CVE-2019-14287 的漏洞预警. 0x00 简介 sudo 是所有 unix操作系统(BSD, MacOS, GNU/Linux) 基本集成 ...
- docker本地仓库&镜像
镜像的命名规则: 1.[冷数据]/[base镜像]例如:ansible,centos 2. lastest{最新的意思} 不是真的(随便命名) 3. [image name]=[repository ...
- 【Vue.js游戏机实战】- Vue.js实现九宫格水果机抽奖游戏总结
大家好!先上图看看本次案例的整体效果. 完整版实战课程附源码:[Vue.js游戏机实战]- Vue.js实现九宫格水果机抽奖 实现思路: Vue component实现九宫格水果机组件,可以嵌套到任意 ...
- Git的使用(4) —— 分支的概念和使用
1. 概念 在SVN中,分支并不是很便于使用.但是在Git中,分支就变成了特别好用的功能呢,受到大多数使用者的青睐. 分支中有几个概念: (1) 分支:分支就是每一次提交创建的点连接成的线. (2) ...
- 第十七周助教工作总结——NWNU李泓毅
助教博客链接:https://www.cnblogs.com/NWNU-LHY/ 本次作业的要求:软件测试与ALPHA冲刺:https://www.cnblogs.com/nwnu-daizh/p/1 ...
- T-MAX-测试总结
一.项目相关: 作业相关 具体描述 所属班级 2019秋福大软件工程实践Z班 作业要求 团队作业第五次-项目冲刺 作业正文 T-MAX组--测试总结 团队名称 T-MAX小组 作业目标 将团队的项目做 ...
- TL-WR941N路由器刷DD-WRT和OPENWRT教程及使用花生壳
今天没事做,于是决定把自己的TL-WR941N路由器刷成OPENWRT系统的.虽然说没买小米路由,但是刷成OPENWRT系统的话还是能增强不少的功能.下面写出经过一下午折腾的详细安装步骤,同样适用于其 ...
- GWAS 全基因组关联分析 | summary statistic 概括统计 | meta-analysis 综合分析
有很多概念需要明确区分: 人有23对染色体,其中22对常染色体autosome,另外一对为性染色体sex chromosome,XX为女,XY为男. 染色体区带命名:在标示一特定的带时需要包括4项:① ...
- SQL中join和cross join的区别
SQL中的连接可以分为内连接,外连接,以及交叉连接 . 1. 交叉连接CROSS JOIN 如果不带WHERE条件子句,它将会返回被连接的两个表的笛卡尔积,返回结果的行数等于两个表行数的乘积: 举例, ...