效果

分析

已经了解到vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的,无疑这个方法是本文中最重要、最基础的内容之一,如果不熟悉defineProperty,猛戳https://blog.csdn.net/c_kite/article/details/77950326
整理了一下,要实现mvvm的双向绑定,就必须要实现以下几点:
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm入口函数,整合以上三者

数据绑定

MVVM

// 创建一个Mvvm构造函数
function Mvvm(options = {}) {   // vm.$options Vue上是将所有属性挂载到上面// 所以我们也同样实现,将所有属性挂载到了$optionsthis.$options = options;// this._data 这里也和Vue一样let data = this._data = this.$options.data;// 数据劫持observe(data);
}

数据劫持

数据劫持主要是采用es5的definePropeerty

// 此处注意两个函数名单词的大小写// 创建一个Observe构造函数
// 写数据劫持的主要逻辑
function Observe(data) {// 所谓数据劫持就是给对象增加get,set// 先遍历一遍对象再说for (let key in data) {     // 把data属性通过defineProperty的方式定义属性let val = data[key];observe(val);   // 递归继续向下找,实现深度的数据劫持Object.defineProperty(data, key, {configurable: true,get() {return val;},set(newVal) {   // 更改值的时候if (val === newVal) {   // 设置的值和以前值一样就不理它return;}val = newVal;   // 如果以后再获取值(get)的时候,将刚才设置的值再返回去observe(newVal);    // 当设置为新值后,也需要把新值再去定义成属性}});}
}// 外面再写一个函数
// 不用每次调用都写个new
// 也方便递归调用
function observe(data) {// 如果不是对象的话就直接return掉// 防止递归溢出if (!data || typeof data !== 'object') return;return new Observe(data);
}

数据代理

数据代理就是让我们每次拿data里的数据时,不用每次都写一长串,如mvvm._data.a.b这种,我们其实可以直接写成mvvm.a.b这种显而易见的方式

下面我们在Mvvm这个函数里增加如下代码实现数据代理

function Mvvm(options = {}) {// ...省略// 数据劫持observe(data);// this 代理了this._datafor (let key in data) {Object.defineProperty(this, key, {configurable: true,get() {return this._data[key];     // 如this.a = {b: 1}},set(newVal) {this._data[key] = newVal;}});}
}

写到这里数据劫持和数据代理都实现了,那么接下来就需要编译一下了,把{{}}里面的内容解析出来

数据编译

function Mvvm(options = {}) {// ...省略// 编译    new Compile(options.el, this);
}
// 创建Compile构造函数
function Compile(el, vm) {// 将el挂载到实例上方便调用vm.$el = document.querySelector(el);// 在el范围里将内容都拿到,当然不能一个一个的拿// 可以选择移到内存中去然后放入文档碎片中,节省开销let fragment = document.createDocumentFragment();while (child = vm.$el.firstChild) {fragment.appendChild(child);    // 此时将el中的内容放入内存中/* appendChild 成功后,会把节点从原来的节点位置移除;当进入 while 循环的下次执行(child = node.firstChild) 时, 这里面运算的 firstChild 已经变成了原先移除的下一个节点;直到 node 中再也没有节点时,(child = node.firstChild) 的返回值会为「false」, 这时循环就结束了,appendChild 也完成了。 */}// 对el里面的内容进行替换function replace(frag) {Array.from(frag.childNodes).forEach(node => {let txt = node.textContent;let reg = /\{\{\s*([^}]+\S)\s*\}\}/g;   // 正则匹配{{}}if (node.nodeType === 3 && reg.test(txt)) { // 即是文本节点又有大括号的情况{{}}console.log(RegExp.$1); // 匹配到的第一个分组 如: a.b, clet arr = RegExp.$1.split('.');let val = vm;arr.forEach(key => {val = val[key];     // 如this.a.b});// 用trim方法去除一下首尾空格node.textContent = txt.replace(reg, val).trim();}// 如果还有子节点,继续递归replaceif (node.childNodes && node.childNodes.length) {replace(node);}});}replace(fragment);  // 替换内容vm.$el.appendChild(fragment);   // 再将文档碎片放入el中
}

现在数据已经可以编译了,但是我们手动修改后的数据并没有在页面上发生改变

下面我们就来看看怎么处理,其实这里就用到了特别常见的设计模式,发布订阅模式

发布订阅

发布订阅主要靠的就是数组关系,订阅就是放入函数,发布就是让数组里的函数执行

// 发布订阅模式  订阅和发布 如[fn1, fn2, fn3]
function Dep() {// 一个数组(存放函数的事件池)this.subs = [];
}
Dep.prototype = {addSub(sub) {   this.subs.push(sub);    },notify() {// 绑定的方法,都有一个update方法this.subs.forEach(sub => sub.update());}
};// 监听函数
// 通过Watcher这个类创建的实例,都拥有update方法
function Watcher(fn) {this.fn = fn;   // 将fn放到实例上
}
Watcher.prototype.update = function() {this.fn();
};let watcher = new Watcher(() => console.log(111));  //
let dep = new Dep();
dep.addSub(watcher);    // 将watcher放到数组中,watcher自带update方法, => [watcher]
dep.addSub(watcher);
dep.notify();   //  111, 111

数据更新视图

现在我们要订阅一个事件,当数据改变需要重新刷新视图,这就需要在replace替换的逻辑里来处理
通过new Watcher把数据订阅一下,数据一变就执行改变内容的操作

function replace(frag) {// 省略...node.textContent = txt.replace(reg, val).trim();// 监听变化// 给Watcher再添加两个参数,用来取新的值(newVal)给回调函数传参new Watcher(vm, RegExp.$1, newVal => {node.textContent = txt.replace(reg, newVal).trim();    });
}// 重写Watcher构造函数
function Watcher(vm, exp, fn) {this.fn = fn;this.vm = vm;this.exp = exp;// 添加一个事件// 这里我们先定义一个属性Dep.target = this;let arr = exp.split('.');let val = vm;arr.forEach(key => {    // 取值val = val[key];     // 获取到this.a.b,默认就会调用get方法});Dep.target = null; // 上面获取val[key]的时候会调用get方法, 因此使用完毕之后需要把该属性置位null
}

当获取值的时候就会自动调用get方法,于是我们去找一下数据劫持那里的get方法

function Observe(data) {let dep = new Dep();// 省略...Object.defineProperty(data, key, {get() {Dep.target && dep.addSub(Dep.target);   // 将watcher添加到订阅事件中 [watcher]return val;},set(newVal) {if (val === newVal) {return;}val = newVal;observe(newVal);dep.notify();   // 让所有watcher的update方法执行即可}})
}

当set修改值的时候执行了dep.notify方法,这个方法是执行watcherupdate方法,那么我们再对update进行修改一下

Watcher.prototype.update = function() {// notify的时候值已经更改了// 再通过vm, exp来获取新的值let arr = this.exp.split('.');let val = this.vm;arr.forEach(key => {    val = val[key];   // 通过get获取到新的值});this.fn(val);   // 将每次拿到的新值去替换{{}}的内容即可
};

那么以上就是Vue的数据绑定的分析, 下面我们再来看看双向绑定


双向绑定

    // html结构<input v-model="c" type="text">// 数据部分data: {a: {b: 1},c: 2}function replace(frag) {// 省略...if (node.nodeType === 1) {  // 元素节点let nodeAttr = node.attributes; // 获取dom上的所有属性,是个类数组Array.from(nodeAttr).forEach(attr => {let name = attr.name;   // v-model  typelet exp = attr.value;   // c        textif (name.includes('v-')){node.value = vm[exp];   // this.c 为 2}// 监听变化new Watcher(vm, exp, function(newVal) {node.value = newVal;   // 当watcher触发时会自动将内容放进输入框中});node.addEventListener('input', e => {let newVal = e.target.value;// 相当于给this.c赋了一个新值// 而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新vm[exp] = newVal;   });});}if (node.childNodes && node.childNodes.length) {replace(node);}}

computed(计算属性) && mounted(钩子函数)

    // html结构<p>求和的值是{{sum}}</p>data: { a: 1, b: 9 },computed: {sum() {return this.a + this.b;},noop() {}},mounted() {setTimeout(() => {console.log('所有事情都搞定了');}, 1000);}function Mvvm(options = {}) {// 初始化computed,将this指向实例initComputed.call(this);     // 编译new Compile(options.el, this);// 所有事情处理好后执行mounted钩子函数options.mounted.call(this); // 这就实现了mounted钩子函数}function initComputed() {let vm = this;let computed = this.$options.computed;  // 从options上拿到computed属性   {sum: ƒ, noop: ƒ}// 得到的都是对象的key可以通过Object.keys转化为数组Object.keys(computed).forEach(key => {  // key就是sum,noopObject.defineProperty(vm, key, {// 这里判断是computed里的key是对象还是函数// 如果是函数直接就会调get方法// 如果是对象的话,手动调一下get方法即可// 如: sum() {return this.a + this.b;},他们获取a和b的值就会调用get方法// 所以不需要new Watcher去监听变化了get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,set() {}});});}

总结

下面是完整代码实现
https://github.com/TheKiteRunners/Vue-2KindsOfBinding

参考链接:

https://segmentfault.com/a/1190000006599500
https://juejin.im/post/5abdd6f6f265da23793c4458

Vue数据绑定以及双向绑定原理分析相关推荐

  1. angular 强制更新视图_angular,vue,react数据双向绑定原理分析

    在不同的 MVVM 框架中,实现双向数据绑定的技术有所不同. Angular数据绑定 Angular 采用"脏值检测"的方式,数据发生变更后,对于所有的数据和视图的绑定关系进行一次 ...

  2. Vue 3.0双向绑定原理的实现

    proxy方法 vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过new Proxy()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调. Vu ...

  3. vue的数据双向绑定原理

    前言: 什么是数据双向绑定? vue是一个mvvm框架,即数据双向绑定,即当数据发生变化的时候,视图也就发生变化,当视图发生变化,数据也会跟着同步变化.这也算是vue的精髓之处了.单项数据绑定是使用状 ...

  4. 西安电话面试:谈谈Vue数据双向绑定原理,看看你的回答能打几分

    最近我参加了一次来自西安的电话面试(第二轮,技术面),是大厂还是小作坊我在这里按下不表,先来说说这次电面给我留下印象较深的几道面试题,这次先来谈谈Vue的数据双向绑定原理. 情景再现: 当我手机铃声响 ...

  5. 前端技巧|vue双向绑定原理,助你面试成功

    在面试一些大厂的时候,面试官可能会问到你vue双向数据绑定的原理是什么?有些小伙伴不知道是什么东西,这样你在面试官的眼里就大打折扣了.今天小千就来给大家介绍一下vue的双向绑定原理,千万不要错过啦. ...

  6. vue的双向绑定原理及实现

    前言 使用vue也好有一段时间了,虽然对其双向绑定原理也有了解个大概,但也没好好探究下其原理实现,所以这次特意花了几晚时间查阅资料和阅读相关源码,自己也实现一个简单版vue的双向绑定版本,先上个成果图 ...

  7. 记录vue的双向绑定原理及实现

    这里写自定义目录标题 思路分析 实现过程 1.实现一个Observer 2.实现Watcher 此文章是学习以为大神____chen的 <vue的双向绑定原理及实现> vue数据双向绑定是 ...

  8. v-model双向绑定原理_Vue数据绑定

    这是一篇简单的学习笔记.在学习一段时间Vue后,尝试实现一下Vue的数据绑定. 相关源码:https://github.com/buchuitoudegou/Data-Binding-demo Vue ...

  9. [vue] 什么是双向绑定?原理是什么?

    [vue] 什么是双向绑定?原理是什么? 双向数据绑定个人理解就是存在data→view,view→data两条数据流的模式.其实可以简单的理解为change和bind的结合.目前双向数据绑定都是基于 ...

最新文章

  1. 超干货!一位博士生80篇机器学习相关论文及笔记下载
  2. 奥巴马就职委员会选择微软Silverlight技术
  3. 014_CSS伪类选择器
  4. java 储存过程_Java储存过程
  5. 从零写一个编译器(九):语义分析之构造抽象语法树(AST)
  6. SpringBoot2.0系列(03)---SpringBoot之使用freemark视图模板
  7. WP7 Tip:改变启动页
  8. 3dmax phoenix fd4.0汉化补丁_教你屏蔽 Win10 Flash 删除补丁 - Windows 10
  9. angular 手动注入_手动引导Angular JS应用程序
  10. Flutter进阶第10篇: 本地存储,封装本地存储类,实现最简单的状态管理
  11. IntelliJ IDEA创建和配置Maven项目并运行
  12. 多道程序设计模拟——C语言实现
  13. 【读书笔记】期权交易策略(2)—— 差价策略
  14. 为什么每天都在学习,生活还是没有任何改善?
  15. Axure 8 团队协作
  16. AndroidQ文件存储适配
  17. 今天Sapphire来短消息问我“一件有点隐私”的事情,:)
  18. C++ 0X学习 (1)
  19. GBase 8a语法格式
  20. MYSQL-mysql中的truncate的用法

热门文章

  1. iPhone3和iPhone4图片处理
  2. iOS获取设备的序列号,自定义名,设备名,手机版本号,手机序列号,,手机型号,地方型号,当前App名称,App版本号......
  3. Mac 新建超级管理员账号
  4. python祝福祖国代码_“小程序 大梦想”之创意编程校园邀请赛------53信息技术学科周...
  5. 【二分】Kevin喜欢零
  6. markdown编辑器@uiw/react-md-editor深入使用
  7. 全国计算机等级一级考试中的基本操作题是如何保存提交?
  8. java 整数字符串转成财务表示形态
  9. android设置title字体大小,Android自定义TitleView标题开发实例
  10. opensips系列之共享内存,进程个数配置