Vue数据绑定以及双向绑定原理分析
效果
分析
已经了解到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
方法,这个方法是执行watcher
的update
方法,那么我们再对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数据绑定以及双向绑定原理分析相关推荐
- angular 强制更新视图_angular,vue,react数据双向绑定原理分析
在不同的 MVVM 框架中,实现双向数据绑定的技术有所不同. Angular数据绑定 Angular 采用"脏值检测"的方式,数据发生变更后,对于所有的数据和视图的绑定关系进行一次 ...
- Vue 3.0双向绑定原理的实现
proxy方法 vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过new Proxy()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调. Vu ...
- vue的数据双向绑定原理
前言: 什么是数据双向绑定? vue是一个mvvm框架,即数据双向绑定,即当数据发生变化的时候,视图也就发生变化,当视图发生变化,数据也会跟着同步变化.这也算是vue的精髓之处了.单项数据绑定是使用状 ...
- 西安电话面试:谈谈Vue数据双向绑定原理,看看你的回答能打几分
最近我参加了一次来自西安的电话面试(第二轮,技术面),是大厂还是小作坊我在这里按下不表,先来说说这次电面给我留下印象较深的几道面试题,这次先来谈谈Vue的数据双向绑定原理. 情景再现: 当我手机铃声响 ...
- 前端技巧|vue双向绑定原理,助你面试成功
在面试一些大厂的时候,面试官可能会问到你vue双向数据绑定的原理是什么?有些小伙伴不知道是什么东西,这样你在面试官的眼里就大打折扣了.今天小千就来给大家介绍一下vue的双向绑定原理,千万不要错过啦. ...
- vue的双向绑定原理及实现
前言 使用vue也好有一段时间了,虽然对其双向绑定原理也有了解个大概,但也没好好探究下其原理实现,所以这次特意花了几晚时间查阅资料和阅读相关源码,自己也实现一个简单版vue的双向绑定版本,先上个成果图 ...
- 记录vue的双向绑定原理及实现
这里写自定义目录标题 思路分析 实现过程 1.实现一个Observer 2.实现Watcher 此文章是学习以为大神____chen的 <vue的双向绑定原理及实现> vue数据双向绑定是 ...
- v-model双向绑定原理_Vue数据绑定
这是一篇简单的学习笔记.在学习一段时间Vue后,尝试实现一下Vue的数据绑定. 相关源码:https://github.com/buchuitoudegou/Data-Binding-demo Vue ...
- [vue] 什么是双向绑定?原理是什么?
[vue] 什么是双向绑定?原理是什么? 双向数据绑定个人理解就是存在data→view,view→data两条数据流的模式.其实可以简单的理解为change和bind的结合.目前双向数据绑定都是基于 ...
最新文章
- 超干货!一位博士生80篇机器学习相关论文及笔记下载
- 奥巴马就职委员会选择微软Silverlight技术
- 014_CSS伪类选择器
- java 储存过程_Java储存过程
- 从零写一个编译器(九):语义分析之构造抽象语法树(AST)
- SpringBoot2.0系列(03)---SpringBoot之使用freemark视图模板
- WP7 Tip:改变启动页
- 3dmax phoenix fd4.0汉化补丁_教你屏蔽 Win10 Flash 删除补丁 - Windows 10
- angular 手动注入_手动引导Angular JS应用程序
- Flutter进阶第10篇: 本地存储,封装本地存储类,实现最简单的状态管理
- IntelliJ IDEA创建和配置Maven项目并运行
- 多道程序设计模拟——C语言实现
- 【读书笔记】期权交易策略(2)—— 差价策略
- 为什么每天都在学习,生活还是没有任何改善?
- Axure 8 团队协作
- AndroidQ文件存储适配
- 今天Sapphire来短消息问我“一件有点隐私”的事情,:)
- C++ 0X学习 (1)
- GBase 8a语法格式
- MYSQL-mysql中的truncate的用法
热门文章
- iPhone3和iPhone4图片处理
- iOS获取设备的序列号,自定义名,设备名,手机版本号,手机序列号,,手机型号,地方型号,当前App名称,App版本号......
- Mac 新建超级管理员账号
- python祝福祖国代码_“小程序 大梦想”之创意编程校园邀请赛------53信息技术学科周...
- 【二分】Kevin喜欢零
- markdown编辑器@uiw/react-md-editor深入使用
- 全国计算机等级一级考试中的基本操作题是如何保存提交?
- java 整数字符串转成财务表示形态
- android设置title字体大小,Android自定义TitleView标题开发实例
- opensips系列之共享内存,进程个数配置