实现一个简单的被监听对象

本小节,我将会阐述 nx-observe 的底层实现。首先,我将向您展示如何检测到可观察到的属性的变化并与观察者配对。然后我将会阐述怎么运行这些由改变所触发的监听函数方法。

注册变化

变化是通过把被监听对象封装到 ES6 代理来注册的。这些代理使用 Reflection API 无缝地拦截 get 和 set 操作。

以下代码使用 currentObserver 变量和 queueObserver(),但是只会在下一小节中进行解释。现在只需要知道的是 currentObserver 总是指向目前运行的监听函数,而 queueObserver() 把将要执行的监听函数插入队列。

  1. /* 映射被监听对象属性到监听函数集,监听函数集会使用监听对象属性 */
  2. const observers = new WeakMap()
  3. /* 指向当前运行的监听函数可以为 undefined */
  4. let currentObserver
  5. /* 利用把对象封装为一个代理来把对象转换为一个可监听对象,
  6. 它也可以添加一个空白映射,用作以后保存被监听对象-监听函数对。
  7. */
  8. function observable (obj) {
  9. observers.set(obj, new Map())
  10. return new Proxy(obj, {get, set})
  11. }
  12. /* 这个陷阱拦截 get 操作,如果当前没有执行监听函数它不做任何事 */
  13. function get (target, key, receiver) {
  14. const result = Reflect.get(target, key, receiver)
  15. if (currentObserver) {
  16. registerObserver(target, key, currentObserver)
  17. }
  18. return result
  19. }
  20. /* 如果一个监听函数正在运行,这个函数会配对监听函数和当前取得的被
  21. 监听对象属性,并保存到一个监听函数映射之中 */
  22. function registerObserver (target, key, observer) {
  23. let observersForKey = observers.get(target).get(key)
  24. if (!observersForKey) {
  25. observersForKey = new Set()
  26. observers.get(target).set(key, observersForKey)
  27. }
  28. observersForKey.add(observer)
  29. }
  30. /* 这个陷阱拦截 set 操作,它把每个关联当前 set 属性的监听函数加入队列以备之后执行 */
  31. function set (target, key, value, receiver) {
  32. const observersForKey = observers.get(target).get(key)
  33. if (observersForKey) {
  34. observersForKey.forEach(queueObserver)
  35. }
  36. return Reflect.set(target, key, value, receiver)
  37. }

如果没有设置 currentObserver , get 陷阱不做任何事。否则,它配对获取的可监听属性和目前运行的监听函数,然后把它们保存入监听者 WeakMap。监听者会被存入每个被监听对象属性的 Set 之中。这样可以保证没有重复的监听函数。

set 陷阱函数获得所有改变了值的被监听者属性配对的监听函数,并且把他们插入队列以备之后执行。

在下面,你可以找到一个图像和逐步的描述来解释 nx-observe 的示例代码。

实现一个简单的被监听对象 - 图1

  • 创建被监听对象 person
  • 设置 currentObserverprint
  • print 开始执行
  • print 中获得 person.name
  • person 中的 代理 get 陷阱函数被调用
  • observers.get(person).get('name') 获得属于 (person, name) 对的监听函数集合
  • currentObserver(print) 被加入监听集合中
  • person.age 再次执行步骤 4-7
  • 控制台输出 ${person.name}, ${person.age}
  • print 结束运行
  • currentObserver 被设置为 undefined
  • 其它代码开始执行
  • person.age 被赋值为 22
  • person 中的 set 代理 陷阱被调用
  • observers.get(person).get('age') 获得 (person, age) 对中的监听集合
  • 监听集合中的监听函数(包括 print )被插入队列以运行
  • print 再次运行

运行监听函数

在一个批处理中,异步执行队列中的监听函数,会带来很好的性能。在注册阶段,监听函数被同步加入 queuedObservers Set。一个 Set 不会有有重复的监听函数,所以多次加入同一个 observer 也不会导致重复执行。如果之前 Set 是空的,那么会加入一个新任务在一段时间后迭代并执行所有排队的 observer。

  1. /* contains the triggered observer functions,
  2. which should run soon */
  3. const queuedObservers = new Set()
  4. /* points to the currently running observer,
  5. it can be undefined */
  6. let currentObserver
  7. /* the exposed observe function */
  8. function observe (fn) {
  9. queueObserver(fn)
  10. }
  11. /* adds the observer to the queue and
  12. ensures that the queue will be executed soon */
  13. function queueObserver (observer) {
  14. if (queuedObservers.size === 0) {
  15. Promise.resolve().then(runObservers)
  16. }
  17. queuedObservers.add(observer)
  18. }
  19. /* runs the queued observers,
  20. currentObserver is set to undefined in the end */
  21. function runObservers () {
  22. try {
  23. queuedObservers.forEach(runObserver)
  24. } finally {
  25. currentObserver = undefined
  26. queuedObservers.clear()
  27. }
  28. }
  29. /* sets the global currentObserver to observer,
  30. then executes it */
  31. function runObserver (observer) {
  32. currentObserver = observer
  33. observer()
  34. }

以上代码确保无论何时运行一个监听函数,全局的 currentObserver 就指向它。设置 currentObserver 切换 get 陷阱函数,以便监听和配对 currentObserver 和其运行时使用的所有的可监听的属性。