本期内容是带着大家熟悉 Vue 的基本组成逻辑,并手把手的帮助大家完成一个简易版本的 Vue。
内容篇幅较长,请耐心观看。
演示
准备工作
创建好文件夹,起名叫做 Mini_Vue
。再在文件夹中分别创建好 index.html
和 js
文件夹。在 js 文件下创建如下内容:
Mini_Vue
--------
├─ js
│ ├─vue.js
│ ├─observer.js
│ ├─compiler.js
│ ├─dep.js
│ └─watcher.js
└─ index.html
需要在index.html
中写入数据。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mini Vue</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="./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 Mini Vue",
count: 12,
person: {
name: "xiao",
},
},
});
console.log(vm);
</script>
</body>
</html>
准备好文件后,我们开始逐一分析。
整体分析
Vue
- 把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter
Observer
- 能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知 Dep
Compiler
- 解析每个元素中的指令/插值表达式,并替换成相应的数据
Dep
- 添加观察者(watcher),当数据变化通知所有观察者
Watcher
- 数据变化更新视图
Vue
功能
- 负责接收初始化的参数(选项)
- 负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter
- 负责调用 observer 监听 data 中所有属性的变化
- 负责调用 compiler 解析指令/插值表达式
结构 大致内容
Vue
|
├─ $options
├─ $el
├─ $data
├─ _proxyData()
├─ ..... 等等属性
梳理
先行解读 Vue 模块中的参数:
- $options
此属性代指初始化 Vue 时(new Vue())传入的自定义属性数据。例如传入 router、store、render()、i18n。 - $el
Vue 实例绑定的 DOM 节点 - $data
读取数据属性对象 - _proxyData()
我们理解为对 data 进行数据劫持。
简单的了解参数后,实现功能:
- 接收初始化的参数。首先应该将需要的属性进行声明,而属性的值都来自于传入的
option
。 - 通过
Object.defineProperty
将 data 转换成 getter/setter
代码
首先打开 vue.js 文件,我们开始 Vue 类的建立。
- 实现 Vue 属性的加载
class Vue {
constructor(option) {
// 1.通过属性保存选项的数据
this.$option = option || {};
this.$data = option.data || {};
this.$el = typeof option.el === "string" ? document.querySelector(option.el) : option.el;
// 2.把data中的成员转换成getter和setter,并注入到Vue实例中
this._proxyData(this.$data);
// 3.调用observer对象,监听数据变化
// 4.调用compiler对象,解析指令和差值表达式
}
}
在如上代码中可以看到,我们在第二步调用了 this._proxyData()函数,目的是为了将 data 中的成员转换成 getter 和 setter。现在开始实现。
- 实现 this._proxyData()
class Vue {
// 此处与第一步中的一致,不再复制
constructor(option) {
// xxxxxxx
}
_proxyData(data) {
// 遍历data中的所有属性,把data的所有属性注入到实例中
Object.keys(data).forEach((key) => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key];
},
set(newValue) {
if (newValue === data[key]) {
return;
}
data[key] = newValue;
},
});
});
}
}
如果对此方法不熟悉的,可以先去看看这篇文章。
此时我们对 Vue 类进行了简单的处理。但是还有两个功能并没有实现,我们先放着,接着往下走。
Observer
功能
- 负责把 data 选项中的属性转换成响应式数据
- data 中的某个属性也是对象,把该属性转换成响应式数据
- 数据变化发送通知
结构 大致内容
Observer
|
├─ walk(data)
├─ defineReactive(data,key,value)
├─ .....
梳理
- walk()
用来判断传入的值是否为对象。如果不是对象就返回,是对象的话遍历对象的所有属性调用defineReactive()
来转换为 getter/setter - defineReactive()
将传入的对象进行转换,并进行递归操作。
代码
打开 observe.js。
// 负责数据劫持
// 把 $data 中的成员转换成 getter/setter
class Observer {
constructor(data) {
this.walk(data);
}
// 1.负责把 data 选项中的属性转换成响应式数据
walk(data) {
// a.判断data是否是对象
if (!data || typeof data !== "object") {
return;
}
// b.遍历data对象的所有属性
Object.keys(data).forEach((key) => {
this.defindReactive(data, key, data[key]);
});
}
defindReactive(obj, key, val) {
var this_ = this;
// 2.如果val是对象,把对象中的属性也转换成响应式数据
this.walk(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addSub(Dep.target);
return val;
},
set(newValue) {
if (newValue === val) {
return;
}
val = newValue;
this_.walk(newValue);
},
});
}
}
此时完成了前两个功能,已经拥有了简单的处理能力。数据变化发送通知需要在后面进行处理。
我们需要把 Observer 类实例化,此时需要在 Vue 类的第三个功能下 new Observer()并传入 this.$data.
打开 vue.js 文件,在第三步中调用。
class Vue {
constructor(option) {
// 1.通过属性保存选项的数据
this.$option = option || {};
this.$data = option.data || {};
this.$el = typeof option.el === "string" ? document.querySelector(option.el) : option.el;
// 2.把data中的成员转换成getter和setter,并注入到Vue实例中
this._proxyData(this.$data);
// 3.调用observer对象,监听数据变化
new Observer(this.$data);
// 4.调用compiler对象,解析指令和差值表达式
}
}
Compiler
功能
- 负责编译模板,解析指令/插值表达式
- 负责页面的首次渲染
- 当数据变化后重新渲染视图
结构 大致内容
Compiler
|
├─ el
├─ vm
├─ compile(el)
├─ compilerElement(node)
├─ compilerText(node)
├─ isDirective(attrName)
├─ isTextNode(node)
├─ isElementNode(node)
├─ .....
梳理
属性介绍:
- el
Vue 实例化的初始 DOM 对象 - vm
vue 实例 - compile(el)
顾名思义编译 DOM 节点,判断 el 下的内容是元素节点还是文本节点,进行对应的操作 - compilerElement(node)
解析元素节点的内容 - compilerText(node)
解析文本节点的内容 - isDirective(attrName)
判断元素节点里的属性是否为 Vue 属性,以"v-"开头 - isTextNode(node)
判断元素是否为文本节点 - isElementNode(node)
判断元素是否为元素节点
代码
打开 compiler.js 文件。
class Compiler {
constructor(vm) {
this.el = vm.$el;
this.vm = vm;
this.compiler(this.el);
}
// 编译模板,出来文本节点和元素节点
compiler(el) {
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节点,是否有子节点 如果有子节点 要递归处理compliler
if (node.childNodes && node.childNodes.length) {
this.compiler(node);
}
});
}
// 编译文本节点
compileText(node) {}
// 编译属性节点
compileElement(node) {}
// 判断元素属性的名字是否是指令
isDirective(attrName) {
return attrName.startsWith("v-");
}
// 判断元素是否为文本节点
isTextNode(node) {
return node.nodeType === 3;
}
// 判断元素是否为元素节点
isElementNode(node) {
return node.nodeType === 1;
}
}
分步实现两个未定义的函数。
compileText()
- 负责编译插值表达式
此步骤是用来提取页面中被 {{}}
包裹的参数。
// 编译文本节点
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]);
// 未来实现数据响应式。。。。。
}
}
compileElement()
- 负责编译元素的指令
- 处理 v-text 的首次渲染
- 处理 v-model 的首次渲染
我们在这里先实现 v-text v-model, 可以理解在初始化时,如果元素节点中绑定了指令,那么在解析它时必须先对 attr 进行遍历,拿到带有指令的属性。然后建立对应的指令方法,将指令功能实现。
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="name",获取的是name;
const key = attr.value;
// 处理不同的指令
this.update(node, key, attrName);
}
});
}
// 负责更新 DOM
update(node, key, attrName) {
// node 节点,key 数据的属性名称,dir 指令的后半部分
const updaterFn = this[attrName + "Updater"];
updaterFn && updaterFn(node, this.vm[key]);
}
// v-text 指令的更新方法
textUpdater(node, value) {
node.textContent = value;
}
// v-model 指令的更新方法
modelUpdater(node, value) {
node.value = value;
}
此时我们需要将 Compiler 类实例化到 Vue 类的第四个功能当中。
打开 vue.js,在第四步中实例化 Compiler。
class Vue {
constructor(option) {
// 1.通过属性保存选项的数据
this.$option = option || {};
this.$data = option.data || {};
this.$el = typeof option.el === "string" ? document.querySelector(option.el) : option.el;
// 2.把data中的成员转换成getter和setter,并注入到Vue实例中
this._proxyData(this.$data);
// 3.调用observer对象,监听数据变化
new Observer(this.$data);
// 4.调用compiler对象,解析指令和差值表达式
new Compiler(this);
}
}
Dep(Dependency)
功能
- 收集依赖,添加观察者(watcher)
- 通知所有观察者
结构 大致内容
Dep
|
├─ subs
├─ addSub(sub)
├─ notify()
├─ .....
梳理
解读属性:
- subs
存放所有的观察者 - addSub(sub)
添加观察者 - notify()
通知所有的观察者
代码
打开 dep.js 文件。
class Dep {
constructor() {
// 存储所有的观察者
this.subs = [];
}
// 添加观察者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub);
}
}
// 通知所有观察者
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
简单的 Dep 订阅者已经建立好了,这时候需要在数据监听的地方将订阅者激活,也就是数据劫持 Observe 类。
所以需要将原本在 Observe 中的defindReactive
进行部分改写。
defindReactive(obj, key, val) {
var this_ = this;
// 负责收集依赖,并发送通知
let dep = new Dep();
// 2. 如果val是对象,把对象中的属性也转换成响应式数据
this.walk(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 获取数据的时候将观察者添加进来
Dep.target && dep.addSub(Dep.target);
return val;
},
set(newValue) {
if (newValue === val) {
return;
}
val = newValue;
this_.walk(newValue);
// 发送通知
dep.notify();
},
});
}
需要将 Dep 的方法注册在数据劫持函数内,已达到全局的订阅中心。
Watcher
功能
- 当数据变化触发依赖,dep 通知所有的 Watcher 实例更新视图
- 自身实例化的时候往 dep 对象中添加自己
结构 大致内容
Watcher
|
├─ vm
├─ key
├─ cb
├─ oldValue
├─ update()
├─ .....
梳理
- vm
vue 实例 - key
data 中属性名称 - cb
回调函数,负责更新视图 - oldValue
用来触发 Observe 中定义的 get 方法,调用 Dep 的 addSub 方法。 - update()
当数据发生变化的时候更新视图
可以大致理解,Watcher 干的事情就是负责处理视图变化,由 Dep 在数据更新的时候告诉它,调用它的 update 方法,然后通过回调函数来更新视图。
代码
打开 watcher.js 文件。
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
// data中的属性名称
this.key = key;
// 回调函数 负责更新视图
this.cb = cb;
// 把watcher对象记录到Dep类的静态属性target
Dep.target = this;
// 触发get方法,在get方法中会调用addSub
this.oldValue = vm[key];
// 添加后清除当前Watcher
Dep.target = null;
}
// 当数据发生变化的时候更新视图
update() {
let newValue = this.vm[this.key];
if (this.oldValue === newValue) {
return;
}
this.cb(newValue);
}
}
那么需要在哪里调用 Watcher 类呢? 当然是在 Compiler 类中解析元素数据的时候,比如指令的对应函数,解析文本节点时。所以就需要对原本的函数进行处理升级。
打开 compiler.js 文件。
compilerText()
compilerText(node) {
// console.dir(node);
// {{ msg }}
let reg = /\{\{(.+?)\}\}/;
let val = node.textContent;
if (reg.test(val)) {
let key = RegExp.$1.trim()
node.textContent = val.replace(reg, this.vm[key])
// 添加 观察者
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
}
textUpdater()
// 处理v-text
textUpdater(node, value, key) {
node.textContent = value;
// 添加 观察者
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
modelUpdater()
我们假设目前只给 input 做双向数据绑定,那个监听它的 input 事件,更新数据就可以触发一圈内容。
// 处理v-model
modelUpdater(node, value, key) {
node.value = value;
// 添加 观察者
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
// 双向绑定
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
至此简单的 Vue 就封装完毕了,这时候我们就需要调试了,但是作为文章显示,不是特别好做调试演示,就将这块省略了。
感谢能阅读到这里。如果有什么问题,可以自行调试排查或者在文字底部留言。
Caleb https://reinness.com/posts/134 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自小陈博客 !