MVVM 大家都不陌生了,它把前端开发者用复杂的 dom 操作中彻底解放了出来,从当年让人惊为天人的 Angularjs,到今天前端必备技能 React 和 Vue,始终贯彻着数据驱动UI
这一模式,好处之多无需赘言。想要实现这一点有很多途径,我自己也尝试着做了一套,实现了一些基本功能。
准备工作
首先要知道需要实现的功能,参考主流的框架大概如下:
- 挂载到页面中的某个节点
- 数据 - 和模板的数据进行双向绑定
- 计算属性 - 额外的数据,通过转换数据再显示在模板上
- 模板
- 生命周期
- 回调方法
然后定义一套仿vue的数据结构模型
1 | new MVVM({ |
访问器
想要对数据的访问进行监听,就需要通过Object.defineProperty
(文档)这个方法,它提供了让我们可以拦截属性赋值和访问的途径,让我们可以在属性改变时做一些额外的操作。
下面将属性的声明进行封装,通过一个额外的cb
回调属性对数据的改动进行监听。
1 | class Token { |
封装的额外好处是可以将上一次的值缓存,这样一来在值改变时,可以和上一次的值作比较,从而避免触发无用的回调。
上面我们可以监听单个数据的变化了,但是还远远不够。
对象处理
我们定义的data
是对象,而且字段也可能是对象,我们不仅需要监听data
上的属性变化,还需要进一步监听嵌套的对象的属性变化,而且还需要处理对象结构变化的情况。
举个例子:
1 | new MVVM({ |
上面的代码中,初始化时 userInfo
为 null
,然后拉取详情后,userInfo
赋值为对象,赋值过后我们也需要同步的监听userInfo.name
,userInfo.age
的变化,不然 this.userInfo.age
执行时,就无法触发值的更改事件。
观察者(Watcher)
Watcher
本质上是一个事件的订阅发布中心,它有着以下功能
- 将目标的属性以及嵌套的对象的数据转换成访问器访问。
- 订阅来自模板的属性更改事件
- 代理属性值更改的回调,并派发属性的更改事件
同时有以下约定
- 订阅的事件名是属性的访问路径(例如 this.userInfo.name = ‘xx’ 触发的事件名是 userInfo.name)
- 如果没有传入事件名,则是全局监听
实现
首先我们定义一个构造函数,接受owner
(指当前的 mvvm 实例)和data
( 定义的数据),
1 | class Watcher { |
traverseData
用于遍历一个对象,并返回一个属性名相同,但是属性全部变成访问器的形式,并监听了属性更改,然后通过mergeDescriptor
方法将对象的数据的访问器全部拷贝至了owner
。
1 | traverseData(data: any, path = '') { |
handleValueChange
接受了 3 个参数,后面 2 个比较好理解,而fullPath
是指值从this
的访问路径。
在触发观察者中的回调之前,我们先检查数据是否是plainObject
(简单对象),如果是则继续遍历属性生成访问器。这样一来,给userInfo
赋值后,后面的userInfo.name
也能触发对应的事件。
1 | handleValueChange(fullPath: string, newValue: any, oldValue: any) { |
接下来就是通过trigger
触发事件了,需要注意的是,如果更改了userInfo
,那么监听的userInfo.name
以及userInfo.age
事件也应该触发。
1 | trigger(path: string, newValue: any, oldValue: any) { |
试一试
1 | const owner: any = {}; |
运行结果为
下一步
现在已经可以通过Watcher
对数据更改进行监听,后续我们将通过模板监听对应的数据
,然后实现驱动UI
!