浅谈mvvm-simple双向绑定简单实现


Posted in Javascript onApril 18, 2018

mvvm模式解放DOM枷锁

浅谈mvvm-simple双向绑定简单实现

mvvm原理分析

JavaScript在浏览器中操作HTML经历了几个不同阶段

第一阶段 直接用浏览器提供的原生API操作DOM元素

var dom = document.getElementById('id');
dom.innerHTML = 'hello mvvm';

第二阶段 jQuery的出现解决了原生API的复杂性和浏览器间的兼容性等问题,提供了更加简易方便的API

$('#id').text('hello mvvm')

第三阶段 MVC模式使前端可以和后端配合,修改服务端渲染后的页面内容

而随着产品对于用户体验的重视,交互体验越来越重要,仅用jQuery远远不够。 MVVM模型解决了频繁操作的痛点,Model-View-ViewModel模式将数据与视图的同步交由ViewModel完成

jQuery修改节点内容:

<p>name: <span id="name">vist</span>!</p>
<p>age: <span id="age">25</span>.</p>

var name = 'bestvist';
var age = 26;
$('#name').text(name);
$('#age').text(age);

MVVM模式下只需要关注数据结构:

var me = {
  name: 'vist',
  age: 25
}

修改相应属性就好

me.name = 'bestvist';
me.age = 26;

mvvm实现

mvvm实现数据绑定的几种方式:

  1. 发布-订阅模式
  2. 脏值检查
  3. 数据劫持

比较流行的vue采用的就是数据劫持和发布-订阅模式,通过劫持es5提供的Object.defineProperty()中各个属性的get,set方法, 数据更新时触发消息给订阅者,实现数据绑定功能。

Object.defineProperty(obj, prop, descriptor)方法直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,并返回这个对象。 该方法接受3个参数:

  • obj 定义属性的对象。
  • prop 被定义或修改的属性名。
  • descriptor 被定义或修改的属性的描述符。

一般情况通过直接给对象属性赋值来创建属性或者修改对应属性,而使用Object.defineProperty可以修改对象属性的一些额外默认配置。 如:

const obj = {name: 'Tom'};
Object.defineProperty(obj, 'name', {
 get: function(val) {
   return 'Jerry'; 
 }
})
console.log(obj.name);
//输出: Jerry

Object.defineProperty详细解释,请戳这里

mvvm实现的主要流程:

  • 数据代理,访问实例上的属性时直接返回对应data里的属性
  • 数据监听,对实例上的属性监听,如果数据改变通知订阅者更新
  • 指令解析,对每个元素节点进行解析,替换数据并绑定更新函数
  • 链接数据监听和指令解析,保证每个数据的更新,指令解析都可以获取并更新视图

实例化类:

new MVVM({
  el: '#app',
  data() {
    return {
      message: 'hello mvvm'
    }
  }
})

数据代理:

class MVVM {
  constructor(options) {
    this.$options = options || {};
    let data = this._data = this.$options.data();

    // 数据代理 vm.xxx => vm._data.xxx
    Object.keys(data).forEach(key => {
      this._proxyData(key);
    });

    // observe(data, this);
    // this.$compile = new Compile(options.el || document.body, this);

  }

  _proxyData(key) {
    Object.defineProperty(this, key, {
      configurable: true,
      enumerable: true,
      get: () => {
        return this._data[key];
      },
      set: newVal => {
        this._data[key] = newVal;
      }
    });
  }

}

数据监听,劫持实例属性更新

class Observer {
  constructor(data) {
    this.data = data;
    Object.keys(this.data).forEach(key => {
      this.defineReactive(key, this.data[key]);
    })
  }

  // 定义反应
  defineReactive(key, val) {
    let dep = new Dep();
    Object.defineProperty(this.data, key, {
      enumerable: true,
      configurable: false,
      get: () => {
        return val;
      },
      set: newVal => {
        if (val === newVal) {
          return;
        }
        val = newVal;
        // 赋值对象再进行劫持
        observe(val);
        ... // 数据修改通知
      }
    })
  }

}

function observe(val) {
  if (!val || typeof val !== 'object') {
    return;
  }
  return new Observer(val);
}

指令解析部分代码

class Compile {
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    if (this.$el) {
      this.$fragment = this.node2Fragment(this.$el);
      this.init();
      this.$el.appendChild(this.$fragment);
    }
  }

  init() {
    this.compileElement(this.$fragment);
  }

  node2Fragment(el) {
    let fragment = document.createDocumentFragment(), child;

    // 原生节点拷贝到fragment
    while (child = el.firstChild) {
      // appendChild将元素从dom上移到fragment
      fragment.appendChild(child);
    }
    return fragment;
  }

  compileElement(el) {
    let childNodes = el.childNodes;

    [].slice.call(childNodes).forEach(node => {
      let text = node.textContent;
      let reg = /\{\{(.*)\}\}/;

      if (this.isElementNode(node)) {
        this.compile(node);
      } else if (this.isTextNode(node) && reg.test(text)) {
        this.compileText(node, RegExp.$1);
      }

      if (node.childNodes && node.childNodes.length) {
        this.compileElement(node);
      }
    })
  }

}

其中

while (child = el.firstChild) {
  // appendChild将元素从dom上移到fragment
  fragment.appendChild(child);
}

通过appendChild改变原dom结构特点,逐步把dom元素节点移到fragment中。

完整代码 Vue源码

总结

以数据流为导向的mvvm模式极大的简化前端对于dom的操作,加快前端开发速度,同时也提高了用户体验。

参考:

剖析Vue原理&实现双向绑定MVVM mvvm廖雪峰

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

Javascript 相关文章推荐
[转]JS宝典学习笔记
Feb 07 Javascript
30个让人兴奋的视差滚动(Parallax Scrolling)效果网站
Mar 04 Javascript
Jquery+CSS3实现一款简洁大气带滑动效果的弹出层
May 15 Javascript
JavaScript prototype 使用介绍
Aug 29 Javascript
jQuery团购倒计时特效实现方法
May 07 Javascript
基于jQuery实现仿百度首页换肤背景图片切换代码
Aug 25 Javascript
js 动态给元素添加、移除事件的实现方法
Jul 19 Javascript
boostrapTable的refresh和refreshOptions区别浅析
Jan 22 Javascript
JS日程管理插件FullCalendar中文说明文档
Feb 06 Javascript
js实现超级玛丽小游戏
Mar 18 Javascript
记一次vue跨域的解决
Oct 21 Javascript
详解Vue3.0 + TypeScript + Vite初体验
Feb 22 Vue.js
JS点击动态添加标签、删除指定标签的代码
Apr 18 #Javascript
jQuery实现的手动拖动控制进度条效果示例【测试可用】
Apr 18 #jQuery
浅谈vuepress 踩坑记
Apr 18 #Javascript
使用webpack-dev-server处理跨域请求的方法
Apr 18 #Javascript
详解webpack-dev-server 设置反向代理解决跨域问题
Apr 18 #Javascript
jQuery实现的两种简单弹窗效果示例
Apr 18 #jQuery
vue双向数据绑定知识点总结
Apr 18 #Javascript
You might like
php自动加载的两种实现方法
2010/06/21 PHP
PHP实现下载功能的代码
2012/09/29 PHP
php Calender(日历)代码分享
2014/01/03 PHP
JS版网站风格切换实例代码
2008/10/06 Javascript
Ajax提交与传统表单提交的区别说明
2014/02/07 Javascript
一个非常全面的javascript URL解析函数和分段URL解析方法
2014/04/12 Javascript
jQuery实现分章节锚点“回到顶部”动画特效代码
2015/10/23 Javascript
如何通过js实现图片预览功能【附实例代码】
2016/03/30 Javascript
js中class的点击事件没有效果的解决方法
2016/10/13 Javascript
js 提交form表单和设置form表单请求路径的实现方法
2016/10/25 Javascript
Javascript自定义事件详解
2017/01/13 Javascript
js实现短信发送倒计时功能(正则验证)
2017/02/10 Javascript
JavaScript函数节流和函数去抖知识点学习
2018/07/31 Javascript
javascript实现手动点赞效果
2019/04/09 Javascript
js作用域和作用域链及预解析
2019/04/11 Javascript
详解使用WebPack搭建React开发环境
2019/08/06 Javascript
jQuery+css实现的点击图片放大缩小预览功能示例【图片预览 查看大图】
2020/05/29 jQuery
Vue ElementUI实现:限制输入框只能输入正整数的问题
2020/07/31 Javascript
JS实现公告上线滚动效果
2021/01/10 Javascript
python opencv实现运动检测
2018/07/10 Python
详解flask入门模板引擎
2018/07/18 Python
3分钟学会一个Python小技巧
2018/11/23 Python
对pycharm 修改程序运行所需内存详解
2018/12/03 Python
PyQt5 QTable插入图片并动态更新的实例
2019/06/18 Python
详解python中*号的用法
2019/10/21 Python
Pycharm配置PyQt5环境的教程
2020/04/02 Python
python获取响应某个字段值的3种实现方法
2020/04/30 Python
基于Keras的格式化输出Loss实现方式
2020/06/17 Python
Python Map 函数的使用
2020/08/28 Python
CSS3实战第一波 让我们尽情的圆角吧
2010/08/27 HTML / CSS
Styleonme中文网:韩国高档人气品牌
2017/06/21 全球购物
英国女性运动服品牌:Sweaty Betty
2018/11/08 全球购物
工程造价与管理专业应届生求职信
2013/11/23 职场文书
树转促学习心得体会
2014/09/10 职场文书
简单的辞职信怎么写
2015/02/28 职场文书
再次探讨go实现无限 buffer 的 channel方法
2021/06/13 Golang