Vue 是国内目前最火的前端框架,它功能强大而又上手简单,基本成为前端工程师们的标配,但很多同学都只是停留在如何使用上,知其然不知所以然,对它内部的实现原理一知半解,今天就带领大家动手写一个类Vue的迷你库,进一步加深对Vue的理解,在前端进阶的道路上如虎添翼!
内容摘要
- MVVM 简介和流程分析
- 核心入口-MyVue类的实现
- 观察者 - Watcher 类的实现
- 发布订阅 - Dep 类的实现
- 数据劫持 - Observer 类的实现
- 大功告成,运行 MyVue
MVVM 简介和流程分析
作为前端最火的框架之一,Vue是MVVM设计模式实现的典型代表,什么是MVVM呢?MVVM是Model-View-ViewModel的简写,M - 数据模型(Model),V - 视图层(View),VM - 视图模型(ViewModel),它本质上就是MVC 的改进版。
MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。
mvvm 实现原理的可以用下图简略表示·
mvvm 实现流程分析
本案例我们通过实现 vue 中的插值表达式解析和指令 v-model 功能来探究vue的基本运行原理。
1. 核心入口-MyVue类的实现
实现数据代理
先了解些必备知识,Object.defineProperty(obj, prop, descriptor)
,该方法可以定义或修改对象的属性描述符
MyVue 的创建
class MyVue {
constructor(option) {
this.$el = document.querySelector(option.el);
this.$data = option.data;
if (this.$el) {
// 1, 代理数据
this.proxyData();
// 2, 数据劫持
// new Observer(this.$data);
// 3, 编译数据
// new Compile(this);
}
}
// 代理数据,用于监听对 data 数据的访问和修改
proxyData() {
for (const key in this.$data) {
Object.defineProperty(this, key, {
enumerable: true, // 设为false后,该属性无法被删除。
configurable: false, // 设为true后,该属性可以被 for...in或Object.keys 枚举到。
get() {
return this.$data[key]
},
set(newVal) {
this.$data[key] = newVal
}
})
}
}
}
2. 编译模板 - Compile 类的实现
class Compile {
constructor(vm) {
// 要编译的容器
this.el = vm.$el
// 挂载实例对象,方便其他实例方法访问
this.vm = vm
// 通过文档片段来编译模板
// 1,createDocumentFragment()方法,是用来创建一个虚拟的节点对象
// 2,DocumentFragment(以下简称DF)节点不属于文档树,它有如下特点:
// 2-1 当把该节点插入文档树时,插入的不是该节点自身,而是它所有的子孙节点
// 2-2 当添加多个dom元素时,如果先将这些元素添加到DF中,再统一将DF添加到页面,会减少
// 页面的重排和重绘,进而提升页面渲染性能。
// 2-3 使用 appendChild 方法将dom树中的节点添加到 DF 中时,会删除原来的节点
// 1,获取文档片段
const fragment = this.nodeToFragment(this.el)
// 2,编译模板
this.compile(fragment)
// 3,将编译好的子元素重新追加到模板容器中
this.el.appendChild(fragment)
// console.log(fragment, fragment.nodeType, this.el.nodeType)
}
// dom元素转为文档片段
nodeToFragment(element) {
// 1,创建文档片段
const f = document.createDocumentFragment()
// 2, 迁移子元素
while(element.firstChild) {
f.appendChild(element.firstChild)
}
// 3,返回文档片段
return f
}
// 编译方法
compile(fragment) {
// 1,获取所有的子节点
const childNodes = fragment.childNodes;
// 2,遍历子节点数组
childNodes.forEach(node => {
// 分别处理元素节点(nodeType: 1)和文档节点(nodeType:3)
const ntype = node.nodeType
if (ntype === 1) {
// 如果是元素节点,解析指令
this.compileElement(node)
} else if (ntype === 3) {
// 如果是文档节点,解析双花括号
this.compileText(node)
}
// 如果存在子节点则递归调用 compile
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
// 编译元素节点
compileElement(node){
// 获取元素的所有属性
const attrs = node.attributes;
Array.from(attrs).forEach(atr => {
const {name, value} = atr
if (name.startsWith('v-')) {
// 对指令做处理
// name == v-model
const [, b] = name.split('-')
if (b === 'model') {
node.value = this.vm[value]
}
}
})
}
// 编译文档节点
compileText(node) {
const con = node.textContent;
const reg = /\{\{(.+?)\}\}/g;
if (reg.test(con)) {
const value = con.replace(reg, (...args) => {
// console.log(args)
return this.vm[args[1]]
})
// 更新文档节点内容
node.textContent = value
}
}
}
3. 观察者 - Watcher 类的实现
class Watcher {
// 当观察者对应的数据发生变化时,使其可以更新视图
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
// 保存旧值
this.oldVal = this.getOldVal()
}
getOldVal() {
Dep.target = this
const oldVal = this.vm[this.key]
Dep.target = null
return oldVal
}
// 更新视图
update() {
this.cb()
}
}
// 接下来处理:1,谁来通知观察者去更新视图;2,在什么时机更新视图
4. 发布订阅 - Dep 类的实现
// 收集依赖
class Dep {
constructor() {
// 初始化观察者列表
this.subs = []
}
// 收集观察者
addSub(watcher) {
this.subs.push(watcher)
}
// 通知观察者去更新视图
notify() {
this.subs.forEach(w => w.update())
}
}
5. 数据劫持 - Observer 类的实现
class Observer {
constructor(data) {
this.observer(data)
}
observer(data) {
// 实例化依赖收集器,专门收集所有的观察者对象
const dep = new Dep();
for (const key in data) {
let val = data[key]
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() {
// 当观察者实例化的时候会访问对应属性,进而触发get函数,然后添加订阅者
// 之后,数据的变化就会触发 set 函数,而 set 函数触发就会执行 dep.notify()
// 从而实现,数据变化驱动视图更新。
Dep.target && dep.addSub(Dep.target);
return val
},
set(newVal) {
val = newVal
// 既然数据更新,视图也应该随之更新
dep.notify()
}
})
}
}
}
6. 大功告成,运行 MyVue
对之前代码进行调整
- 首先是在 MyVue 中
class MyVue {
constructor(option) {
this.$el = document.querySelector(option.el);
this.$data = option.data;
if (this.$el) {
// 1, 代理数据
this.proxyData();
// 2, 数据劫持
- // new Observer(this.$data);
+ new Observer(this.$data);
// 3, 编译数据
- // new Compile(this);
+ new Compile(this);
}
}
}
- 然后是在 Compile 中
class Compile {
...
// 编译元素节点
compileElement(node){
// 获取元素的所有属性
const attrs = node.attributes;
Array.from(attrs).forEach(atr => {
const {name, value} = atr
if (name.startsWith('v-')) {
// 对指令做处理
// name == v-model
const [, b] = name.split('-')
if (b === 'model') {
// 将v-model 的绑定的数据解析到输入框中
node.value = this.vm[value]
+ // 输入框内容变化,则修改数据(这一步是从视图到数据的变化,需要手动添加)
+ node.addEventListener('input', (e) => {
+ this.vm.$data[value] = e.target.value;
+ });
+ // 通过添加一个观察者实例对象,当数据发生任何变化则自动更新视图
+ new Watcher(this.vm, value, () => {
+ node.value = this.vm.$data[value];
+ });
}
}
})
}
// 编译文档节点
compileText(node) {
const con = node.textContent;
const reg = /\{\{(.+?)\}\}/g;
if (reg.test(con)) {
const value = con.replace(reg, (...args) => {
// console.log(args)
+ // 通过添加一个观察者实例对象,当数据发生任何变化则自动更新视图
+ new Watcher(this.vm, args[1], () => {
+ node.textContent = con.replace(reg, (...args) => {
+ return this.vm[args[1]]
+ })
+ })
return this.vm[args[1]]
})
// 更新文档节点内容
node.textContent = value
}
}
}
引入前面创建的五个文件,就可以 new 一个自己的vue实例对象了,赶紧去试试吧!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MyVue</title>
</head>
<body>
<div id="app">
<!-- v-model 指令功能的实现 -->
<input type="text" v-model="msg">
<div>
<!-- 插值表达式 -->
<p>{{msg}} --- {{info}}</p>
</div>
</div>
<script src="js/Dep.js"></script>
<script src="js/Observer.js"></script>
<script src="js/Watcher.js"></script>
<script src="js/Compile.js"></script>
<script src="js/MyVue.js"></script>
<script>
var mv = new MyVue({
el: "#app",
data: {
msg: "Hello",
info: "World"
}
})
</script>
</body>
</html>
网友评论