使用Proxy实现简单的MVVM模型
时间:2023-04-02 17:48:32
HTML
绑定历史绑定实现的基础是propertyChange事件。如何知道viewModel成员值的变化一直是MVVM框架开发的首要问题。主流框架的处理主要分为三类:开发另一套API。典型框架:Backbone.jsBackbone有自己的模型类和集合类。虽然框架开发简单,运行效率高,但是开发者不得不使用这套API来操作viewModel,导致上手复杂,代码繁琐。脏检查机制。典型框架:angularjs的特点是直接使用JS原生操作对象的语法来操作viewModel,方便开发者使用,易于编码。但是脏检查机制带来了性能问题。这一点在我的另一篇博文《Angular 1 深度解析:脏数据检查与 angular 性能优化》中有详细说明,这里不再赘述。更换属性。典型框架:vuejsvuejs将开发者定义的viewModel对象(即data函数返回的对象)中的所有成员(以某些前缀开头的除外)全部替换为属性。这样既可以使用JS原生操作对象的语法,又可以主动触发propertyChange事件,效率也很高。不过这种方法也有一些局限性,后面会分析。Object.observeObject.observe是Google为简化双向绑定机制所做的尝试,在Chrome49中引入。但是由于性能等问题,一直没有被其他主流浏览器和ES标准接受。经过一段时间的挣扎,GoogleChrome团队宣布撤回Object.observe提案,并在Chrome50中彻底移除了Object.observe的实现。ProxyProxy(代理)是ES2015新增的特性,用于定义自定义行为对于一些基本的操作,类似于其他语言中的面向切面编程。它的用途之一是作为双向绑定的Object.observe的(部分)替代品。例如,有一个对象letviewModel={};可以构造相应的代理类来监听viewModel的属性赋值操作:viewModel=newProxy(viewModel,{set(obj,prop,value){if(obj[prop]!==value){obj[prop]=value;console.log(`${prop}属性更改为${value}`);}returntrue;}});此时所有给viewModel的属性赋值的操作都不会直接生效,而是将这个操作转发给Proxy中注册的set方法,其中参数obj为原始对象(注意a不能是直接使用,否则会触发代理函数,导致无限递归),prop被赋值为的属性名,value为要赋值的值。如果有:viewModel.test=1;然后输出测试属性将更改为1。与代理的简单单向绑定。通过Proxy,可以知道viewModel中属性的变化,需要更新页面上绑定该属性的元素。为了简单起见,我们用this来表示viewModel本身,用this.XXX来表示对XXX属性的依赖。使DOM如下:
首先,获取所有使用单向绑定的元素:constbindingElements=[...document.querySelectorAll('[my-bind]')];获取绑定表达式公式:bindingElements.forEach(el=>{constexpression=el.getAttribute('my-bind');});由于得到的表达式是一个字符串,所以需要构造一个函数来执行它,得到表达式的结果:constexpression=el.getAttribute('my-bind');constresult=newFunction('"usestrict";\nreturn'+expression).call(viewModel);代码中会动态创建一个函数,内容是将字符转换成字符串解析后执行,返回结果(类似eval,但更安全)。只需将结果放在页面上:el.textContent=result;结合上面的viewModel:constbindingElements=[...document.querySelectorAll('[my-bind]')];window.viewModel=newProxy({},{//设置全局变量方便调试set(obj,prop,value){if(obj[prop]!==value){obj[prop]=value;bindingElements.forEach(el=>{constexpression=el.getAttribute('my-bind');constresult=newFunction('"usestrict";\nreturn'+expression).call(obj);el.textContent=result;});}returntrue;}});如果真正运行在浏览器中,改变viewModel中属性的值就会触发页面的更新。在示例中,写了循环将更新所有绑定的元素。更好的方法是仅更新依赖于当前已更改属性的元素。这时候就需要分析绑定表达式的属性依赖关系。为简单起见,可以使用正则表达式来解析属性依赖:letmatch;while(match=/this(?:\.(\w+))+/g.exec(expression)){match[1]//属性依赖}添加事件绑定事件绑定就是绑定原生事件,当事件触发时执行绑定表达式,表达式调用viewModel中的某个回调函数。以点击事件为例。依旧是获取所有绑定点击事件的元素,执行表达式(表达式的值被丢弃)。与单项绑定不同的是需要传入事件的event参数来执行表达式。[...document.querySelectorAll('[my-click]')].forEach(el=>{constexpression=el.getAttribute('my-click');constfn=newFunction('$event','"usestrict";\n'+expression);el.addEventListener('click',event=>{fn.call(viewModel,event);});});函数对象构造函数,第一个n-1个参数是生成的函数对象的参数名,最后一个是函数体。代码中构造了一个包含$event参数的函数,函数体是直接执行绑定表达式。双向绑定双向绑定是单项绑定和事件绑定的结合。绑定元素的input事件修改viewModel的属性,然后通过单独绑定元素的value属性来修改元素的值。这里有一个更完整的例子:http://sandbox.runjs.cn/show/…。完整代码放在我的github仓库使用Proxy实现双向绑定的优缺点与vuejs属性替换相比,Proxy实现的绑定至少有以下三个优点:不需要预先定义属性被束缚。vuejs要替换属性(getters,setters),首先要知道需要替换哪些属性,这就导致需要预先定义需要替换的属性,也就是vuejs中的data方法。vuejs中的data方法必须完整定义所有的绑定属性,否则对应的绑定将无法正常工作。Vue无法检测到对象属性的增删:属性或方法“XXX”未在实例上定义,但在渲染时被引用。确保在数据选项中声明响应式数据属性。Proxy不需要它,因为它监听整个对象。对阵列兼容性很好。虽然数组中的方法可以替换(push、pop等),但是数组下标不能用属性替换,所以必须创建一个set方法给数组下标赋值。ViewModel对象更容易调试。由于vuejs将对象中的所有成员都替换成了属性,所以如果想直接使用Chrome自带的调试工具查看属性值,就得在属性后面一一点击(...):因为获取到的值propertyisactuallyexecution方法的执行可能会产生副作用,Chrome将这个决定留给了开发者。不需要代理对象。Proxy的set方法只是一层包装。Proxy对象本身维护着原始对象的值。自然是可以直接把原来的值展示给开发者。查看一个Proxy对象,只需要展开它的内置属性[[Target]],就可以看到原对象所有成员的值。你甚至可以看到哪些get和set函数包装了原始对象——如果那是你感兴趣的话。虽然使用Proxy实现双向绑定的优点很明显,但缺点也很明显:Proxy是ES2015的一个特性,不能编译成ES5,也不能Polyfilled。IE自然被消灭;其他主流浏览器也较晚实现:Chrome49、Safari10。浏览器兼容性大大限制了Proxy的使用。但相信随着时间的推移,基于Proxy的前端MVVM框架也会出现在开发者的面前。