转载

受保护的对象

原文链接

虽然 JavaScript 没有多线程变量共享的问题,但是在一些场景中,我们还是希望能对某些对象进行适当的保护(锁定),防止发生一些不可预期的错误。

本文主要从如下两个实际场景展开:

  • 任务执行器;
  • 事件基类。

任务执行器

现在,我们需要一个 DOM 操作的任务执行器,这个任务执行器满足的主要功能有:

  • 能够添加任务;
  • 能够批量执行任务;
  • 能够随时启动和停止任务的执行。

为啥需要这么个东西呢?假设其中有下面三步 DOM 操作:

  • 设置节点 a 的文本: a.innerText = 'text1'
  • 设置节点 a 的文本: a.innerText = 'text2'
  • 设置节点 a 的文本: a.innerText = 'text3'

如果老老实实设置三次,感觉太不划算了!实际上只需要设置最后一次就好了,这样就可以减少两次无谓的 DOM 操作了。

初步看起来,我们的任务执行器代码大致会像这个样子:

const TASKS = Symbol('tasks'); const COUNTER = Symbol('counter'); const EXECUTE = Symbol('execute'); const IS_RUNNING = Symbol('isRunning');  export default class DomUpdater {     constructor() {         this[TASKS] = {};         this[COUNTER] = 0;     }      /**      * 获取任务ID,每一种类型操作对应一个任务ID,      * 比如对某个节点的innerText就可以算是一种类型的操作,具有唯一的任务ID      *      * @public      * @return {string} 任务ID      */     getTaskId() {         return '' + ++this[COUNTER];     }      /**      * 添加任务函数      *      * @public      * @param {Function} taskFn   任务函数      * @param {Function} notifyFn 任务执行完成之后的回调函数      */     add(taskId, taskFn, notifyFn) {         const task = this[TASKS][taskId] || {};          task.taskFn = taskFn;          // 为啥notifyFns会是一个数组,而taskFn不是数组呢?         // 因为我们期望后续同类型的(taskId相同)的任务能够覆盖掉之前的任务,         // 而之前任务的回调函数需要保留,这样就可以保证一定会通知外界某个任务已经执行完成了。         task.notifyFns = task.notifyFns || [];         task.notifyFns.push(notifyFn);          this[TASKS][taskId] = task;          this[EXECUTE]();     }      /**      * 启动任务执行      *      * @public      */     start() {         this[IS_RUNNING] = true;         this[EXECUTE]();     }      stop() {         this[IS_RUNNING] = false;     }      /**      * 执行任务      *      * @private      */     [EXECUTE]() {         if (!this[IS_RUNNING]) {             return;         }          window.requestAnimationFrame(() => {             for (let taskId in this[TASKS]) {                 const task = this[TASKS][taskId];                 if (!task) {                     continue;                 }                  let result;                 let error;                 try {                     result = task.taskFn();                 }                 catch (err) {                     error = err;                 }                  for (let i = 0, il = task.notifyFns.length; i < il; ++i) {                     task.notifyFns[i](error, result);                 }                  this[TASKS][taskId] = null;             }         });     }      destroy() {         this[TASKS] = null;     } } 

好了,看起来似乎可以了,那就到实际环境遛遛吧!

不遛不知道,一遛吓一跳,跳出来一些莫名其妙的问题:

  • 代码的第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 对象的键并没有销毁,所以每次任务执行的时候,遍历次数都会只增不减。

事件基类

在搭建前端框架的时候,一般都会期望各个功能模块能够解耦合。通常情况下,会使用事件来达到这个效果。

第一次写这个类的话,很有可能就写成了这样:

import {isFunction} from './util';  const EVENTS = Symbol('events'); const STATE = Symbol('state'); const STATE_READY = Symbol('stateReady'); const STATE_DESTROIED = Symbol('stateDestroied'); const CHECK_READY = Symbol('checkReady');  export default class Event {     constructor() {         this[EVENTS] = {};         this[STATE] = STATE_READY;     }      /**      * 在调用on、trigger、safeTrigger、asyncTrigger、off的时候,要检查一下当前event对象的状态。      *      * @private      */     [CHECK_READY]() {         if (this[STATE] !== STATE_READY) {             throw new Error('wrong event state: the event object is not ready.');         }     }      /**      * 绑定事件      *      * @public      * @param  {string}   eventName 事件名字      * @param  {Function} fn        回调函数      * @param  {Object=}   context   上下文对象      */     on(eventName, fn, context) {         this[CHECK_READY]();          if (!isFunction(fn)) {             return;         }          let events = this[EVENTS];         events[eventName] = events[eventName] || [];         events[eventName].push({fn, context});     }      /**      * 同步触发事件      *      * @public      * @param  {string}    eventName 事件名字      * @param  {...[*]} args      要传给事件回调函数的参数列表      */     trigger(eventName, ...args) {         this[CHECK_READY]();          let fnObjs = this[EVENTS][eventName];         for (let fnObj of fnObjs) {             fnObj.context::fnObj.fn(...args);         }     }      /**      * 移除事件回调      *      * @public      * @param  {...[*]} args eventName,fn,context      * @param  {string=} args.0 参数名字      * @param  {function=} args.1 回调函数      * @param  {Object=} args.2 上下文对象      */     off(...args) {         this[CHECK_READY]();          let [eventName, fn, context] = args;         if (args.length === 0) {             this[EVENTS] = {};         }          let iterator = checkFn => {             let fnObjs = this[EVENTS][eventName];             let newFnObjs = [];             for (let fnObj of fnObjs) {                 if (checkFn(fnObj)) {                     newFnObjs.push(fnObj);                 }             }             this[EVENTS][eventName] = newFnObjs;         };          if (args.length === 1) {             this[EVENTS][eventName] = null;         }         else if (args.length === 2) {             iterator(fnObj => fn !== fnObj.fn);         }         else if (args.length === 3) {             iterator(fnObj => fn !== fnObj.fn || context !== fnObj.context);         }     }      destroy() {         this[EVENTS] = null;         this[STATE] = STATE_DESTROIED;     } } 

trigger() 循环事件处理器的时候,事件回调函数很可能会通过 on() 间接修改 this[EVENTS] ,因此,我们需要使用 ProtectObject 来对 this[EVENTS] 进行锁定。

总结

本质上,这类问题就是传入的函数中做了不希望做的事情,所以如何禁止或者兼容这些 不希望做的事情 是关键点。

本文为作者在实践中总结出来的方案,能力有限,期待读者提出更好的方案。

原文  http://div.io/topic/1762
正文到此结束
Loading...