使用 Proxy 实现简单的 MVVM 模型

绑定实现的历史

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

  1. 另外开发一套 API。典型框架:Backbone.js
    Backbone 有本身的 模型类集合类。这样作虽然框架开发简单运行效率也高,但开发者不得不使用这套 API 操做 viewModel,致使上手复杂、代码繁琐。html

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

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

Object.observe

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

Proxy

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

例若有一个对象angularjs

let viewModel = {};

能够构造对应的代理类实现对 viewModel 的属性赋值操做的监听:github

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 属性被改成 1express

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

有了 Proxy 就能够得知 viewModel 中属性的变动了,还须要更新页面上绑定此属性的元素。

简单起见,咱们用 this 表示 viewModel 自己,使用 this.XXX 就表示依赖 XXX 属性。有 DOM 以下:

<div my-bind="'str1 + str2 = ' + (this.str1 + this.str2)"></div>
  <div my-bind="'num1 - num2 = ' + (this.num1 - this.num2)"></div>

首先要得到全部使用了单向绑定的元素:

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";\nreturn ' + 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";\nreturn ' + 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";\n' + expression);
  el.addEventListener('click', event => {
    fn.call(viewModel, event);
  });
});

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

双向绑定

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

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

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

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

  1. 无需预先定义待绑定的属性。
    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 不须要,由于它监听的是整个对象。

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

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

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

注:本文同时发布在个人 sf 专栏

相关文章
相关标签/搜索