自制简易 mvvm 框架
为了深入理解 mvvm 框架,笔者自己动手实现一个简易的 mvvm 框架,并以 vue2 作为仿照的模板。
第一步,以 vue2 模板作为目标
让我们把 vue2 的模板作为我们要实现的目标,我们就有了具体的实现需求,如我们将创建一个 mvvm 实例,选项中支持挂载元素 el,数据 data,且数据是响应式的。而且我们应该保证代码的可维护性,容许增加更多的功能。
</html>
<body>
<div id="app">
<p>{{number}}<button id="addButton">+1</button></p>
</div>
<script type="module">
import Mvvm from './lib/mvvm.js'
let mvvm = new Mvvm({
el: '#app',
data: {
number:0
}
});
const addButton=document.querySelector('#addButton')
addButton.addEventListener('click',()=>{
mvvm.number+=1
})
</script>
</body>
</html>
第二步,实现数据劫持监听
要做到数据响应式的大前提是监视你的数据,监视获取数据和修改数据的操作。vue2 实现的核心是Object.defineProperty(),这是属于 ES5 的语法,因此支持版本低于 ES5 的浏览器是无法正常运行的 vue2 的。
通过observe方法,我们将递归 data 下的所有数据,所有数据的读的操作将通过 getter,写的操作将通过 setter。
function observe(data={}){
if(typeof data !== 'object')return
for(let key in data){
let val =data[key]
if(typeof val === 'object')observe(val)
Object.defineProperty(data,key,{
configurable:true,
get(){
return val
},
set(newVal){
if(val===newVal)return
val =newVal
observe(newVal)
// 当你修改数据时,你将打印出'数据更新了'
console.log('数据更新了')
}
})
}
}
第三步,数据代理
使用 vue2 进行开发时,我们会发现选项中 data 下的数据,会直接在实例下找到。实现思路如下:
function proxy(data,vm){
for(let key in data){
Object.defineProperty(vm,key,{
configurable:true,
get(){
return vm._data[key]
},
set(newVal){
vm._data[key]=newVal
}
})
}
}
第四步,模板编译
vue2 中,要求我们在模板中将数据写在{{}}之中,很明显 vue2 为我们将符合规则的部分进行了转换。
核心实现思路为,使用正则匹配模板中含有{{}}的文本节点,拿到其中表示数据的字符串,替换为实例上对应的数据。其中重要的一个优化点是将挂载元素中所有的节点放入 fragment,即内存中,因此不会频繁引起回流,优化性能。
function compile(el, vm) {
vm.$el = document.querySelector(el);
let fragment = document.createDocumentFragment();
// 将 DOM 树中的节点放到内存中
while (child = vm.$el.firstChild) {
fragment.appendChild(child);
}
replace(fragment);
// 对整个代码片段替换的方法
function replace(frag) {
Array.from(frag.childNodes).forEach(function (node) {
let txt = node.textContent;
let reg = /\{\{(.*?)\}\}/g;
// 即是文本节点又有{{}}
if (node.nodeType === 3 && reg.test(txt)) {
// 对一个符合的文本节点替换的方法
!function replaceTxt() {
node.textContent = txt.replace(reg, (matched, placeholder) => {
return placeholder.split('.').reduce((val, key) => {
return val[key];
}, vm);
});
}();
}
// 如果该节点拥有子节点,那么递归 replace
if (node.childNodes && node.childNodes.length) {
replace(node);
}
});
}
vm.$el.appendChild(fragment);
}
第五步,发布订阅模式
到此为止我们的数据依旧不是响应式的,模板编译后修改数据不会引起 DOM 树的任何改变。 因此我们需要在数据的修改和DOM 树的修改之间建立联系。
核心实现思路,利用 Watcher 创建一个侦听器实例,侦听器上放更新 DOM 的方法,以及执行该方法所需要的信息。然后利用 Dep 创建一个依赖实例,依赖实例有一个存放侦听器的队列,以及添加侦听器和触发侦听器队列的方法。
// Watcher
function Watcher(vm, exp, fn) {
this.fn = fn;
}
Watcher.prototype.update = function () {
this.fn(val); // newVal
};
// Dep
function Dep() {
this.subs = [];
}
Dep.prototype.addSub = function (sub) {
this.subs.push(sub);
};
Dep.prototype.notify = function () {
this.subs.forEach(sub=>sub.update())
};
第六步,数据响应式
有了这个发布订阅模式,现在我们就可以着手连接数据和视图了。
首先,我们在 complie 函数中,为所有参加了编译的文本节点添加侦听器
// compile函数中
!function replaceTxt() {
node.textContent = txt.replace(reg, (matched, placeholder) => {
// 新增代码:添加侦听器
+ new Watcher(vm, placeholder, replaceTxt);
return placeholder.split('.').reduce((val, key) => {
return val[key];
}, vm);
});
}();
然后,我们需要改造 Watcher,确保侦听器上有执行更新方法所需要的参数 vm,exp。一个 watcher 侦听器制作好了之后,我们就需要将它放到依赖的队列中,我们通过 Dep.target 和一个小操作将侦听器传递到 observe 中。
function Watcher(vm, exp, fn) {
this.fn = fn;
this.vm = vm;
this.exp = exp;
// 通过以下方法添加到依赖队列中
Dep.target = this;
let val = vm;
let arr = exp.split('.');
arr.forEach(key => {
val = val[key];
});
Dep.target = null;
}
Watcher.prototype.update = function () {
let val = this.vm;
let arr = this.exp.split('.');
arr.forEach(key => {
val = val[key];
});
this.fn(val); // newVal
};
接下来在 observe 中接应,getter 中将传递的侦听器添加侦听器队列中,setter 即数据改动时执行执行侦听器队列。
function observe(data={}){
let dep =new Dep()
// 省略了未改动代码
get(){
Dep.target && dep.addSub(Dep.target); // [watcher]
return val
},
set(newVal){
if(val===newVal)return
val =newVal
observe(newVal)
console.log('数据更新了')
dep.notify();
}
}
如此我们就实现了一个具备数据劫持、数据代理、数据响应式功能的简单 mvvm 框架。最后可以 rollup 一下,发布到 npm 上再看看效果。
往后可以实现更多功能
相关资料
试试看?
yarn add friday-mvvvm