前言
原理部分不在叙述,网上很多博客都有提,我是在掘金看了别的博主的文章(不好意思!耽误你的十分钟,让 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 (let key in data) {
Object.defineProperty(this, key, {
enumerable: true, // 可被枚举
set: function (newVal) {
data[key] = newVal;
},
get: function () {
return data[key];
}
});
}
}
}
// 改变methods里面的方法this指向
_bind(methods) {
for (let 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 (let key in data) {
let val = data[key];
let dep = new Dep(); // 发布订阅类实例
this._traverse(val); // 递归遍历,深度劫持
Object.defineProperty(data, key, {
enumerable: true, // 可被枚举
set: function (newVal) {
if (val !== newVal) {
val = newVal;
dep.notify(key); // 数据更新,通知订阅者
return newVal;
}
},
get: function () {
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);
let 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 => {
let text = node.textContent;
let 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) {
let attrs = node.attributes; // 获取dom节点的属性
Array.from(attrs).forEach(attr => {
console.log(attr);
let name = attr.name;
let 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, function (newVal) {
node.value = newVal; // 当watcher触发时会自动将内容放进输入框中
});
node.addEventListener('input', function (e) {
// 监听input事件,输入时更新数据
let newVal = e.target.value;
vm[exp] = newVal;
});
});
}
if (node.childNodes && node.childNodes.length) {
this._replace(node, vm); // 递归遍历节点
}
});
}
}
总结
目前还需要一段时间去消化这些知识,这篇就当作学习记录吧!不敢说是技术分享,讲的实在太烂了呜呜呜…