为什么要零源码认识Vue响应式原理?
- 所有的框架都是对JavaScript的封装
- 根据框架应用反推代码实现,可以培养逆向思维
- 通过代码比对,提升代码质量
- 好处多多, 不再列举......
Vue的初步分析结构如下:
1. Vue简单应用及分析
1.1 Vue入门使用
<!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>vue2响应式原理基础版</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>
<script src="../../node_modules/vue/dist/vue.js"></script>
<script>
let vm = new Vue({
el: "#app",
data: {
msg: "hello",
count: 12,
},
});
setTimeout(() => {
vm.msg = "hello world";
}, 2000);
</script>
</body>
</html>
1.2 Vue基本分析
功能分析如下
- 负责接收初始化的对象选项参数 ==> new Vue(options)
- 将data中的属性注入到Vue实例上,并转为getter/setter方法 ==> vm.msg
- 负责调用observer监听data中所有属性的变化
- 负责调用compile解析页面标签、指令和插值表达式
2. Vue基本结构
根据上述分析,需要给Vue安排如下内容:
- 对象选项参数的初始化: $options
- 页面挂载位置:$el
- 数据初始化: $data
- 数据代理到Vue实例上: _proxyData
- 调用特定的方法进行处理所有的data属性并解析页面
class Vue {
constructor(options) {
// 保存选项中的数据, 初始化基本数据 $options, $data和$el
this.$options = options || {};
this.$data = options.data || {};
const el = options.el;
this.$el = typeof el === "string" ? document.querySelector(el) : el;
// 将data中的数据代理到vue实例上 (注入到vue实例上)
this._proxyData(this.$data);
// 使用observer对data数据进行劫持(待完成 a.数据劫持)
// 使用compiler对页面进行解析,解析dom、插值表达式和指令等;(待完成 b.解析html)
}
_proxyData(data) {
// 遍历所有的data属性, 将其代理到vm上
Object.keys(data).forEach((key) => {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newValue) {
if (data[key] === newValue) {
return;
}
data[key] = newValue;
},
});
});
}
}
3. 数据劫持
3.1 功能分析
- 负责将data选项中的属性转换为响应式数据 ==> defineReactive
- 此处需要递归处理data中的属性
- 使用数据时,在 发布者 中注册 观察者 (相关概念见发布者和观察者)
- 数据变化发送更新通知
3.2 结构分析
3.3 代码实现
// 负责数据劫持
// 将$data中的成员转化为 getter/setter
class Observer {
constructor(data) {
this.walk(data);
}
// 遍历数据
walk(data) {
// 1. 判断是否是对象,如果不是对象直接返回
// 2. 如果是对象,就遍历对象的所有属性,设置getter/setter
if (!data || typeof data !== "object") {
return;
}
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key]);
});
}
// 定义响应式成员
defineReactive(data, key, val) {
const self = this;
// 如果val是对象, 继续将其内部的属性设置为响应式数据
this.walk(val);
// 创建dep对象收集依赖(待完成,c.注册发布者)
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
// 使用数据时,进行收集依赖(待完成,d.发布者进行收集依赖)
return val;
},
set(newValue) {
if (newValue === val) {
return;
}
// 如果newValue是对象, 继续将其内部的属性设置为响应式数据
self.walk(newValue);
val = newValue;
// 数据变化后,发送更新通知(待完成,e.发布者通知观察者)
},
});
}
}
4. 解析html
4.1 功能分析:
- 负责编译html,负责解析指令和插值表达式
- 负责页面的首次渲染
- 负责重新渲染视图
4.2 结构分析
4.3 代码实现
// 负责解析指令、插值表达式、dom等
class Compiler {
constructor(vm) {
this.vm = vm;
this.el = vm.$el;
// 编译模板
this.compile(this.el);
}
// 处理各种节点
compile(el) {
const nodes = el.childNodes;
// 将类数组转为真实数组
Array.from(nodes).forEach((node) => {
// 判断是否是文本节点还是元素节点
if (this.isTextNode(node)) {
this.compileText(node);
} else if (this.isElementNode(node)) {
this.compileElement(node);
}
if (node.childNodes && node.childNodes.length) {
// 如果有子节点,递归编译
this.compile(node);
}
});
}
// 判断文本节点
isTextNode(node) {
return node.nodeType === 3;
}
// 判断元素节点
isElementNode(node) {
return node.nodeType === 1;
}
// 判断指令
isDirective(attrName) {
return attrName.startsWith("v-");
}
// 编译文本节点
compileText(node) {
const reg = /\{\{(.+)\}\}/;
// 获取文本节点的内容
const value = node.textContent;
if (reg.test(value)) {
// 插值表达式中的值就是我们要的属性名称
const key = RegExp.$1.trim();
// 把插值表达式替换成具体的值
node.textContent = value.replace(reg, this.vm[key]);
// 文本节点中使用到数据需要一个渲染watcher,观察数据的变化(待完成,f.注册观察者)
}
}
// 编译元素节点
compileElement(node) {
// 遍历元素节点中的所有属性,判断是否有指令
Array.from(node.attributes).forEach((attr) => {
// 获取元素属性的名称
let attrName = attr.name;
// 判断是否是指令
if (this.isDirective(attrName)) {
// attrName 的形式 v-text v-model
// 截取属性的名称 获取到text和model
attrName = attrName.substr(2);
// 获取到属性的名称,属性的名称就是我们数据对象的属性v-text、v-model
const key = attr.value;
// 处理不同的指令
this.update(node, key, attrName);
}
});
}
// 负责初始化和更新dom
// 创建 渲染watcher
update(node, key, attrName) {
const updateFn = this[attrName + "Updater"];
updateFn && updateFn.call(this, node, this.vm[key], key);
}
// v-text的处理方法
textUpdater(node, value, key) {
node.textContent = value;
// 每个指令中创建一个watcher, 观察数据的变化(待完成,g.注册观察者)
}
// v-model的处理方法
modelUpdater(node, value, key) {
node.value = value;
// 每个指令中创建一个watcher, 观察数据的变化(待完成,h.注册观察者)
// 监听视图的变化(待完成,i.处理双向绑定)
}
}
5. 发布者
5.1 功能分析
- 收集依赖,添加观察者(watcher)
- 通知所有的观察者
5.2 结构分析
(见发布者和观察者)
5.3 代码实现
class Dep {
constructor() {
// 存储所有的观察者
this.subs = [];
}
// 添加观察者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub);
}
}
// 通知观察者更新
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
5.4 代码补充
// 3. 数据劫持 ==> defineReactive ==> c.注册发布者 ==> 增加如下代码
const dep = new Dep();
// 3. 数据劫持 ==> defineReactive ==> getter中 ==> d.发布者进行收集依赖 ==> 增加如下代码
Dep.target && dep.addSub(Dep.target);
// 3. 数据劫持 ==> defineReactive ==> setter中 ==> e.发布者通知观察者 ==> 增加如下代码
dep.notify();
6. 观察者
6.1 功能分析
- 当数据变化时触发依赖, dep通知所有的watcher实例更新视图
- 自身实例化的时候往dep对象中添加自身
6.2 结构分析
6.3 代码实现
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
// data中的属性名称
this.key = key;
// 在数据变化时,调用cb进行更新视图
this.cb = cb;
// 在Dep的静态属性上记录当前的watcher对象,当访问数据的时候可以添加到subs中
Dep.target = this;
// 触发一次getter, 让dep为当前的key记录watcher
this.oldValue = vm[key];
// 清空target
Dep.target = null;
}
update() {
const newValue = this.vm[this.key];
if (this.oldValue === newValue) {
return;
}
this.cb(newValue);
}
}
6.4 代码补充
// 4. 解析html ==> compileText ==> f.注册观察者 ==> 增加如下代码
new Watcher(this.vm, key, (value) => {
node.textContent = value.replace(reg, this.vm[key]);
});
// 4. 解析html ==> textUpdater(处理v-text指令) ==> g.注册观察者 ==> 增加如下代码
new Watcher(this.vm, key, (value) => {
node.textContent = value;
});
// 4. 解析html ==> modelUpdater(处理v-model指令) ==> h.注册观察者 ==> 增加如下代码
new Watcher(this.vm, key, (value) => {
node.value = value;
});
// 4. 解析html ==> modelUpdater ==> i.处理双向绑定 ==> 增加如下代码
if (
node.tagName.toLowerCase() === "input" &&
node.type.toLowerCase() === "text"
) {
node.addEventListener("input", (e) => {
this.vm[key] = node.value;
});
}
7. 总结
Vue响应式原理的整体流程回顾:
- Vue
- 记录传入的选项,设置私有属性el等
- 将data数据注入到Vue实例中
- 负责调用Observer实现数据响应式处理
- 负责调用Compiler编译html页面
- Observer
- 数据劫持
- 负责将data中的成员递归转为getter/setter
- 对新对象递归转为getter/setter
- 添加Dep和Watcher的依赖关系
- 数据变化发送通知
- 数据劫持
- Compiler
- 负责编译html页面,解析指令/插值表达式等
- 负责页面的首次渲染过程
- 当数据变化后重新渲染
- Dep
- 收集依赖,添加订阅者(watcher)
- 通知所有订阅者
- Watcher
- 自身实例化的时候 向dep对象上添加自己
- 当数据变化时被dep通知 watcher进行更新视图
后续将进行源码阅读,对照自己完成的响应式原理,分析差别并进行学习源码。[如果需要源码,请留言一起学习学习]