一、数据驱动
- 数据响应式: 数据模型仅仅是 JavaScript 对象,当数据发生改变时,视图就会更新,避免了繁琐的 dom 操作,提高开发效率.
- 双向绑定: 数据改变,视图改变;视图改变,数据改变.
- 数据驱动: 开发过程只需要注重数据本身,不需要关注数据是如何渲染到视图.
二、数据响应式原理
Vue 2.x
- Vue2.x 深入响应式原理
- 自定义 obsever 实现响应式,除了针对普通对象属性进行数据劫持之外,也对值为 object 类型的属性进行了递归拦截. 效果如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h1 class="msg">msg: hello world</h1>
<h1 class="date">
<span class="start">start: 2020</span>
- <span class="end">end: 2021</span>
</h1>
<h1 class="talk">talk: say...</h1>
</div>
<script>
let data = {
msg: "hello world",
date: {
start: 2020,
end: 2021,
},
person: {
skill: {
talk: "say..."
}
}
};
obsever(data);
function obsever(data) {
// 遍历获取所有的 key
Object.keys(data).forEach(key => {
let val = data[key];
// 当前值为对象,进行递归
if (Object.prototype.toString.call(val) === "[object Object]") {
obsever(val);
} else {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
console.log("getter...", key);
},
set(newVal) {
if (newVal === val) return;
console.log("setter...", key);
document
.querySelector(`.${key}`)
.textContent = `${key}: ${newVal}`;
}
});
}
});
}
</script>
</body>
</html>
Vue 3.x
- MDN - Proxy,直接监听对象,而不是属性.
- ES6 新增,不支持 IE,性能由浏览器优化,所以优化方面要比 defineProperty 更好.
- proxy 实现响应式,效果如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script>
let data = {
msg: "hello world",
};
let proxy = obsever(data);
function obsever(data) {
return new Proxy(data, {
get(target, key, receiver) {
console.log("getter...", key, Reflect.get(target, key, receiver));
return target[key];
},
set(target, key, newVal, receiver) {
const oldValue = target[key];
if (oldValue == newVal) return true;
let result = Reflect.set(target, key, newVal, receiver);
console.log('setter...', key, newVal);
update();
return result;
}
});
}
update();
function update() {
document.querySelector("#app").innerHTML = `
<h1 class="msg">msg: ${proxy.msg}</h1>`
}
</script>
</body>
</html>
三、发布/订阅模式 & 观察者模式
发布/订阅模式
假定存在一个信号中心,当某个任务完成之后,就向信号中心进行 发布(public) 信号,其他任务可以向信号中心 订阅(subscribe),从而知道自己什么时候开始执行.
- 订阅者
- 发布者
- 信号中心
3.1 Vue 自定义事件
- Vue 自定义事件
- 从下图中并不能很好的区分 订阅者、发布者、信号中心
3.2 兄弟组件通信
- 假设 A 和 B 两个兄弟组件需要进行通信,我们通过 Vue 创建一个 eventHub,通过它来实现发布和订阅.
- A 组件 中存在 addTo 方法实现消息的发布;B 组件 中在 created 中实现对消息的订阅.
- eventHub(信号中心)、ComponentA(发布者)、ComponentB(订阅者)
<script>
// 信号中心
let eventHub = new Vue({});
// ComponentA —— 发布者
addTo() {
// 发布消息(事件)
vm.$emit("dataChange", {
data: 1000
});
}
// ComponentB —— 订阅者
created() {
// 订阅消息(事件)
vm.$on("dataChange", (data) => {
console.log("dataChange.......",data);
})
}
</script>
3.3 模拟 Vue 自定义事件(发布/订阅)
<script>
class EventEmitter {
constructor() {
this.subs = {}; // 用于存储注册的事件
}
// 注册事件(订阅)
on(type, handle) {
if (this.subs[type]) { // 对应事件已存在
this.subs[type].push(handle);
} else { // 对应事件首次注册
this.subs[type] = [handle];
}
}
// 触发事件(发布)
emit(type, params) {
if (this.subs[type]) {
this.subs[type].forEach(handle => {
handle && handle(params);
});
}
}
}
let emitter = new EventEmitter();
emitter.on("click", (data) => {
console.log("click 1", data);
});
emitter.on("click", (data) => {
console.log("click 2", data);
});
emitter.emit("click", {
text: "hello world"
});
</script>
观察者模式
- 观察者 (订阅者) —— Watcher
- update():当事件发生时,具体要做的事情
- 目标 (发布者) —— Dep
- subs 数组:存储所有的观察者
- addSubs():添加观察者
- notify():当事件发生时,调用所有的观察者的 update() 方法
- 没有事件中心
观察者模式 和 发布/订阅模式 的概念非常的相似,注意不要混淆.
实现一个简单的观察者模式:
<script>
/**
* 观察者模式
**/
// 发布者——目标
class Dep {
constructor() {
this.subs = []; // 存储所有观察者
}
addSubs(watcher) { // 添加观察者
if (watcher && watcher.update) {
this.subs.push(watcher);
}
}
notify(parms) { // 执行所有的观察者
this.subs.forEach(watcher => {
if (watcher) {
watcher.update(parms);
}
});
}
}
// 订阅者——观察者
class Watcher {
update(params) { // 需要发布者调用
console.log("update......", params);
}
}
// 测试
let dep = new Dep();
let watcher = new Watcher();
dep.addSubs(watcher);
dep.notify({
data: [1, 2, 3, 4, 5, 6]
})
</script>
发布/订阅模式 和 观察者模式 的区别
- 观察者模式:【目标】 和 【观察者】 是相互依赖的关系
- 发布/订阅模式:【发布者】和【订阅者】之间需要通过【事件中心】进行通信,减少【发布者】和【订阅者】之间的依赖,更灵活
四、模拟 Vue 响应式原理
- vue 基本结构
- vue 实例对象
- 整体结构
- Vue: 把 data 转换成 gette/setter,并把 data 中的成员注入到 Vue 实例上.
- Observer: 能够对数据对象的所有属性进行监听,数据发生变动时会拿到最新值,并通知 Dep , Dep 会通知所有的 Watcher 进行更新.
- Dep & Watcher: 熟悉的观察者模式,Dep 负责把所有的观察者 Watcher 添加进来,Watcher 中的 update 方法负责视图的更新.
要模拟实现 Vue 的功能
- 1. 实现 Vue 类
- 负责接收初始化参数(选项)
- 负责把 data 中的数据注入到 Vue 实例上,并转换成对应的 gette/setter
- 负责调用 Observer 监听 data 中所有属性的变化
- 负责调用 Compile 解析 指令/插值表达式
class Vue {
constructor(options) {
// 1. 接收初始化参数(选项) options
this.$options = options || {};
this.$data = options.data || {};
this.$el =
typeof options.el === "string"
? document.querySelector(options.el)
: options.el;
// 2. 把 data 中的数据注入到 Vue 实例上,并转换成对应的 gette/setter
this._proxyData(this.$data);
// 3. 调用 Observer 监听 data 中所有属性的变化
new Observer(this.$data);
// 4. 调用 Compile 解析 指令/插值表达式
new Compiler(this);
}
_proxyData(data) {
Object.keys(data).forEach((key) => {
// 将 data 中的数据注入到 this 上
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get() {
return data[key];
},
set(newVal) {
if (data[key] === newVal) return;
data[key] = newVal;
},
});
});
}
}
- 2. 实现 Observer 类
- 负责把 data 中的属性转换为响应式
- 如果 data 中的属性为对象,也要将这个对象转换为响应式
- 当 data 中数据发生变化时,要发送通知
class Observer {
constructor(data) {
this.walk(data);
}
walk(data) {
// 1. 判断 data 不为空 或者 不是一个对象
if (!data || typeof data !== "object") return;
// 2. 否则遍历 data 中的所有属性
Object.keys(data).forEach((key) => {
this.defineReative(data, key, data[key]);
});
}
// 调用 Object.defineProperty 将属性转换成 getter/setter
defineReative(obj, key, val) {
const that = this;
// 收集依赖,发送通知
const dep = new Dep();
// 如果 val 是对象,那么把 val 内部的属性也转换成响应式数据
this.walk(val);
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
// 收集依赖
Dep.target && dep.addSub(Dep.target);
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
// 防止当前属性被重新赋值为一个新对象时,失去响应式
that.walk(val);
// 发送通知
dep.notify();
},
});
}
}
- 3. 实现 Compiler 类
- 负责编译模板,解析指令/插值表达式,实例化 Watcher 实例,触发 get 方法,向 Dep 添加 Watcher 实例
- 负责页面的首次渲染
- 当数据变化后重新渲染视图
class Compiler {
constructor(vm) {
this.el = vm.$el;
this.vm = vm;
// 页面的初始化渲染
this.compile(this.el);
}
// 编译模板,处理文本节点和元素节点
compile(el) {
let childNodes = el.childNodes; // childNodes 伪数组
Array.from(childNodes).forEach((node) => {
if (this.isTextNode(node)) {
// 处理文本节点
this.compileText(node);
} else if (this.isElementNode(node)) {
// 处理元素节点
this.compileElement(node);
}
// 判断 node 是否存在子节点,如果存在,要递归遍历子节点
if (node.childNodes && node.childNodes.length) {
this.compile(node);
}
});
}
// 编译元素节点,处理指令
compileElement(node) {
// console.log(node.attributes); // 伪元素
// 遍历所有的属性节点
Array.from(node.attributes).forEach((attr) => {
let attrName = attr.name;
// 判断是否是指令
if (this.isDiretive(attrName)) {
// 去除 v- 前缀,如:v-text ——> text
attrName = attrName.substr(2);
let key = attr.value;
this.update(node, key, attrName);
}
});
}
// 根据指令调用不同的 updater
update(node, key, attrName) {
let updateFunc = this[attrName + "Upater"];
updateFunc && updateFunc.call(this, node, this.vm[key], key);
}
// 处理 v-text 指令
textUpater(node, value, key) {
node.textContent = value;
// 创建 Watcher 对象,当数据改变更新视图
new Watcher(this.vm, key, (newVlaue) => {
node.textContent = newVlaue;
});
}
// 处理 v-model 指令
modelUpater(node, value, key) {
node.value = value;
// 创建 Watcher 对象,当数据改变更新视图
new Watcher(this.vm, key, (newVlaue) => {
node.value = newVlaue;
});
// 注册事件
node.addEventListener("input", () => {
this.vm[key] = node.value;
});
}
// 编译文本节点,处理插值表达式
compileText(node) {
// console.dir(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, (newVlaue) => {
node.textContent = newVlaue;
});
}
}
// 判断元素属性是否是指令,判断属性是否是 v- 开头
isDiretive(attrName) {
return attrName.startsWith("v-");
}
// 判断节点是否为文本节点
isTextNode(node) {
return node.nodeType === 3;
}
// 判断节点是否为元素节点
isElementNode(node) {
return node.nodeType === 1;
}
}
- 4. 实现 Dep(Dependcy) 类
- 收集依赖,添加观察者(Watcher)
- 依赖变化,通知观察者更新
class Dep {
constructor() {
this.subs = []; // 存储所有的观察者
}
// 添加观察者
addSub(sub) {
// 约定 sub 必须为 watcher 类
if (sub && sub.update) {
this.subs.push(sub);
}
}
// 通知观察者
notify() {
this.subs.forEach((subs) => {
subs.update();
});
}
}
- 5. 实现 Watcher 类
- 当数据变化触发依赖,Dep 通知所有的 Watcher 更新视图
- 在实例化自身时,往 Dep 中添加自己的实例
class Watcher {
constructor(vm, key, cb) {
this.vm = vm; // vue 的实例对象
this.key = key; // data 中属性的名称
this.cb = cb; // 回调函数,负责视图更新
// 1. 把当前的 Whatcher 实例记录在 Dep.target 这个静态属性中
Dep.target = this;
// 2. 触发属性的 get 方法,在 get 方法中鬼调用 dep.addSub 方法添加观察者
this.oldVal = vm[key]; // data 中对应 key 上一次的值
// 3. 每次添加完 watcher 实例后,清空 Dep.target
Dep.target = null;
}
// 当数据发生变化,更细视图
update() {
let newVal = this.vm[this.key];
if (newVal === this.oldVal) return;
this.cb(newVal);
}
}
- 6. 测试功能
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>my-vue</title>
</head>
<body>
<div id="app">
<h1>text</h1>
<h2>{{count}}</h2>
<h2>{{msg}}</h2>
<hr />
<h1>v-text</h1>
<h2 v-text="msg"></h2>
<hr />
<h1>v-model</h1>
<label for="msg"> msg:</label>
<input id="msg" type="text" v-model="msg">
<label for="count">count:</label>
<input id="count" type="text" v-model="count">
</div>
<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/vue.js"></script>
<script>
let vm = new Vue({
el: "#app",
data: {
msg: 'hello world',
count: 1,
person: {
name: '张三',
age: 30
}
}
});
</script>
</body>
</html>