实现一个简单的被监听对象
本小节,我将会阐述 nx-observe 的底层实现。首先,我将向您展示如何检测到可观察到的属性的变化并与观察者配对。然后我将会阐述怎么运行这些由改变所触发的监听函数方法。
注册变化
变化是通过把被监听对象封装到 ES6 代理来注册的。这些代理使用 Reflection API 无缝地拦截 get 和 set 操作。
以下代码使用 currentObserver
变量和 queueObserver()
,但是只会在下一小节中进行解释。现在只需要知道的是 currentObserver
总是指向目前运行的监听函数,而 queueObserver()
把将要执行的监听函数插入队列。
/* 映射被监听对象属性到监听函数集,监听函数集会使用监听对象属性 */
const observers = new WeakMap()
/* 指向当前运行的监听函数可以为 undefined */
let currentObserver
/* 利用把对象封装为一个代理来把对象转换为一个可监听对象,
它也可以添加一个空白映射,用作以后保存被监听对象-监听函数对。
*/
function observable (obj) {
observers.set(obj, new Map())
return new Proxy(obj, {get, set})
}
/* 这个陷阱拦截 get 操作,如果当前没有执行监听函数它不做任何事 */
function get (target, key, receiver) {
const result = Reflect.get(target, key, receiver)
if (currentObserver) {
registerObserver(target, key, currentObserver)
}
return result
}
/* 如果一个监听函数正在运行,这个函数会配对监听函数和当前取得的被
监听对象属性,并保存到一个监听函数映射之中 */
function registerObserver (target, key, observer) {
let observersForKey = observers.get(target).get(key)
if (!observersForKey) {
observersForKey = new Set()
observers.get(target).set(key, observersForKey)
}
observersForKey.add(observer)
}
/* 这个陷阱拦截 set 操作,它把每个关联当前 set 属性的监听函数加入队列以备之后执行 */
function set (target, key, value, receiver) {
const observersForKey = observers.get(target).get(key)
if (observersForKey) {
observersForKey.forEach(queueObserver)
}
return Reflect.set(target, key, value, receiver)
}
如果没有设置 currentObserver
, get
陷阱不做任何事。否则,它配对获取的可监听属性和目前运行的监听函数,然后把它们保存入监听者 WeakMap。监听者会被存入每个被监听对象属性的 Set
之中。这样可以保证没有重复的监听函数。
set
陷阱函数获得所有改变了值的被监听者属性配对的监听函数,并且把他们插入队列以备之后执行。
在下面,你可以找到一个图像和逐步的描述来解释 nx-observe 的示例代码。
- 创建被监听对象
person
- 设置
currentObserver
为print
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
被赋值为 22person
中的set
代理 陷阱被调用observers.get(person).get('age')
获得(person, age)
对中的监听集合- 监听集合中的监听函数(包括
print
)被插入队列以运行 print
再次运行
运行监听函数
在一个批处理中,异步执行队列中的监听函数,会带来很好的性能。在注册阶段,监听函数被同步加入 queuedObservers
Set
。一个 Set
不会有有重复的监听函数,所以多次加入同一个 observer 也不会导致重复执行。如果之前 Set
是空的,那么会加入一个新任务在一段时间后迭代并执行所有排队的 observer。
/* contains the triggered observer functions,
which should run soon */
const queuedObservers = new Set()
/* points to the currently running observer,
it can be undefined */
let currentObserver
/* the exposed observe function */
function observe (fn) {
queueObserver(fn)
}
/* adds the observer to the queue and
ensures that the queue will be executed soon */
function queueObserver (observer) {
if (queuedObservers.size === 0) {
Promise.resolve().then(runObservers)
}
queuedObservers.add(observer)
}
/* runs the queued observers,
currentObserver is set to undefined in the end */
function runObservers () {
try {
queuedObservers.forEach(runObserver)
} finally {
currentObserver = undefined
queuedObservers.clear()
}
}
/* sets the global currentObserver to observer,
then executes it */
function runObserver (observer) {
currentObserver = observer
observer()
}
以上代码确保无论何时运行一个监听函数,全局的 currentObserver
就指向它。设置 currentObserver
切换 get
陷阱函数,以便监听和配对 currentObserver
和其运行时使用的所有的可监听的属性。