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 相关文章推荐
再论Javascript下字符串连接的性能
Mar 05 Javascript
jquery与prototype框架的详细对比
Nov 21 Javascript
使用JQuery库提供的扩展功能实现自定义方法
Sep 09 Javascript
JS获取Table中td值的方法
Mar 19 Javascript
浅谈bootstrap源码分析之scrollspy(滚动侦听)
Jun 06 Javascript
javascript数组常用方法汇总
Sep 10 Javascript
浅谈jquery中next与siblings的区别
Oct 27 Javascript
vue-router路由与页面间导航实例解析
Nov 07 Javascript
js计算两个日期间的天数月的实例代码
Sep 20 Javascript
解决ng-repeat产生的ng-model中取不到值的问题
Oct 02 Javascript
JavaScript实现连连看连线算法
Jan 05 Javascript
详解Nuxt内导航栏的两种实现方式
Apr 16 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
PHP面向对象分析设计的经验原则
2008/09/20 PHP
php+mysql结合Ajax实现点赞功能完整实例
2015/01/30 PHP
Symfony2安装的方法(2种方法)
2016/02/04 PHP
PHP实现验证码校验功能
2017/11/16 PHP
jquery 页面全选框实践代码
2010/04/02 Javascript
jquery延迟加载外部js实现代码
2013/01/11 Javascript
jQuery表单域选择器用法分析
2015/02/10 Javascript
javascript手风琴下拉菜单实现代码
2015/11/12 Javascript
jQuery和JavaScript节点插入元素的方法对比
2016/11/18 Javascript
Javascript中 带名 匿名 箭头函数的重要区别(推荐)
2017/01/29 Javascript
详解AngularJS通过ocLazyLoad实现动态(懒)加载模块和依赖
2017/03/01 Javascript
jQuery UI Grid 模态框中的表格实例代码
2017/04/01 jQuery
详解vue-cli快速构建项目以及引入bootstrap、jq
2017/05/26 Javascript
Angular.js自动化测试之protractor详解
2017/07/07 Javascript
常用的 JS 排序算法 整理版
2018/04/05 Javascript
Vue.js组件通信之自定义事件详解
2019/10/19 Javascript
基于Vue3.0开发轻量级手机端弹框组件V3Popup的场景分析
2020/12/30 Vue.js
[49:08]OpTic vs Serenity 2018国际邀请赛小组赛BO2 第一场 8.18
2018/08/19 DOTA
Python查看多台服务器进程的脚本分享
2014/06/11 Python
Python升级提示Tkinter模块找不到的解决方法
2014/08/22 Python
python实现对求解最长回文子串的动态规划算法
2018/06/02 Python
python使用PIL和matplotlib获取图片像素点并合并解析
2019/09/10 Python
Python列表操作方法详解
2020/02/09 Python
python爬虫beautifulsoup解析html方法
2020/12/07 Python
管事部库房保管员岗位职责
2014/02/21 职场文书
财务会计自荐信范文
2014/02/21 职场文书
物控部经理职务说明书
2014/02/25 职场文书
超市开业庆典活动策划方案
2014/09/15 职场文书
机关中层领导干部群众路线教育实践活动个人对照检查材料
2014/09/24 职场文书
幼儿园中秋节活动总结
2015/03/23 职场文书
离婚撤诉申请书范本
2015/05/18 职场文书
2015年行政人事工作总结
2015/05/21 职场文书
小学班级口号大全
2015/12/25 职场文书
《黄山奇石》教学反思
2016/02/18 职场文书
强烈推荐:小学生:暑假作息时间表(值得收藏)
2019/07/09 职场文书
Mysql 如何合理地统计一个数据库里的所有表的数据量
2022/04/18 MySQL