• 0

  • 488

从实际开发中来看JavaScript事件循环的使用场景

云哥

关注云计算

3星期前

前言: 本文是介绍结合DOM事件流和JavaScript事件循环解决一个工作中的实际问题的过程,很多东西不只是面试的时候才会用得到

文中涉及到的代码demo地址:drag-and-eventloop

背景

近期在开发某需求的时候,遇到了一个需要同时有拖拽和点击的场景,首席CV(复制粘贴)工程师如我🐶,当然是使用react-draggable开源组件了,大概的代码结构(已脱敏)如下

import Draggable from 'react-draggable';
import React from 'react';

const Demo = (props) => {
    return (
    <Draggable>
        <div onClick={() => { console.log('被点击了!') }}>
            拖拽区域
        </div>
    </Draggable>
  )
}  
复制代码

但是,这样存在一个问题在拖拽完毕之后,拖拽区会响应一次点击事件,这是不合理也是不符合预期的。

梳理了一下react-draggable原理和业务代码的流程,整个过程可以总结为下图👇

因为react-draggable是使用mousedown和mouseup来实现拖拽的,而且并没有暴露出方法或者参数来处理mouseup之后同时会触发click事件的问题,所以只能我们自己处理这个问题了。

解法

标志位

我的方法是加一个canClick标志位,用来标识当前是否可以点击,在拖动时设置它为false,拖动结束时设置它为true

import Draggable from 'react-draggable';
import React from 'react';

const Demo = (props) => {
  let canClick = true;
  const _handleDraging = (e) => {
      canClick = false;
  }
  const _handleDragStop = (e) => {
      canClick = true;
  }
    return (
    <Draggable onDrag={_handleDraging} onStop={_handleDragStop}>
        <div onClick={() => { if(canClick) { console.log('被点击了!') } }}>
            拖拽区域
        </div>
    </Draggable>
  )
}  
复制代码

但是这样子依然不行,仔细思考之后发现这里会涉及到一些事件循环的原理,JavaScript是单线程语言,要实现非阻塞的去处理一些异步任务,就会把相关的异步回调放入到事件队列中,例如DOM事件回调这样的方法会被放入到宏事件(macroTask)队列中去,所以当前宏事件(macroTask)队列应该是这样的:

如上图,宏事件(macroTask)队列从左往右执行,在_handleDragStop方法中,已经把标志位canClick置为了true,所以在拖拽结束时,仍然是可以响应click事件的。

结合事件循环宏事件(macroTask)队列的执行,我们可以对代码做一些小修改,即可以达到目的:

import Draggable from 'react-draggable';
import React from 'react';

const Demo = (props) => {
  let canClick = true;
  const _handleDraging = (e) => {
      canClick = false;
  }
  const _handleDragStop = (e) => {
        setTimeout(() => {
            canClick = true;
        }, 0);
  }
    return (
    <Draggable onDrag={_handleDraging} onStop={_handleDragStop}>
        <div onClick={() => { if(canClick) { console.log('被点击了!') } }}>
            拖拽区域
        </div>
    </Draggable>
  )
}  
复制代码

此时,宏事件(macroTask)队列就变成了下图这样子👇

把设置canClick=true的语句放到setTimeout中,这样就把执行顺序放到了onClick事件回调的后面,标志位就开始生效了!

事件循环(eventLoop)

什么是事件循环?貌似这个概念只有在面试的时候会被问到,然后在面试完一周之内就会忘记,别笑,因为我之前面试也问过别人这个概念😅

事实上,理解事件循环的工作方式对于代码优化很重要,有时对于正确的架构也很重要。

概念

简单的说,事件循环(eventLoop)是单线程的JavaScript在处理异步事件时进行的一种循环过程,具体来讲,对于异步事件它会先加入到事件队列中挂起,等主线程空闲时会去执行事件队列中的事件。

宏事件(macroTask)队列和微事件(microTask)队列

因为异步事件种类的不同、执行优先级不同,不同的异步事件被分为宏任务和微任务。

常见的宏事件:

  • setTimeout

  • setInterval

  • UI 事件

  • 网络请求

  • ....

常见的微事件

  • Promise.resolve、Promise.reject

  • Mutation Observer

  • queueMicrotask(func)

宏任务和微任务的执行顺序是这样的:当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行

事件循环的其他应用

拆分CPU过载任务

demo:intensiveCalculate.html

对于一些CPU耗时过长的任务,我们可以使用事件循环来拆分避免浏览器长时间不响应页面事件(比如点击操作)

假想有一个很长的计算任务,耗时超过5秒以上,那么在这5秒以内,界面会被阻塞住,用户在界面上的所有的操作都无法响应,如果我们并不是要计算完成才可以响应页面(比如一个超长的文档做高亮),那么我们可以使用宏事件拆分计算任务来做优化。

例如这样子一个超长的计算任务,大概耗时8秒以上

let i = 0;
let start = Date.now();

function count() {
    // 做一个繁重的任务
    for (let j = 0; j < 1e9; j++) {
        i++;
    }

    alert("Done in " + (Date.now() - start) + 'ms');
}

const btn = document.getElementById('start');
btn.onclick = count;
复制代码

如果这些计算并非是响应页面前必须的,那么可以这样子优化:

let i = 0;
let start = Date.now();

function count() {

    setTimeout(() => {
        count();
    }, 0);

    // 做一个繁重的任务
    for (let j = 0; j < 1e6; j++) {
        i++;
    }

    if (i === 1e9) {
        alert("Done in " + (Date.now() - start) + 'ms');
    }
}

const btn = document.getElementById('start');
btn.onclick = count;
复制代码

代码把一个连续的计算任务分切成了1000个小任务,并且不会阻塞主线程的工作,完美解决。

总结

很多东西不只是面别人或者被别人面的时候才有用,牛逼的工程师善于解决问题。

免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

云计算

488

相关文章推荐

未登录头像

暂无评论