一、为什么需要响应式?
通过电商网站购物车案例,演示传统 DOM 操作 vs Vue 自动更新的效率对比:
html
<!-- 传统方式 -->
<div id="cart">数量:0</div>
<button onclick="updateCart()">+1</button>
<script>
let count = 0
function updateCart() {
count++
document.getElementById('cart').innerText = `数量:${count}`
}
</script>
<!-- Vue 方式 -->
<template>
<div>数量:{{ count }}</div>
<button @click="count++">+1</button>
</template>
二、响应式系统架构全景
Vue2 响应式系统架构
#### **核心流程说明**
-
数据劫持(初始化阶段)
- 当组件声明数据(
data()
返回的对象)时,Vue2 通过Object.defineProperty
遍历对象的每个属性,为其定义getter
和setter
。 - 目的:拦截属性的访问(读)和修改(写),分别用于依赖收集和派发更新。
- 当组件声明数据(
-
依赖收集(读操作触发)
- 当模板或计算属性中访问数据属性(如
{{ msg }}
)时,会触发属性的getter
。 - 此时,Vue2 会将当前正在渲染的组件的
Watcher
(称为activeWatcher
)收集到该属性对应的dep
(依赖集合)中。 - 关键点 :每个属性对应一个
dep
,每个dep
存储所有依赖该属性的Watcher
。
- 当模板或计算属性中访问数据属性(如
-
派发更新(写操作触发)
- 当属性值被修改时,触发
setter
,此时 Vue2 会遍历该属性的dep
列表,通知所有Watcher
执行update
方法。 Watcher
的update
方法会将组件标记为需要重新渲染,并通过虚拟 DOM 的 diff 算法更新真实 DOM。
- 当属性值被修改时,触发
-
关键角色
- Watcher:与组件渲染函数绑定的监听器,负责在数据变化时触发组件更新。
- dep :每个属性的依赖集合,本质是一个
Set
,存储所有依赖该属性的Watcher
。
Vue3 响应式系统架构
### **核心流程解析**
-
响应式对象创建
- 通过
reactive
、ref
等 API 创建响应式对象,本质是用Proxy
代理目标对象。 Proxy
拦截范围 :包括属性访问
、修改
、删除
等操作。
- 通过
-
依赖收集(
track
过程)
当访问响应式对象的属性(触发
get
拦截)时,调用track
函数收集依赖:
activeEffect
:全局变量,记录当前正在执行的副作用函数(如组件的渲染函数、watch
回调、计算属性的 getter 等)。- 其核心目标是:将"当前活跃的副作用函数"与"被访问的属性"建立依赖关系。
targetMap
:结构如下:target
:被代理的响应式对象。key
:对象的属性名。effects
:依赖该属性的effect
集合(避免重复收集)。
js
targetMap = {
target1: {
count: Set([effect1, effect2])
}
}
- 派发更新(
trigger
过程)
当修改响应式对象的属性(触发
set
拦截)时,调用trigger
函数触发更新:
- 从
targetMap
中获取该属性对应的所有effect
。 - 通过 调度器(
scheduler
) 批量处理effect
的执行,避免频繁更新导致性能损耗。 - 调度机制 :默认将
effect
加入微任务队列(基于Promise.then
或queueMicrotask
),在同一事件循环末尾合并执行。
三 minVue
基于vue2写了一个小小的demo,只支持单模版({{test}})和v-model的处理
1. 依赖管理器 Dep
js
class Dep {
constructor() {
this.subs = []; // 当前属性对应的所有 Watcher 实例
}
addSub(sub) {
this.subs.push(sub); // 收集依赖
}
notify() {
this.subs.forEach(sub => sub.update()); // 通知所有依赖更新
}
}
Dep.target = null; // 当前被收集的 Watcher
2.观察者 Watcher
js
class Watcher {
constructor(data, key, cb) {
this.data = data;
this.key = key;
this.cb = cb;
Dep.target = this; // 当前 watcher 设为全局 target
this.value = data[key]; // 触发 getter,从而添加到 dep.subs
Dep.target = null;
}
update() {
const newVal = this.data[this.key];
if (newVal !== this.value) {
this.value = newVal;
this.cb(newVal); // 执行视图更新逻辑
}
}
}
3. 响应式系统 observe
js
observe(data) {
if (!data || typeof data !== 'object') return;
Object.keys(data).forEach(key => {
let value = data[key];
const dep = new Dep();
Object.defineProperty(data, key, {
get() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set(newVal) {
if (newVal !== value) {
value = newVal;
dep.notify();
}
}
});
this.observe(value); // 递归子对象
});
}
4. 模版编译器
js
compile() {
const el = document.querySelector(this.$options.el);
this.compileNode(el);
}
compileNode(node) {
if (node.nodeType === 1) {
Array.from(node.attributes).forEach(attr => {
if (attr.name === 'v-model') {
this.bindModel(node, attr.value);
}
});
node.childNodes.forEach(child => this.compileNode(child));
} else if (node.nodeType === 3) {
this.bindText(node);
}
}
5. 处理 bindText
与 bindModel
bindText
:绑定插值表达式
js
bindText(node) {
const regex = /\{\{(.*?)\}\}/g;
const matches = node.textContent.match(regex);
if (matches) {
const key = matches[0].slice(2, -2).trim();
node.textContent = this._data[key];
new Watcher(this._data, key, (newVal) => {
node.textContent = newVal;
});
}
}
bindModel
:绑定输入框 v-model
js
bindModel(node, key) {
node.value = this._data[key];
node.addEventListener('input', (e) => {
this._data[key] = e.target.value;
});
new Watcher(this._data, key, (newVal) => {
node.value = newVal;
});
}
完整代码
html
<!DOCTYPE html>
<html>
<head>
<title>MiniVue Demo</title>
</head>
<body>
<!-- 示例模板 -->
<div id="app">
<input v-model="message">
<p>{{ message }}</p>
<p>{{ status }}</p>
<div>Counter: {{ counter }}</div>
</div>
<script>
// ================================================================================
// 依赖管理器 (Dep)
// 功能:管理某个数据属性的所有 Watcher,当数据变化时通知所有 Watcher 更新
// ================================================================================
class Dep {
constructor() {
this.subs = []; // 存储所有依赖(即 Watcher 实例)
}
// 添加依赖(Watcher)
addSub(sub) {
this.subs.push(sub);
}
// 通知所有依赖更新
notify() {
this.subs.forEach(sub => sub.update());
}
}
// 全局变量,用于暂存当前正在处理的 Watcher
Dep.target = null;
// ================================================================================
// 观察者 (Watcher)
// 功能:连接数据和视图,当数据变化时触发回调函数更新视图
// ================================================================================
class Watcher {
constructor(data, key, cb) {
this.data = data;
this.key = key;
this.cb = cb;
// 触发getter,将当前 Watcher 实例添加到 Dep 中
Dep.target = this;
this.value = data[key];
Dep.target = null;
}
// 更新函数
update() {
const newVal = this.data[this.key];
if (newVal !== this.value) {
this.value = newVal;
this.cb(newVal); // 调用回调函数更新视图
}
}
}
// ================================================================================
// 核心 MiniVue 类
// ================================================================================
class MiniVue {
constructor(options) {
this.$options = options; // 用户传入的配置项
this._data = options.data(); // 初始化数据(注意:data 是函数)
this.observe(this._data); // 将数据变为响应式
this.compile(); // 编译模板
}
// ------------------------------------------------------------------------------
// 响应式系统:通过 Object.defineProperty 实现数据劫持
// ------------------------------------------------------------------------------
observe(data) {
if (!data || typeof data !== 'object') return;
Object.keys(data).forEach(key => {
let value = data[key];
const dep = new Dep(); // 每个属性对应一个 Dep 实例
// 劫持属性的 getter/setter
Object.defineProperty(data, key, {
get() {
// 收集依赖:如果有 Watcher 正在读取此属性,将其添加到 Dep 中
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set(newVal) {
if (newVal === value) return;
value = newVal;
dep.notify(); // 数据变化时通知所有 Watcher 更新
}
});
// 递归处理嵌套对象
this.observe(value);
});
}
// ------------------------------------------------------------------------------
// 模板编译:解析 DOM 中的指令和插值表达式
// ------------------------------------------------------------------------------
compile() {
const el = document.querySelector(this.$options.el);
this.compileNode(el);
}
compileNode(node) {
// 处理元素节点(如 div、input)
if (node.nodeType === 1) {
// 解析指令(只处理 v-model)
console.log(Array.from(node.attributes), 'node.attributes');
Array.from(node.attributes).forEach(attr => {
if (attr.name === 'v-model') {
this.bindModel(node, attr.value);
}
});
// 递归处理子节点
node.childNodes.forEach(child => this.compileNode(child));
// 处理文本节点(如 {{ message }})
} else if (node.nodeType === 3) {
this.bindText(node);
}
}
// 绑定文本插值({{ ... }})
bindText(node) {
const regex = /\{\{(.*?)\}\}/g;
const matches = node.textContent.match(regex);
// 不处理复杂表达式,只处理简单的 {{ message }}
if (matches) {
const key = matches[0].slice(2, -2).trim(); // 提取属性名(如 "message")
// 初始化文本内容
node.textContent = this._data[key];
// 创建 Watcher,当数据变化时更新文本
new Watcher(this._data, key, (newVal) => {
node.textContent = newVal;
});
}
}
// 绑定 v-model 指令(双向绑定)
bindModel(node, key) {
// 初始化输入框的值
node.value = this._data[key];
// 监听 input 事件,更新数据
node.addEventListener('input', (e) => {
this._data[key] = e.target.value;
});
// 创建 Watcher,当数据变化时更新输入框的值
new Watcher(this._data, key, (newVal) => {
node.value = newVal;
});
}
}
// ================================================================================
// 使用示例
// ================================================================================
const app = new MiniVue({
el: '#app', // 挂载目标
data: () => ({
message: 'Hello miniVue!',
status: 'I am sad',
counter: 0
})
});
// 测试:修改数据,观察视图是否更新
setTimeout(() => {
app._data.status = 'I am happy';
}, 2000);
setInterval(() => {
app._data.counter++;
}, 1000);
</script>
</body>
</html>
小小测试了一下,v-model和定时器触发对应依赖导致页面更新都是没有问题的
