小程序里实现 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() {
        return 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() {
        return 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的封装改造!!!

原文地址:https://www.cnblogs.com/hjj2ldq/p/11930037.html

时间: 2024-10-08 10:44:39

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

微信小程序里各种文件是干什么的?

微信小程序首次进入 其他地方略过,这里讲一下,为什么要勾选"在当前目录中创建quick start项目",quick start项目相当于word文档中的模板,可以在模板的基础上直接修改. 创建好了以后,会看到这个 中间的部分的目录,就是微信小程序的工程结构重要的是图中的这些文件分别是干什么的? 这些文件可以分为四类,分别是以js.wxml.wxss和json结尾的文件.以js结尾的文件,一般情况下是负责功能的,比如,点击一个按钮,按钮就会变颜色. 以wxml为后缀的文件,一般情况下负

小程序里使用es7的async await语法

我们做小程序开发时,有时候想让自己代码变得整洁,异步操作时避免回调地狱.我们会使用es6的promise. es7的async,await .promise在小程序和云开发的云函数里都可以使用. async和await只能在云开发的云函数里使用.我们在小程序的代码里直接使用,就会报如下错误. 这个报错就是告诉我们不能在小程序里直接使用es7的async和await语法.但是这么好的语法我们用起来确实显得代码整洁,逼格高.那接下来我就教大家如何在小程序代码里使用es7的async和await语法.

小程序里input宽度与文字显示的问题

不知道是不是bug,微信小程序里input宽度缩小,input可输入文字的区域会缩小的更多,比如说你把input宽度设置为90%,则input文字输入可显示的区域可能只有80%左右. //(存在疑点=>)目前的解决方法:在input输入框外面套一层view,通过改变view的宽度控制input的长度,这样不会影响文字显示 注意 input框上 不能加display:flex 属性 原文地址:https://www.cnblogs.com/panghu123/p/12178187.html

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

使用 Promise 解决小程序页面因为需要app.js onLaunch 参数导致的请求失败 app.js onLaunch 的代码 1 "use strict"; 2 Object.defineProperty(exports, "__esModule", { 3 value: true 4 }); 5 const http = require('./utils/http.js'); 6 const api = require('./config.js'); 7

在微信小程序里自动获得当前手机所在的经纬度并转换成地址

效果:我在手机上打开微信小程序,自动显示出我当前所在的地理位置: 具体步骤: 1. 使用微信jssdk提供的getLocation API拿到经纬度: 2. 调用高德地图的api使用经纬度去换取地址的文字描述. wx.ready(() => { wx.getLocation({ type: "gcj02", success: function(res) { var location = "&location=" + res.longitude + &q

在小程序里添加跳转外部链接web-view(使用的是uni-app)

1.创建一个空的文件 2.在文件夹里放置web-view <view> <web-view :src="url" :progress="false"> </web-view> </view> 3.在js里接收传递过来的链接 <script> export default { data() { return { url: '' }; }, onLoad(val) { this.url = decodeURIC

小程序里禁止页面滑动

<view class="modal_bg" catchtouchmove="true" fixed="true" wx:if="{{ modal_show }}"></view> 原文地址:https://www.cnblogs.com/Basu/p/8434665.html

微信小程序里自定义组件,canvas组件没有效果

methods: { /** * el:画圆的元素 * r:圆的半径 * w:圆的宽度 * 功能:画背景 */ drawCircleBg: function (el, r, w) { const ctx = wx.createCanvasContext(el); ctx.setLineWidth(w);// 设置圆环的宽度 ctx.setStrokeStyle('#E5E5E5'); // 设置圆环的颜色 ctx.setLineCap('round') // 设置圆环端点的形状 ctx.begi

小程序里的自定义组件:组件的外部样式externalClasses的使用

启用外部样式: 自定义组件: v-tag 在html 引入 在组件写外部样式的css : .ex-tag { background-color: #fffbdd ; } 在组件使用该外部的样式 这个时候我们发现没有效果 我们应该使用!important 强制覆盖样式 .ex-tag { background-color: #fffbdd !important; } 效果: 原文地址:https://www.cnblogs.com/guangzhou11/p/11470590.html