资讯专栏INFORMATION COLUMN

使用 Proxy 实现简单的 MVVM 模型

MarvinZhang / 1413人阅读

摘要:绑定实现的历史绑定的基础是事件。但脏检查机制随之带来的就是性能问题。是谷歌对于简化双向绑定机制的尝试,在中引入。挣扎了一段时间后谷歌团队宣布收回的提议,并在中完全删除了实现。自然全军覆没其他各大浏览器实现的时间也较晚。

绑定实现的历史

绑定的基础是 propertyChange 事件。如何得知 viewModel 成员值的改变一直是开发 MVVM 框架的首要问题。主流框架的处理有一下三大类:

另外开发一套 API。典型框架:Backbone.js

Backbone 有自己的 模型类 和 集合类。这样做虽然框架开发简单运行效率也高,但开发者不得不使用这套 API 操作 viewModel,导致上手复杂、代码繁琐。

脏检查机制。典型框架:angularjs

特点是直接使用 JS 原生操作对象的语法操作 viewModel,开发者上手简单、代码简单。但脏检查机制随之带来的就是性能问题。这点在我另外的一篇博文 《Angular 1 深度解析:脏数据检查与 angular 性能优化》 有详细讲解这里不另加赘述。

替换属性。典型框架:vuejs
vuejs 把开发者定义的 viewModel 对象(即 data 函数返回的对象)中所有的(除某些前缀开头的)成员替换为属性。这样既可以使用 JS 原生操作对象的语法,又是主动触发 propertyChange 事件,效率也高。但这种方法也有一些限制,后文会分析。

Object.observe

Object.observe 是谷歌对于简化双向绑定机制的尝试,在 Chrome 49 中引入。然而由于性能等问题,并没有被其他各大浏览器及 ES 标准所接受。挣扎了一段时间后谷歌 Chrome 团队宣布收回 Object.observe 的提议,并在 Chrome 50 中完全删除了 Object.observe 实现。

Proxy

Proxy(代理)是 ES2015 加入的新特性,用于对某些基本操作定义自定义行为,类似于其他语言中的面向切面编程。它的其中一个作用就是用于(部分)替代 Object.observe 以实现双向绑定。

例如有一个对象

let viewModel = {};

可以构造对应的代理类实现对 viewModel 的属性赋值操作的监听:

viewModel = new Proxy(viewModel, {
  set(obj, prop, value) {
    if (obj[prop] !== value) {
      obj[prop] = value;
      console.log(`${prop} 属性被改为 ${value}`);
    }
    return true;
  }
});

这时所有对 viewModel 的属性赋值的操作都不会直接生效,而是将这个操作转发给 Proxy 中注册的 set 方法,其中的参数 obj 是原始对象(注意不能直接用 a,否则还会触发代理函数,造成无限递归),prop 是被赋值的属性名,value 是待赋的值。
如果有:

viewModel.test = 1;

这时就会输出 test 属性被改为 1

用 Proxy 实现简单的单向绑定。

有了 Proxy 就可以得知 viewModel 中属性的变更了,还需要更新页面上绑定此属性的元素。

简单起见,我们用 this 表示 viewModel 本身,使用 this.XXX 就表示依赖 XXX 属性。有 DOM 如下:

  

首先要获得所有使用了单向绑定的元素:

const bindingElements = [...document.querySelectorAll("[my-bind]")];

获取绑定表达式:

bindingElements.forEach(el => {
  const expression = el.getAttribute("my-bind");
});

由于获得的表达式是个字符串,需要构造一个函数去执行它,得到表达式的结果:

const expression = el.getAttribute("my-bind");
const result = new Function(""use strict";
return " + expression).call(viewModel);

代码中会动态创建一个函数,内容就是将字符串解析执行后将其结果返回(类似 eval,但更安全)。将结果放到页面上就可以了:

el.textContent = result;

与上文的 viewModel 结合起来:

const bindingElements = [...document.querySelectorAll("[my-bind]")];

window.viewModel = new Proxy({}, { // 设置全局变量方便调试
  set(obj, prop, value) {
    if (obj[prop] !== value) {
      obj[prop] = value;

      bindingElements.forEach(el => {
        const expression = el.getAttribute("my-bind");
        const result = new Function(""use strict";
return " + expression)
          .call(obj);
        el.textContent = result;
      });
    }
    return true;
  }
});

如果实际放在浏览器中运行的话,改变 viewModel 中属性的值就会触发页面的更新。

示例中写了循环会更新所有绑定元素,比较好的方式是只更新对当前变更属性有依赖的元素。这时就要分析绑定表达式的属性依赖。
简单起见可以使用正则表达式解析属性依赖:

let match;
while (match = /this(?:.(w+))+/g.exec(expression)) {
  match[1] // 属性依赖
}
添加事件绑定

事件绑定即绑定原生事件,在事件触发时执行绑定表达式,表达式调用 viewModel 中的某个回调函数。

click 事件为例。依然是获取所有绑定了 click 事件的元素,并执行表达式(表达式的值被丢弃)。与单项绑定不同的是:执行表达式需要传入事件的 event 参数。

[...document.querySelectorAll("[my-click]")].forEach(el => {
  const expression = el.getAttribute("my-click");
  const fn = new Function("$event", ""use strict";
" + expression);
  el.addEventListener("click", event => {
    fn.call(viewModel, event);
  });
});

Function 对象的构造函数,前 n-1 个参数是生成的函数对象的参数名,最后一个是函数体。代码中构造了包含一个 $event 参数的函数,函数体就是直接执行绑定表达式。

双向绑定

双向绑定就是单项绑定和事件绑定的结合体。绑定元素的 input 事件来修改 viewModel 的属性,然后再单项绑定元素的 value 属性修改元素的值。

这里是一个较为完整的示例:http://sandbox.runjs.cn/show/...。完整的代码放在我的 GitHub 仓库

使用 Proxy 实现双向绑定的优缺点

相较于 vuejs 的属性替换,Proxy 实现的绑定至少有如下三个优点:

无需预先定义待绑定的属性。

vuejs 要做属性(getter, setter 方法)替换,首先需要知道有哪些属性需要替换,这样导致必须预先定义需要替换的属性,也就是 vuejs 中的 data 方法。vuejs 中 data 方法必须定义完整所有绑定属性,否则对应绑定不能正常工作。
Vue 不能检测到对象属性的添加或删除:Property or method "XXX" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
Proxy 不需要,因为它监听的是整个对象。

对数组相性良好。

虽说数组里的方法可以替换(push、pop等),但是数组下标却不能替换为属性,以致必须搞出一个 set 方法用于对数组下标赋值。

更容易调试的 viewModel 对象。

由于 vuejs 把对象中的所有成员全部替换成了属性,如果想直接用 Chrome 的原生调试工具查看属性值,你不得不挨个去点属性后面的 (...):因为获取属性的值其实是执行了属性的 get 方法,执行一个方法可能会产生副作用,Chrome 把这个决定权留给开发者。
Proxy 对象不需要。Proxyset 方法只是一层包装,Proxy 对象自身维护原始对象的值,自然也可以直接拿出原始值给开发者看。查看一个 Proxy 对象,只需要展开其内置属性 [[Target]] 即可看到原始对象的所有成员的值。你甚至还可以看到包装原始对象的哪些 getset 函数——如果你感兴趣的话。

虽说使用 Proxy 实现双向绑定的优点很明显,但是缺点也很明显:ProxyES2015 的特性,它无法被编译为 ES5,也无法 Polyfill。IE 自然全军覆没;其他各大浏览器实现的时间也较晚:Chrome 49、Safari 10。浏览器兼容性极大的限制了 Proxy 的使用。但是我相信,随着时间的推移,基于 Proxy 的前端 MVVM 框架也会出现在开发者眼前。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/84516.html

相关文章

  • 使用 Proxy 实现简单 MVVM 模型

    摘要:绑定实现的历史绑定的基础是事件。但脏检查机制随之带来的就是性能问题。是谷歌对于简化双向绑定机制的尝试,在中引入。挣扎了一段时间后谷歌团队宣布收回的提议,并在中完全删除了实现。自然全军覆没其他各大浏览器实现的时间也较晚。 绑定实现的历史 绑定的基础是 propertyChange 事件。如何得知 viewModel 成员值的改变一直是开发 MVVM 框架的首要问题。主流框架的处理有一下三...

    BetaRabbit 评论0 收藏0
  • 前端 MVVM 原理

    摘要:原来是在改变数据时,还要手动。现在只需要直接改变数据,会自动,更新元素。参考资料现代前端技术解析,张成文,年月第版,和的图示,阮一峰,年月日, author: 陈家宾 email: 617822642@qq.com date: 2018/3/1 MVVM 背景 都说懒惰使人进步,MVVM 的进化史,正印证了这句话,是一步步让开发人员更懒惰更简单的历史: 直接 DOM 操作 -> MVC...

    leiyi 评论0 收藏0
  • 学习MVVM及框架双向绑定笔记

    摘要:的数据劫持版本内部使用了来实现数据与视图的双向绑定,体现在对数据的读写处理过程中。这样就形成了数据的双向绑定。 MVVM由以下三个内容组成 View:视图模板 Model:数据模型 ViewModel:作为桥梁负责沟通View和Model,自动渲染模板 在JQuery时期,如果需要刷新UI时,需要先取到对应的DOM再更新UI,这样数据和业务的逻辑就和页面有强耦合。 在MVVM中,U...

    VioletJack 评论0 收藏0
  • 试着用Proxy 实现一个简单mvvm

    摘要:套数据,实现界面先把计算属性这个注释掉,后面进行实现计算属性然后在函数中增加一个编译函数,号表示是添加的函数添加一个编译函数上面我们添加了一个的构造函数。 Proxy、Reflect的简单概述 Proxy 可以理解成,在目标对象之前架设一层拦截,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它...

    fnngj 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<