新闻资讯

新闻资讯 行业动态

Vue 如何保证 UI 与状态同步

编辑:008     时间:2020-02-17

UI 在 MVVM 中指的是 View,状态在 MVVM 中指的是 Modal,而保证 View 和 Modal 同步的是 View-Modal。

Vue 通过一个响应式系统保证了View 与 Modal的同步,由于要兼容IE,Vue 选择了 Object.defineProperty作为响应式系统的实现,但是如果不考虑 IE 用户的话,Object.defineProperty并不是一个好的选择target=https%3A%2F%2Fjuejin.im%2Fpost%2F5acd0c8a6fb9a028da7cdfaf)。

我们将用 Proxy 实现一个响应式系统。 

1 发布订阅中心

一个响应式系统离不开发布订阅模式,因为我们需要一个 Dep保存订阅者,并在 Observer 发生变化时通知保存在 Dep 中的订阅者,让订阅者得知变化并更新视图,这样才能保证视图与状态的同步。

/**
 * [subs description] 订阅器,储存订阅者,通知订阅者
 * @type {Map}
 */ export default class Dep { constructor() { // 我们用 hash 储存订阅者 this.subs = new Map();
  } // 添加订阅者 addSub(key, sub) { // 取出键为 key 的订阅者 const currentSub = this.subs.get(key); // 如果能取出说明有相同的 key 的订阅者已经存在,直接添加 if (currentSub) {
      currentSub.add(sub);
    } else { // 用 Set 数据结构储存,保证唯一值 this.subs.set(key, new Set([sub]));
    }
  } // 通知 notify(key) { // 触发键为 key 的订阅者们 if (this.subs.get(key)) { this.subs.get(key).forEach(sub => {
        sub.update();
      });
    }
  }
}

2 监听者的实现

我们在订阅器 Dep 中实现了一个notify方法来通知相应的订阅这们,然而notify方法到底什么时候被触发呢?

当然是当状态发生变化时,即 MVVM 中的 Modal 变化时触发通知,然而Dep 显然无法得知 Modal 是否发生了变化,因此我们需要创建一个监听者Observer来监听 Modal, 当 Modal 发生变化的时候我们就执行通知操作。

vue 基于Object.defineProperty来实现了监听者,我们用 Proxy 来实现监听者.

与Object.defineProperty监听属性不同, Proxy 可以监听(实际是代理)整个对象,因此就不需要遍历对象的属性依次监听了,但是如果对象的属性依然是个对象,那么 Proxy 也无法监听,所以我们实现了一个observify进行递归监听即可。

/**
 * [Observer description] 监听器,监听对象,触发后通知订阅
 * @param {[type]}   obj [description] 需要被监听的对象
 */ const Observer = obj => { const dep = new Dep(); return new Proxy(obj, { get: function(target, key, receiver) { // 如果订阅者存在,直接添加订阅 if (Dep.target) {
        dep.addSub(key, Dep.target);
      } return Reflect.get(target, key, receiver);
    }, set: function(target, key, value, receiver) { // 如果对象值没有变,那么不触发下面的操作直接返回  if (Reflect.get(receiver, key) === value) { return;
      } const res = Reflect.set(target, key, observify(value), receiver); // 当值被触发更改的时候,触发 Dep 的通知方法 dep.notify(key); return res;
    },
  });
}; /**
 * 将对象转为监听对象
 * @param {*} obj 要监听的对象
 */ export default function observify(obj) { if (!isObject(obj)) { return obj;
  } // 深度监听 Object.keys(obj).forEach(key => {
    obj[key] = observify(obj[key]);
  }); return Observer(obj);
}

3 订阅者的实现

我们目前已经解决了两个问题,一个是如何得知 Modal 发生了改变(利用监听者 Observer 监听 Modal 对象),一个是如何收集订阅者并通知其变化(利用订阅器收集订阅者,并用notify通知订阅者)。

我们目前还差一个订阅者(Watcher)

// 订阅者 export default class Watcher { constructor(vm, exp, cb) { this.vm = vm; // vm 是 vue 的实例 this.exp = exp; // 被订阅的数据 this.cb = cb; // 触发更新后的回调 this.value = this.get(); // 获取老数据 } get() { const exp = this.exp;
    let value;
    Dep.target = this; if (typeof exp === 'function') {
      value = exp.call(this.vm);
    } else if (typeof exp === 'string') {
      value = this.vm[exp];
    }
    Dep.target = null; return value;
  } // 将订阅者放入待更新队列等待批量更新 update() {
    pushQueue(this);
  } // 触发真正的更新操作 run() { const val = this.get(); // 获取新数据 this.cb.call(this.vm, val, this.value); this.value = val;
  }
}

4 批量更新的实现

我们在上一节中实现了订阅者( Watcher),但是其中的update方法是将订阅者放入了一个待更新的队列中,而不是直接触发,原因如下:

因此这个队列需要做的是异步去重,因此我们用 Set作为数据结构储存 Watcher 来去重,同时用Promise模拟异步更新。

// 创建异步更新队列 let queue = new Set() // 用Promise模拟nextTick function nextTick(cb) { Promise.resolve().then(cb)
} // 执行刷新队列 function flushQueue(args) {
    queue.forEach(watcher => {
            watcher.run()
        }) // 清空 queue = new Set()
} // 添加到队列 export default function pushQueue(watcher) {
    queue.add(watcher) // 下一个循环调用 nextTick(flushQueue)
}

5 小结

我们梳理一下流程, 一个响应式系统是如何做到 UI(View)与状态(Modal)同步的?

我们首先需要监听 Modal, 本文中我们用 Proxy 来监听了 Modal 对象,因此在 Modal 对象被修改的时候我们的 Observer 就可以得知。

我们得知Modal发生变化后如何通知 View 呢?要知道,一个 Modal 的改变可能触发多个 UI 的更新,比如一个用户的用户名改变了,它的个人信息组件、通知组件等等组件中的用户名都需要改变,对于这种情况我们很容易想到利用发布订阅模式来解决,我们需要一个订阅器(Dep)来储存订阅者(Watcher),当监听到 Modal 改变时,我们只需要通知相关的订阅者进行更新即可。

那么订阅者来自哪里呢?其实每一个组件实例对应着一个订阅者(正因为一个组件实例对应一个订阅者,才能利用 Dep 通知到相应组件,不然乱套了,通知订阅者就相当于间接通知了组件)。

当订阅者得知了具体变化后它会进行相应的更新,将更新体现在 UI(View)上,至此UI 与 Modal 的同步完成了。

郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。

回复列表

相关推荐