使用proxy实现一个简单完整的MVVM库
前言
MVVM
是当前时代前端日常业务开发中的必备模式(相关框架如react
,vue
,angular
等), 使用 MVVM
可以将开发者的精力更专注于业务上的逻辑,而不需要关心如何操作 dom
。虽然现在都 9012 年了,mvvm
相关原理的介绍已经烂大街了,但出于学习基础知识的目的(使用 proxy
实现的 vue
3.0 还在开发中), 在参考了之前 vue.js
的整体思路之后,自己动手实现了一个简易的通过 proxy
实现的 mvvm
。
本项目代码已经开源在github,项目正在持续完善中,欢迎交流学习,喜欢请点个 star 吧!
最终效果
1 | <html> |
1 | import MVVM from '@fe_korey/mvvm'; |
结构概览
Complier
模块实现解析、收集指令,并初始化视图Observer
模块实现了数据的监听,包括添加订阅者和通知订阅者Parser
模块实现解析指令,提供该指令的更新视图的更新方法Watcher
模块实现建立指令与数据的关联Dep
模块实现一个订阅中心,负责收集,触发数据模型各值的订阅列表
流程为:Complier
收集编译好指令后,根据指令不同选择不同的Parser
,根据Parser
在Watcher
中订阅数据的变化并更新初始视图。Observer
监听数据变化然后通知给 Watcher
,Watcher
再将变化结果通知给对应Parser
里的 update
刷新函数进行视图的刷新。
模块详解
Complier
将整个数据模型
data
传入Observer
模块进行数据监听1
this.$data = new Observer(option.model).getData();
循环遍历整个
dom
,对每个dom
元素的所有指令进行扫描提取1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19function collectDir(element) {
const children = element.childNodes;
const childrenLen = children.length;
for (let i = 0; i < childrenLen; i++) {
const node = children[i];
const nodeType = node.nodeType;
if (nodeType !== 1 && nodeType !== 3) {
continue;
}
if (hasDirective(node)) {
this.$queue.push(node);
}
if (node.hasChildNodes() && !hasLateCompileChilds(node)) {
collectDir(element);
}
}
}对每个指令进行编译,选择对应的解析器
Parser
1
const parser = this.selectParsers({ node, dirName, dirValue, cs: this });
将得到的解析器
Parser
传入Watcher
,并初始化该dom
节点的视图1
2const watcher = new Watcher(parser);
parser.update({ newVal: watcher.value });所有指令解析完毕后,触发
MVVM
编译完成回调$mounted()
1
this.$mounted();
使用文档碎片
document.createDocumentFragment()
来代替真实dom
节点片段,待所有指令编译完成后,再将文档碎片追加回真实dom
节点1
2
3
4
5
6
7
8let child;
const fragment = document.createDocumentFragment();
while ((child = this.$element.firstChild)) {
fragment.appendChild(child);
}
//解析完后
this.$element.appendChild(fragment);
delete $fragment;
Parser
在
Complier
模块编译后的指令,选择不同听解析器解析,目前包括ClassParser
,DisplayParser
,ForParser
,IfParser
,StyleParser
,TextParser
,ModelParser
,OnParser
,OtherParser
等解析模块。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28switch (name) {
case 'text':
parser = new TextParser({ node, dirValue, cs });
break;
case 'style':
parser = new StyleParser({ node, dirValue, cs });
break;
case 'class':
parser = new ClassParser({ node, dirValue, cs });
break;
case 'for':
parser = new ForParser({ node, dirValue, cs });
break;
case 'on':
parser = new OnParser({ node, dirName, dirValue, cs });
break;
case 'display':
parser = new DisplayParser({ node, dirName, dirValue, cs });
break;
case 'if':
parser = new IfParser({ node, dirValue, cs });
break;
case 'model':
parser = new ModelParser({ node, dirValue, cs });
break;
default:
parser = new OtherParser({ node, dirName, dirValue, cs });
}不同的解析器提供不同的视图刷新函数
update()
,通过update
更新dom
视图1
2
3
4//text.js
function update(newVal) {
this.el.textContent = _toString(newVal);
}OnParser
解析事件绑定,与数据模型中的methods
字段对应1
2
3
4//详见 https://github.com/zhaoky/mvvm/blob/master/src/core/parser/on.ts
el.addEventListener(handlerType, (e) => {
handlerFn(scope, e);
});ForParser
解析数组1
//详见 https://github.com/zhaoky/mvvm/blob/master/src/core/parser/for.ts
ModelParser
解析双向绑定,目前支持input[text/password] & textarea
,input[radio]
,input[checkbox]
,select
四种情况的双向绑定,双绑原理:数据变化更新表单:跟其他指令更新视图一样,通过
update
方法触发更新表单的value
1
2
3function update({ newVal }) {
this.model.el.value = _toString(newVal);
}表单变化更新数据:监听表单变化事件如
input
,change
,在回调里set
数据模型1
2
3this.model.el.addEventListener('input', (e) => {
model.watcher.set(e.target.value);
});
Observer
MVVM
模型中的核心,一般通过Object.defineProperty
的get
,set
方法进行数据的监听,在get
里添加订阅者,set
里通知订阅者更新视图。在本项目采用Proxy
来实现数据监听,好处有三:Proxy
可以直接监听对象而非属性Proxy
可以直接监听数组的变化Proxy
有多达 13 种拦截方法,查阅
而劣势是兼容性问题,且无法通过polyfill
磨平。查阅兼容性
- 注意
Proxy
只会监听自身的每一个属性,如果属性是对象,则该对象不会被监听,所以需要递归监听 - 设置监听后,返回一个
Proxy
替代原数据对象
1 | var proxy = new Proxy(data, { |
Watcher
在
Complier
模块里对每一个解析后的Parser
进行指令与数据模型直接的绑定,并触发Observer
的get
监听,添加订阅者(Watcher
)1
this._getter(this.parser.dirValue)(this.scope || this.parser.cs.$data);
当数据模型变化时,就会触发 ->
Observer
的set
监听 ->Dep
的notfiy
方法(通知订阅者的所有订阅列表) -> 执行订阅列表所有Watcher
的update
方法 -> 执行对应Parser
的update
-> 完成更新视图Watcher
里的set
方法用于设置双向绑定值,注意访问层级
Dep
MVVM
的订阅中心,在这里收集数据模型的每个属性的订阅列表- 包含添加订阅者,通知订阅者等方法
- 本质是一种发布/订阅模式
1 | class Dep { |
后记
目前该 mvvm
项目只实现了数据绑定
和视图更新
的功能,通过这个简易轮子的实现,对 dom
操作,proxy
,发布订阅模式
等若干基础知识都进行了再次理解,查漏补缺。同时欢迎大家一起探讨交流,后面会继续完善!