事件防抖引发的对事件循环机制的思考

303 阅读5分钟

文章背景:

  • 事件防抖引发的对事件循环机制的思考,部分异步放在闭包外部的执行顺序
  • 代码实战:React 事件处理函数中 onClick = 直接调用函数 "this.fn()",或在调用时使用 bind绑定this "this.fn.bind(this)"、或在调用的时候使用箭头函数绑定this "() => this.handleClick()"三者区别

先说结论

防抖中,部分异步放在闭包外面,会导致结果如下:


...

// 部分异步不防抖
    _this.props.dispatch({
        type: "test/debounce",
        param
    })
    .then(() => {
        _this.editData()
    })
    // 部分异步不防抖 end
    .then(() => {
        return function () {
            console.log('---第一次 timeout: ', timeout)  // 2 2 
            clearTimeout(timeout);  //每次触发input值改变时,首先清空上一次的setTimeout
            console.log('---第二次 timeout: ', timeout);  // 3 3
            timeout = setTimeout(() => { // 4 4
                    _this.updateAge()   // 重新获取页面数据
                console.log('---我是setTimeout, 防抖清除函数们', "---timeout的值: ", timeout)
            }, 1000) // 注意的是如果你写的代码用有用到this,要提前声明this是谁,因为在箭头函数中没有this指向
            console.log('---debounce中的闭包执行完毕', timeout)
        }()
    })
    ...
    
    
  • 先去执行完同步代码(即两次点击后),setTimeout最后才会去执行;
  • 由于clearTimeout时,setTimeout并未执行,timeout并没有定义上setTimeout,所以其实并未清除 this.updateAge
  • setTimeout 最终执行的次数即为点击的次数


作者:林轩
链接:juejin.cn/post/698919… 来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

写法一 —— 正确写法

第一步:render 中的定义click方法

  • 下方代码中方式一:不可行;     因为bind是改变this,返回的一个函数,会将 函数debounce 改变下this,直接原封不动的返回;

    而我们要的是 debounce()执行一下,需要让 onClick = debounce(),返回一个闭包,最终我们执行的是 debounce中的返回的函数

  • 下方代码中方式二:可行;

    debounce(),返回的是里面定义的闭包;

    render时,会执行 debounce(),不过没啥太大关系,我们主要执行的是 debounce()里返回的函数,并且 debounce函数定义前面没太多与逻辑相关的代码,不会出错的

// render
// 方式一
{/* <span className={styles.left} onClick={this.debounce.bind(this, 'subtract')}>-</span>  */}  

// 方式二
<span className={styles.left} onClick={this.debounce('subtract')}>-</span> 

第二步:定义debounce 事件

  • 方法一:把所有函数写到setTimeout里面,可行

    正确合理使用闭包,清除函数是要清除所有的函数,而不能在外边 —— 例写法二中的debounce 定义

debounce = (option) => {
    console.log('---进入debounce方法')
    let param={};
    //  处理一些逻辑
    if (option === 'str') {
        param = {a:1}
    } else {
        param = {a:1}
    }

    const _this = this;
    let timeout = null;
    return function () {
        console.log('---第一次 timeout: ', timeout)
        clearTimeout(timeout);  
        console.log('---第二次 timeout: ', timeout)
        timeout = setTimeout(() => { // 4 4
            **// 防抖的函数们** 
            _this.props.dispatch({
                type: "test/debounce",
                param
            })
            .then(() => {
                _this.editData()
            })
            .then(() => {
                _this.updateAge()
            })
            **// 防抖的函数们 end** 

            console.log('---我是setTimeout, 防抖清除函数们', "---timeout的值: ", timeout)
        }, 500) 
        console.log('---debounce中的闭包执行完毕', timeout)
    }
}
  • 下方为快速点击两次按钮时,执行顺序如下:

setTimeout中把所有函数都清除了,只会执行最后一次请求的函数们,所以只会打印一次 '~~~我是请求'

setTimeout的变量值为数字,用来唯一标识setTimeout

image.png

写法二 —— 错误写法(部分异步写到闭包外面,只想清除部分函数抖动)

第一步:render定义

  • 由于写法二的错误写法,是把部分异步写到闭包外面,只想清除部分函数抖动,

  • render时,debounce() 中的异步们就会执行,可能某些数据还会更新好,render时就执行异步可能会导致报错

  • bind(),表示改变this,返回一个函数体,而不是执行函数;就不会导致render时,debounce执行了,而是onClick的时候去执行 debounce

  • 只能如下写,两种写法,这两种写法区别详见:react.docschina.org/docs/handli…

<span className={styles.left} onClick={this.debounce.bind(this, 'subtract')}>-</span> 

第二步:定义 debounce

  • 把部分异步写到闭包外面,不防抖;只想清除部分函数抖动,

  • 执行结果看下图,原因见下方的 "结论"

// 加减年龄
debounce = (option) => {
    console.log('---进入debounce方法')
    let param={};
    //  处理一些逻辑
    if (option === 'str') {
        param = {a:1}
    } else {
        param = {a:1}
    }

    const _this = this;
    let timeout = null;

    // 部分异步不防抖
    _this.props.dispatch({
        type: "test/debounce",
        param
    })
    .then(() => {
        _this.editData()
    })
    // 部分异步不防抖 end
    .then(() => {
        return function () {
            console.log('---第一次 timeout: ', timeout)  // 2 2 
            clearTimeout(timeout);
            console.log('---第二次 timeout: ', timeout);  // 3 3
            timeout = setTimeout(() => { // 4 4
                _this.updateAge()
                console.log('---我是setTimeout, 防抖清除函数们', "---timeout的值: ", timeout)
            }, 1000)
            console.log('---debounce中的闭包执行完毕', timeout)
        }()
    })

}

image.png

错误写法执行结果的原因

参考来源:segmentfault.com/a/119000002…

  • 因为事件循环机制是先 主任务 —> 微任务 —> 宏任务,setTimeout 为宏任务,将 需要在下一个时间周期执行

  • 事件循环机制流程图如下:

image.png

  • 详解如下:

1 主线程每次执行时,先看看要执行的是同步任务,还是异步的API 2 同步任务就继续执行,一直执行完 3 遇到异步API就将它交给对应的异步线程,自己继续执行同步任务 4 异步线程执行异步API,执行完后,将异步回调事件放入事件队列上 5 主线程手上的同步任务干完后就来事件队列看看有没有任务 6 主线程发现事件队列有任务,就取出里面的任务执行 7 主线程不断循环上述流程

定时器不准

Event Loop的这个流程里面其实还是隐藏了一些坑的,最典型的问题就是总是先执行同步任务,然后再执行事件队列里面的回调。这个特性就直接影响了定时器的执行,我们想想我们开始那个2秒定时器的执行流程:

1 主线程执行同步代码 2 遇到setTimeout,将它交给定时器线程 3 定时器线程开始计时,2秒到了通知事件触发线程 4 事件触发线程将定时器回调放入事件队列,异步流程到此结束 5 主线程如果有空,将定时器回调拿出来执行,如果没空这个回调就一直放在队列里。

结论

上述错误写法的执行结果,结论如下:

  • 先去执行完同步代码(即两次点击后),setTimeout最后才会去执行;

  • 由于clearTimeout时,setTimeout并未执行,timeout并没有定义上setTimeout,所以其实并未清除 this.updateAge

  • setTimeout 最终执行的次数即为点击的次数

举个例子:原理同上

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title></title>
</head>

<body>
    <div></div>
    <input id="inp" />
    <script>
        inp.addEventListener('input', () => {
            console.log('script start');
            setTimeout(function () {
                console.log('---setTimeout')
            }, 5000);

            Promise.resolve().then(function () {
                console.log('promise1');
            }).then(function () {
                console.log('promise2');
            })
        });
    </script>
</body>

</html>

打印如下:

image.png