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 相关文章推荐
仅IE9/10同时支持script元素的onload和onreadystatechange事件分析
Apr 27 Javascript
原生js实现复制对象、扩展对象 类似jquery中的extend()方法
Aug 30 Javascript
jquery向上向下取整适合分页查询
Sep 06 Javascript
firefox浏览器用jquery.uploadify插件上传时报HTTP 302错误
Mar 01 Javascript
jQuery实现ToolTip元素定位显示功能示例
Nov 23 Javascript
js实现倒计时效果(小于10补零)
Mar 08 Javascript
Vue.js实现文章评论和回复评论功能
May 30 Javascript
基于vue2.0动态组件及render详解
Mar 17 Javascript
jquery实现联想词搜索框和搜索结果分页的示例
Oct 10 jQuery
小程序实现搜索框
Jun 19 Javascript
redux处理异步action解决方案
Mar 22 Javascript
微信小程序实现分页加载效果
Nov 19 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
WordPress中限制非管理员用户在文章后只能评论一次
2015/12/31 PHP
WordPress中获取所使用的模板的页面ID的简单方法
2015/12/31 PHP
php使用curl模拟多线程实现批处理功能示例
2019/07/25 PHP
基于jQuery的ajax功能实现web service的json转化
2009/08/29 Javascript
JQuery 选项卡效果(JS与HTML的分离)
2010/04/01 Javascript
Javascript 类与静态类的实现(续)
2010/04/02 Javascript
js 设置选中行的样式的实现代码
2010/05/24 Javascript
JS实现同时搜索百度和必应的方法
2015/01/27 Javascript
分享9点个人认为比较重要的javascript 编程技巧
2015/04/27 Javascript
基于jQuery实现的仿百度首页滑动选项卡效果代码
2015/11/16 Javascript
jQuery EasyUI 布局之动态添加tabs标签页
2015/11/18 Javascript
JS实现简单的二维矩阵乘积运算
2016/01/26 Javascript
浅谈JavaScript对象的创建方式
2016/06/13 Javascript
jQuery加密密码到cookie的实现代码
2017/04/18 jQuery
你可能不知道的JSON.stringify()详解
2017/08/17 Javascript
基于jQuery.i18n实现web前端的国际化
2018/05/04 jQuery
微信小程序实现自定义picker选择器弹窗内容
2020/05/26 Javascript
Vue中axios的封装(报错、鉴权、跳转、拦截、提示)
2019/08/20 Javascript
JS call()及apply()方法使用实例汇总
2020/07/11 Javascript
[03:17]2014DOTA2 国际邀请赛中国区预选赛 四强专访
2014/05/23 DOTA
python之pandas用法大全
2018/03/13 Python
让Django支持Sql Server作后端数据库的方法
2018/05/29 Python
Python实现的远程登录windows系统功能示例
2018/06/21 Python
Python中使用logging和traceback模块记录日志和跟踪异常
2019/04/09 Python
使用python获取(宜宾市地震信息)地震信息
2019/06/20 Python
python读写csv文件实例代码
2019/07/05 Python
python文件编写好后如何实践
2020/07/07 Python
Python实现简单猜数字游戏
2021/02/03 Python
css3实现元素环绕中心点布局的方法示例
2019/01/15 HTML / CSS
css3实现文字首尾衔接跑马灯的示例代码
2020/10/16 HTML / CSS
城市轨道专业个人求职信范文
2013/09/23 职场文书
总裁秘书岗位职责
2013/12/04 职场文书
客服工作职责
2013/12/11 职场文书
医院信息公开实施方案
2014/05/09 职场文书
创优争先心得体会
2014/09/11 职场文书
居委会工作总结2015
2015/05/18 职场文书