这是我参与8月更文挑战的第22天,活动详情查看:8月更文挑战
MVVM
MVVM 是一种设计模式,或者也可以称为一种架构模式。它的全拼是 Model-View-ViewModel。
- Model 代表模型
- View 代表视图
- ViewModel 代表了一个控制器
这其中,我们可以将 model 看作是后端接口返回的数据
{
name: '晴天',
age: '18',
mark: 'always 18'
}
而 view 就是前端显示在浏览器中让用户看到的页面
那一段 json 数据是如何显示到页面里的呢?这就是 ViewModel 负责的事情。
所以,它们之间的关系可以用一个图来表示
前端最常见的 MVVM 的框架,应该就是
- AngularJs ,不过这个框架,国内用的貌似比较少
- 尤大大的 vue,我们经常在 vue 的 demo 中看到
var vm = new Vue(...)
,这个 vm 就是 viewmodel 的简写。
响应式原理
MVVM 的初衷是想利用数据绑定函数,从视图层面删除所有和界面数据渲染逻辑相关的代码。
那么,应该如何使用或者如何编写数据绑定函数,才能达到这样的效果呢?
我们看 vue 的表现:
<div id="counter">
Counter: {{ counter }}
</div>
const Counter = {
data() {
return {
counter: 0
}
}
}
Vue.createApp(Counter).mount('#counter')
当然这个例子和 Object.defineProperty 没有关系,我只是在这里体现 “数据绑定后从视图层面删除了和界面数据渲染逻辑相关的代码” 这句话
很明显,我们没有自己编写
document.getElementById(counter).innerText = counter
或者
$('#counter).text(counter)
类似这种的渲染代码。
你可能觉得这只是一行代码啊,这么简单。
其实不然,假如我们需要counter
做递增
mounted() {
setInterval(() => {
this.counter++
}, 1000)
}
你就知道 MVVM 到底帮我们省了多大的力气。
好了,到这里已经展示了 MVVM 的表现,那么回到正题,如何利用 Object.defineProperty 简单实现 MVVM 呢?
其实 vue2 的响应式原理,是数据劫持,即数据变化的时候,自动重新渲染相关页面。
其实这个需求是非常容易的,但是首先要理解一个方法,叫做 Object.defineProperty
,理解之后大部分人都可以模拟一个简单的实现。
虽然跟 vue 差很多,但是面试考察的也不是让你去写个 vue 。
defineProperty 数据劫持
Object.defineProperty 方法会在一个对象上添加一个新的属性,或者修改一个对象的已有属性,最后返回此对象.
Object.defineProperty 方法可以接收三个参数
- object (required, 要定义属性的对象)
- propertyname (required, 要定义或修改的属性的名称)
- descriptor (required, 要定义或修改的属性描述符)
Object.defineProperty(object, propertyname, descriptor)
针对 descriptor,它是一个对象类型,用于配置 propertyname 的属性描述符,因此 descriptor 的属性可以选择如下两种中的一种:
- 数据描述符
key | 值类型 | 描述 | 默认值 |
---|---|---|---|
value | any | object.propertyname 的值 | undefined |
writable | boolean | object.propertyname 是否可以被赋值运算符修改 | false |
configurable | boolean | object.propertyname 是否可以被修改和删除 | false |
enumerable | boolean | object.propertyname 是否可以被枚举 | false |
- 存取描述符
key | 值类型 | 描述 | 默认值 |
---|---|---|---|
get | function | 读取 object.propertyname 时调用的函数 | undefined |
set | function | 设置 object.propertyname 时调用的函数 | undefined |
configurable | boolean | object.propertyname 是否可以被修改和删除 | false |
enumerable | boolean | object.propertyname 是否可以被枚举 | false |
小伙伴可能看出来了,数据描述符和存取描述符具有共同的 key,也有不同的 key,那么当一个描述符内部没有value
、writable
、get
和set
时,它默认是一个数据描述符。
下面的例子展示了存取描述符的作用:
let data = {}, temp = 'aa'
Object.defineProperty(data, 'key1', {
set(value){
console.log('this is a new value: ' + value)
temp = value
//some code like $('div').html(value) will automatic execute when key1 changed
},
get(){
return temp
}
})
data.key1 = 'Jack'
当我们在浏览器运行上面这段代码,控制台会输出:
this is a new value: Jack
这就是存取描述符的作用,他可以用来做数据劫持。
订阅模式
我们题目要实现的订阅与数据劫持,就是要通过Object.defineProperty
的存取描述符来实现。
订阅器,我们可以简单的将其理解为一个队列,队列内都是即将在某个时刻执行的函数。
当然,为了方便查找,我们可以将其定义为一个对象类型,其中的每个属性,都是数组类型。
var a = {a:[], b:[], c:[]}
下面我们来实现一个订阅器:
let Deep = {
deepList: {},
listen(key, fn){
if(!this.deepList[key])
this.deepList[key] = []
this.deepList[key].push(fn)
},
trigger(){
let key = Array.prototype.shift.call(arguments)
let fnList = this.deepList[key]
if(!key || !fnList || !fnList.length)
return false
for(let i=0, fn; fn = fnList[i++];) {
fn.apply(this.arguments)
}
}
}
将订阅器与数据劫持绑定到一起
这里就是要实现,数据劫持发生后,去执行订阅器内相应的代码。
这样,就可以实现类似 vue 的,改变了某个 message,页面能同步渲染最新的结果。
这部分代码的逻辑十分简单:
- 首先,我们通过 Deep.listen 将页面标签与内容绑定到一起并放入到订阅器中
- 然后,我们在数据劫持中调用订阅器的 trigger 方法,更新数据的同时同步更新 html
let dataHijack = ({data, tag, datakey, selector}) => {
let value = '', el = document.querySelector(selector);
Object.defineProperty(data, datakey, {
get(){
return value
},
set(newVlaue){
value = newVlaue
Deep.trigger(tag, newVlaue)
}
})
Deep.listen(tag, content=>{
el.innerHTML = content
})
}