找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

626

积分

1

好友

79

主题
发表于 13 小时前 | 查看: 2| 回复: 0

图片

在为耗时的代码选择执行策略时,通常有以下几种方案。

1.1 立即执行 (Eager Evaluation)

立即执行有一个明显的缺点:如果用户在代码执行期间尝试与页面交互,浏览器必须等待代码执行完成后才能响应。此时如果页面看起来已经准备好,却无法响应用户输入,将严重影响用户体验。同时,立即执行的代码越多,页面可交互所需的时间就越长。

class MyComponent {
  constructor() {
    addEventListener('click', () => this.handleUserClick());
    this.formatter = new Intl.DateTimeFormat('en-US', {
      timeZone: 'America/Los_Angeles',
    }); // 构造函数中立即执行
  }
  handleUserClick() {
    console.log(this.formatter.format(new Date()));
  }
}

1.2 惰性执行 (Lazy Evaluation)

惰性执行意味着等到真正需要时才执行代码。但该方案的问题在于,它仍可能阻塞用户输入。对于推迟网络加载资源有意义,但对于访问 localStorage、处理大型数据集等大多数场景,开发者仍希望这些操作在用户交互前完成。

class MyComponent {
    constructor() {
       addEventListener('click', () => this.handleUserClick());
    }
    handleUserClick() {
        // 在用户点击时惰性初始化 formatter
        if (!this.formatter) {
            this.formatter = new Intl.DateTimeFormat('en-US', {
                timeZone: 'America/Los_Angeles',
            });
        }
        console.log(this.formatter.format(new Date()));
    }
}

1.3 其他选择

  • 延迟执行 (Deferred evaluation):使用 setTimeout 等方法调度代码在未来运行。
  • 空闲执行 (Idle evaluation):使用 requestIdleCallback 等 API 调度代码在浏览器空闲期运行。

以上两种策略通常比立即或惰性求值更好,因为它们不太可能产生阻塞用户输入的单一长任务。浏览器虽然无法中断单个任务,但可以在任务队列之间响应用户输入。换句话说,如果确保所有代码都在简短、独立的任务中运行(最好少于 50 毫秒),那么代码执行就永远不会阻塞用户输入。

需要注意的是,虽然浏览器可以在调度的任务之间运行用户输入回调,但无法在微任务之前运行输入回调。由于 Promise 和 async 函数作为微任务运行,因此将同步代码简单转换为基于 Promise 的代码同样会阻止用户输入。

结合延迟执行和空闲执行的代码示例如下:

const main = () => {
  setTimeout(() => drawer.init(), 0);
  setTimeout(() => contentLoader.init(), 0);
  setTimeout(() => breakpoints.init(), 0);
  setTimeout(() => alerts.init(), 0);
  requestIdleCallback(() => analytics.init());
};
main();

这段代码通过 setTimeout 推迟了 UI 组件的初始化,降低了阻塞用户输入的概率。但问题在于,用户交互时组件可能尚未初始化完成。同时,使用 requestIdleCallback() 延迟的埋点上报,如果在下一个空闲期之前发生交互,数据可能会丢失;如果用户在空闲期到来前离开页面,回调则根本不会执行。

2. 空闲直到紧急 (Idle-Until-Urgent) 的单任务策略

一个更优的策略是:代码默认被推迟到空闲时段执行,但一旦被需要就会立即运行,即 Idle-Until-Urgent。该策略在最坏情况下(急需时)与惰性执行的性能相同,而在最好情况下(空闲时已执行)则完全不会阻塞用户交互。

import {IdleValue} from './path/to/IdleValue.mjs';

class MyComponent {
  constructor() {
    addEventListener('click', () => this.handleUserClick());
    // 不会在构造函数中立即实例化 formatter,改用 IdleValue
    this.formatter = new IdleValue(() => {
      return new Intl.DateTimeFormat('en-US', {
        timeZone: 'America/Los_Angeles',
      });
    });
  }
  handleUserClick() {
    console.log(this.formatter.getValue().format(new Date()));
  }
}

以下是 IdleValue 类的核心实现:

export class IdleValue {
  constructor(init) {
    this._init = init;
    this._value;
    this._idleHandle = requestIdleCallback(() => {
      this._value = this._init();
    });
  }
  getValue() {
    if (this._value === undefined) {
      cancelIdleCallback(this._idleHandle);
      this._value = this._init();
    }
    return this._value;
  }
}

通过 IdleValue 类实现了两个目的:

  1. 如果在 requestIdleCallback 调度之前(即空闲期之前)调用 getValue,则立即执行初始化函数并返回,此时性能与惰性求值一致。
  2. 如果在 requestIdleCallback 调度之后(即空闲期已执行)调用 getValue,则直接返回已计算好的值,不会阻塞用户输入。

对于计算成本高的单个属性值,此策略非常适用。除了 Intl.DateTimeFormat,典型场景还包括:

  • 处理大型数据集集合。
  • localStorage(或 Cookie)获取值,这类数据库/中间件操作。
  • 运行 getComputedStyle()getBoundingClientRect() 或任何其他可能触发主线程样式重计算或布局的 API。

3. 空闲直到紧急 (Idle-Until-Urgent) 的多任务策略

上述技术适用于可由单个函数计算其值的场景。但对于更复杂的逻辑,或者希望将任务分解以避免阻塞主线程时,我们需要一个任务队列。该队列允许调度多个任务在浏览器空闲时运行,并能在需要让出主线程时(如用户交互)暂停执行。

以下是 IdleQueue 类的简化实现:

import {cIC, rIC} from './idle-callback-polyfills.mjs';
import {now} from './lib/now.mjs';
import {queueMicrotask} from './lib/queueMicrotask.mjs';

const DEFAULT_MIN_TASK_TIME = 0;
const isSafari_ = !!(typeof safari === 'object' && safari.pushNotification);

export class IdleQueue {
  constructor({
    ensureTasksRun = false,
    defaultMinTaskTime = DEFAULT_MIN_TASK_TIME,
  } = {}) {
    this.idleCallbackHandle_ = null;
    this.taskQueue_ = [];
    this.isProcessing_ = false;
    this.state_ = null;
    this.defaultMinTaskTime_ = defaultMinTaskTime;
    this.ensureTasksRun_ = ensureTasksRun;

    this.runTasksImmediately = this.runTasksImmediately.bind(this);
    this.runTasks_ = this.runTasks_.bind(this);
    this.onVisibilityChange_ = this.onVisibilityChange_.bind(this);

    if (this.ensureTasksRun_) {
      addEventListener('visibilitychange', this.onVisibilityChange_, true);
      if (isSafari_) {
        addEventListener('beforeunload', this.runTasksImmediately, true);
      }
    }
  }

  pushTask(...args) {
    this.addTask_(Array.prototype.push, ...args);
  }
  unshiftTask(...args) {
    this.addTask_(Array.prototype.unshift, ...args);
  }
  runTasksImmediately() {
    this.runTasks_();
  }
  hasPendingTasks() {
    return this.taskQueue_.length > 0;
  }
  clearPendingTasks() {
    this.taskQueue_ = [];
    this.cancelScheduledRun_();
  }
  getState() {
    return this.state_;
  }
  destroy() {
    this.taskQueue_ = [];
    this.cancelScheduledRun_();
    if (this.ensureTasksRun_) {
      removeEventListener('visibilitychange', this.onVisibilityChange_, true);
      if (isSafari_) {
        removeEventListener(
            'beforeunload', this.runTasksImmediately, true);
      }
    }
  }
  addTask_(arrayMethod, task, {minTaskTime = this.defaultMinTaskTime_} = {}) {
    const state = {
      time: now(),
      visibilityState: document.visibilityState,
    };
    arrayMethod.call(this.taskQueue_, {state, task, minTaskTime});
    this.scheduleTasksToRun_();
  }
  scheduleTasksToRun_() {
    if (this.ensureTasksRun_ && document.visibilityState === 'hidden') {
      // 页面隐藏时,立即通过微任务调度执行
      queueMicrotask(this.runTasks_);
    } else {
      if (!this.idleCallbackHandle_) {
        this.idleCallbackHandle_ = rIC(this.runTasks_);
      }
    }
  }
  /**
   * 真正执行任务
   */
  runTasks_(deadline = undefined) {
    this.cancelScheduledRun_();
    if (!this.isProcessing_) {
      this.isProcessing_ = true;
      while (this.hasPendingTasks() &&
          !shouldYield(deadline, this.taskQueue_[0].minTaskTime)) {
        const {task, state} = this.taskQueue_.shift();
        // 拆分小任务并执行 Task 任务
        this.state_ = state;
        task(state);
        this.state_ = null;
      }
      this.isProcessing_ = false;
      if (this.hasPendingTasks()) {
        this.scheduleTasksToRun_();
      }
    }
  }
  cancelScheduledRun_() {
    cIC(this.idleCallbackHandle_);
    this.idleCallbackHandle_ = null;
  }
  // 页面隐藏是执行空闲任务的最佳时机
  onVisibilityChange_() {
    if (document.visibilityState === 'hidden') {
      this.runTasksImmediately();
    }
  }
}
const shouldYield = (deadline, minTaskTime) => {
  if (deadline && deadline.timeRemaining() <= minTaskTime) {
    return true;
  }
  return false;
};

使用起来非常方便:

const queue = new IdleQueue();
queue.pushTask(() => {
  // 空闲时要运行的任务
});
queue.pushTask(() => {
  // 依赖于上面任务执行的任务
});

IdleQueue 也提供了在需要时立即执行所有任务的方法(runTasksImmediately)。这一点很重要:不仅因为有时需要尽快计算,还因为代码经常需要与同步的第三方 API 集成,同步执行能力保证了兼容性。

在理想情况下,所有 JavaScript API 都是非阻塞、异步的。但现实中,由于遗留代码或第三方库的限制,我们常常别无选择。Idle-Until-Urgent 模式的一大优势在于,它可以轻松应用于大多数现有程序,而无需大规模重写架构。

4. Idle-Until-Urgent 的典型用例

4.1 持久化应用程序状态

例如,使用 Redux 的应用程序需要将状态持久化到 localStorage 中。常见的防抖方案并不完美,因为无法保证执行写入时不会阻塞主线程。

let debounceTimeout;
store.subscribe(() => {
  clearTimeout(debounceTimeout);
  debounceTimeout = setTimeout(() => {
    const jsonData = JSON.stringify(store.getState());
    localStorage.setItem('redux-data', jsonData);
  }, 1000);
});

更好的做法是使用 IdleQueue 来调度写入操作,并利用 ensureTasksRun 配置确保即使用户离开页面,数据也能被保存。

const queue = new IdleQueue({ensureTasksRun: true});
store.subscribe(() => {
  queue.clearPendingTasks(); // 清除旧的待保存任务
  queue.pushTask(() => {
    const jsonData = JSON.stringify(store.getState());
    localStorage.setItem('redux-data', jsonData);
  });
});
4.2 Analytics 埋点数据发送

发送埋点数据是另一个非常适合使用 Idle-Until-Urgent 策略的用例。下面的示例确保即使用户在下一个空闲期之前关闭页面,分析数据也会被发送。

const queue = new IdleQueue({ensureTasksRun: true});
const signupBtn = document.getElementById('signup');
signupBtn.addEventListener('click', () => {
  queue.pushTask(() => {
    ga('send', 'event', {
      eventCategory: 'Signup Button',
      eventAction: 'click',
    });
  });
});

将任务添加到 IdleQueue 不仅能确保“紧急”执行,还能保证埋点发送不会阻塞响应用户点击的其他关键JavaScript代码。事实上,将分析库(如 analytics.js)的所有初始化命令和上报命令都通过 IdleQueue 来调度是一个最佳实践。

const queue = new IdleQueue({ensureTasksRun: true});
queue.pushTask(() => ga('create', 'UA-XXXXX-Y', 'auto'));
queue.pushTask(() => ga('send', 'pageview'));
// 后续所有 ga() 调用都可以通过 queue.pushTask 添加

参考资料




上一篇:defendnot工具详解:通过注册虚拟杀毒软件禁用Windows Defender实战
下一篇:GB28181协议项目开发难点解析:厂商实现差异与兼容性挑战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-10 20:00 , Processed in 0.148616 second(s), 52 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表