给本身提了个bug:setInterval卡顿问题

背景:项目提测后,本身无聊,检查bug(测试没测出来,我自个儿测出来了~),在webview里面,有个倒计时,而后我触摸滑动,一直上下滑动,倒计时回中止,松开后,倒计时又继续了,以下图:react

演示: webpack

演示


分析一下

js是单线程语言,虽然Web Worker容许 JavaScript 脚本建立多个线程,可是子线程彻底受主线程控制,且不能操做 DOM,因此,这个新标准并无改变 JavaScript 单线程的本质git

浏览器是多线程的,事件触发线程、定时触发器线程、异步 HTTP 请求线程github

呈现引擎:又称渲染引擎,也被称为浏览器内核,在线程方面又称为 UI线程(Trident、Gecko、Blink、Webkit、Presto)web

JavaScript 解释器:又称为 JavaScript 解析引擎,又称为 JavaScript 引擎,也能够称为 JavaScript 内核,在线程方面又称为 JavaScript 引擎线程(V八、Chakra、TraceMonkey)浏览器

UI线程JS引擎线程 互斥网络

移动端开发,一些用到UI线程的方法(好比:经过动画、setInterval、setTimeout等频繁操做dom),在引擎线程被占用时,会发生卡顿的现象。多线程

解决方案 --- Web Workers

Web Workers(MDN知乎)能够在独立于主线程的后台线程中,运行一个脚本操做。也就是能够在独立线程中执行耗时的任务,从而容许主线程(一般是UI线程)不会所以被阻塞/放慢。dom

Workers API,这里我就再也不赘述,请看 MDN知乎知乎异步

写 Worker 脚本

根据API文档可知,Worker(aURL)构造函数,它只执行URL指定的脚本,不指定URL时,而由使用Blob建立,也就是说这个 aURL 除了能够是url(同源),还能够是Blob,惋惜的是不支持es模块化,当下的项目大都是用webpack进行打包,考虑到这点,有三种方式来加载这个脚本:

  1. 单独维护这个脚本,放到cdn上
  2. 使用相对路径,把脚本放到静态资源assets中,打包时copy到输出目录dist,此时要考虑部署的问题,部署在非根目录时,为保证在相对目录仍能找到这个脚本,要在这个相对路径中要包含对应环境的public
  3. 借助Blob用内联脚本经过blob URL对象建立,进行模块化,使worker初始化更快,由于消除了网络来回的延迟

我比较喜欢第三种,下面来看看具体实现

/** 建立 blob URL,供worker使用 ./worker/countdown.ts */

const workerScript = ` self.onmessage = function(event) { var num = event.data; var T = setInterval(function() { self.postMessage(--num); if (num <= 0) { clearInterval(T); self.close(); console.log('clearInterval & worker closed'); } }, 1000); }; `;
const workerScriptBlob = new Blob([workerScript]);
const workerScriptBlobUrl = URL.createObjectURL(workerScriptBlob);

export default workerScriptBlobUrl;


/** 具体使用 index.tsx */
import * as React from "react";
import CountdownBolb from "./worker/countdown";

const transfDate = (second: number): (number | string)[] => {
  if (second < 0) {
    return ["--", "--", "--", "--"];
  }
  const DD = second / (24 * 60 * 60);
  const HH = (second % (24 * 60 * 60)) / (60 * 60);
  const mm = ((second % (24 * 60 * 60)) % (60 * 60)) / 60;
  const ss = ((second % (24 * 60 * 60)) % (60 * 60)) % 60;

  return [DD, HH, mm, ss].map(item => {
    item = Math.floor(item);
    if (item < 10) {
      return `0${item}`;
    }
    return item;
  });
};
interface IState {
  remain_second: number;
}
export default class Index extends React.PureComponent<{}, IState> {
  state = {
    remain_second: 30
  };
  _worker = new Worker(CountdownBolb);
  runTime = (): void => {
    // 使用web worker 解决直接使用setInterval的触摸滑动时渲染卡顿问题
    const { remain_second } = this.state;
    this._worker.postMessage(remain_second);
    this._worker.onmessage = event => {
      const s = event.data;
      this.setState({ remain_second: s });
    };
  };
  componentDidMount() {
    try {
      this.runTime();
    } catch (e) {
      console.log(e);
    }
  }
  componentWillUnmount() {
    // 传入0,清除定时器并关闭worker
    this._worker.postMessage(0);
  }
  renderCountdown = () => {
    const { remain_second } = this.state;
    const [DD, HH, mm, ss] = transfDate(remain_second);
    return (
      <div> <span style={{ width: "10px" }} /> {DD}Day {HH}h {mm}m {ss}s </div> ); }; render() { return <div className="App">{this.renderCountdown()}</div>; } } 复制代码

在线预览 codesandbox

【参考】:

  1. developer.mozilla.org/zh-CN/docs/…
  2. developer.mozilla.org/zh-CN/docs/…
  3. developer.mozilla.org/zh-CN/docs/…
  4. zhuanlan.zhihu.com/p/25184390
  5. zhuanlan.zhihu.com/p/93470509