分析
整体分析模拟最小版本的vue步骤:
-
观察Vue基本使用结构
-
打印Vue实例观察即将要模拟的成员:
- 以_开头的是私有成员(例如
_data
),以 开 头 的 是 公 共 成 员 ( 例 如 ‘ 开头的是公共成员(例如` 开头的是公共成员(例如‘data`),我们只模拟公共成员即可。 $el
可以是选择器,也可以是dom对象,如果是选择器需要我们转换成dom对象进行存储。- 最小版本准备模拟
$data、$el、$options
- 以_开头的是私有成员(例如
-
最小版本的vue的整体结构如下,即需要实现的几种类型。
不同类分别的作用如下:
- Vue:把data中的成员注入到Vue实例,并且把data中的成员转成getter/setter;Vue内部会调用Observer和Compiler。
- Observer(数据劫持):能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知Dep(发布者)
- Compiler(解析指令):解析每个元素中的指令及插值表达式,并替换成对应的数据
- Dep(发布者):添加观察者,当数据发生变化时通知所有观察者
- Watcher(观察者):有一个update方法,当数据变化负责更新视图。
实现
准备一个html,里面具有需要解析的结构,如下:
<!DOCTYPE html>
<html lang="cn">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Mini Vue</title>
</head>
<body>
<!-- 需要被解析的html start -->
<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>
<!-- 需要被解析的html end -->
<!-- 自定义的类依次引入,需要被依赖的类优先引入 -->
<!-- <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 Vue",
count: 100,
person: { name: "zs" },
},
});
console.log(vm.msg);
// vm.msg = { test: "Hello" };
vm.test = "abc";
</script>
</body>
</html>
Vue类
-
功能:
- 负责接收初始化的参数(选项)
- 负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter
- 负责调用 observer 监听 data 中所有属性的变化
- 负责调用 compiler 解析指令/插值表达式
-
类图结构:
解释说明:
约定以 下划线 开头的成员都是私有成员。
_proxyData():将data的属性转换成getter和setter,注入到vue实例中。
- 代码:
class Vue {
constructor(options) {
// 1.保存选项的数据
this.$options = options || {};
this.$data = options.data || {};
const el = options.el;
this.$el = typeof el === "string" ? document.querySelector(el) : el;
// 2.把data中的属性注入到vue实例中,转换成getter和setter
this._proxyData(this.$data);
// 3.调用Observer实现数据劫持
new Observer(this.$data);
// 4.调用Compiler解析指令/插值表达式等
new Compiler(this);
}
_proxyData(data) {
Object.keys(data).forEach((key) => {
// 这里的this是指vue实例,所以直接向其注入转换成getter和setter的属性
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get() {
return data[key];
},
set(newVal) {
if (newVal === data[key]) return;
data[key] = newVal;
},
});
});
}
}
Observer (数据劫持)类
-
功能
- 负责把 data 选项中的属性转换成响应式数据
- data 中的某个属性也是对象,把该属性转换成响应式数据
- 数据变化发送通知 ----------------?
-
类图
解释说明:
- walk(data):遍历data的所有属性,会调用defineReactive方法进行数据转换。
- defineReactive(data,key,value):将数据定义为响应式数据,即getter和setter。
- 代码:
class Observer {
constructor(data) {
this.walk(data);
}
// 1.遍历data所有属性
walk(data) {
// 加强程序健壮性,做对象判断
if (!data || typeof data !== "object") return;
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key]);
});
}
// 2.将data转换成响应式数据,即getter和setter
// 特别说明:该方法中需要传入value而不直接使用data[key]的原因是 会造成循环引用的问题。
defineReactive(data, key, value) {
const that = this;
// ______特别加入:为每一个属性增加Dep对象,负责收集依赖,发送通知。
const dep = new Dep();
// 特别情况1:当data的某一属性值也是对象时,也需要将其转换为getter和setter
this.walk(value);
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
// ______特别加入:在getter中收集观察者:
// 判断是否有观察者的方式是在watcher类实例化时,向Dep类上挂载一个target属性,用于存储当前的watcher对象,在这里进行target判断,如果有,就使用dep.addSub()添加观察者。
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newVal) {
if (newVal === value) return;
// 特别情况2:当设置data的某一个新属性值为对象时,也需要将其转换为getter和setter。
// 由于这是在set方法中,this指向并非vue实例,故需要使用外面的that指向vue实例的对象。
that.walk(newVal);
value = newVal;
// ____特别加入:在setter时,需要通知所有的观察者变化。
dep.notify();
},
});
}
}
Compiler(编译器)类
- 功能:(操作dom)
- 负责编译模板,解析指令/插值表达式
- 负责页面的首次渲染
- 当数据变化后重新渲染视图
注意:这里没有使用虚拟dom,直接操作的dom。
- 类图:
解释说明:
- el:传入的dom对象,后面需要渲染dom使用
- vm: vue实例,后面需要vm中的数据
- compiler(el):遍历el这个dom对象的所有节点,不同的节点调用解析文本节点或者元素节点的方法
- compileElement(node):解析元素节点,即解析元素中的指令
- compileText(node):解析文本节点,即解析差值表达式
- isDirective(attrName):判断元素节点上的属性 是否是 vue指令
- isTextNode(node):判断是否是文本节点
- isElementNode(node):判断是否是元素节点
- 代码:
class Compiler {
constructor(vm) {
// 方便后面使用,将vm与el存储
this.vm = vm;
this.el = vm.$el;
// 编译模板
this.compiler(this.el);
}
// 1.遍历所有el中的节点
compiler(el) {
// 注意:这里使用childNodes可以获取所有子节点,而children只能获取所有子元素。这里只获取一层的子节点。
const nodes = el.childNodes;
// nodes是伪数组,需要转换成数组
Array.from(nodes).forEach((node) => {
if (this.isElementNode(node)) {
this.compileElement(node);
} else if (this.isTextNode(node)) {
this.compileText(node);
}
// 当子节点中还有其子节点时,需要递归调用
if (node.childNodes && node.childNodes.length) {
this.compiler(node);
}
});
}
// 1-1 判断是否文本节点
isTextNode(node) {
return node.nodeType === 3;
}
// 1-2 判断是否元素节点
isElementNode(node) {
return node.nodeType === 1;
}
// 1-3 解析差值表达式
compileText(node) {
// 正则匹配 差值表达式
const reg = /\{\{(.+?)\}\}/;
// node.textContent和node.nodeValue都是当前节点的内容
const content = node.textContent;
if (reg.test(content)) {
// 使用$1可获取正则表达式中小括号中的值
const value = RegExp.$1.trim();
// 差值表达式去掉双括号后,就是变量的名字,由于变量已经都在observer.js中挂载到了vm实例中,故这里取值
node.textContent = content.replace(reg, this.vm[value]);
// ____数据响应添加:实例化Watcher对象
new Watcher(this.vm, value, (newValue) => {
node.textContent = newValue;
});
}
}
// 1-4 解析指令
// 首次渲染——只渲染v-text和v-model
compileElement(node) {
const attributes = node.attributes;
Array.from(attributes).forEach((attr) => {
let attrName = attr.name;
if (this.isDirective(attrName)) {
// 根据不同的指令进行不同的渲染
// 不采取if语句,使用字符串拼接方法名的方式,便于扩展更多的指令处理方法
attrName = attrName.substr(2); //从索引2开始截取到最后,获取到v-text指令名去除v-后的字符串。
// 获取该属性的属性值
const key = attr.value;
// 解析不同的指令
this.update(node, key, attrName);
}
});
}
// 1-1-1 判断是否vue指令
isDirective(attrName) {
return attrName.startsWith("v-");
}
// 负责更新Dom,update相当于一个watcher,统一执行不同指令解析的位置
update(node, key, attrName) {
const updaterFn = this[attrName + "Updater"];
updaterFn && updaterFn.call(this, node, this.vm[key], key);
}
// v-text解析方法
textUpdater(node, value, key) {
node.textContent = value;
// ____数据响应添加:实例化Watcher对象
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
// v-model解析方法
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;
});
}
}
Dep(发布者)类
-
功能:
- 收集依赖,添加观察者(watcher)
- 通知所有观察者
-
类图:
解释说明:
-
subs:用来存储所有观察者的数组属性
-
addSub(sub):添加观察者的方法
-
notify():通知所有观察者的方法
-
代码:
class Dep {
constructor() {
// 1.初始化subs存储所有观察者的数组
this.subs = [];
}
// 2.添加观察者
addSub(sub) {
//判断是否有传递观察者,且约定观察者必须有update方法,方便在被通知后,调用update方法
if (sub && sub.update) {
this.subs.push(sub);
}
}
// 3.发送通知给所有观察者
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
Watcher(观察者)类
-
功能:
- 当数据变化就触发依赖,dep通知所有的watcher实时更新视图
- 自身实例化的时候往dep对象中添加自己。
-
类图:
解释说明:
update():方法,update中进行更新视图
cb:属性,不同的watcher更新的操作不一样。通过cb交代
vm:属性,通过vue实例获取属性对应的值
key:属性,当前观察的属性名称
oldValue:属性,存储变化之前的值
- 代码:
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
// 回调函数负责更新视图
this.cb = cb;
// 1.实例化时,要把自己加入Dep类中,
// 1.1 先将自己存储在target静态属性中
Dep.target = this;
// 1.2 再通过取值操作,触发执行属性的getter,在observer.js文件中,属性的getter方法中会通过判断Dep.target属性来进行存储当前的watcher实例
this.oldValue = this.vm[key];
// 1.3 为了避免重复添加,每一次判断后,需要将Dep的target属性置空。
Dep.target = null;
}
// 2.更新视图
update() {
//此时,vue实例对象中data中的key属性值已经变为最新的了
let newValue = this.vm[this.key];
if (newValue === this.oldValue) return;
this.cb(newValue);
}
}
Watcher类在Compiler中的实例化
-
作用:为了实现数据改变=》发送通知=》改变视图,更新视图的时候需要调用update,但在update方法中是通过cb回调函数实现的,所以我们需要实例化Watcher类后,将cb回调函数定义,从而实现该功能。
-
Watcher类实例化的位置:compiler代码中,所有把数据渲染成dom的位置都需要创建一个watcher对象。
-
代码:(compiler文件中新增的代码)
class Compiler {
constructor(vm) {
this.vm = vm;
this.el = vm.$el;
this.compiler(this.el);
}
compiler(el) {
const nodes = el.childNodes;
Array.from(nodes).forEach((node) => {
if (this.isElementNode(node)) {
this.compileElement(node);
} else if (this.isTextNode(node)) {
this.compileText(node);
}
if (node.childNodes && node.childNodes.length) {
this.compiler(node);
}
});
}
isTextNode(node) {
return node.nodeType === 3;
}
isElementNode(node) {
return node.nodeType === 1;
}
compileText(node) {
const reg = /\{\{(.+?)\}\}/;
const content = node.textContent;
if (reg.test(content)) {
const value = RegExp.$1.trim();
node.textContent = content.replace(reg, this.vm[value]);
new Watcher(this.vm, value, (newValue) => {
node.textContent = newValue;
});
}
}
compileElement(node) {
const attributes = node.attributes;
Array.from(attributes).forEach((attr) => {
let attrName = attr.name;
if (this.isDirective(attrName)) {
attrName = attrName.substr(2);
const key = attr.value;
this.update(node, key, attrName);
}
});
}
isDirective(attrName) {
return attrName.startsWith("v-");
}
update(node, key, attrName) {
const updaterFn = this[attrName + "Updater"];
updaterFn && updaterFn.call(this, node, this.vm[key], key);
}
textUpdater(node, value, key) {
node.textContent = value;
// ____数据响应添加:实例化Watcher对象
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
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;
});
}
}
特别说明:
- 在写Dep类和Watcher类的时候,需要随之修改两个文件中的类
- 一是observer.js中的Observer类(因为在这个类中为data中的每一个属性转换为getter和setter形式)
原因是:考虑到需要为每一个data对象中的属性添加一个Dep实例,在getter方法中添加观察者(这里也会涉及辅助Watcher类中实例的存储),在setter方法中通知所有观察者做修改。
- 二是compiler.js中的Compiler类 (因为在这个类中主要做dom操作,数据的双向绑定都需要在这里进行修改)
原因是:Watcher类的实例化位置需要在Compiler类中,因为需要实现数据的变化。
整体流程
当页面首次加载的时候,是通过Compiler更新视图;
当页面数据变化的时候,是通过Watcher更新视图。