虽然 JavaScript 没有多线程变量共享的问题,但是在一些场景中,我们还是希望能对某些对象进行适当的保护(锁定),防止发生一些不可预期的错误。
本文主要从如下两个实际场景展开:
- 任务执行器;
- 事件基类。
任务执行器
现在,我们需要一个 DOM 操作的任务执行器,这个任务执行器满足的主要功能有:
- 能够添加任务;
- 能够批量执行任务;
- 能够随时启动和停止任务的执行。
为啥需要这么个东西呢?假设其中有下面三步 DOM 操作:
- 设置节点 a 的文本:
a.innerText = 'text1'
; - 设置节点 a 的文本:
a.innerText = 'text2'
; - 设置节点 a 的文本:
a.innerText = 'text3'
。
如果老老实实设置三次,感觉太不划算了!实际上只需要设置最后一次就好了,这样就可以减少两次无谓的 DOM 操作了。
初步看起来,我们的任务执行器代码大致会像这个样子:
|
|
好了,看起来似乎可以了,那就到实际环境遛遛吧!
不遛不知道,一遛吓一跳,跳出来一些莫名其妙的问题:
- 代码的第90行报
this[TASKS]
不存在; - 总是会有任务的回调函数没有被调用。
仔细分析一下代码,可以发现:
- 对于第一个问题,在执行传入
requestAnimationFrame
的回调函数的时候,某个 taskFn 或者 notifyFn 可能会调用destroy()
方法,从而将this[TASKS]
设为了 false ,然后再执行到90行,就报错了。 - 对于第二个问题,假设有两个同类型的任务,在 ‘EXECUTE’ 中调用第一个任务的
notifyFn
的时候,添加进第二个任务(调用了 add() 方法),然后执行到90行,将该类型任务置为 null ,这样一来,第二个任务的回调函数就没有机会执行了。
所以,问题的根源就在任务执行过程中调用了不可控的外部函数,从而导致 this[TASKS]
发生变化。
对于第一个类型的问题,可以简单地使用 IS_RUNNING 状态绕开。对于第二种类型的问题,就最好找一种更优雅通用的解决方案了。
我们注意到,传入 requestAnimationFrame
的回调函数体(行范围:[72-90])是一个敏感地带,执行这块代码的时候,应该将 this[TASKS]
锁定,防止不可控的外部函数( taskFn 和 notifyFn )对其进行干扰。
其实简单说起来,这类问题就是 for in
循环中,被遍历的对象应该是可读的
的一个变体,所以,可以抽离出来一个比较通用的类,具体实现代码请移步到这里。
现在,我们的DOM 操作任务执行器
看起来就像这样了。
目前看来,这个 DomUpdater
还有些小地方需要优化:
- TASKS 任务遍历顺序不应该依赖于对象上键的遍历顺序。
- TASKS 对象的键并没有销毁,所以每次任务执行的时候,遍历次数都会只增不减。
事件基类
在搭建前端框架的时候,一般都会期望各个功能模块能够解耦合。通常情况下,会使用事件来达到这个效果。
第一次写这个类的话,很有可能就写成了这样:
|
|
在 trigger()
循环事件处理器的时候,事件回调函数很可能会通过 on()
间接修改 this[EVENTS]
,因此,我们需要使用 ProtectObject 来对 this[EVENTS]
进行锁定。
总结
本质上,这类问题就是传入的函数中做了不希望做的事情,所以如何禁止或者兼容这些不希望做的事情
是关键点。
本文为作者在实践中总结出来的方案,能力有限,期待读者提出更好的方案。