前端MVVM框架解析之双向绑定


Posted in Javascript onJanuary 24, 2018

MVVM 框架

近年来前端一个明显的开发趋势就是架构从传统的 MVC 模式向 MVVM 模式迁移。在传统的 MVC 下,当前前端和后端发生数据交互后会刷新整个页面,从而导致比较差的用户体验。因此我们通过 Ajax 的方式和网关 REST API 作通讯,异步的刷新页面的某个区块,来优化和提升体验。

MVVM 框架基本概念

前端MVVM框架解析之双向绑定

在 MVVM 框架中,View(视图) 和 Model(数据) 是不可以直接通讯的,在它们之间存在着 ViewModel 这个中间介充当着观察者的角色。当用户操作 View(视图),ViewModel 感知到变化,然后通知 Model 发生相应改变;反之当 Model(数据) 发生改变,ViewModel 也能感知到变化,使 View 作出相应更新。这个一来一回的过程就是我们所熟知的双向绑定。

MVVM 框架的应用场景

MVVM 框架的好处显而易见:当前端对数据进行操作的时候,可以通过 Ajax 请求对数据持久化,只需改变 dom 里需要改变的那部分数据内容,而不必刷新整个页面。特别是在移动端,刷新页面的代价太昂贵。虽然有些资源会被缓存,但是页面的 dom、css、js 都会被浏览器重新解析一遍,因此移动端页面通常会被做成 SPA 单页应用。由此在这基础上诞生了很多 MVVM 框架,比如 React.js、Vue.js、Angular.js 等等。

MVVM 框架的简单实现

前端MVVM框架解析之双向绑定

模拟 Vue 的双向绑定流,实现了一个简单的MVVM 框架,从上图中可以看出虚线方形中就是之前提到的 ViewModel 中间介层,它充当着观察者的角色。另外可以发现双向绑定流中的 View 到 Model 其实是通过 input 的事件监听函数实现的,如果换成 React(单向绑定流) 的话,它在这一步交给状态管理工具(比如 Redux)来实现。另外双向绑定流中的 Model 到 View 其实各个 MVVM 框架实现的都是大同小异的,都用到的核心方法是 Object.defineProperty(),通过这个方法可以进行数据劫持,当数据发生变化时可以捕捉到相应变化,从而进行后续的处理。

前端MVVM框架解析之双向绑定

Mvvm(入口文件) 的实现

一般会这样调用 Mvvm 框架

const vm = new Mvvm({
      el: '#app',
      data: {
       title: 'mvvm title',
       name: 'mvvm name'
      },
     })

但是这样子的话,如果要得到 title 属性就要形如 vm.data.title 这样取得,为了让 vm.title 就能获得 title 属性,从而在 Mvvm 的 prototype 上加上一个代理方法,代码如下:

function Mvvm (options) {
 this.data = options.data
 const self = this
 Object.keys(this.data).forEach(key =>
  self.proxyKeys(key)
 )
}
Mvvm.prototype = {
 proxyKeys: function(key) {
  const self = this
  Object.defineProperty(this, key, {
   get: function () { // 这里的 get 和 set 实现了 vm.data.title 和 vm.title 的值同步
    return self.data[key]
   },
   set: function (newValue) {
    self.data[key] = newValue
   }
  })
 }
}

实现了代理方法后,就步入主流程的实现

function Mvvm (options) {
 this.data = options.data
 // ...
 observe(this.data)
 new Compile(options.el, this)
}

observer(观察者) 的实现

observer 的职责是监听 Model(JS 对象) 的变化,最核心的部分就是用到了 Object.defineProperty() 的 get 和 set 方法,当要获取 Model(JS 对象) 的值时,会自动调用 get 方法;当改动了 Model(JS 对象) 的值时,会自动调用 set 方法;从而实现了对数据的劫持,代码如下所示。

let data = {
 number: 0
}
observe(data)
data.number = 1 // 值发生变化
function observe(data) {
 if (!data || typeof(data) !== 'object') {
  return
 }
 const self = this
 Object.keys(data).forEach(key =>
  self.defineReactive(data, key, data[key])
 )
}
function defineReactive(data, key, value) {
 observe(value) // 遍历嵌套对象
 Object.defineProperty(data, key, {
  get: function() {
   return value
  },
  set: function(newValue) {
   if (value !== newValue) {
    console.log('值发生变化', 'newValue:' + newValue + ' ' + 'oldValue:' + value)
    value = newValue
   }
  }
 })
}

运行代码,可以看到控制台输出 值发生变化 newValue:1 oldValue:0,至此就完成了 observer 的逻辑。

Dep(订阅者数组) 和 watcher(订阅者) 的关系

观测到变化后,我们总要通知给特定的人群,让他们做出相应的处理吧。为了更方便地理解,我们可以把订阅当成是订阅了一个微信公众号,当微信公众号的内容有更新时,那么它会把内容推送(update) 到订阅了它的人。

前端MVVM框架解析之双向绑定

那么订阅了同个微信公众号的人有成千上万个,那么首先想到的就是要 new Array() 去存放这些人(html 节点)吧。于是就有了如下代码:

// observer.js
function Dep() {
 this.subs = [] // 存放订阅者
}
Dep.prototype = {
 addSub: function(sub) { // 添加订阅者
  this.subs.push(sub)
 },
 notify: function() { // 通知订阅者更新
  this.subs.forEach(function(sub) {
   sub.update()
  })
 }
}
function observe(data) {...}
function defineReactive(data, key, value) {
 var dep = new Dep()
 observe(value) // 遍历嵌套对象
 Object.defineProperty(data, key, {
  get: function() {
   if (Dep.target) { // 往订阅器添加订阅者
    dep.addSub(Dep.target)
   }
   return value
  },
  set: function(newValue) {
   if (value !== newValue) {
    console.log('值发生变化', 'newValue:' + newValue + ' ' + 'oldValue:' + value)
    value = newValue
    dep.notify()
   }
  }
 })
}

初看代码也比较顺畅了,但可能会卡在 Dep.target 和 sub.update,由此自然而然地将目光移向 watcher,

// watcher.js
function Watcher(vm, exp, cb) {
 this.vm = vm
 this.exp = exp
 this.cb = cb
 this.value = this.get()
}
Watcher.prototype = {
 update: function() {
  this.run()
 },
 run: function() {
  // ...
  if (value !== oldVal) {
   this.cb.call(this.vm, value) // 触发 compile 中的回调
  }
 },
 get: function() {
  Dep.target = this // 缓存自己
  const value = this.vm.data[this.exp] // 强制执行监听器里的 get 函数
  Dep.target = null // 释放自己
  return value
 }
}

从代码中可以看到当构造 Watcher 实例时,会调用 get() 方法,接着重点关注 const value = this.vm.data[this.exp] 这句,前面说了当要获取 Model(JS 对象) 的值时,会自动调用 Object.defineProperty 的 get 方法,也就是当执行完这句的时候,Dep.target 的值传进了 observer.js 中的 Object.defineProperty 的 get 方法中。同时也一目了然地在 Watcher.prototype 中发现了 update 方法,其作用即触发 compile 中绑定的回调来更新界面。至此解释了 Observer 中 Dep.target 和 sub.update 的由来。

来归纳下 Watcher 的作用,其充当了 observer 和 compile 的桥梁。

1 在自身实例化的过程中,往订阅器(dep) 中添加自己

2 当 model 发生变动,dep.notify() 通知时,其能调用自身的 update 函数,并触发 compile 绑定的回调函数实现视图更新

最后再来看下生成 Watcher 实例的 compile.js 文件。

compile(编译) 的实现

首先遍历解析的过程有多次操作 dom 节点,为提高性能和效率,会先将跟节点 el 转换成 fragment(文档碎片) 进行解析编译,解析完成,再将 fragment 添加回原来的真实 dom 节点中。代码如下:

function Compile(el, vm) {
 this.vm = vm
 this.el = document.querySelector(el)
 this.fragment = null
 this.init()
}
Compile.prototype = {
 init: function() {
  if (this.el) {
   this.fragment = this.nodeToFragment(this.el) // 将节点转为 fragment 文档碎片
   this.compileElement(this.fragment) // 对 fragment 进行编译解析
   this.el.appendChild(this.fragment)
  }
 },
 nodeToFragment: function(el) {
  const fragment = document.createDocumentFragment()
  let child = el.firstChild // △ 第一个 firstChild 是 text
  while(child) {
   fragment.appendChild(child)
   child = el.firstChild
  }
  return fragment
 },
 compileElement: function(el) {...},
}

这个简单的 mvvm 框架在对 fragment 编译解析的过程中对 {{}} 文本元素、v-on:click 事件指令、v-model 指令三种类型进行了相应的处理。

Compile.prototype = {
 init: function() {
  if (this.el) {
   this.fragment = this.nodeToFragment(this.el) // 将节点转为 fragment 文档碎片
   this.compileElement(this.fragment) // 对 fragment 进行编译解析
   this.el.appendChild(this.fragment)
  }
 },
 nodeToFragment: function(el) {...},
 compileElement: function(el) {...},
 compileText: function (node, exp) { // 对文本类型进行处理,将 {{abc}} 替换掉
  const self = this
  const initText = this.vm[exp]
  this.updateText(node, initText) // 初始化
  new Watcher(this.vm, exp, function(value) { // 实例化订阅者
   self.updateText(node, value)
  })
 },
 compileEvent: function (node, vm, exp, dir) { // 对事件指令进行处理
  const eventType = dir.split(':')[1]
  const cb = vm.methods && vm.methods[exp]
  if (eventType && cb) {
   node.addEventListener(eventType, cb.bind(vm), false)
  }
 },
 compileModel: function (node, vm, exp) { // 对 v-model 进行处理
  let val = vm[exp]
  const self = this
  this.modelUpdater(node, val)
  node.addEventListener('input', function (e) {
   const newValue = e.target.value
   self.vm[exp] = newValue // 实现 view 到 model 的绑定
  })
 },
}

在上述代码的 compileTest 函数中看到了期盼已久的 Watcher 实例化,对 Watcher 作用模糊的朋友可以往上回顾下 Watcher 的作用。另外在 compileModel 函数中看到了本文最开始提到的双向绑定流中的 View 到 Model 是借助 input 监听事件变化实现的。

项目地址

本文记录了些阅读 mvvm 框架源码关于双向绑定的心得,并动手实践了一个简版的 mvvm 框架,不足之处在所难免,欢迎指正。

项目演示

项目地址

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

Javascript 相关文章推荐
javascript实现随机读取数组的方法
Aug 03 Javascript
详解JavaScript编程中的数组结构
Oct 24 Javascript
12种JavaScript常用的MVC框架比较分析
Nov 16 Javascript
使用Promise链式调用解决多个异步回调的问题
Jan 15 Javascript
vue组件如何被其他项目引用
Apr 13 Javascript
bootstrap3使用bootstrap datetimepicker日期插件
May 24 Javascript
angular2中Http请求原理与用法详解
Jan 11 Javascript
学习React中ref的两个demo示例
Aug 14 Javascript
JS实现的新闻列表自动滚动效果示例
Jan 30 Javascript
利用js-cookie实现前端设置缓存数据定时失效
Jun 18 Javascript
解决vue scoped scss 无效的问题
Sep 04 Javascript
javascript实现点击按钮切换轮播图功能
Sep 23 Javascript
JS运动特效之完美运动框架实例分析
Jan 24 #Javascript
JS运动特效之同时运动实现方法分析
Jan 24 #Javascript
JS运动特效之链式运动分析
Jan 24 #Javascript
Bootstrap popover 实现鼠标移入移除显示隐藏功能方法
Jan 24 #Javascript
JS运动特效之任意值添加运动的方法分析
Jan 24 #Javascript
bootstrap 点击空白处popover弹出框隐藏实例
Jan 24 #Javascript
详解node child_process模块学习笔记
Jan 24 #Javascript
You might like
php中的时间处理
2006/10/09 PHP
ucenter中词语过滤原理分析
2016/07/13 PHP
js 动态添加标签(新增一行,其实很简单,就是几个函数的应用)
2009/03/26 Javascript
爆炸式的JS圆形浮动菜单特效代码
2010/03/03 Javascript
理解Javascript_01_理解内存分配原理分析
2010/10/11 Javascript
jQuery:delegate中select()不起作用的解决方法(实例讲解)
2014/01/26 Javascript
js将滚动条滚动到指定位置的简单实现方法
2016/06/25 Javascript
封装的dialog插件 基于bootstrap模态对话框的简单扩展
2016/08/10 Javascript
JavaScript中全选、全不选、反选、无刷新删除、批量删除、即点即改入库(在yii框架中操作)的代码分享
2016/11/01 Javascript
bootstrap table之通用方法( 时间控件,导出,动态下拉框, 表单验证 ,选中与获取信息)代码分享
2017/01/24 Javascript
vue 数组和对象不能直接赋值情况和解决方法(推荐)
2017/10/25 Javascript
微信小程序实现导航栏选项卡效果
2020/06/19 Javascript
详解使用mpvue开发github小程序总结
2018/07/25 Javascript
微信小程序云开发如何使用云函数生成二维码
2019/05/18 Javascript
JS如何在数组指定位置插入元素
2020/03/10 Javascript
微信小程序实现电子签名并导出图片
2020/05/27 Javascript
Django中间件工作流程及写法实例代码
2018/02/06 Python
pandas使用get_dummies进行one-hot编码的方法
2018/07/10 Python
django框架自定义用户表操作示例
2018/08/07 Python
Python实现登陆文件验证方法
2018/10/06 Python
详解mac python+selenium+Chrome 简单案例
2019/11/08 Python
Python3+Django get/post请求实现教程详解
2021/02/16 Python
详解HTML5中的元素与元素
2015/08/17 HTML / CSS
查找廉价航班和发现新目的地:Kiwi.com
2019/02/25 全球购物
移动通信专业自荐信范文
2013/11/12 职场文书
校园歌手大赛策划书
2014/01/17 职场文书
单位成立周年感言
2014/01/26 职场文书
企业管理毕业生求职信范文
2014/03/07 职场文书
外贸业务员求职信
2014/06/16 职场文书
工作期间打牌检讨书范文
2014/11/20 职场文书
2015年学校后勤工作总结
2015/04/08 职场文书
预备党员入党感想
2015/08/10 职场文书
mysql死锁和分库分表问题详解
2021/04/16 MySQL
SpringCloud Alibaba 基本开发框架搭建过程
2021/06/13 Java/Android
Python办公自动化解决world文件批量转换
2021/09/15 Python
英镑符号 £
2022/02/17 杂记