MVVM
MVVM 由以下三个内容组成
View:界面Model:数据模型ViewModel:作为桥梁负责沟通View和Model
- 在 JQuery 时期,如果需要刷新 UI 时,需要先取到对应的 DOM 再更新 UI,这样数据和业务的逻辑就和页面有强耦合
- 在 MVVM 中,UI 是通过数据驱动的,数据一旦改变就会相应的刷新对应的 UI,UI 如果改变,也会改变对应的数据。这种方式就可以在业务处理中只关心数据的流转,而无需直接和页面打交道。ViewModel 只关心数据和业务的处理,不关心 View 如何处理数据,在这种情况下,View 和 Model 都可以独立出来,任何一方改变了也不一定需要改变另一方,并且可以将一些可复用的逻辑放在一个 ViewModel 中,让多个 View 复用这个 ViewModel
- 在 MVVM 中,最核心的也就是数据双向绑定,例如 Angluar 的脏数据检测,Vue 中的数据劫持
脏数据检测
- 当触发了指定事件后会进入脏数据检测,这时会调用 $digest 循环遍历所有的数据观察者,判断当前值是否和先前的值有区别,如果检测到变化的话,会调用 $watch 函数,然后再次调用 $digest 循环直到发现没有变化。循环至少为二次 ,至多为十次
- 脏数据检测虽然存在低效的问题,但是不关心数据是通过什么方式改变的,都可以完成任务,但是这在 Vue 中的双向绑定是存在问题的。并且脏数据检测可以实现批量检测出更新的值,再去统一更新 UI,大大减少了操作 DOM 的次数
数据劫持
Vue内部使用了Obeject.defineProperty()来实现双向绑定,通过这个函数可以监听到set和get的事件
var data = { name: 'yck' }observe(data)let name = data.name // -> get valuedata.name = 'yyy' // -> change valuefunction observe(obj) {// 判断类型if (!obj || typeof obj !== 'object') {return}Object.keys(data).forEach(key => {defineReactive(data, key, data[key])})}function defineReactive(obj, key, val) {// 递归子属性observe(val)Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter() {console.log('get value')return val},set: function reactiveSetter(newVal) {console.log('change value')val = newVal}})}
以上代码简单的实现了如何监听数据的 set 和 get 的事件,但是仅仅如此是不够的,还需要在适当的时候给属性添加发布订阅
<div>{{name}}</div>
在解析如上模板代码时,遇到
{{name}}就会给属性name添加发布订阅
// 通过 Dep 解耦class Dep {constructor() {this.subs = []}addSub(sub) {// sub 是 Watcher 实例this.subs.push(sub)}notify() {this.subs.forEach(sub => {sub.update()})}}// 全局属性,通过该属性配置 WatcherDep.target = nullfunction update(value) {document.querySelector('div').innerText = value}class Watcher {constructor(obj, key, cb) {// 将 Dep.target 指向自己// 然后触发属性的 getter 添加监听// 最后将 Dep.target 置空Dep.target = thisthis.cb = cbthis.obj = objthis.key = keythis.value = obj[key]Dep.target = null}update() {// 获得新值this.value = this.obj[this.key]// 调用 update 方法更新 Domthis.cb(this.value)}}var data = { name: 'yck' }observe(data)// 模拟解析到 `{{name}}` 触发的操作new Watcher(data, 'name', update)// update Dom innerTextdata.name = 'yyy'
接下来,对 defineReactive 函数进行改造
function defineReactive(obj, key, val) {// 递归子属性observe(val)let dp = new Dep()Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter() {console.log('get value')// 将 Watcher 添加到订阅if (Dep.target) {dp.addSub(Dep.target)}return val},set: function reactiveSetter(newVal) {console.log('change value')val = newVal// 执行 watcher 的 update 方法dp.notify()}})}
以上实现了一个简易的双向绑定,核心思路就是手动触发一次属性的 getter 来实现发布订阅的添加
Proxy 与 Obeject.defineProperty 对比
Obeject.defineProperty虽然已经能够实现双向绑定了,但是他还是有缺陷的。- 只能对属性进行数据劫持,所以需要深度遍历整个对象
- 对于数组不能监听到数据的变化
虽然
Vue中确实能检测到数组数据的变化,但是其实是使用了hack的办法,并且也是有缺陷的
