在线使用-线上测试-源码

//代码:
<div id="app"><input v-model="name" type="text"><h1>{{ name }}</h1>
</div>
<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compile.js"></script>
<script src="./js/index.js"></script>
<script>
const vm = new Mvue({el: "#app",data: {name: "我是摩登"}
});
</script>

数据绑定

在正式开始之前我们先来说说数据绑定的事情,数据绑定我的理解就是让数据M(model)展示到 视图V(view)上。我们常见的架构模式有 MVC、MVP、MVVM模式,目前前端框架基本上都是采用 MVVM 模式实现双向绑定,Vue 自然也不例外。但是各个框架实现双向绑定的方法略有所不同,目前大概有三种实现方式。

  • 发布订阅模式
  • Angular 的脏查机制
  • 数据劫持

而 Vue2.0 则采用的是数据劫持与发布订阅相结合的方式实现双向绑定,数据劫持主要通过 Object.defineProperty 来实现。

Object.defineProperty

这篇文章我们不详细讨论 Object.defineProperty 的用法,我们主要看看它的存储属性 get 与 set。我们来看看通过它设置的对象属性之后有何变化。

var people = {name: "Modeng",age: 18
}
people.age; //18
people.age = 20;

上述代码就是普通的获取/设置对象的属性,看不到什么奇怪的变化。

var modeng = {}
var age;
Object.defineProperty(modeng, 'age', {get: function () {console.log("获取年龄");return age;},set: function (newVal) {console.log("设置年龄");age = newVal;}
});
modeng.age = 18;
console.log(modeng.age);

你会发现通过上述操作之后,我们访问 age 属性时会自动执行 get 函数,设置 age 属性时,会自动执行 set 函数,这就给我们的双向绑定提供了非常大的方便。

剖析

我们知道 MVVM 模式在于数据与视图的保持同步,意思是说数据改变时会自动更新视图,视图发生变化时会更新数据。

所以我们需要做的就是如何检测到数据的变化然后通知我们去更新视图,如何检测到视图的变化然后去更新数据。检测视图这个比较简单,无非就是我们利用事件的监听即可。

那么如何才能知道数据属性发生变化呢?这个就是利用我们上面说到的 Object.defineProperty 当我们的属性发生变化时,它会自动触发 set 函数从而能够通知我们去更新视图。

实现

通过上面的描述与分析我们知道 Vue 是通过数据劫持结合发布订阅模式来实现双向绑定的。我们也知道数据劫持是通过 Object.defineProperty 方法,当我们知道这些之后,我们就需要一个监听器 Observer 来监听属性的变化。得知属性发生变化之后我们需要一个 Watcher 订阅者来更新视图,我们还需要一个 compile 指令解析器,用于解析我们的节点元素的指令与初始化视图。所以我们需要如下:

  • Observer 监听器:用来监听属性的变化通知订阅者
  • Watcher 订阅者:收到属性的变化,然后更新视图
  • Compile 解析器:解析指令,初始化模版,绑定订阅者

监听器 Observer

监听器的作用就是去监听数据的每一个属性,我们上面也说了使用 Object.defineProperty 方法,当我们监听到属性发生变化之后我们需要通知 Watcher 订阅者执行更新函数去更新视图,在这个过程中我们可能会有很多个订阅者 Watcher 所以我们要创建一个容器 Dep 去做一个统一的管理。

function defineReactive(data, key, value) {//递归调用,监听所有属性observer(value);var dep = new Dep();Object.defineProperty(data, key, {get: function () {if (Dep.target) {dep.addSub(Dep.target);}return value;},set: function (newVal) {if (value !== newVal) {value = newVal;dep.notify(); //通知订阅器}}});
}function observer(data) {if (!data || typeof data !== "object") {return;}Object.keys(data).forEach(key => {defineReactive(data, key, data[key]);});
}function Dep() {this.subs = [];
}
Dep.prototype.addSub = function (sub) {this.subs.push(sub);
}
Dep.prototype.notify = function () {console.log('属性变化通知 Watcher 执行更新视图函数');this.subs.forEach(sub => {sub.update();})
}
Dep.target = null;

以上我们就创建了一个监听器 Observer,我们现在可以尝试一下给一个对象添加监听然后改变属性会有何变化。

var modeng = {age: 18
}
observer(modeng);
modeng.age = 20;

我们可以看到浏览器控制台打印出 “属性变化通知 Watcher 执行更新视图函数” 说明我们实现的监听器没毛病,既然监听器有了,我们就可以通知属性变化了,那肯定是需要 Watcher 的时候了。

订阅者 Watcher

Watcher 主要是接受属性变化的通知,然后去执行更新函数去更新视图,所以我们做的主要是有两步:

  1. 把 Watcher 添加到 Dep 容器中,这里我们用到了 监听器的 get 函数
  2. 接收到通知,执行更新函数。
function Watcher(vm, prop, callback) {this.vm = vm;this.prop = prop;this.callback = callback;this.value = this.get();
}
Watcher.prototype = {update: function () {const value = this.vm.$data[this.prop];const oldVal = this.value;if (value !== oldVal) {this.value = value;this.callback(value);}},get: function () {Dep.target = this; //储存订阅器const value = this.vm.$data[this.prop]; //因为属性被监听,这一步会执行监听器里的 get方法Dep.target = null;return value;}
}

这一步我们把 Watcher 也给弄了出来,到这一步我们已经实现了一个简单的双向绑定了,我们可以尝试把两者结合起来看下效果。

function Mvue(options, prop) {this.$options = options;this.$data = options.data;this.$prop = prop;this.$el = document.querySelector(options.el);this.init();
}
Mvue.prototype.init = function () {observer(this.$data);this.$el.textContent = this.$data[this.$prop];new Watcher(this, this.$prop, value => {this.$el.textContent = value;});
}

这里我们尝试利用一个实例来把数据与需要监听的属性传递进来,通过监听器监听数据,然后添加属性订阅,绑定更新函数。

<div id="app">{{ name }}</div>
const vm = new Mvue({el: "#app",data: {name: "我是摩登"}
}, "name");

我们可以看到数据已经正常的显示在页面上,那么我们在通过控制台去修改数据,发生变化后视图也会跟着修改。

到这一步我们我们基本上已经实现了一个简单的双向绑定,但是不难发现我们这里的属性都是写死的,也没有指令模板的解析,所以下一步我们来实现一个模板解析器。

Compile 解析器

Compile 的主要作用一个是用来解析指令初始化模板,一个是用来添加添加订阅者,绑定更新函数。

因为在解析 DOM 节点的过程中我们会频繁的操作 DOM, 所以我们利用文档片段(DocumentFragment)来帮助我们去解析 DOM 优化性能。

function Compile(vm) {this.vm = vm;this.el = vm.$el;this.fragment = null;this.init();
}
Compile.prototype = {init: function () {this.fragment = this.nodeFragment(this.el);},nodeFragment: function (el) {const fragment = document.createDocumentFragment();let child = el.firstChild;//将子节点,全部移动文档片段里while (child) {fragment.appendChild(child);child = el.firstChild;}return fragment;}
}

然后我们就需要对整个节点和指令进行处理编译,根据不同的节点去调用不同的渲染函数,绑定更新函数,编译完成之后,再把 DOM 片段添加到页面中。

Compile.prototype = {compileNode: function (fragment) {let childNodes = fragment.childNodes;[...childNodes].forEach(node => {let reg = /\{\{(.*)\}\}/;let text = node.textContent;if (this.isElementNode(node)) {this.compile(node); //渲染指令模板} else if (this.isTextNode(node) && reg.test(text)) {let prop = RegExp.$1;this.compileText(node, prop); //渲染 {{ }}模板}//递归编译子节点if (node.childNodes && node.childNodes.length) {this.compileNode(node);}});},compile: function (node) {let nodeAttrs = node.attributes;[...nodeAttrs].forEach(attr => {let name = attr.name;if (this.isDirective(name)) {let value = attr.value;if (name === "v-model") {this.compileModel(node, value);}node.removeAttribute(name);}});},//略
}

因为代码比较长如果全部贴出来会影响阅读,我们主要是讲整个过程实现的思路,文章结束我会把源码发出来,有兴趣的可以去查看全部代码。

到这里我们的整个的模板编译也已经完成,不过这里我们并没有实现过多的指令,我们只是简单的实现了 v-model 指令,本意是通过这篇文章让大家熟悉与认识 Vue 的双向绑定原理,并不是去创造一个新的 MVVM 实例。所以并没有考虑很多细节与设计。

现在我们实现了 Observer、Watcher、Compile,接下来就是把三者给组织起来,成为一个完整的 MVVM。

创建 Mvue

这里我们创建一个 Mvue 的类(构造函数)用来承载 Observer、Watcher、Compile 三者。

function Mvue(options) {this.$options = options;this.$data = options.data;this.$el = document.querySelector(options.el);this.init();
}
Mvue.prototype.init = function () {observer(this.$data);new Compile(this);
}

然后我们就去测试一下结果,看看我们实现的 Mvue 是不是真的可以运行。

<div id="app"><h1>{{ name }}</h1>
</div>
<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compile.js"></script>
<script src="./js/index.js"></script>
<script>const vm = new Mvue({el: "#app",data: {name: "完全没问题,看起来是不是很酷!"}});
</script>

我们尝试去修改数据,也完全没问题,但是有个问题就是我们修改数据时时通过 vm.$data.name 去修改数据,而不是想 Vue 中直接用 vm.name 就可以去修改,那这个是怎么做到的呢?其实很简单,Vue 做了一步数据代理操作。

数据代理

我们来改造下 Mvue 添加数据代理功能,我们也是利用 Object.defineProperty 方法进行一步中间的转换操作,间接的去访问。

function Mvue(options) {this.$options = options;this.$data = options.data;this.$el = document.querySelector(options.el);//数据代理Object.keys(this.$data).forEach(key => {this.proxyData(key);});this.init();
}
Mvue.prototype.init = function () {observer(this.$data);new Compile(this);
}
Mvue.prototype.proxyData = function (key) {Object.defineProperty(this, key, {get: function () {return this.$data[key]},set: function (value) {this.$data[key] = value;}});
}

到这里我们就可以像 Vue 一样去修改我们的属性了,非常完美

基于Vue2.0数据双向绑定原理-详解相关推荐

  1. Vue3.0之双向绑定原理——Proxy

    了解代理模式 一个例子 作为一个单身钢铁直男程序员,小王最近逐渐喜欢上了前端小妹,不过呢,他又和前台小妹不熟,所以决定委托与前端小妹比较熟的UI小姐姐帮忙给自己搭桥引线.小王于是请UI小姐姐吃了一顿大 ...

  2. 手动实现vue2.0的双向数据绑定原理

    vue2.0的双向数据绑定原理(手动实现) 一句话概括:数据劫持(Object.defineProperty)+发布订阅模式 一.首先了解什么是发布订阅模式 二.new Vue()的时候做了什么? 一 ...

  3. this指向-作用域、作用域链-预解析 变量提升-Vue组件传值 父子 子父 非父子-Vue数据双向绑定原理

    目录 this指向 作用域.作用域链 预解析 变量提升 Vue组件传值 父子 子父 非父子 Vue数据双向绑定原理 1.this指向 函数的this指向 看调用.不看声明 (1)普通函数调用 ①函数名 ...

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

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

  5. 操作系统:基于页面置换算法的缓存原理详解(下)

    概述: 在上一篇<操作系统:基于页面置换算法的缓存原理详解(上)>中,我们主要阐述了FIFO.LRU和Clock页面置换算法.接着上一篇说到的,本文也有三个核心算法要讲解.分别是LFU(L ...

  6. Vue数据双向绑定原理(vue2向vue3的过渡)

    众所周知,Vue的两大重要概念: 数据驱动 组件系统 接下来我们浅析数据双向绑定的原理 一.vue2 1.认识defineProperty vue2中的双向绑定是基于defineProperty的ge ...

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

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

  8. vue数据双向绑定原理

    vue 双向绑定原理 官网–https://cn.vuejs.org/v2/guide/reactivity.html 1.vue双向数据绑定是 通过 数据劫持 并结合 发布-订阅模式 的方法来实现的 ...

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

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

最新文章

  1. 使用ssh连接到centos7中docker容器
  2. 专访季虎:如何突破瓶颈构建高质量风控系统?
  3. iOS12系统应用发送普通邮件构建邮件
  4. 从双十一强化体验认知,看苏宁的“自增强回路”增长飞轮
  5. 对象水平对齐,并且按照竖直方向排列
  6. [Asp.Net Core] Blazor Server Side 项目实践 - 切换页面时保留状态
  7. 直播笔记 | Unity中射线检测详解
  8. element 方法返回的boolean被当成字符串了_quot;==quot;和 equals 方法有什么区别
  9. 一句话让你明白伪元素和伪类的区别
  10. onclick与addEventListener的区别
  11. 关于redis客户端连接不上
  12. 如何获得哔哩哔哩上下载的教学视频在电脑上看?(bilibili音频视频分离)
  13. 成都理工大学计算机考研资料汇总
  14. 中国足球深度剖析之五,破除周期性怪圈
  15. Android 入门教程:安装 Android Studio
  16. k8s-学习总结(控制器:RS\Deploy\DS\Job\CJ)
  17. Win7系统下发生一个文件夹,无法删除,解决记录
  18. html 单元格被撑开_表格单元格td设置宽度无效始终有内部的内容撑开
  19. hypothesisTest
  20. 【报告分享】德勤:2023中国消费者洞察与市场展望.pdf(附下载链接)

热门文章

  1. 河狸家创始人孟醒:10亿估值仅用半年 刷新雕爷牛腩记录
  2. jeecg-boot字典翻译改造(支持实体类详情查询自动翻译)
  3. 去除 字符串中空格/回车
  4. 如何搭建内网地图服务器
  5. Lettuce连接池
  6. linux ssh怎样删除文件夹,Linux服务器一键删除文件夹SSH命令
  7. 机器学习与分布式机器学习_我如何学习机器学习
  8. word页眉页脚操作
  9. 圣诞节,来棵Golang的圣诞树吧
  10. 前后端分离重复提交_防止表单重复提交(二)