绑定的基础是 propertyChange
事件。如何得知 viewModel
成员值的改变一直是开发 MVVM
框架的首要问题。主流框架的处理有一下三大类:css
另外开发一套 API。典型框架:Backbone.js
Backbone 有本身的 模型类 和 集合类。这样作虽然框架开发简单运行效率也高,但开发者不得不使用这套 API 操做 viewModel,致使上手复杂、代码繁琐。html
脏检查机制。典型框架:angularjs
特色是直接使用 JS 原生操做对象的语法操做 viewModel,开发者上手简单、代码简单。但脏检查机制随之带来的就是性能问题。这点在我另外的一篇博文 《Angular 1 深度解析:脏数据检查与 angular 性能优化》 有详细讲解这里不另加赘述。前端
替换属性。典型框架:vuejs
vuejs 把开发者定义的 viewModel 对象(即 data 函数返回的对象)中全部的(除某些前缀开头的)成员替换为属性。这样既可使用 JS 原生操做对象的语法,又是主动触发 propertyChange
事件,效率也高。但这种方法也有一些限制,后文会分析。vue
Object.observe 是谷歌对于简化双向绑定机制的尝试,在 Chrome 49 中引入。然而因为性能等问题,并无被其余各大浏览器及 ES 标准所接受。挣扎了一段时间后谷歌 Chrome 团队宣布收回 Object.observe 的提议,并在 Chrome 50 中彻底删除了 Object.observe 实现。react
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 属性被改成 1
。express
有了 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 仓库
相较于 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
对象不须要。Proxy
的 set
方法只是一层包装,Proxy
对象自身维护原始对象的值,天然也能够直接拿出原始值给开发者看。查看一个 Proxy
对象,只须要展开其内置属性 [[Target]]
便可看到原始对象的全部成员的值。你甚至还能够看到包装原始对象的哪些 get
、set
函数——若是你感兴趣的话。
虽然说使用 Proxy
实现双向绑定的优势很明显,可是缺点也很明显:Proxy
是 ES2015
的特性,它没法被编译为 ES5,也没法 Polyfill。IE 天然全军覆没;其余各大浏览器实现的时间也较晚:Chrome 4九、Safari 10。浏览器兼容性极大的限制了 Proxy
的使用。可是我相信,随着时间的推移,基于 Proxy
的前端 MVVM
框架也会出如今开发者眼前。
注:本文同时发布在个人 sf 专栏