200行代码实现简易的 mvvm - vue
程序员文章站
2022-03-30 10:41:40
...
第一个知识点 - Object.defineProperty()
-
说明: 直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
-
备注: 应当直接在
Object
构造器对象上调用此方法,而不是在任意一个Object
类型的实例上调用。
1. 在这里我们需要了解 get
和 set
方法
const data = {}
let value = ''
Object.defineProperty(this.data, "msg", {
get(){
// 当对象的 key 被访问的时候会执行这个方法
// 这里添加我们自己的方法就会优先执行
return value
},
set(newVal){
// 与 get 方法相似,当给当前属性赋值的时候会自调用 set 方法
// 自己的方法
if (value === newVal) return
value = newVal
}
})
2. 当同时使用 set
get
方法时需要一个真实的中间变量,而我们又不想将这个变量暴露在外面,因此我们将其封装
// 我们封装这样一个函数,这样 value 可以充当中间变量
// 这里会触发闭包,value 这个值一直保存在内存中
defineReactive(obj, key, value) {
Object.defineProperty(obj, key, {
get() {
console.log(value);
return value
},
set: (newVal) => {
if (value === newVal) return
console.log(value, newVal);
value = newVal
}
})
}
第二个知识点 - 发布订阅模式和观察者模式
-
这两种设计模式一直傻傻分不清楚,知道有一天我逛 知乎 我发现其中的奥妙,没啥区别 - -!
-
其核心思想就是通过感知变化从而做出反应
1. 举个例子来说明下观察者模式
// 定义一个被观察者 Subject 或者叫 Observable
class Subject {
constructor() {
this.observers = [] // 维护一个观察者(Observer)的集合 - 观察列表
this.data = {}
this.defineReactive(this.data, "msg", '')
}
// 将 this.data 进行数据劫持,当给这个属性赋值时向订阅者推送消息
defineReactive(obj, key, value) {
Object.defineProperty(obj, key, {
set: (newVal) => {
if (value === newVal) return
this.publicMsg(newVal)
value = newVal
}
})
}
publicMsg(msg) {
this.observers.forEach(observer => [
observer.receive(msg)
])
}
addObserver(observer) {
this.observers.push(observer)
}
}
// 定义观察者,需要接收一个参数一个被观察者,将自己添加到其观察列表
class Observer {
constructor(name, subject) {
this.name = name
subject.addObserver(this)
}
receive(msg) {
console.log(`${this.name} 收到了消息 ${msg}`);
}
}
const sub = new Subject
const obs1 = new Observer('limy1', sub)
const obs2 = new Observer('limy2', sub)
sub.data.msg = '观察者模式'
现在我们来实现一个精简版的 vue
- 说明:因为是精简版的
vue
我们只看实现原理,一些特殊情况不做考虑,以最理想的最精简的方式展现 MVVM
1. 准备一个测试的数据,我们先建一个 class Vue
拿到外部传入的数据
class Vue {
constructor(options) {
this.$el = options.el
this.$data = options.data
}
}
2. 怎么将页面上的 {{obj.name}}
和 v-text
这种类似于槽的地方填上我们传入的数据呢
// 我们创建一个编译的类
class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
const fragment = this.node2Fragment(this.el) // 将 dom 转化为文档碎片
this.compile(fragment) // 在这里完成 dom 上的槽与 data 上的数据结合
this.el.appendChild(fragment) // 合并好的数据添加到页面
}
node2Fragment(node) {
const fragment = document.createDocumentFragment()
let firstChild
while (firstChild = node.firstChild) {
fragment.appendChild(firstChild)
}
return fragment
}
// 文本节点和元素节点不同,所以我们分别处理,考虑到 dom 节点会出现嵌套,因此使用递归完成深度遍历
compile(node) {
const childNodes = node.childNodes;
[...childNodes].forEach(childNode => {
this.isElementNode(childNode) ? this.compileElement(childNode) : this.compileText(childNode);
(childNode.childNodes && childNode.childNodes.length) && this.compile(childNode)
})
}
compileText(node) {
const text = node.textContent
if (/\{\{(.+?)\}\}/g.test(text)) {
compileUtil.text(node, text, this.vm)
}
}
compileElement(node) {
const [...attrs] = node.attributes
attrs.forEach(attr => {
const {
name,
value
} = attr
if (name.startsWith('v-')) { // 找到以 v-开头的属性
const [_, directive] = name.split('-') // ["v", "text"]
compileUtil[directive](node, value, this.vm)
}
})
}
isElementNode(node) {
return node.nodeType === 1
}
}
// 解耦 将处理不同格式的数据封装
const compileUtil = {
getVal(expr, vm) { // 将传入的表达式在 data 中取值
return expr.split('.').reduce((data, currentVal) => data[currentVal], vm.$data)
},
text(node, expr, vm) { // 这里是精简版不考虑 {{obj.name}} -- {{obj.name}} 这种情况
let val
if (expr.indexOf('{{') !== -1) { // expr {{obj.name}}
val = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
expr = args[1]
return this.getVal(args[1], vm)
})
} else { // expr v-text
val = this.getVal(expr, vm)
}
// new Watcher(vm, expr, newVal => this.updater(node, newVal))
this.updater(node, val)
},
updater(node, val) {
node.textContent = val
}
}
完成这些 我们就能在网页上看到合并后的结果,控制台也没有出现错误
3. 劫持监听 $data
上的所有属性
class Observer {
constructor(data) {
this.observe(data)
}
observe(obj) {
if (obj && typeof obj === 'object') {
Object.keys(obj).forEach(key => {
this.defineReactive(obj, key, obj[key])
})
}
}
defineReactive(obj, key, value) {
this.observe(value) // 考虑到数据嵌套,我们对其递归处理
Object.defineProperty(obj, key, {
get() {
return value
},
set(newVal) {
console.log('newVal', newVal);
if (value !== newVal) {
value = newVal
}
}
})
}
}
可以看到当我们对 vue
实例上 $data
的 msg
属性进行赋值时,会打印出 newVal newMsg
,说明我们已经完成了对 $data
数据的劫持监听
4. Dep 是一个简单的观察者模式实现,它的 subs 用来存储所有订阅它的 Watcher
class Dep {
constructor() {
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher);
}
// 通知变化
notify() {
this.subs.forEach(w => w.update());
}
}
5. Watcher 可以看作一个更新函数,每一个数据都有自己的更新函数
class Watcher {
constructor(vm, expr, cb) {
// 观察新值和旧值的变化,如果有变化 更新视图
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先把旧值存起来
this.oldVal = this.getOldVal();
}
getOldVal() {
Dep.target = this; // 在这里我们将 watcher 实例挂在到 Dep.target 上
// 在执行时会访问 $data 上的属性,这样就会触发劫持的 get() 方法
// 在 get 方法中 我们通过 Dep.target 就能够获取到当前实例 将其添加到 subs 中,这样就完成了对应
let oldVal = compileUtil.getVal(this.expr, this.vm);
Dep.target = null; // 防止同时添加多个 watcher 我们将 Dep.target 置空
return oldVal;
}
update() {
// 更新操作 数据变化后 Dep会发生通知 告诉观察者更新视图
let newVal = compileUtil.getVal(this.expr, this.vm);
if (newVal !== this.oldVal) {
this.cb(newVal);
}
}
}
6. vue 中访问或者修改属性可以通过实例直接修改,怎么弄的呢
// 对数据代理,使之可以通过实例访问属性 vm.$data.msg => vm.msg
proxyData() {
Object.keys(this.$data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return this.$data[key]
},
set(newVal) {
this.$data[key] = newVal
}
})
})
}
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue</title>
</head>
<body>
<div id="app">
<h2>{{obj.name}}</h2>
<h2>{{obj.age}}</h2>
<h3 v-text='obj.name' id="h3"></h3>
<h4 v-text='msg'></h4>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
</div>
<script>
class Vue {
constructor(options) {
this.$el = options.el
this.$data = options.data
new Observer(this.$data)
this.proxyData()
new Compile(this.$el, this)
}
}
class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
const fragment = this.node2Fragment(this.el) // 将 dom 转化为文档碎片
this.compile(fragment) // 在这里完成 dom 上的槽与 data 上的数据结合
this.el.appendChild(fragment) // 合并好的数据添加到页面
}
node2Fragment(node) {
const fragment = document.createDocumentFragment()
let firstChild
while (firstChild = node.firstChild) {
fragment.appendChild(firstChild)
}
return fragment
}
// 文本节点和元素节点不同,所以我们分别处理,考虑到 dom 节点会出现嵌套,因此使用递归完成深度遍历
compile(node) {
const childNodes = node.childNodes;
[...childNodes].forEach(childNode => {
this.isElementNode(childNode) ? this.compileElement(childNode) : this.compileText(childNode);
(childNode.childNodes && childNode.childNodes.length) && this.compile(childNode)
})
}
compileText(node) {
const text = node.textContent
if (/\{\{(.+?)\}\}/g.test(text)) {
compileUtil.text(node, text, this.vm)
}
}
compileElement(node) {
const [...attrs] = node.attributes
attrs.forEach(attr => {
const {
name,
value
} = attr
if (name.startsWith('v-')) { // 找到以 v-开头的属性
const [_, directive] = name.split('-') // ["v", "text"]
compileUtil[directive](node, value, this.vm)
}
})
}
isElementNode(node) {
return node.nodeType === 1
}
}
const compileUtil = {
getVal(expr, vm) { // 将传入的表达式在 data 中取值
return expr.split('.').reduce((data, currentVal) => data[currentVal], vm.$data)
},
text(node, expr, vm) { // 这里是精简版不考虑 {{obj.name}} -- {{obj.name}} 这种情况
let val
if (expr.indexOf('{{') !== -1) { // expr {{obj.name}}
val = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
expr = args[1]
return this.getVal(args[1], vm)
})
} else { // expr v-text
val = this.getVal(expr, vm)
}
new Watcher(vm, expr, newVal => this.updater(node, newVal))
this.updater(node, val)
},
updater(node, val) {
node.textContent = val
}
}
class Observer {
constructor(data) {
this.observe(data)
}
observe(obj) {
if (obj && typeof obj === 'object') {
Object.keys(obj).forEach(key => {
this.defineReactive(obj, key, obj[key])
})
}
}
defineReactive(obj, key, value) {
this.observe(value) // 考虑到数据嵌套,我们对其递归处理
const dep = new Dep
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addSub(Dep.target)
return value
},
set(newVal) {
console.log('newVal', newVal);
if (value !== newVal) {
value = newVal
}
dep.notify()
}
})
}
}
class Watcher {
constructor(vm, expr, cb) {
// 观察新值和旧值的变化,如果有变化 更新视图
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先把旧值存起来
this.oldVal = this.getOldVal();
}
getOldVal() {
Dep.target = this;
let oldVal = compileUtil.getVal(this.expr, this.vm);
Dep.target = null;
return oldVal;
}
update() {
// 更新操作 数据变化后 Dep会发生通知 告诉观察者更新视图
let newVal = compileUtil.getVal(this.expr, this.vm);
if (newVal !== this.oldVal) {
this.cb(newVal);
}
}
}
class Dep {
constructor() {
this.subs = []
}
// 添加订阅者
addSub(watcher) {
this.subs.push(watcher);
}
// 通知变化
notify() {
// 观察者中有个update方法 来更新视图
this.subs.forEach(w => w.update());
}
}
const vm = new Vue({
el: '#app',
data: {
obj: {
name: 'limy',
age: 24,
},
msg: 'vue 简易版',
}
})
</script>
</body>
</html>
上一篇: APUE 进程环境
下一篇: docker基本命令
推荐阅读