小程序里的自定义组件里是有数据监听器的,可以监听对应数据的变化来执行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. Ceph osd故障恢复

    1  调高osd的日志等级 加上红框那一行就可以了 osd的日志路径:/var/log/ceph/ceph-osd.3.log 注意:加上了这一行后日志会刷很多,所以要特别注意日志容量的变化,以防把v ...

  2. 关于连接sftp以及本地配置sftp的事情

    1.window下配置sftp服务器 参考:https://blog.csdn.net/zhangliang_571/article/details/45598939 下载:http://www.fr ...

  3. Spring boot druid 的配置使用

    依赖加入 <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artif ...

  4. 【JVM】虚拟机字节码执行引擎

    概念模型上,典型的帧栈结构如下(栈是线程私有的,也就是每个线程都会有自己的栈).                     典型的帧栈结构 局部变量表 存放方法参数和方法内部定义的局部变量.在编译阶段, ...

  5. 第10组 Alpha冲刺(2/6)

    链接部分 队名:女生都队 组长博客: 博客链接 作业博客:博客链接 小组内容 恩泽(组长) 过去两天完成了哪些任务 描述 了解了如何根据系统获取的实际情况进行后端任务的调整 网易云音乐推荐算法的分析 ...

  6. html5中hgroup和address标签使用总结

    html5中hgroup和address标签使用总结 一.总结 一句话总结: hgroup元素(不推荐使用):用来给标题分组,通常放在header中: address元素:斜体显示:用来说明作者的联系 ...

  7. 与 ES5 相比,React 的 ES6 语法有何不同?

    以下语法是 ES5 与 ES6 中的区别: 1.require 与 import // ES5 var React = require('react'); // ES6 import React fr ...

  8. Wamp 本地访问特别慢,原因在这

      Wamp 本地访问特别慢.打开空的页面都要400ms,彻底疯了     什么localhost改为127.0.0.1 什么 清理日志缓存,都不好使, 重点在Xdebug,安装了Xdebug之后变慢 ...

  9. 更换python版本后出现 No module named "apt_pkg"

    本文链接:https://blog.csdn.net/jaket5219999/article/details/78464310 $ sudo apt-get remove --purge pytho ...

  10. openresty开发系列27--openresty中封装redis操作

    openresty开发系列27--openresty中封装redis操作 在关于web+lua+openresty开发中,项目中会大量操作redis, 重复创建连接-->数据操作-->关闭 ...