MVVM 三种实现方式

0. 实现 MVVM 三要素

mvvm 作为中间层,处理数据变化后的回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// mvvm 实现
const mvvm = {
// 注册的回调函数队列
callbacks: {},
// 通知 prop 变化
pub(prop, value) {
// 依次调用已经注册的回调函数
(this.callbacks[prop] || []).forEach(callback => callback(prop, value))
},
// 监听 prop 变化
sub(prop, callback = () => {}) {
this.callbacks[prop] = this.callbacks[prop] || []
this.callbacks[prop].push(callback)
}
}

MVVM 实现的三要素:

  1. Model 绑定到对应的 View(比如可以通过 h5 data-* 属性绑定),即 bind(Model, view)

    1
    2
    <input data-bind='name' />
    <input data-bind='age' />
  2. View 的改变会触发绑定的 Model 变化(比如监听元素的 change 事件),即 Model = f(View)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 全局 change 回调
    const changeCb = e => {
    // 元素绑定的属性名
    const bindProp = e.target.dataset.bind
    // 元素当前 value
    const value = e.target.value
    // 如果元素绑定了 model 的某个属性,则触发 model 变化
    bindProp && mvvm.pub(bindProp, value)
    }
    // 监听全局的 change 事件
    document.addEventListener('change', changeCb )
  3. Model 的改变会触发绑定了该 ModelView 变化(下面会提及三种方式),即 View = g(Model)

1. 发布订阅模式

User Model为例,当 User 属性变化后,通知 mvvm 执行回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// User model
const User = ({name,age}) => {
const user = {
// 读取 user 属性
get(prop) {return this[prop]},
set(prop, value) {
// 改变 user 属性值时告知 mvvm
this[prop] = value
mvvm.pub(prop,value)
}
}
// 初始化 user
user.set('name', name)
user.set('age', age)
return user
}

demo

2-1. 发布订阅模式升级版-数据劫持

上述发布订阅模式在读取和写入 user 属性时,分别采用的是 gettersetter 方法,即

1
2
3
4
5
// 读取 user.name
const name = user.get('name')
// 设置 user.name
user.set(name, 'chongya')

当然也可以通过其他自定义方法来读写 user 的属性,但是我们更希望通过直接读写对象属性的方式在做双向绑定:

1
2
3
4
const name = user.name
// user.name 改变时,自动刷新页面
user.name = 'chongya'

数据劫持的方式就是通过劫持对象属性的方式来监听数据变动,数据劫持可以通过如下方式实现:

  • ES5 Object.defineProperty()
  • ES6 new Proxy()

因此 User model 可以变为:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// user model
const user = {}
// ES5 监听 user 的 name, age 属性
Object.defineProperties(user, {
__name: {
writable: true,
configurable: false,
enumerable: false
},
__age: {
writable: true,
configurable: false,
enumerable: false
},
name: {
get() {return this.__name},
set(name) {
// 通知 mvvm name 发生改变
mvvm.pub('name', name)
this.__name = name
}
},
age: {
get() {return this.__age},
set(age) {
// 通知 mvvm age 发生改变
mvvm.pub('age', age)
this.__age = age
}
}
})
// ES6 监听 user 的 name、age 属性
const user = new Proxy({}, {
// 读取属性值
get(target, prop){
return Reflect.get(target, prop)
},
// 写入属性值
set(target, prop, value){
mvvm.pub(prop, value)
return Reflect.set(target, prop, value)
}
})

demo

2-2. Vue mvvm

数据劫持 + 发布订阅模式进一步抽象化,参考 Vue 中双向绑定的实现,得到如下 ObserverCompileWatcher 三个模块:

vue-mvvm.png

3. 脏值检测

一言蔽之,脏值检测就是在特定条件下轮询检测数据是否变动。

这里借鉴 Angular1 的实现方式:

angular-mvvm.png

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// Scope
class Scope {
constructor(initValue){
// 初始值
this.value = initValue
// watcher 数组
this.$$watchers = []
}
// 注册 watcher
$watch(watchFn, listenerFn) {
// watcher
const watcher = {
watchFn,
listenerFn,
// watcher 关注数据的最新值,初始化为函数可以保证后续脏值检测成功
last: () => {}
}
this.$$watchers.push(watcher)
}
// 轮询检测脏值
$digest(){
let dirty = true
while(dirty) {
dirty = this.$digestOnce()
}
}
// 检测脏值
$digestOnce(){
let newValue, oldValue, dirty
// 遍历每个 watcher
this.$$watchers.forEach(
watcher => {
newValue = watcher.watchFn(this)
oldValue = watcher.last
// 检测到脏值
if(newValue !== oldValue) {
watcher.last = newValue
// 执行回调
watcher.listenerFn(newValue)
dirty = true
}
}
)
return dirty
}
}
// user Model
const user = new Scope({name: 'chongya', age: 20})
// 监听 user.name 变化,并将变化告知 mvvm
user.$watch(
scope => scope.value.name,
newName => mvvm.pub('name', newName)
)
// 监听 user.age 变化,并将变化告知 mvvm
user.$watch(
scope => scope.value.age,
newAge => mvvm.pub('age', newAge)
)

demo