MVVM 双向绑定的实现代码


Posted in Javascript onJune 21, 2018

这篇文章主要记录学习 JS 双向绑定过程中的一些概念与具体的实现

MVVM 具体概念

MVVM 中有一些概念是通用的,具体如下

Directive (指令)

自定义的执行函数,例如 Vue 中的 v-click、v-bind 等。这些函数封装了 DOM 的一些基本可复用函数API。

Filter (过滤器)

用户希望对传入的初始数据进行处理,然后将处理结果交给 Directive 或者下一个 Filter。例如:v-bind="time | formatTime"。formatTime 是将 time 转换成指定格式的 Filter 函数。

表达式

类似前端普通的页面模板表达式,作用是控制页面内容安装具体的条件显示。例如:if...else 等

ViewModel

传入的 Model 数据在内存中存放,提供一些基本的操作 API 给开发者,使其能够对数据进行读取与修改

双向绑定(数据变更检测)

View 层的变化改变 Model:通过给元素添加 onchange 事件来触发对 Model 数据进行修改

Model 层的变化改变 View:

  1. 手动触发绑定
  2. 脏数据检测
  3. 对象劫持
  4. Proxy

实现方式

手动触发绑定

即 Model 对象改变之后,需要显示的去触发 View 的更新

首先编写 HTML 页面

Two way binding

编写实现 MVVM 的 代码

// Manual trigger
let elems = [document.getElementById('el'), document.getElementById('input')]
// 数据 Model
let data = {
 value: 'hello'
}

// 定义 Directive
let directive = {
 text: function(text) {
  this.innerHTML = text
 },
 value: function(value) {
  this.setAttribute('value', value)
  this.value = value
 }
}

// 扫描所有的元素
function scan() {
 // 扫描带指令的节点属性
 for (let elem of elems) {
  elem.directive = []
  for (let attr of elem.attributes) {
   if (attr.nodeName.indexOf('q-') >= 0) {
    directive[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue])
    elem.directive.push(attr.nodeName.slice(2))
   }
  }
 }
}

// ViewModel 更新函数
function ViewModelSet(key, value) {
 // 修改数据对象后
 data[key] = value
 // 手动地去触发 View 的修改
 scan()
}

// View 绑定监听
elems[1].addEventListener('keyup', function(e) {
 ViewModelSet('value', e.target.value)
}, false)

// -------- 程序执行 -------
scan()
setTimeout(() => {
 ViewModelSet('value', 'hello world')
}, 1000);

数据劫持

数据劫持是目前比较广泛的方式,Vue 的双向绑定就是通过数据劫持实现。实现方式是通过 Object.defineProperty 和 Object.defineProperies 方法对 Model 对象的 get 和 set 函数进行监听。当有数据读取或赋值操作时,扫描(或者通知)对应的元素执行 Directive 函数,实现 View 的刷新。

HTML 的代码不变,js 代码如下

// Hijacking
let elems = [document.getElementById('el'), document.getElementById('input')]
let data = {
 value: 'hello'
}

// 定义 Directive
let directive = {
 text: function(text) {
  this.innerHTML = text
 },
 value: function(value) {
  this.setAttribute('value', value)
  this.value = value
 }
}

// 定义对象属性设置劫持
// obj: 指定的 Model 数据对象
// propName: 指定的属性名称
function defineGetAndSet(obj, propName) {
 let bValue
 // 使用 Object.defineProperty 做数据劫持
 Object.defineProperty(obj, propName, {
  get: function() {
   return bValue
  },
  set: function(value) {
   bValue = value
   // 在 vue 中,这里不会去扫描所有的元素,而是通过订阅发布模式,通知那些订阅了该数据的 view 进行更新
   scan()
  },
  enumerable: true,
  configurable: true
 })
}

// View 绑定监听
elems[1].addEventListener('keyup', function(e) {
 data.value = e.target.value
}, false)

// 扫描所有的元素
function scan() {
 // 扫描带指令的节点属性
 for (let elem of elems) {
  elem.directive = []
  for (let attr of elem.attributes) {
   if (attr.nodeName.indexOf('q-') >= 0) {
    directive[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue])
    elem.directive.push(attr.nodeName.slice(2))
   }
  }
 }
}

// -------- 程序执行 -------
scan()
defineGetAndSet(data, 'value')
setTimeout(() => {
 // 这里为数据设置新值之后,在 set 方法中会去更新 view
 data.value = 'Hello world'
}, 1000);

基于 Proxy 的实现

Proxy 是 ES6 中的新特性。可以在已有的对象基础上定义一个新对象,并重新定义对象原型上的方法。例如 get 和 set 方法。

// Hijacking
let elems = [document.getElementById('el'), document.getElementById('input')]

// 定义 Directive
let directive = {
 text: function(text) {
  this.innerHTML = text
 },
 value: function(value) {
  this.setAttribute('value', value)
  this.value = value
 }
}

// 设置对象的代理
let data = new Proxy({}, {
 get: function(target, key, receiver) {
  return target.value
 },
 set: function (target, key, value, receiver) { 
  target.value = value
  scan()
  return target.value
 }
})

// View 绑定监听
elems[1].addEventListener('keyup', function(e) {
 data.value = e.target.value
}, false)

// 扫描所有的元素
function scan() {
 // 扫描带指令的节点属性
 for (let elem of elems) {
  elem.directive = []
  for (let attr of elem.attributes) {
   if (attr.nodeName.indexOf('q-') >= 0) {
    directive[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue])
    elem.directive.push(attr.nodeName.slice(2))
   }
  }
 }
}

// -------- 程序执行 -------
data['value'] = 'Hello'
scan()
setTimeout(() => {
 data.value = 'Hello world'
}, 1000);

脏数据监测

基本原理是在 Model 对象的属性值发生变化的时候找到与该属性值相关的所有元素,然后判断数据是否发生变化,若变化则更新 View。

编写页面代码如下:Two way binding

js 代码如下:

// Dirty detection
let elems = [document.getElementById('el'), document.getElementById('input')]
let data = {
 value: 'hello'
}

// 定义 Directive
let directive = {
 text: function(text) {
  this.innerHTML = text
 },
 value: function(value) {
  this.setAttribute('value', value)
  this.value = value
 }
}

// 脏数据循环检测
function digest(elems) {
 for (let elem of elems) {
  if (elem.directive === undefined) {
   elem.directive = {}
  }
  for (let attr of elem.attributes) {
   if (attr.nodeName.indexOf('q-event') >= 0) {
    let dataKey = elem.getAttribute('q-bind') || undefined
    // 进行脏数据检测,如果数据改变,则重新执行命令
    if (elem.directive[attr.nodeValue] !== data[dataKey]) {
     directive[attr.nodeValue].call(elem, data[dataKey])
     elem.directive[attr.nodeValue] = data[dataKey]
    }
   }
  }
 }
}

// 数据监听
function $digest(value) {
 let list = document.querySelectorAll('[q-bind=' + value + ']')
 digest(list)
}

// View 绑定监听
elems[1].addEventListener('keyup', function(e) {
 data.value = e.target.value
 $digest(e.target.getAttribute('q-bind'))
}, false)

// -------- 程序执行 -------
$digest('value')
setTimeout(() => {
 data.value = "Hello world"
 $digest('value')
}, 1000);

总结

上面只是简单地实现了双向绑定,但实际上一个完整的 MVVM 框架要考虑很多东西。在上面的实现中数据劫持的方法更新View 是使用了 Scan 函数,但实际的实现中(比如 Vue)是使用了发布订阅的模式。它只会去更新那些与该 Model 数据绑定的元素,而不会去扫描所有元素。而在脏数据检测中,它去找到了所有绑定的元素,然后判断数据是否发生变化,这种方式只有一定的性能开销的。

参考

《现代前端技术解析》

代码下载:https://github.com/OreChou/twowaybinding

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Ext JS Grid在IE6 下宽度的问题解决方法
Feb 15 Javascript
js点击页面其它地方将某个显示的DIV隐藏
Jul 12 Javascript
JQuery创建DOM节点的方法
Jun 11 Javascript
浅谈javascript函数式编程
Sep 06 Javascript
jquery 全选、全不选、反选效果的实现代码【推荐】
May 05 Javascript
Node.js 实现简单小说爬虫实例
Nov 18 Javascript
JavaScript编写的网页小游戏,很给力
Aug 18 Javascript
vue项目中使用axios上传图片等文件操作
Nov 02 Javascript
web前端vue之vuex单独一文件使用方式实例详解
Jan 11 Javascript
node实现mock-plugin中间件的方法
Dec 25 Javascript
微信小程序图片自适应实现解析
Jan 21 Javascript
JavaScript数组 几个常用方法总结
Nov 11 Javascript
在vue2.0中引用element-ui组件库的方法
Jun 21 #Javascript
vue树形结构获取键值的方法示例
Jun 21 #Javascript
vue使用jsonp抓取qq音乐数据的方法
Jun 21 #Javascript
Vue 获取数组键名的方法
Jun 21 #Javascript
Taro集成Redux快速上手的方法示例
Jun 21 #Javascript
vue2.0项目实现路由跳转的方法详解
Jun 21 #Javascript
JS实现快递单打印功能【推荐】
Jun 21 #Javascript
You might like
第三节--定义一个类
2006/11/16 PHP
php页面防重复提交方法总结
2013/11/25 PHP
使用PHP导出Redis数据到另一个Redis中的代码
2014/03/12 PHP
php根据年月获取季度的方法
2014/03/31 PHP
自己写的兼容低于PHP 5.5版本的array_column()函数
2014/10/24 PHP
php视频拍照上传头像功能实现代码分享
2015/10/08 PHP
jQuery使用元素属性attr赋值详解
2015/02/27 Javascript
JavaScript中的cacheStorage使用详解
2015/07/29 Javascript
JS时间特效最常用的三款
2015/08/19 Javascript
网页中JS函数自动执行常用三种方法
2016/03/30 Javascript
JavaScript 数组中最大最小值
2016/06/05 Javascript
Javascript获取随机数的实现方法
2016/06/22 Javascript
微信小程序 页面跳转如何实现传值
2017/04/05 Javascript
JavaScript数据结构之单链表和循环链表
2017/11/28 Javascript
vue项目中的webpack-dev-sever配置方法
2017/12/14 Javascript
angular4 共享服务在多个组件中数据通信的示例
2018/03/30 Javascript
vue 注册组件的使用详解
2018/05/05 Javascript
js设计模式之单例模式原理与用法详解
2019/08/15 Javascript
node爬取新型冠状病毒的疫情实时动态
2020/02/06 Javascript
浅谈vue在html中出现{{}}的原因及解决方式
2020/11/16 Javascript
[01:41]DOTA2超级联赛专访YYF 称一辈子难忘TI2
2013/05/28 DOTA
深入解析Python中的lambda表达式的用法
2015/08/28 Python
教你用Python脚本快速为iOS10生成图标和截屏
2016/09/22 Python
Python三级菜单的实例
2017/09/13 Python
Python3获取拉勾网招聘信息的方法实例
2019/04/03 Python
python操作cfg配置文件方式
2019/12/22 Python
python实现超级马里奥
2020/03/18 Python
Python 测试框架unittest和pytest的优劣
2020/09/26 Python
马克华菲官方商城:Mark Fairwhale
2016/09/04 全球购物
联想印度官方网上商店:Lenovo India
2019/08/24 全球购物
单位单身证明范本
2014/01/11 职场文书
小学数学课题方案
2014/06/15 职场文书
学校开除通知书
2015/04/25 职场文书
关于法制教育的宣传语
2015/07/13 职场文书
python单元测试之pytest的使用
2021/06/07 Python
用Java实现简单计算器功能
2021/07/21 Java/Android