最近想学一下vue2源码,于是兴致冲冲的跑到[vue官网](深入响应式原理 --- Vue.js)了解响应式原理
当你把一个普通的 JavaScript 对象传入 Vue 实例作为
data
选项,Vue 将遍历此对象所有的 property,并使用Object.defineProperty
把这些 property 全部转为 getter/setter。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把"接触"过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
大概理解了一下,vue通过Object.defineProperty
劫持属性,然后通过setter触发watcher来进行组件更新,然后去翻一下vue2的源码,上万行的代码,亚麻呆住了!
于是我想开始实现一个极简的vue,由简入繁,方便理解,回忆一下平时怎么用vue
js
const app = new Vue({
el: '#app',
data: {
},
methods: {
}
});
首先想到是要创建一个Vue类,构造函数中挂载属性和方法,同时还要进行模板编译,工作流程是这样的
- 1.初始化 :Vue实例创建并将数据转换为响应式
- 2.依赖收集 :首次访问数据时,收集依赖的Watcher
- 3.数据更新 :用户交互或其他操作修改数据
- 4.通知更新 :数据变化触发setter,Dep通知所有相关的Watcher
- 5.视图更新 :Watcher执行回调函数,更新DOM展示
先给出大概流程图,暂时不考虑边界情况
graph TD
Vue[Vue实例]
Data[Observer]
Watcher[Watcher]
DOM[视图]
User[数据]
Vue -->|初始化| Data
Vue -->|挂载| DOM
DOM -->|读取| Data
Data -->|依赖收集| Watcher
Watcher -->|更新| DOM
User -->|修改| Data
Data -->|通知| Watcher
Watcher -->|重新渲染| DOM
style Vue fill:#42b883
style Data fill:#4299e1
style Watcher fill:#9f7aea
style DOM fill:#ed8936
style User fill:#e53e3e
js
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods || {};
// 将data数据转换为响应式
this._observe(this.$data);
// 将methods绑定到this上下文
this._bindMethods();
// 编译模板
this._compile(this.$el);
}
_observe(data) {
}
_bindMethods() {
}
_compile(el) {
}
}
接下来实现_observe,
- 1.遍历data,将属性用Object.defineProperty进行劫持,转换为getter/setter
- 2.为每个属性创建一个 Dep 实例(依赖收集器)
- 3.数据变化时通知所有依赖该属性的Watcher进行更新
_observe
_observe(data) {
const self = this;
Object.keys(data).forEach(key => {
let value = data[key];
// 为每个属性创建一个依赖收集器
const dep = new Dep();
Object.defineProperty(data, key, {
get() {
// 如果有Watcher,就将其添加到依赖中
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set(newValue) {
if (value !== newValue) {
value = newValue;
// 通知所有依赖更新
dep.notify();
}
}
});
// 将data属性代理到Vue实例上
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newValue) {
this.$data[key] = newValue;
}
});
});
}
Dep:用于添加观察者,当数据更新的时候通知观察者
- 1.收集依赖,添加观察者
- 2.通知所有观察者
Dep
// 依赖收集器
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => {
sub.update();
});
}
}
watcher:关联数据和视图
- 1.将自身设置为 Dep.target ,然后读取数据属性触发 getter ,从而被添加到对应数据的依赖收集器( Dep 实例)中
- 2.Watcher 负责监听特定数据的变化。当数据发生变化时, Dep 会通知所有订阅的 Watcher 调用 update 方法
watcher
// 观察者
class Watcher {
constructor(vm, key, callback) {
this.vm = vm;
this.key = key;
this.callback = callback;
// 把watcher对象记录到Dep类的静态属性target
Dep.target = this;
// 触发getter,进行依赖收集
this.oldValue = vm[key];
// 清除标记
Dep.target = null;
}
update() {
const newValue = this.vm[this.key];
if (this.oldValue !== newValue) {
this.callback(newValue);
this.oldValue = newValue;
}
}
}
_compile:解析插值表达式和数据更新视图
_compile
_compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
// 处理文本节点
if (node.nodeType === 3) {
const text = node.textContent;
const reg = /\{\{(.*?)\}\}/g;
if (reg.test(text)) {
const key = RegExp.$1.trim();
// 创建Watcher实例
new Watcher(this, key, (newValue) => {
node.textContent = text.replace(reg, newValue);
});
// 初始渲染数据
node.textContent = text.replace(reg, this.$data[key]);
}
}
// 递归处理子节点
else if (node.nodeType === 1) {
this._compile(node);
}
});
}
_bindMethods:绑定方法执行
_bindMethods
_bindMethods() {
const self = this;
Object.keys(this.$methods).forEach(key => {
self[key] = this.$methods[key].bind(self);
});
}
让我们梳理一下过程
- 1.Vue实例创建后调用 _compile 方法,遍历DOM节点,检测文本节点中的 {{}} 语法。
- 2.Watcher 构造函数执行时,会将自身设置为 Dep.target ,然后访问 vm[key] 触发数据属性的 getter,这样当前创建的watcher实例会被添加到依赖Dep中,data里面每个属性都对应一个Dep实例
- 3.当数据发生改变时,触发 setter ,调用 dep.notify() 通知所有依赖的 Watcher ,然后 Watcher 调用 update 方法更新视图,而this.subs其实存放的是观察者,这样通过观察者watcher的callback来进行视图更新。
下面是完整的代码:
Vue极简框架
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods || {};
// 将data数据转换为响应式
this._observe(this.$data);
// 将methods绑定到this上下文
this._bindMethods();
// 编译模板
this._compile(this.$el);
}
_observe(data) {
const self = this;
Object.keys(data).forEach(key => {
let value = data[key];
// 为每个属性创建一个依赖收集器
const dep = new Dep();
Object.defineProperty(data, key, {
get() {
// 如果有Watcher,就将其添加到依赖中
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set(newValue) {
if (value !== newValue) {
value = newValue;
// 通知所有依赖更新
dep.notify();
}
}
});
// 将data属性代理到Vue实例上
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newValue) {
this.$data[key] = newValue;
}
});
});
}
_bindMethods() {
const self = this;
Object.keys(this.$methods).forEach(key => {
self[key] = this.$methods[key].bind(self);
});
}
_compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
// 处理文本节点
if (node.nodeType === 3) {
const text = node.textContent;
const reg = /\{\{(.*?)\}\}/g;
if (reg.test(text)) {
const key = RegExp.$1.trim();
// 创建Watcher实例
new Watcher(this, key, (newValue) => {
node.textContent = text.replace(reg, newValue);
});
// 初始渲染数据
node.textContent = text.replace(reg, this.$data[key]);
}
}
// 递归处理子节点
else if (node.nodeType === 1) {
this._compile(node);
}
});
}
}
// 依赖收集器
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => {
sub.update();
});
}
}
// 观察者
class Watcher {
constructor(vm, key, callback) {
this.vm = vm;
this.key = key;
this.callback = callback;
// 把watcher对象记录到Dep类的静态属性target
Dep.target = this;
// 触发getter,进行依赖收集
this.oldValue = vm[key];
// 清除标记
Dep.target = null;
}
update() {
const newValue = this.vm[this.key];
if (this.oldValue !== newValue) {
this.callback(newValue);
this.oldValue = newValue;
}
}
}
页面测试:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>简易Vue实现</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.container {
border: 1px solid #ddd;
padding: 20px;
border-radius: 5px;
}
button {
background-color: #4CAF50;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
margin-right: 10px;
}
button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<div class="container">
<h1>简易Vue实现</h1>
<div id="app">
<h2>数据绑定演示</h2>
<p>姓名: {{ name }}</p>
<p>年龄: {{ age }}</p>
<p>欢迎信息: {{ greeting }}</p>
<button onclick="app.incrementAge()">增加年龄</button>
<button onclick="app.changeName()">改变姓名</button>
</div>
</div>
<script src="./simple_vue.js"></script>
<script>
// 创建Vue实例
const app = new Vue({
el: '#app',
data: {
name: '张三',
age: 25,
greeting: '欢迎使用简易Vue实现!'
},
methods: {
incrementAge() {
this.age++;
},
changeName() {
this.name = '李四';
this.greeting = '姓名已更新为: ' + this.name;
}
}
});
</script>
</body>
</html>
成功发生了改变!
目前框架存在问题:
1. 数据响应式的局限性
- 嵌套对象处理 : _observe 方法只处理了对象的第一层属性,嵌套对象的属性不会被转换为响应式。
- 数组支持 :无法检测数组的变化(如 push 、 pop 、 splice 等操作),数组元素的修改不会触发视图更新。
- 新增属性 :无法检测对象新增属性的变化,只能监听初始化时已存在的属性。
2. 模板编译功能有限
- 仅支持文本插值 :只实现了 {{}} 文本插值,不支持 Vue 的指令系统(如 v-model 、 v-for 、 v-if 等)。
- 无优化策略 :编译过程是简单的字符串替换,没有实现模板缓存或虚拟 DOM 等优化手段。
- 静态内容未优化 :所有内容都会被视为动态内容,即使是不会变化的静态文本。
3. 事件处理机制原始
- 直接绑定 DOM 事件 :示例中使用原生 onclick 绑定事件,而非 Vue 的事件系统。
- 无事件修饰符 :没有实现 .stop 、 .prevent 、 .capture 等事件修饰符。
- 无自定义事件 :不支持组件间的自定义事件通信。
4. 性能优化缺失
- 无批量更新 :每次数据变化都会立即触发视图更新,没有实现批量更新策略。
- 无虚拟 DOM :直接操作真实 DOM,频繁更新可能导致性能问题。
- Watcher 粒度问题 :每个数据属性对应一个 Watcher,当数据量大时可能造成性能开销。
5. 功能不完整
- 无计算属性 :没有实现计算属性(computed)功能。
- 无侦听器 :没有实现 watch 功能来监听特定数据的变化。
- 无组件系统 :不支持组件化开发,无法拆分复杂应用。
- 无生命周期钩子 :缺少 created、mounted 等生命周期钩子函数。