在微信小程序中实现数据绑定、自动更新(一)

我们在开发微信小程序通常会因为实例和数据池的数据推送而烦恼
通俗的说这样一个痛点就是,什么时候setData,在哪setData,怎么把实例的数据中取出来进行setData

那么有没有一种方法
当我们对变量或实例修改后,就可以自动的渲染界面而不用再setData呢?
当然有!

分析需求测试可行性

在微信小程序中

Page({
data:{
    //存放数据
    test:{
        content:"helloworld"
    }
}
})
<block>{{test.content}}</block>

即可显示出“helloworld”

这表示前端页面对数据具备多层访问的能力

继续,如果是访问一个不存在的数据呢

<block>{{test.content2.nodata}}</block>

我们将得到一个为空的页面,并没有任何报错,即是具备访问安全性的

那么我们把一个对象放在data中也能正常访问吗?经过测试这也是可行的。
也就是说我们完全可以把任何Object放在data中。

通过阅读微信小程序的开发文档,我们可以知道

如果你放置一个对象在data中,在前端进行渲染时会进行 JSON.stringify + JSON.parse 对数据进行一次操作后再安全的引用,但不会修改源数据

所以,如果我们在data中放置一个类,也是能被正常引用的

class Person{
    constructor(name){
        this.name = name;
    }
}

let myPerson = new Person("张三");
let pageInstance;
Page({
  data:{
      person:myPerson
  },
  onLoad(){
    pageInstance = this;
  }
})
<block>{{person.name}}</block>

前端取值时等同于

JSON.parse(JSON.stringify(data)).?person.?name => "张三"

其中.?表示安全取值

那么如果需要修改名字则需要

myPerson.name = "李四";
pageInstance.setData({
  person:myPerson
})

总结需求

我们需要在设置名字的同时就能够立即响应到页面,也就是说

myPerson.name = "李四";//的同时,页面就得到了更新

解决方案 Proxy + 语法欺骗

  • 我们可以通过Proxy对类的实例进行链式(深度)监听,并生成事件响应来进行setData
  • 但Proxy会造成类的实例方法提示功能丢失,并不方便Coding,所以需要进行构造函数的语法欺骗

代码部分

我们先来看一下经过修改后的实例

// pages/myPage/myPage.js

const { EasyPage, LaunchEnv, EasyModule } = require("../../lib/EasyAPP/EasyAPP");

class Person{
  constructor(name){
    this.name = name;

    //语法欺骗,同时为该类进行链式(深度)监听
    return new EasyModule(this);
  }

}

class MyPage extends EasyPage{

  async onShow() {//同微信小程序自带的onShow方法
    if(await Platform.isLogin_sync()){

      if(Platform.getUser().credits == undefined){
        //获取用户Credits
        Platform.getUser().getCredits();
      }
    }

  }
}

new LaunchEnv({
  page:new MyPage (),
  data:{
    Person:new Person("张三");
  }
})

可以看到,我们替换了微信原生的使用Page()来启动页面的方式
通过定义Mine对象,来设置页面的事件。

再通过new LaunchEnv()来生成界面,并定义它的的数据,而这次,我们简单明了的传入了一个Person的对象
如果此时任何引用去修改person的name属性,都将自动发起界面的更新
同时,在微信小程序的热更新下,这种链接性也不会丢失。

如此一来,我们可以将编程模式和前端数据“完全隔离”开来,而不必将功能混杂在各个页面中,我们只需潜心开发我们的业务相关的类,在任何时候,任何地方甚至控制台,都能修改数据并显示在屏幕上。
前端需要什么数据直接访问即可。

位于"../../lib/EasyAPP/EasyAPP"下的代码EasyAPP.js
在之后的章节中,将对代码进行功能刨析


// Auther MingZeY
// Email: 1552904342@qq.com

/**
 * 微信小程序自带的一些APP方法提示接口
 */
class AppInterface{
  /**
   * 生命周期回调——监听小程序初始化。  
   * 小程序初始化完成时触发,全局只触发一次。参数也可以使用 wx.getLaunchOptionsSync 获取。  
   * 参数:与 wx.getLaunchOptionsSync 一致  
   */
  onLaunch(obj){}

  /**
   * 生命周期回调——监听小程序启动或切前台。  
   * 小程序启动,或从后台进入前台显示时触发。也可以使用 wx.onAppShow 绑定监听。  
   * 参数:与 wx.onAppShow 一致  
   */
  onShow(obj){}

  /**
   * 生命周期回调——监听小程序切后台。  
   * 小程序从前台进入后台时触发。也可以使用 wx.onAppHide 绑定监听。  
   */
  onHide(){}

  /**
   * 错误监听函数。  
   * 小程序发生脚本错误或 API 调用报错时触发。也可以使用 wx.onError 绑定监听。  
   * 参数:与 wx.onError 一致  
   */
  onError(error){}

  /**
   * 页面不存在监听函数。  
   * 1.9.90  
   * 小程序要打开的页面不存在时触发。也可以使用 wx.onPageNotFound 绑定监听。注意事项请参考 wx.onPageNotFound。  
   * 参数:与 wx.onPageNotFound 一致  
   */
  onPageNotFound(obj){}

  /**
   * 未处理的 Promise 拒绝事件监听函数。  
   * 2.10.0  
   * 小程序有未处理的 Promise 拒绝时触发。也可以使用 wx.onUnhandledRejection 绑定监听。注意事项请参考 wx.onUnhandledRejection。  
   * 参数:与 wx.onUnhandledRejection 一致  
   */
  onUnhandledRejection(obj){}

  /**
   * 监听系统主题变化  
   * 2.11.0  
   * 系统切换主题时触发。也可以使用 wx.onThemeChange 绑定监听。  
   * 参数:与 wx.onThemeChange 一致  
   */
  onThemeChange(obj){}
}

/**
 * 微信小程序自带的一些Page方法提示接口
 */
class PageInterface{

  data(){}

  /**
   * 页面加载时触发。一个页面只会调用一次,可以在 onLoad 的参数中获取打开当前页面路径中的参数。  
   */
  onLoad(query){}

  onShow(){}

  onReady(){}

  onHide(){}

  onUnload(){}

  onPullDownRefersh(){}

  onReachBottom(){}

  onShareAppMessage(){}

  onShareTimeline(){}

  onAddToFavorites(){}

  onPageScroll(){}

  onResize(){}

  onTabItemTap(){}

  onSaveExitState(){}

}

/**
 * APP 主类
 */
class EasyAPP extends AppInterface{

  constructor(){
    super();
  }


  /**
   * 创建页面,但不创建Page实例,只有onLoad事件触发后才能有相关操作
   */
  launch(){
    let that = this;

    let config = {};
    let keys = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
    for(let key of keys){
      config[key] = function(){
        that[key]();
      }
    }
    App(config);
  }
}

/**
 * Page 主类
 */
class EasyPage extends PageInterface{


  constructor(){
    super();
    this.pageInstance = undefined;

    this.dataContiner = this.data() || {};
  }

  launch(passData = {}){
    let that = this;
    let config = {};

    //载入数据
    Object.assign(that.dataContiner,passData);
    
    Object.assign(config,{
      data:that.dataContiner
    })

    //载入方法
    let keys = Object.getOwnPropertyNames(Object.getPrototypeOf(that));

    if(keys.indexOf("onLoad") == -1){
      //修复没有Load无法刷新界面的问题
      keys.push("onLoad");
    }

    for(let key of keys){
      if(key == "onLoad"){
        config[key] = function(...args){
          that.pageInstance = this;
          //TODO 更新/挂载数据
          this.setData(that.dataContiner);
          that["onLoad"].apply(that,args);
        }
        continue;
      }
      if(key == "data"){
        continue;
      }
      config[key] = function(...args){
        that.pageInstance = this;
        that[key].apply(that,args);
      }
    }
    Page(config);
  }

  setData(data){
    if(this.pageInstance == undefined){
      // console.log(`[Warn] ${this.constructor.name} 的 Page 对象实例未生成!更新界面失败,操作该页面以重新获得 Page 对象`);
      return;
    }
    Object.assign(this.dataContiner,data);
    this.pageInstance.setData(data);
  }
}

/**
 * 数据虚拟空间对象
 */
class DataVM{
  constructor(obj,listener = function(){}){

    this.source = obj;
    this.proxy = this.bind();
    this.listeners = [listener];

    return this.proxy;
  }

  update(t,k,v,r){
    console.log("更新渲染界面!");
    for(let f of this.listeners){
      f(t,k,v,r);
    }
  }

  bind(){
      let that = this;

      let getCache = [];
      let handler = {
          get:function(t,k,r){

              if(k == "__DataVM"){
                  return that;
              }

              if(k == "__Refersh"){
                that.update(t,k,undefined,r);
                return;
              }
              if(k == "__NOTRACK"){
                return t;
              }

              if(t == that.source){
                getCache = [];
              }
              
              getCache.push(t[k]);
              let isRepeat = function(o){
                for(let i = 0; i < getCache.length-1; i++){
                  if(o instanceof Object && getCache[i] == o){
                    return true;
                  }
                }
                return false;
              }

              

              if(typeof t[k] == "object" && t[k].__DataVM == undefined){
                if(isRepeat(t[k])){
                  // console.warn("循环访问对象,已拒绝");
                  return undefined;
                }
                return reactive(t[k]);
              }else{
                return t[k];
              }
          },
          set:function(t,k,v,r){
              t[k] = v;

              //唤起更新事件
              that.update(t,k,v,r);
              return true;
          }
      }

      let reactive = function(obj){
          return new Proxy(obj,handler);
      }

      return reactive(this.source);
  }

  getProxy(){
    return this.proxy;
  }

  getSource(){
    return this.source;
  }

  addListener(f){
    this.listeners.push(f);
  }
}

class EasyModule extends DataVM{}

class LaunchEnv{

  constructor(obj){
    let {
      page,
      data,
    } = obj;

    this.page = page;
    this.data = data;

    this.build();
  }

  async build(){

    

    let page = this.page;
    let data = this.data;
    if(!page instanceof EasyPage){
      throw Error();
    }
    page.launch(data);


    //绑定数据更新事件
    for(let name in data){
      
      let targetEnv = data[name];

      let dataBuilder = function(key,value){
        let obj = {};
        obj[key] = value;
        return obj;
      }
      if(targetEnv.__DataVM instanceof DataVM){

        let vm = targetEnv.__DataVM;

        vm.addListener((target,key,value,reciver) => {
          page.setData(dataBuilder(
            name,
            vm.getProxy()
          ))
        })
      }
    }
  }

}

module.exports.EasyAPP = EasyAPP;
module.exports.EasyPage = EasyPage;
module.exports.EasyModule = EasyModule;

module.exports.DataVM = DataVM;
module.exports.LaunchEnv = LaunchEnv;

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,820评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,648评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,324评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,714评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,724评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,328评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,897评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,804评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,345评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,431评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,561评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,238评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,928评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,417评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,528评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,983评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,573评论 2 359

推荐阅读更多精彩内容