实例化
先上Demo
<body>
<div id="app">
<h1>差值表达式</h1>
<div>
{{ name }}<br />
{{ age }}
</div>
<h1>v-text</h1>
<div v-text="name"></div>
<h1>v-model</h1>
<input type="text" v-model="name" />
<input type="text" v-model="age" />
</div>
</body>
<script src="./js/vue.js"></script>
<script>
var vm = new Vue({
el: "#app",
data: {
name: "dfklqw",
age: 10,
friend: {
name: "ccccc",
},
},
});
</script>
Demo页面就长这个样子:
正常使用Vue时,引入Vue.js并实例化,接下来就是实现Vue.js。
Vue.js
class Vue {
constructor(options) {
// 存储实例化Vue时传入的选项
// 保存data
this.$data = options?.data || {};
// 保存el
this.$el = options?.el;
// 将data中的属性添加到Vue实例上并添加getter和setter
this.proxyData(this.$data);
// 调用Observer把data中的数据转换为getter和setter
new Observer(this.$data);
// 将Vue实例传入Compiler
new Compiler(this);
}
proxyData($data) {
// 遍历data中的属性,并添加getter和setter
Object.keys($data).forEach((key) => {
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get() {
return $data[key];
},
set(newValue) {
if (newValue !== $data[key]) {
$data[key] = newValue;
}
},
});
});
}
}
Vue.js主要负责创建实例属性保存实例化Vue时传入的options选项,同时将data中的属性转换为getter和setter并注入到Vue实例上,方便后续使用。接着实例化Observer对象, 完成data中对象数据的响应式处理,以及实例化Compiler完成模板编译。
Observer.js
class Observer {
constructor($data) {
// 遍历$data对象
this.walk($data);
}
walk($data) {
if ($data && typeof $data === "object") {
Object.keys($data).forEach((key) => {
// 之所以传入$data[key]是为了防止发生死递归
this.defineReactive($data, key, $data[key]);
});
}
}
defineReactive($data, key, value) {
// 判断value是否是对象,如果是的话,将对象中的属性也转换为getter和setter
this.walk(value);
// 保存当前this ——> Observer
let that = this;
Object.defineProperty($data, key, {
enumerable: true,
configurable: true,
get() {
return value;
},
set(newValue) {
if (newValue !== value) {
value = newValue;
// 判断新值是不是对象
that.walk(value);
}
},
});
}
}
Observer的主要作用是负责将data中的属性转换为getter和setter,如果data中的某个属性也是对象,则将对象中的属性也进行转换。
需要注意的是这个地方:
Object.keys($data).forEach((key) => {
this.defineProperty($data, key, $data[key]);
});
如果不传入$data[key],则会在defineReactive中绑定get时发生死递归
...
walk($data) {
...
Object.keys($data).forEach((key) => {
this.defineReactive($data, key);
});
...
}
defineReactive($data, key) {
...
Object.defineProperty($data, key, {
enumerable: true,
configurable: true,
get() {
return $data[key];
},
...
});
}
...
按照这种不传入$data[key]的写法,当给data中的任意属性重新赋值时会报错
因为当给Vue实例中的属性重新赋值时,会触发对应的set(Vue.js中给Vue实例上的属性绑定了getter和setter)
// vue.js
set(newValue) {
if (newValue !== $data[key]) {
$data[key] = newValue;
}
}
而set中$data[key]又会触发对应的get(Observer.js中给data中的的属性绑定了getter和setter),
// Observer.js
get() {
return $data[key];
}
而get又返回$data[key],这就造成了无限递归导致超出最大调用堆栈大小的异常。
Compiler.js
到此为止,数据的响应式处理就已经处理完成了
Vue实例上的数据及data中的数据已经都转换为getter和setter。 然后实例化Compiler进行模板编译
// compiler.js
class Compiler {
constructor(vm) {
// 保存vm实例
this.vm = vm;
// 保存el
this.el = typeof vm.$el === "string" ? document.querySelector(vm.$el) : vm.$el;
this.compiler(this.el);
}
compiler(el) {
// 获取#app的子节点
let childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => {
// 判断子节点是元素节点还是文本节点
if (this.isTextNode(node)) {
this.compilerText(node);
} else if (this.isElementNode(node)) {
this.compilerElement(node);
}
// 判断node是否有子节点
if (node.childNodes && node.childNodes.length) {
this.compiler(node);
}
});
}
// 判断是否是文本节点(差值表达式)
isTextNode(node) {
return node.nodeType === 3;
}
// 判断是否是元素节点(指令)
isElementNode(node) {
return node.nodeType === 1;
}
// 判断是否是vue指令
isDirective(attrName) {
// 属性如果以v-开始是vue指令,返回对应的指令v-之后的部分,否则返回false
return attrName.startsWith("v-") ? attrName.slice(2) : false;
}
compilerText(node) {
// 匹配差值表达式{{ xxx }}
let reg = /\{\{(.+?)\}\}/;
if (reg.test(node.textContent)) {
// 获取匹配到的内容
let key = RegExp.$1.trim();
// 将数据替换差值表达式
node.textContent = node.textContent.replace(reg, this.vm[key]);
}
}
compilerElement(node) {
// 获取节点所有属性
let attrs = node.attributes;
Array.from(attrs).forEach((attr) => {
// 判断是否是vue指令
// 如果是vue指令,则可以得到对应的指令名, v-text ——> text
let name = this.isDirective(attr.name);
if (name) {
// 获取指令对应的数据 v-text="age" ——> age
let key = attr.value;
// 调用更新视图方法
this.update(node, name, key);
}
});
}
update(node, name, key) {
// 获取方法名 'text' + 'Update' = 'textUpdate'
let fn = name + "Update";
this[fn] && this[fn](node, key);
}
textUpdate(node, key) {
// 替换数据
node.textContent = this.vm[key];
}
modelUpdate(node, key) {
// 替换数据
node.value = this.vm[key];
}
}
在HTML中引入对应的js文件
<script src="./js/compiler.js"></script>
<script src="./js/observer.js"></script>
<script src="./js/vue.js"></script>
此时页面已经可以正常渲染差值表达式和指令了
发布者 - 订阅者
接着来就是数据的双向绑定,先上代码
// dep.js 发布者
class Dep {
constructor() {
// 存储订阅者
this.subs = [];
}
// 存储订阅者
static sub = null;
// 添加订阅者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub);
}
}
// 发送通知
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
// watcher.js 订阅者
class Watcher {
constructor(vm, key, cb) {
// 存储vue实例
this.vm = vm;
// 存储当前key
this.key = key;
// 存储更新视图回调
this.cb = cb;
// 将当前this存储到dep中
Dep.sub = this;
// 存储旧值,当前key对应的数据
this.oldValue = this.vm[this.key]; // 此时触发vue.js中的get,继而触发observer.js中的get把当前订阅者添加到dep中
// 清空Dep.sub,防止重复添加
Dep.sub = null;
}
// 更新视图
update() {
// 当触发更新视图方法时,key对应的数据已经最新的数据
let newValue = this.vm[this.key];
if (newValue !== this.oldValue) {
// 调用回调,更新视图
this.cb && this.cb(newValue);
}
}
}
// compiler.js
compilerText(node) {
// 匹配差值表达式{{ xxx }}
let reg = /\{\{(.+?)\}\}/;
if (reg.test(node.textContent)) {
// 获取匹配到的内容
let key = RegExp.$1.trim();
let value = this.vm[key];
// 将数据替换差值表达式
node.textContent = node.textContent.replace(reg,value);
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
}
textUpdate(node, key) {
// 替换数据
node.textContent = this.vm[key];
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
modelUpdate(node, key) {
node.value = this.vm[key];
new Watcher(this.vm, key, (newValue) => {
node.value = newValue;
});
// 当输入框输入内容时,更新数据
node.addEventListener("input", () => {
this.vm[key] = node.value;
});
}
// observer.js
...
defineReactive($data, key, value) {
...
// 实例化dep
let dep = new Dep();
Object.defineProperty($data, key, {
enumerable: true,
configurable: true,
get() {
Dep.sub && dep.addSub(Dep.sub);
return value;
},
set(newValue) {
if (newValue !== value) {
value = newValue;
// 更新视图
dep.notify();
// 判断新值是不是对象
that.walk(value);
}
},
});
}
核心逻辑编译模板时实例化Watcher对象,在Watcher内部存储当前Watcher实例Dep.sub = this;,同时存储旧值this.oldValue = this.vm[this.key],此时触发对应的get方法,在get方法中将watcher实例添加到订阅者数组中Dep.sub && dep.addSub(Dep.sub);。当数据再次更新时,触发对应的set方法,发布通知dep.notify();,触发实例化Watcher时传入的回调,更新视图。
总结
完整代码如下:
HTML
<!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>差值表达式</h1>
<div>
{{ name }}<br />
{{ age }}
</div>
<h1>v-text</h1>
<div v-text="name"></div>
<h1>v-model</h1>
<input type="text" v-model="name" />
<input type="text" v-model="age" />
</div>
</body>
<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>
var vm = new Vue({
el: "#app",
data: {
name: "name",
age: 10,
friend: {
name: "ccccc",
},
},
});
</script>
</html>
Vue.js
class Vue {
constructor(options) {
// 存储实例化Vue时传入的选项
// 保存data
this.$data = options?.data || {};
// 保存el
this.$el = options?.el;
// 将data中的属性添加到Vue实例上并添加getter和setter
this.proxyData(this.$data);
// 调用Observer把data中的数据转换为getter和setter
new Observer(this.$data);
// 将Vue实例传入Compiler
new Compiler(this);
}
proxyData($data) {
// 遍历data中的属性,并添加getter和setter
Object.keys($data).forEach((key) => {
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get() {
return $data[key];
},
set(newValue) {
if (newValue !== $data[key]) {
$data[key] = newValue;
}
},
});
});
}
}
Observer.js
class Observer {
constructor($data) {
// 遍历$data对象
this.walk($data);
}
walk($data) {
if ($data && typeof $data === "object") {
Object.keys($data).forEach((key) => {
this.defineReactive($data, key, $data[key]);
});
}
}
defineReactive($data, key, value) {
// 判断value是否是对象,如果是的话,将对象中的属性也转换为getter和setter
this.walk(value);
// 保存当前this ——> Observer
let that = this;
// 实例化dep
let dep = new Dep();
Object.defineProperty($data, key, {
enumerable: true,
configurable: true,
get() {
Dep.sub && dep.addSub(Dep.sub);
return value;
},
set(newValue) {
if (newValue !== value) {
value = newValue;
// 更新视图
dep.notify();
// 判断新值是不是对象
that.walk(value);
}
},
});
}
}
Compiler.js
class Compiler {
constructor(vm) {
// 保存vm实例
this.vm = vm;
// 保存el
this.el = typeof vm.$el === "string" ? document.querySelector(vm.$el) : vm.$el;
this.compiler(this.el);
}
compiler(el) {
// 获取#app的子节点
let childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => {
// 判断子节点是元素节点还是文本节点
if (this.isTextNode(node)) {
this.compilerText(node);
} else if (this.isElementNode(node)) {
this.compilerElement(node);
}
// 判断node是否有子节点
if (node.childNodes && node.childNodes.length) {
this.compiler(node);
}
});
}
// 判断是否是文本节点(差值表达式)
isTextNode(node) {
return node.nodeType === 3;
}
// 判断是否是元素节点(指令)
isElementNode(node) {
return node.nodeType === 1;
}
// 判断是否是vue指令
isDirective(attrName) {
// 属性如果以v-开始是vue指令,返回对应的指令v-之后的部分,否则返回false
return attrName.startsWith("v-") ? attrName.slice(2) : false;
}
compilerText(node) {
// 匹配差值表达式{{ xxx }}
let reg = /\{\{(.+?)\}\}/;
if (reg.test(node.textContent)) {
// 获取匹配到的内容
let key = RegExp.$1.trim();
let value = this.vm[key];
// 将数据替换差值表达式
node.textContent = node.textContent.replace(reg, value);
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
}
compilerElement(node) {
// 获取节点所有属性
let attrs = node.attributes;
Array.from(attrs).forEach((attr) => {
// 判断是否是vue指令
// 如果是vue指令,则可以得到对应的指令名, v-text ——> text
let name = this.isDirective(attr.name);
if (name) {
// 获取指令对应的数据 v-text="age" ——> age
let key = attr.value;
// 调用更新视图方法
this.update(node, name, key);
}
});
}
update(node, name, key) {
// 获取方法名 'text' + 'Update' = 'textUpdate'
let fn = name + "Update";
// this[fn]直接调用的this指向,将this指向改为this
this[fn] && this[fn](node, key);
}
textUpdate(node, key) {
// 替换数据
node.textContent = this.vm[key];
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
modelUpdate(node, key) {
node.value = this.vm[key];
new Watcher(this.vm, key, (newValue) => {
node.value = newValue;
});
// 当输入框输入内容时,更新数据
node.addEventListener("input", () => {
this.vm[key] = node.value;
});
}
}
Dep.js
class Dep {
constructor() {
// 存储订阅者
this.subs = [];
}
// 存储订阅者
static sub = null;
// 添加订阅者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub);
}
}
// 发送通知
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
Watcher.js
class Watcher {
constructor(vm, key, cb) {
// 存储vue实例
this.vm = vm;
// 存储当前key
this.key = key;
// 存储更新视图回调
this.cb = cb;
// 将当前this存储到dep中
Dep.sub = this;
// 存储旧值,当前key对应的数据
this.oldValue = this.vm[this.key]; // 此时触发vue.js中的get,把当前订阅者添加到dep中
// 清空Dep.sub,防止重复添加
Dep.sub = null;
}
// 更新视图
update() {
// 当触发更新视图方法时,key对应的数据已经最新的数据
let newValue = this.vm[this.key];
if (newValue !== this.oldValue) {
this.cb && this.cb(newValue);
}
}
}