Vue.js响应式原理,数据双向绑定
响应式原理
数据驱动
- 数据响应式
- 数据模型仅仅是普通的JavaScript对象,而当我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率
- 双向绑定
- 数据改变,视图改变;视图改变,数据也随之改变
- 我们可以使用v-model在表单元素上创建双向绑定数据
- 数据驱动
- 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图
Vue2 数据响应式核心原理:
- defineProperty介绍
- 浏览器兼容IE8以上
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">
hello
</div>
</body>
<script>
// 模拟Vue中的data选项
let data = {
msg: "hello",
count: 10,
};
// 模拟Vue中的实例
let vm = {};
// 调用封装响应式函数
proxyData(data);
function proxyData(data) {
// 遍历data对象的所有属性
Object.keys(data).forEach((key) => {
// 数据劫持:当访问或者设置vm中的成员的时候,做一些干预操作
Object.defineProperty(vm, key, {
// 可枚举
enumerable: true,
// 可配置(可以使用delete删除,可以通过defineProperty重新定义)
configurable: true,
// 当前获取值时候执行
get() {
console.log("get:", data[key]);
return data[key];
},
// 当设置值时候执行
set(newValue) {
console.log("set", newValue);
if (newValue === data[key]) {
return;
}
data[key] = newValue;
// 数据更改,更新DOM的值
document.querySelector("#app").textContent = data[key];
},
});
});
}
// 测试
vm.msg = "Hello World";
console.log(vm.msg);
</script>
</html>
Vue3 数据响应式原理:
- Proxy介绍
- 直接监听对象,而非属性
- ES6中新增,IE不支持,性能由浏览器优化
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">hello</div>
</body>
<script>
// 模拟Vue中的data选项
let data = {
msg: "hello",
count: 22,
};
// 模拟Vue实例
let vm = new Proxy(data, {
// 当访问vm的成员时会执行
get(target, key) {
console.log("get:", key, target[key]);
return target[key];
},
// 当设置 vm 的成员时会执行
set(target, key, newValue) {
console.log(console.log("set:", key, newValue));
if (target[key] === newValue) {
return;
}
target[key] = newValue;
document.querySelector("#app").textContent = target[key];
},
});
</script>
</html>
- 使用Proxy代理的是整个对象,对象中所有属性在访问或设置时都会触发get,set
- 使用defineProperty处理多个属性的时候需要循环
发布-订阅模式
- 发布者
- 订阅者
- 信号中心(事件中心)
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"一个信号,其他任务可以向信号中心"订阅"这个信号,从而知道什么时候自己可以开始自行. 这就叫做"发布-订阅模式"
- 兄弟组件通信过程
- 通过事件中心来进行通信
// eventBus.js
// 事件中心
const eventHub = new Vue()
// ComponentA.vue
// 发布者
addTodo: function() {
// 发布消息(事件)
eventHub.$emit('add-todo',{ text: this.newTodoText })
this.newTodoText = ''
}
// ComponentB.vue
// 订阅者
create: function() {
// 订阅消息(事件)
eventHub.$on('add-todo', this.addTodo)
}
- 模拟vue自定义事件(事件中心)
- 可以注册多个事件名称,也可以为一个事件注册多个处理函数
- 内部以对象的形式存储注册的事件
- 以下是不考虑传参情况的事件处理中心
- 有了事件中心之后在兄弟组件之间通信就可以很清晰的了解发布-订阅模式
// 事件触发器
class EventEmitter {
constructor() {
// 不设置原型的对象方法,可以提高性能
// 对象的形式{ 'click': [fn1, fn2], 'change': [fn]}
this.subs = Object.create(null);
}
// 注册事件(订阅)
$on(eventType, handler) {
// 确保事件类型的值一定是一个数组
this.subs[eventType] = this.subs[eventType] || [];
// 赋值操作,将事件的处理函数赋值给事件名
this.subs[eventType].push(handler);
}
// 触发事件(发布)
$emit(eventType) {
if (this.subs[eventType]) {
this.subs[eventType].forEach((handler) => {
handler();
});
}
}
}
let em = new EventEmitter();
// 注册事件(订阅)
em.$on("dataChange", () => {
console.log("dataChange1");
});
em.$on("dataChange", () => {
console.log("dataChange2");
});
// 触发事件(发布)
em.$emit("dataChange");
观察者模式
-
观察者(订阅者) --Watcher
- update(): 当事件发生时,具体要做的事情
-
目标(发布者) --Dep
- subs数组: 存储所有的观察者
- addSub(): 添加观察者
- notify(): 当事件发生,调用所有观察者的update()方法
-
没有事件中心
// 发布者(目标)
class Dep {
constructor() {
// 记录所有的订阅者
this.subs = [];
}
// 添加订阅者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub);
}
}
// 调用订阅者update方法
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
// 订阅者(观察者)
class Watcher {
update() {
console.log("update");
}
}
// 创建发布者
let dep = new Dep();
// 创建订阅者
let watcher = new Watcher();
// 添加订阅者
dep.addSub(watcher);
// 通知所有订阅者,调用订阅者update方法
dep.notify();
- 总结:
- 观察者模式是由具体目标调度,比如当事件触发,Dep就会去调用观察者方法,所以观察者模式的订阅者与发布者之间是存在依赖的
- 发布-订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在.
Vue 响应式原理
一个简易版Vue的核心功能
- 负责接收初始化参数
- 负责把data中属性注入到Vue实例,转换成getter/setter
- 负责调用observer监听data中所有属性的变化
- 负责调用compiler解析指令/插值表达式
- 通过dep收集依赖(收集观察者),watcher更新视图
- 下面用代码来具体体现一下
首先创建一个Vue类
结构
- $options
- $el
$data
- _proxyData()
// 创建Vue类
class Vue {
constructor(options) {
// 1.通过属性保存选项数据
this.$options = options || {};
this.$data = options.data || {};
this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el;
// 2.把data中成员转换成getter和setter,注入到vue实例中
this._proxyData(this.$data);
// 3.调用observer对象,监听数据的变化
new Observer(this.$data);
// 4.调用compiler对象,解析指令和插值表达式
new Compiler(this);
}
// 代理数据,让vue实例代理options中data的属性
_proxyData(data) {
// 遍历data中所有的属性
Object.keys(data).forEach(key => {
// 把data的属性注入vue的实例
Object.defineProperty(this, key, {
// 可枚举
enumerable: true,
// 可遍历
configurable: true,
// 获取值
get() {
return data[key];
},
// 设置值
set(newValue) {
if (data[key] === newValue) {
return;
}
data[key] = newValue;
},
});
});
}
}
Observer类(数据劫持)
功能:
- 负责把data选项中的属性转换成响应式数据
- data中某个属性也是对象,把该属性转换成响应式数据
- 数据变化发送通知
结构
- walk(data)
- defineReactive(data, key, value)
class Observer {
constructor(data) {
this.walk(data);
}
walk(data) {
// 判断data是否有值且对象, 如果不是对象则直接返回
if (!data || typeof data !== "object") {
return;
}
// 遍历data对象的所有属性,(如果接收的data是对象则将对象内部的值设置为响应式的)
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
});
}
// 1. 传入的data 2. 属性 3. 属性对应的值
defineReactive(obj, key, val) {
const self = this;
// 负责收集依赖, 并发送通知;
const dep = new Dep();
// 接收到的值如果是对象则调用walk方法,将val内部属性转换成响应式数据
this.walk(val);
// 注意: 这里拦截obj的属性key,如果下面get的时候是获取的obj[key]会陷入一个死循环
Object.defineProperty(obj, key, {
// 可枚举
enumerable: true,
// 可配置
configurable: true,
get() {
Dep.target && dep.addSub(Dep.target);
// 这里必须是返回val,如果返回自身的obj[key]会无限触发get,陷入死循环
return val;
},
set(newValue) {
if (newValue === val) {
return;
}
val = newValue;
// 把重新赋值的如果是个新对象内部的值也设置为响应式的
self.walk(val);
// 发送通知
dep.notify();
},
});
}
}
Compiler类(解析指令文本等)
功能:
- 负责编译模板,解析指令/插值表达式
- 负责页面的首次渲染
- 当数据变化后重新渲染视图
结构:
- el
vm
- compile(el)
- compileElement(node)
- compileText()
// 1. 负责编译模板,解析指令/插值表达式
// 2. 负责页面的首次渲染
// 3. 当数据变化后重新渲染视图
class Compiler {
constructor(vm) {
// 模板
this.el = vm.$el;
// vue实例
this.vm = vm;
this.compile(this.el);
}
// 编译模板,处理元素节点和文本节点
compile(el) {
// 获取所有的子节点。这是一个伪数组
let childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => {
if (this.isTextNode(node)) {
// 处理文本节点
this.compileText(node);
} else if (this.isElementNode(node)) {
// 处理元素节点
this.compileElement(node);
}
// 判断node节点是否存在子节点,如果有子节点要递归调用compile
if (node.childNodes && node.childNodes.length) {
this.compile(node);
}
});
}
// 编译元素节点,处理指令
compileElement(node) {
// 获取dom的属性节点 遍历
Array.from(node.attributes).forEach((attr) => {
let attrName = attr.name;
// 判断节点是否是指令
if (this.isDirective(attrName)) {
// 如果用if去判断,指令非常的多,判断起来代码会非常的多
// 以下进行的判断不使用if语句
// v-text -> text 截取前面的v-
attrName = attrName.substr(2);
// 属性的值就是变量名
let key = attr.value;
// 根据不同的指令名称调用对应的处理函数
this.update(node, key, attrName);
}
});
}
// 集中判断是什么指令然后调用相对应的指令函数
update(node, key, attrName) {
// 通过截取v-的指令名称匹配相对应的指令处理函数
let updateFn = this[attrName + "Updater"];
// 如果匹配到了这个函数,才执行该函数的调用需要指定调用者
updateFn && updateFn.call(this, node, this.vm[key], key);
}
// 关联指令的方法
// 处理text指令,传入dom和最后输出的值
textUpdater(node, value, key) {
node.textContent = value;
// 创建watcher对象,当数据改变更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
// v-html处理
htmlUpdater(node, key, value) {
const decoder = document.createElement('div');
decoder.innerHTML = value;
node.innerHTML = decoder;
new Watcher(this.vm, key, (newValue) => {
node.innerHTML = newValue;
});
}
// v-on处理 只考虑了单一事件
onUpdater(node) {
// 获取属性
const list = node.attributes;
// 遍历
for (let i = 0; i < list.length; i++) {
// 取得属性名
let name = list[i];
// 取得属性值
const value = node.getAttribute(name);
// 判断以v-on或者@开头
if (/^@|^v-on:/.test(name)) {
// 替换@或v-on为空
name = name.replace(/^@|^v-on:/, '');
// 向node节点添加事件
node.addEventListener(name, value);
}
}
}
// 更新表单元素
modelUpdater(node, value, key) {
node.value = value;
// 创建watcher对象,当数据改变更新视图
new Watcher(this.vm, key, (newValue) => {
node.value = newValue;
});
// 监听input事件,视图值变化时改变数据
node.addEventListener("input", () => {
this.vm[key] = node.value;
});
}
// 编译文本节点,处理插值表达式
compileText(node) {
//插值表达式例子: {{ msg}} 用正常匹配插值表达式中的变量名
let reg = /\{\{(.+?)\}\}/;
// 获取到文本节点的内容
let value = node.textContent;
if (reg.test(value)) {
// 获取到匹配的第一个分组内容(其实就是插值表达式中的变量)
let key = RegExp.$1.trim();
// 重新赋值
node.textContent = value.replace(reg, this.vm[key]);
// 创建watcher对象,当数据改变更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = value.replace(reg, newValue);
});
}
}
// 判断元素属性是否是指令
isDirective(attrName) {
// 指令是v-开头的
return attrName.startsWith("v-");
}
// 判断节点是否是文本节点
isTextNode(node) {
return node.nodeType === 3;
}
// 判断节点是否是元素节点
isElementNode(node) {
return node.nodeType === 1;
}
}
Dep类(收集观察者)
功能:
- 收集依赖,添加观察者(watcher)
- 通知所有观察者
结构:
subs
- addSub(sub)
- notify()
// 发布者 class Dep { constructor() { // 存储所有的观察者 this.subs = []; }
// 添加观察者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub);
}
}
// 发送通知
notify() {
this.subs.forEach(sub => {
sub.update();
});
}
}
### Watcher类(更新视图)
>功能
1. 当数据变化触发依赖,dep通知所有的Watcher 实例更新视图
2. 自身实例化的时候往dep对象中添加自己
>
>结构:
- vm
- key
- cb
- oldValue
---
- update()
```javascript
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
// data中的属性名称
this.key = key;
// 回调函数, 负责更新视图
this.cb = cb;
// 把watcher对象记录到Dep类的静态属性target
Dep.target = this;
// 触发get方法,在get方法中会调用addSub
this.oldValue = this.vm[this.key];
Dep.target = null;
}
// 当数据发生变化的时候更新视图
update() {
// 在Observer类中监听了所有数据的变化,所以一旦值改变这里获取到的就是新值
let newValue = this.vm[this.key];
if (this.oldValue === newValue) {
return;
}
// 更新视图
this.cb(newValue);
}
}
最终引用使用
- 接下来我们引入这些文件,创建一个vue实例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>
简易版Vue响应式流程
</title>
</head>
<body>
<div id="app">
<h1>插值表达式</h1>
<h3>你好{{msg}}</h3>
<h3>{{count}}</h3>
<h1>v-text</h1>
<div v-text="msg"></div>
<h1>v-model</h1>
<input type="text" v-model="msg" />
<input type="text" v-model="count" />
</div>
</body>
<script src="js/dep.js"></script>
<script src="js/watcher.js"></script>
<script src="js/compiler.js"></script>
<script src="js/observer.js"></script>
<script src="js/miniVue.js"></script>
<script>
// 使用vue
const vm = new Vue({
el: "#app",
data: {
msg: "Hello Vue",
count: 123,
person: { name: "zs" },
},
});
</script>
</html>
- 最终的使用结果如下图
所有定义的属性对象均具备了get,set方法,并且修改了msg也同步更新到了视图上了!!!
此贴备受大伙儿青睐呀~!