前言
原理部分不在叙述,网上很多博客都有提,我是在掘金看了别的博主的文章(不好意思!耽误你的十分钟,让 MVVM 原理还给你),然后按自己的理解模仿着实现了基础的 demo,在此基础上又添加了 methods、v-show 和@click 的实现。
由于自己还没彻底消化,所以叙述会有点烂 😢,当成一个菜鸟的学习记录吧!下面提到的东西可能是有错误的 😓
完整代码:github 传送门
demo 演示:demo 传送门
具体实现
数据代理
这里主要是 data 和 methods 的代理,代理的目的很简单,在 Vue 中,我们可以直接使用 this.xxx 来访问数据,而数据代理就是达到该目的的实现之一。
另外,如果 methods 里面的方法也能使用 this.xxx 来访问数据,那么还需要改变 method 的 this 指向,这里我写了个_bind()
方法来实现
class MVVM {
constructor(options = {}) {
this.$options = options
this._proxy(options.data)
this._proxy(options.methods)
this._bind(options.methods)
}
// 将数据挂载到实例上,this代理options.data/methods,即可以直接使用this.key访问data的数据/methods的方法
_proxy(data) {
if (typeof data === 'object') {
for (const key in data) {
Object.defineProperty(this, key, {
enumerable: true, // 可被枚举
set(newVal) {
data[key] = newVal
},
get() {
return data[key]
}
})
}
}
}
// 改变methods里面的方法this指向
_bind(methods) {
for (const key in methods) {
methods[key] = methods[key].bind(this)
}
}
}
数据劫持 + 订阅发布
数据劫持是通过Object.defineProperty()
方法来实现,用 ES6 的Proxy
来实现也可,有时间再更新。
这个模式好像是观察者+发布订阅的结合使用,不知道对不对,感觉是这样。
关于这两个设计模式可以看一下我的另外两篇文章:手撕观察者模式、手撕发布-订阅模式
Dep
通过这个类是发布-订阅的具体实现
class Dep {
constructor() {
this.subscribeObj = {}
}
subscribe(key, sub) {
this.subscribeObj[key] = sub
}
notify(key) {
this.subscribeObj[key].update()
}
}
Observer
这个类的作用主要是作为一个拦截器(数据劫持),订阅数据,发布通知,数据的获取和修改都需要经过这里(不出意外的话
class Observer {
constructor(data) {
for (const key in data) {
let val = data[key]
const dep = new Dep() // 发布订阅类实例
this._traverse(val) // 递归遍历,深度劫持
Object.defineProperty(data, key, {
enumerable: true, // 可被枚举
set(newVal) {
if (val !== newVal) {
val = newVal
dep.notify(key) // 数据更新,通知订阅者
return newVal
}
},
get() {
Dep.target && dep.subscribe(key, Dep.target) // 增加订阅者,监听数据
return val
}
})
}
}
_traverse(data) {
if (data && typeof data === 'object') {
return new Observer(data)
}
}
}
Watcher
监听者,update
函数就是用来更新数据的
class Watcher {
constructor(vm, exp, cb) {
// 实例本身,模板键值(如v-model="obj.key"的obj.key),回调函数
this.vm = vm
this.exp = exp
this.cb = cb
Dep.target = this
let val = vm
exp.split('.').forEach((key) => {
val = val[key]
})
}
update() {
let val = this.vm
this.exp.split('.').forEach((key) => {
val = val[key]
})
this.vm.vShow.forEach((obj) => {
// 检查vShow数组里面存储的v-show指令绑定值的状态
obj.node.style.display = this.vm[obj.key] ? '' : 'none'
})
this.cb(val)
}
}
数据编译
数据的更新啥的都弄好了,下面就得进行最后一步数据渲染了!
下面节点的更新有用到DocumentFragment
,这里稍微偏一下题,使用DocumentFragment
来来临时存储节点是有性能优化的作用的,比如下面的节点更新,如果一个一个节点的插入到 DOM 树中,就会有大量的 DOM 操作,引起多次的重绘和重排,从而影响到渲染的性能,将需要更新的节点存放到DocumentFragment
中,最后再一次性更新,只有一次 DOM 操作,因此这里使用DocumentFragment
是有原因滴~
class Compile {
constructor(el, vm) {
vm.$el = document.querySelector(el)
const fragment = document.createDocumentFragment()
let child
while ((child = vm.$el.firstChild)) {
fragment.appendChild(child)
}
this._replace(fragment, vm)
// 再将文档碎片放入el中
vm.$el.appendChild(fragment)
}
_replace(fragment, vm) {
Array.from(fragment.childNodes).forEach((node) => {
const text = node.textContent
const reg = /\{\{(.*?)\}\}/g // 匹配{{}}的内容
/*
* nodeType: 1 元素节点,3 文本节点
*/
if (node.nodeType === 3 && reg.test(text)) {
function _replaceText() {
// 替换节点文本
node.textContent = text.replace(reg, (matched, placeholder) => {
console.log(matched, placeholder)
new Watcher(vm, placeholder, _replaceText)
return placeholder.split('.').reduce((val, key) => {
return val[key]
}, vm)
})
}
_replaceText()
}
if (node.nodeType === 1) {
const attrs = node.attributes // 获取dom节点的属性
Array.from(attrs).forEach((attr) => {
console.log(attr)
const name = attr.name
const exp = attr.value
if (name.includes('v-model')) {
// v-model
node.value = vm[exp]
}
else if (name.includes('@click')) {
// 绑定点击事件
node.addEventListener('click', vm[exp])
}
else if (name.includes('v-show')) {
// v-show指令处理
vm.vShow.push({
node,
type: 'v-show',
key: exp
})
node.style.display = vm[exp] ? '' : 'none'
console.log(vm)
}
new Watcher(vm, exp, (newVal) => {
node.value = newVal // 当watcher触发时会自动将内容放进输入框中
})
node.addEventListener('input', (e) => {
// 监听input事件,输入时更新数据
const newVal = e.target.value
vm[exp] = newVal
})
})
}
if (node.childNodes && node.childNodes.length) {
this._replace(node, vm) // 递归遍历节点
}
})
}
}
总结
目前还需要一段时间去消化这些知识,这篇就当作学习记录吧!不敢说是技术分享,讲的实在太烂了呜呜呜…