文档章节

使用 Proxy 实现简单的 MVVM 模型

 老帖子
发布于 2017/07/30 00:57
字数 1897
阅读 728
收藏 18
点赞 1
评论 2

绑定实现的历史

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

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

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

  3. 替换属性。典型框架: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 如下:

  <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 49、Safari 10。浏览器兼容性极大的限制了 Proxy 的使用。但是我相信,随着时间的推移,基于 Proxy 的前端 MVVM 框架也会出现在开发者眼前。

注:本文同时发布在我的 sf 专栏

© 著作权归作者所有

共有 人打赏支持
粉丝 6
博文 5
码字总数 13140
作品 0
浦东
程序员
加载中

评论(2)

老帖子

引用来自“lzszone”的评论

cool,之前还不理解proxy的应用场景,解惑了
Proxy 的另外一个应用是单元测试中 mock 成员方法,这些在 Java、C# 里面用得很多
l
lzszone
cool,之前还不理解proxy的应用场景,解惑了
Mvvm设计模式

最近前端圈子里面,发现大家都在热炒概念,什么knockout,angularJs,都被捧成神了,鄙人不才,最近心情也不好,特地写这篇文章来找骂 写代码的码农都知道,Java社区虽然不是一个提出分层思想...

村长大神 ⋅ 2015/04/24 ⋅ 0

MVP?MVC?移动开发如何选择正确的框架?

  【IT168 评论】设计模式和架构对创建一个成功可靠的应用程序至关重要,可是具备哪些特征才算得上一个好的架构呢?MVP、MVC和MVVM似乎都不错,该如何选择呢?   为什么以及如何选择正确的...

it168网站 ⋅ 2017/06/26 ⋅ 0

看完这篇关于MVVM的文章,面试通过率提升了80%

来看看目前最火的MVVM 今天面试又被问到什么是MVVM? 光靠说理论已经糊弄不过去了? 什么!MVVM的实现不止一种啊? 往下看~ 亲手带你剖析MVVM原理! 先来总结下MVVM的实现方式 传统的MVC中通过发布...

凌晨夏沫 ⋅ 05/14 ⋅ 0

Prism初研究之使用Prism实现WPF的MVVM模式

MVVM模式帮助你清晰地将业务逻辑和UI逻辑分离开。维持清晰的UI和业务逻辑的分离有助于专注于分发开发和设计,并且使你的应用更容易测试、维护和迭代。MVVM还有改善代码重用,允许开发者和UI设...

andrewniu ⋅ 2017/12/04 ⋅ 0

Knockout.Js官网学习(简介)

前言 最近一段时间在网上经常看到关于Knockout.js文章,于是自己就到官网看了下,不过是英文的,自己果断搞不来,借用google翻译了一下。然后刚刚发现在建立asp.net mvc4.0的应用程序的时候,...

aehyok ⋅ 2013/09/29 ⋅ 0

android MVVM 模式探索与实践之路

本文是之前写的文章,架构在写法上有很多不足之处,谨做参考 1、概述 在如今DataBinding这么火热的年代,如何构建MVVM模式已成为迫切的需求。再此讲讲自己对mvvm模式的认知,以及mvvm模式如何...

iceuncle ⋅ 2017/09/28 ⋅ 0

使用Kotlin构建更适合Android的MVVM应用程序

简书地址:www.jianshu.com/p/77e42aebd… 概述 说到MVVM,大家都会想起前端的MVVM框架,相较于前端MVVM的火热,它在移动开发领域就不那么热门了。Google在2015年才推出DataBinding框架,起步...

ditclear ⋅ 2017/11/28 ⋅ 0

iOS架构模式-揭秘MVC,MVP,MVVM和VIPER

iOS架构模式 揭秘MVC,MVP,MVVM和VIPER 英文原文:https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52#.w3sovqjl3 作者:Bohdan Orlov 翻译:http://blog.x......

cuibo1123 ⋅ 2016/02/17 ⋅ 0

写给iOS小白的MVVM教程(序)

这几天,需要重构下部分代码,这里简要记录下.但是涉及的技术要点还是很多,所以分为多个篇章叙述.此教程来源于,并将于应用于实践,不做过多的概念性阐释和争论.每个篇章都会附上实际的可执行的代...

ios122 ⋅ 2015/10/13 ⋅ 0

MVVM的理解与应用

以前做项目时大多都使用的是MVC的架构,但是随着业务逻辑越来越复杂时,C层就会显得越来越重,不便于维护。近来就尝试使用MVVM的架构来搭建项目,简单来说,MVVM就是为了给MVC模式下的ViewC...

小时候De_我 ⋅ 2017/02/28 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

用SQL命令查看Mysql数据库大小

要想知道每个数据库的大小的话,步骤如下: 1、进入information_schema 数据库(存放了其他的数据库的信息) use information_schema; 2、查询所有数据的大小: select concat(round(sum(da...

源哥L ⋅ 15分钟前 ⋅ 0

两个小实验简单介绍@Scope("prototype")

实验一 首先有如下代码(其中@RestController的作用相当于@Controller+@Responsebody,可忽略) @RestController//@Scope("prototype")public class TestController { @RequestMap...

kalnkaya ⋅ 20分钟前 ⋅ 0

php-fpm的pool&php-fpm慢执行日志&open_basedir&php-fpm进程管理

12.21 php-fpm的pool pool是PHP-fpm的资源池,如果多个站点共用一个pool,则可能造成资源池中的资源耗尽,最终访问网站时出现502。 为了解决上述问题,我们可以配置多个pool,不同的站点使用...

影夜Linux ⋅ 29分钟前 ⋅ 0

微服务 WildFly Swarm 管理

Expose Application Metrics and Information 要公开关于我们的微服务的有用信息,我们需要做的就是将监视器模块添加到我们的pom.xml中: 这将使在管理和监视功能得到实现。从监控角度来看,...

woshixin ⋅ 30分钟前 ⋅ 0

java连接 mongo伪集群部署遇到的坑

部署mongo伪集群 #创建mongo数据存放文件地址mkdir -p /usr/local/config1/datamkdir -p /usr/local/config2/data mkdir -p /usr/local/config3/data mkdir -p /usr/local/config1/l......

努力爬坑人 ⋅ 30分钟前 ⋅ 0

React Native & Weex 区别

JS引擎 Weex使用V8, React native使用JSCore JS开发框架 ( Js Framework ) Weex基于vue.js(2W+ star)。小巧轻量的前端开发框架,组件化,数据绑定,2.0引入virtual dom。 ReactNative使用...

东东笔记 ⋅ 39分钟前 ⋅ 1

UIkit 分页组件动态加载简单实现

1. 问题描述 使用过UIkit分页组件的都清楚,UIkit的分页不能动态刷新数据,也就是不能在点击下一页的时候,动态从后台加载数据,并且刷新页数以及该页数上的数据,下面是一个简单实现,没有做...

影狼 ⋅ 40分钟前 ⋅ 0

Mobx入门之三:Provider && inject

上一节中<App/>组件传递状态temperatures给children -- <TemperatureInput />,如果组建是一个tree, 那么属性的传递则会非常繁琐。redux使用Provider给子组件提供store, connect将子组件和s...

pengqinmm ⋅ 41分钟前 ⋅ 0

魔兽世界 7.0版本 S23/S24/S25全职业普通+精锐套

  死亡骑士   (联盟)   (部落)   (精锐)   恶魔猎手   (联盟)   (部落)   (精锐)   德鲁伊   (联盟)   (部落)   (精锐)   猎人   (联盟) ...

wangchen1999 ⋅ 49分钟前 ⋅ 0

maven顶级pom和子pom的版本号批量修改

当一个版本发布,新起一个版本时,我们只需要手动修改一下项目中pom.xml的版本号就可以了。但是如果这个maven项目有很多的子模块项目,那么一个个手动的去改就显得费时费力又繁琐了。还好,m...

ArlenXu ⋅ 58分钟前 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部