相关文章推荐
小胡子的数据线  ·  郑州十九高的传承_发展·  4 月前    · 
欢快的红茶  ·  DB004225-凸优化理论方法·  4 月前    · 
稳重的皮带  ·  娄彩敏_百度百科·  1 年前    · 
从未表白的茄子  ·  哥布林洞窟1–3资源无偿 - 百度·  2 年前    · 
胡子拉碴的便当  ·  《全职高手》神之领域上来就遇贵人,楼冠宁是叶 ...·  2 年前    · 
Code  ›  Vue.js双向绑定原理和MVVM框架的简单实现 | 微信开放社区
遍历 dom mvvm
https://developers.weixin.qq.com/community/develop/article/doc/0004cad012c4e80f46e8e6fa75b013
不要命的铁链
2 年前

交流专区
服务市场
微信学堂
文档
小程序
  • 常用主页

    小程序

    小游戏

    企业微信

    微信支付

  • 服务市场
    微信学堂
    文档
登录
评论

置顶 Vue.js双向绑定原理和MVVM框架的简单实现 精选 热门

凡科网 2019-07-23
4286 浏览
1 评论

研究vue.js双向数据绑定原理,实现一个简单的MVVM的框架,并成功的实现了双向数据绑定的效果。

前言

由于近两年前端技术变革速度太快,vue不论针对web项目开发,网站制作,还是app,小程序开发,都越来越流行,其便捷性及易用程度都让你不得不考虑去学习。这里就围绕着Vue.js框架的双向数据绑定的实现原理进行一个剖析。

前端主流MVVM框架双向绑定原理简述:

1、AngularJs(脏检测机制):
脏检查机制,主要是依据 $watch 对象来监测数据是否更新。
2、Vue.js(前端数据对象劫持):
Vue.js是通过数据劫持结合发布者-订阅者模式的方式来实现的。
3、React(手动触发绑定):
React本身并没有提到双向绑定的概念,但是可以利用setState api对states数据进行更新,从而实现数据层于视图层的同步更新(结构更新,重新“渲染”子树(虚拟DOM),找出最小改动步骤,打包DOM操作,对真实DOM树进行修改)。

what is mvvm ?

Model:数据模型,存储数据
View:带特殊属性、指令的 html 模板
ViewModel:依靠模板上面的指令,修改model数据后自动渲染view视图

Vue.js实现双向绑定的核心API:

Object.defineProperty(),这个es5提供的API是实现双向绑定的核心,最主要的作用是重写数据的get、set方法,当我们访问或设置对象的属性的时候,都会触发相对应的函数动一些手脚做点我们自己想做的事情,实现“数据劫持”(发布者-订阅者模式)的效果。

详细用法 & 解释:

https://www.jianshu.com/p/8fe1382ba135

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

enumerable: true, // 是否可以被枚举 默认是不能被枚举(遍历) // get,set 设置时不能设置 writable和value,要一对一对设置,交叉设置/同时存在会报错 // value: 23, // 设置属性的值 // writable: true, // 是否可以修改对象 get() { // 获取 obj.age 的时候就会调用get方法 getToDo(age); return age; set(val) { // 将值重新赋给 obj.age setToDo(val, age); age = val; function getToDo (val) { console.log('get - ' + val); function setToDo (nVal, oVal) { console.log('set - nVal = ' + nVal + ', oVal = ' + oVal); console.log(obj.age); // 23 delete obj.age; // configurable设为false 删除无效 console.log(obj.age); // 23 obj.age = 24; console.log(obj.age); // 24 for (let key in obj) { // 默认情况下通过defineProperty定义的属性是不能被枚举(遍历)的,需要设置enumerable为true才可以 否则只能拿到singer 属性 console.log(key); // name, age

实现的思路:

实现数据监听器Observer,用Object.defineProperty()重写数据的get、set,值更新就在set中通知订阅者更新数据。

实现模板编译Compile,深度遍历dom树,对每个元素节点的指令模板进行替换数据以及订阅数据。

实现Watch用于连接Observer和Compile,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。

简单的实现一个MVVM框架:

  • View 含有属性和指令的模板
  • <div id="app"> <input type="text" v-model="name"> <h3 v-bind="name"></h3> <input type="text" v-model="testData1"> <h3>{{ testData1 }}</h3> <input type="text" v-model="testData2"> <h3>{{ testData2 }}</h3> </div>
  • 调用方法,初始化实例
  • window.onload = function () { var app = new MiniVue({ el: '#app', // dom data: { // 数据 testData1: 'Mini Vue', testData2: '双向绑定', name: 'JesBrian'
  • 实现MiniVue类
  • function MiniVue(options = {}) { this.$options = options; // 配置挂载 this.$el = document.querySelector(options.el); // 获取挂载的dom元素 this._data = options.data; // 数据挂载 this._watcherTpl = {}; // watcher池 this._observer(this._data); // 传入数据,执行函数,重写数据的get set this._compile(this.$el); // 传入dom,执行函数,编译模板 发布订阅
  • MiniVue里具体实现的发布-订阅
  • // Watch类: // 1、在模板编译_compile()阶段发布订阅 // 2、在赋值操作的时候,更新视图 function Watcher(el, vm, val, attr) { this.el = el; // 指令对应的DOM元素 this.vm = vm; // myVue实例 this.val = val; // 指令对应的值 this.attr = attr; // dom获取值,如value获取input的值 / innerHTML获取dom的值 this.update(); // 更新视图 Watcher.prototype.update = function () { this.el[this.attr] = this.vm._data[this.val]; // 获取data的最新值 赋值给dom 更新视图 // 数据监听器_observer: // 用Object.defineProperty()遍历data重写所有属性的get set。然后在给对象的某个属性赋值的时候,就会触发set。在set中我们可以监听到数据的变化,然后就可以触发watch更新视图。 MiniVue.prototype._observer = function (obj) { var _this = this; Object.keys(obj).forEach(key => { // 遍历数据 _this._watcherTpl[key] = { // 每个数据的订阅池() _directives: [] var value = obj[key]; // 获取属性值 var watcherTpl = _this._watcherTpl[key]; // 数据的订阅池 Object.defineProperty(_this._data, key, { // 双向绑定最重要的部分 重写数据的set get configurable: true, // 可以删除 enumerable: true, // 可以遍历 get() { console.log(`${key}获取值:${value}`); return value; // 获取值的时候 直接返回 set(newVal) { // 改变值的时候 触发set console.log(`${key}更新:${newVal}`); if (value !== newVal) { value = newVal; for (let item of watcherTpl._directives) { // 遍历订阅池 item.update(); // 遍历所有订阅的地方(v-model+v-bind+{{}}) 触发this._compile()中发布的订阅Watcher 更新视图
  • MiniVue的_compile模板编译实现
  • // 首先是深度遍历dom树,遍历每个节点以及子节点。 // 将模板中的变量替换成数据,初始化渲染页面视图。 // 把指令绑定的属性添加到对应的订阅池中 // 一旦数据有变动,收到通知,更新视图。 MiniVue.prototype._compile = function (el) { var _this = this, nodes = el.children; // 获取要挂载app的dom for (var i = 0, len = nodes.length; i < len; i++) { // 遍历dom节点 var node = nodes[i]; if (node.children.length) { _this._compile(node); // 递归深度遍历 dom树 // 如果有v-model属性,并且元素是input或者textarea,监听它的input事件 if (node.hasAttribute('v-model') && (node.tagName = 'INPUT' || node.tagName == 'TEXTAREA')) { node.addEventListener('input', (function (key) { var attVal = node.getAttribute('v-model'); // 获取v-model绑定的值 _this._watcherTpl[attVal]._directives.push(new Watcher( // 将dom替换成属性的数据并发布订阅在set的时候更新数据 node, _this, attVal, 'value' return function () { _this._data[attVal] = nodes[key].value; // input值改变的时候将新值赋给数据触发set 进而触发watch 更新视图 })(i)); if (node.hasAttribute('v-bind')) { // v-bind指令 var attrVal = node.getAttribute('v-bind'); // 绑定的data _this._watcherTpl[attrVal]._directives.push(new Watcher( // 将dom替换成属性的数据并发布订阅 在set的时候更新数据 node, _this, attrVal, 'innerHTML' var reg = /\{\{\s*([^}]+\S)\s*\}\}/g, txt = node.textContent; // 正则匹配{{}} if (reg.test(txt)) { node.textContent = txt.replace(reg, (matched, placeholder) => { // matched匹配的文本节点包括{{}}, placeholder 是{{}}中间的属性名 var getName = _this._watcherTpl; // 所有绑定watch的数据 getName = getName[placeholder]; // 获取对应watch 数据的值 if (!getName._directives) { // 没有事件池, 创建事件池 getName._directives = []; getName._directives.push(new Watcher( // 将dom替换成属性的数据并发布订阅在set的时候更新数据 node, _this, placeholder, 'innerHTML' return placeholder.split('.').reduce((val, key) => { return _this._data[key]; //获取数据的值触发get返回当前值 }, this.$el);
  • ES6 新的 API Proxy
  • MDN 详细文档

    阮一峰es6要点总结——Proxy

  • 为什么 Vue 3 要将 Object.defineProperty 转为使用 Proxy
  • Object.defineProperty 无法检测数组的变化

    Object.defineProperty 当对象属性增删的时候,是监控不到的 (Vue.$set())

  • 使用 Proxy 重构 _observer 原型方法
  • MiniVue.prototype._observer = function (obj) { var _this = this // 把代理器返回的对象存到 this._data 里面 for (let key in obj) { // 遍历数据 _this._watcherTpl[key] = { // 每个数据的订阅池() _directives: [] _this._data = new Proxy(obj, { set(target, key, value) { console.log(`${key}更新:${value}`); // 利用 Reflect 还原默认的赋值操作 let res = Reflect.set(target, key, value) var watcherTpl = _this._watcherTpl[key]; // 数据的订阅池 for (let item of watcherTpl._directives) { // 遍历订阅池 item.update(); // 遍历所有订阅的地方(v-model+v-bind+{{}}) 触发this._compile()中发布的订阅Watcher 更新视图 return res

    很好,经过一轮的大费周折,我们终于简单的实现了一个MVVM的框架,成功的实现了双向数据绑定的效果。我们在日常使用轮子的时候不仅仅要会用,更重要的是知道这个轮子的实现原理,并且对其进行深入的探究,甚至是自己模仿思路重新造一个轮子。

    最后一次编辑于 2019-07-24
    点赞 1
    收藏
    分享

    扫描小程序码分享

    复制链接

    删除文章后,文章内容和评论将一并被删除,且不可恢复。

    删除 取消
    评论
    关闭

    请选择投诉理由

    • 广告内容
    • 违法违规
    • 恶意灌水内容
    • 其他

    1 个评论

    • 天魔&龙魂
      天魔&龙魂
      2019-07-23
     
    推荐文章
    小胡子的数据线  ·  郑州十九高的传承_发展
    4 月前
    欢快的红茶  ·  DB004225-凸优化理论方法
    4 月前
    稳重的皮带  ·  娄彩敏_百度百科
    1 年前
    从未表白的茄子  ·  哥布林洞窟1–3资源无偿 - 百度
    2 年前
    胡子拉碴的便当  ·  《全职高手》神之领域上来就遇贵人,楼冠宁是叶修的合作人之一|网游|陈果_网易订阅
    2 年前
    今天看啥   ·   Py中国   ·   codingpro   ·   小百科   ·   link之家   ·   卧龙AI搜索
    删除内容请联系邮箱 2879853325@qq.com
    Code - 代码工具平台
    © 2024 ~ 沪ICP备11025650号