JavaScript下的setTimeout(fn,0)意味着什么?

本文探讨了JavaScript作为单线程引擎的特点,通过具体示例分析了setTimeout的作用机制及其在任务调度中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

作者:linkFly
原文:http://www.cnblogs.com/silin6/p/4333999.html
出处:www.cnblogs.com/silin6/


本文内容如下:

起因
单线程的JavaScript
setTimeout背后意味着什么
参考和引用


起因

上午在SegmentFault上看到了这个问题《关于SetTimeout 时间设为0时》,原提问者注明了问题来源:《JS setTimeout延迟时间为0的详解》。这个问题来源也是转载的,我后来找到了出处
在问题来源的那篇的文章中(后者),讲述了JS是单线程引擎:它把任务放到队列中,不会同步去执行,必须在完成一个任务后才开始另外一个任务。
而后,转载的那篇文章列出并补充了原文的栗子:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>setTimeout</title>
    <script type="text/javascript">
        function get(id) {
            return document.getElementById(id);
        }
        window.onload = function () {
            //第一个例子:未使用setTimeout
            get('makeinput').onmousedown = function () {
                var input = document.createElement('input');
                input.setAttribute('type', 'text');
                input.setAttribute('value', 'test1');
                get('inpwrapper').appendChild(input);
                input.focus();
                input.select();
            }
            //第二个例子:使用setTimeout
            get('makeinput2').onmousedown = function () {
                var input = document.createElement('input');
                input.setAttribute('type', 'text');
                input.setAttribute('value', 'test1');
                get('inpwrapper2').appendChild(input);
                //setTimeout
                setTimeout(function () {
                    input.focus();
                    input.select();
                }, 0);
            }
            //第三个例子,onkeypress输入的时候少了一个值
            get('input').onkeypress = function () {
                get('preview').innerHTML = this.value;
            }
        }
    </script>
</head>
<body>
    <h1><code>setTimeout</code></h1>
    <h2>1、未使用 <code>setTimeout</code></h2>
    <button id="makeinput">生成 input</button>
    <p id="inpwrapper"></p>


    <h2>2、使用 <code>setTimeout</code></h2>
    <button id="makeinput2">生成 input</button>
    <p id="inpwrapper2"></p>


    <h2>3、另一个例子</h2>
    <p>
        <input type="text" id="input" value="" /><span id="preview"></span>
    </p>
</body>
</html>

代码运行实例请戳这里

原文中有这么一段话,描述的有点抽象:
JavaScript引擎在执行onmousedown时,由于没有多线程的同步执行,不可能同时去处理刚创建元素的focus 和select方法,由于这两个方法都不在队列中,在完成onmousedown后,JavaScript 引擎已经丢弃了这两个任务,正如第一种情况。而在第二种情况中,由于setTimeout可以把任务从某个队列中跳脱成为新队列,因而能够得到期望的结果

我看到这里就觉得非常不对劲了。因为按照这种任务会被丢弃的说法,那么只要在事件触发的函数中再触发其他的事件都会被丢弃,浏览器是绝对不会这么做的,于是我编写了测试代码:

 window.onload = function () {
        //第一个例子:未使用setTimeout
        get('makeinput').onmousedown = function () {
            var input = document.createElement('input');
            input.setAttribute('type', 'text');
            input.setAttribute('value', 'test1');
            get('inpwrapper').appendChild(input);
            //按照文中的理论,这里的click不会被触发,但它却成功触发了
            get('inpwrapper').click();//触发了inpwrapper的onclick事件
        }
        get('inpwrapper').onclick = function () {
            alert('linkFly');
        };
    }

下面的onclick()最终是执行了:弹出了”linkFly”。

而在转载的文中为了引人深思,又提出了第三个例子:

在此,你可以看看例子 3,它的任务是实时更新输入的文本,现在请试试,你会发现预览区域总是落后一拍,比如你输 a, 预览区并没有出现 a, 在紧接输入b时,a才不慌不忙地出现。

而文中最后留给大家的思考的问题,解决方案就是使用setTimeout再次调整浏览器的代码任务运行队列。

    var domInput = get('input');
    domInput.onkeypress = function () {
        setTimeout(function () {
            //第三个例子的问题就这样就会被解决
            get('preview').innerHTML = domInput.value;
        })
    }

原文和转载的文章中都对setTimeout(fn,0)进行了思考,但原文指出的问题本质漏洞百出,所以才出了这篇文章,我们的正文,现在开始。


单线程的JavaScript

首先我们来看浏览器下的JavaScript:
浏览器的内核是多线程的,它们在内核制控下相互配合以保持同步,一个浏览器至少实现三个常驻线程:javascript引擎线程,GUI渲染线程,浏览器事件触发线程。

①javascript引擎是基于事件驱动单线程执行的,JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行JS程序。
②GUI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。但需要注意 GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
③事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeOut、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。(当线程中没有执行任何同步代码的前提下才会执行异步代码)

js的单线程在这一段面试代码中尤为明显(理解即可,请不要尝试…浏览器会假死的):

        var isEnd = true;
        window.setTimeout(function () {
            isEnd = false;//1s后,改变isEnd的值
        }, 1000);
        //这个while永远的占用了js线程,所以setTimeout里面的函数永远不会执行
        while (isEnd);
        //alert也永远不会弹出
        alert('end');

在我工作中对js的认识,个人认为js的任务单位是函数。即,一个函数表示着一个任务,这个函数没有执行结束,则在浏览器中当前的任务即没有结束。
上面的代码中,当前任务因为while的执行而造成永远无法执行,所以后面的setTimeout也永远不会被执行。它在浏览器的任务队列中如图所示:
这里写图片描述

setTimeout背后意味着什么

这篇文章一直在使用setTimeout为我们展现和理解js单线程的设计,只是它错误的使用了Event来进行演示,并过度解读了Event。
这里原文和转载的文章忽略了这些基础的事件触发,而且也偏偏挑了两套本身设计就比较复杂的API:onmouseXXX系和onkeyXXX系。

onKeyXXX系的API触发顺序如图:
这里写图片描述
而我个人所理解它们对应的功能:

①onkeydown - 主要获取和处理当前按下按键,例如按下Enter后进行提交。在这一层,并没有更新相关DOM元素的值。
②onkeypress - 主要获取和处理长按键,因为onkeypress在长按键盘的情况下会反复触发直到释放,这里并没有更新相关DOM元素的值,值得注意的是:keypress之后才会更新值,所以在长按键盘反复触发onkeypress事件的时候,后一个触发的onkeypress能得到上一个onkeypress的值。所以出现了onkeypress每次取值都会是上一次的值而不是最新值。
③onkeyup - 触发onkeyup的DOM元素的值在这里已经更新,可以拿到最新的值,所以这里主要处理相关DOM元素的值

流程就是上面的图画的那样:

onkeydown => onkeypress => onkeyup

使用了setTimeout之后,流程应该是下面这样子的:

onkeydown => onkeypress => function => onkeyup

使用setTimeout(fn,0)之后,在onkeypress后面插入了我们的函数function。上面所说,浏览器在onkeypress之后就会更新相关DOM元素的状态(input[type=text]的value),所以我们的function里面可以拿到最新的值。
所以我们在onkeypress里面挂起setTimeout能拿到正确的值,下面的代码可以测试使用setTimeout(fn,0)之后的流程:

 window.onload = function () {
        var domInput = get('input'), view = get('preview');
        //onkeypress兼容性和说明:http://www.w3school.com.cn/jsref/jsref_events.asp
        domInput.onkeypress = function () {
            setTimeout(function () {
                //这个函数在keypress之后,keyup之前执行
                console.log('linkFly');
            });
        };
        domInput.onkeyup = function () {
            console.log('up');
        };
    };

然后我们再来谈谈原代码中的示例1和示例2,示例1和示例2的区别在这里:

 //示例1
        input.focus();
        input.select();

        //示例2
        setTimeout(function () {
            input.focus();
            input.select();
        }, 0);

原文章中说示例1的focus()和select()在onmousedown事件中被丢弃,从而导致了没有选中,但原文的作者忽略了他注册的事件是:onmousedown。
我们暂且不讨论onmouseXXX系的其他API,我们仅关注和点击相关的,它们的执行顺序是:

mousedown - 鼠标按钮按下
mouseup - 鼠标按钮释放
click - 完成单击

我们在onmousedown里面新建了input,并且选中input的值(调用了input.focus(),input.select())。
那么为什么没有被选中呢?这样,我们来做一次测试,看看我们的onfocus到底是被丢弃了,还是触发了。我们把原文的代码进行改写:

window.onload = function () {
        var makeBtn = get('makeinput');
        //观察onmouseXXX系完成整个单击的顺序
        makeBtn.onmousedown = function (e) {
            console.log(e.type);
            var input = document.createElement('input');
            input.setAttribute('type', 'text');
            input.setAttribute('value', 'test1');
            get('inpwrapper').appendChild(input);
            input.onfocus = function () {//观察我们新生成的input什么时候获取焦点的,或者它有没有像原文作者说的那样被丢弃了
                console.info('input focus');
            };
            input.focus();
            input.select();
        }
        makeBtn.onclick = function (e) {
            console.log(e.type);
        };
        makeBtn.onmouseup = function (e) {
            console.log(e.type);
        };
        makeBtn.onfocus = function () {//观察我们生成按钮什么时候获取焦点的
            console.log('makeBtn focus');
        }
    };

代码运行的结果是这样的:
这里写图片描述

我们的input focus执行了——那么它为什么没有获取到焦点呢?我们再看看后面执行的函数:我们点击的按钮,在mousedown之后,才获得焦点,也就是说:我们的input本来已经得到了focus(),但在onmousedown之后,我们点击的按钮才迟迟触发了自己的onfocus(),导致我们的input被覆盖。
我们再加上setTimeout进行测试:

window.onload = function () {
        var makeBtn = get('makeinput');
        makeBtn.onmousedown = function (e) {
            console.log(e.type);
            var input = document.createElement('input');
            input.setAttribute('type', 'text');
            input.setAttribute('value', 'test1');
            get('inpwrapper').appendChild(input);
            input.onfocus = function () {
                console.info('input focus');
            };
            //我们加上setTimeout,看看会发生什么
            setTimeout(function () {
                input.focus();
                input.select();
            });
        }
        makeBtn.onclick = function (e) {
            console.log(e.type);
        };
        makeBtn.onmouseup = function (e) {
            console.log(e.type);
        };
        makeBtn.onfocus = function () {
            console.log('makeBtn focus');
        }
    };

执行结果是这样:
这里写图片描述

可以看见当我们点击”生成”按钮的时候,按钮的focus正确的执行了,然后才执行了input focus。
在示例1中,我们在onmousedown()中执行了input.focus()导致input得到焦点,而onmousedown之后,我们点击的按钮才迟迟得到了自己的焦点,造成了我们input刚拿到手还没焐热的焦点被转移。
而示例2中的代码,我们延迟了焦点,当按钮获得焦点之后,我们的input再把焦点抢过来,所以,使用setTimeout(fn,0)之后,我们的input可以得到焦点并选中文本。
这里值得思考的focus()的执行时机,根据这次测试观察,发现focus事件好像挂载在mousedown之内的最后面,而不是直接挂在mousedown的后面。它和mousedown仿佛是一体的。
我们使用setTimeout之前的任务流程是这样的(->表示在上一个任务中,=>表示在上一个任务后):

onmousedown -> onmousedown中执行了input.focus() -> button.onfocus => onmouseup => onclick

这里写图片描述
而我们使用了setTimeout之后的任务流程是这样的:

onmousedown -> button.onfocus => input.focus => onmouseup => onclick

这里写图片描述

而从上面的流程上我们得知了另外的消息,我们还可以把input.focus挂在mouseup和click下,因为在这些事件之前,我们的按钮已经得到过焦点了,不会再抢我们的焦点了。

 makeBtn.click = function (e) {
            console.log(e.type);
            var input = document.createElement('input');
            input.setAttribute('type', 'text');
            input.setAttribute('value', 'test1');
            get('inpwrapper').appendChild(input);
            input.onfocus = function () {//观察我们新生成的input什么时候获取焦点的
                console.info('input focus');
            };
            input.focus();
            input.select();
        }

我们应该认识到,利用setTimeout(fn,0)的特性,可以帮助我们在某些极端场景下,修正浏览器的下一个任务。

到了这里,我们已经可以否定原文所说的:”JavaScript引擎已经丢弃了这两个任务”。
我仍然相信,浏览器是爱我们的(除了IE6和移动端一些XXOO的浏览器!!!!)浏览器并不会平白无故的丢弃我们辛劳写下的代码,多数时候,只是因为我们没有看见背后的真相而已。

当我们踏进计算机的世界写下”hello world”的时候就应该坚信,这个二进制的世界里,永远存在真相。

参考和引用

JavaScript异步机制
什么是 Event Loop
javascript线程解释

<think>好的,我现在需要回答用户关于JavaScript中“一等函数”意味着什么的问题。首先,我得确保自己完全理解这个概念。一等函数(First-class Functions)在编程语言中是一个常见的术语,但具体到JavaScript中,它有哪些具体表现呢? 用户之前询问过如何使用对象字面量创建对象,现在转向函数特性,可能需要更深入的语言特性解释。我需要从基本定义入手,解释什么是一等函数,然后分点说明其具体体现,比如函数可以被赋值给变量、作为参数传递、作为返回值、存储在数据结构中等。 接下来,我应该举一些例子,帮助用户理解每个特点。例如,函数赋值给变量的例子,回调函数作为参数传递的例子,闭包作为返回值的例子,以及对象中的方法存储函数的例子。还要注意提到箭头函数和普通函数的区别,尤其是在处理this时的不同。 同时,用户可能对为什么一等函数重要感兴趣,需要结合实际应用场景,比如高阶函数、函数组合、事件处理等。这样用户能明白这个概念的实际用途,而不仅仅是理论上的定义。 还要注意术语的准确性,确保解释清晰,避免误解。可能需要提到函数式编程的相关概念,但不过多深入,以免偏离主题。最后,总结一等函数带来的优势,如灵活性、代码简洁性、模块化等,让用户理解其在JavaScript开发中的重要性。 检查是否有遗漏点,比如动态创建函数、函数的方法和属性(如name、length属性,call、apply方法),这些是否属于一等函数的一部分?可能需要简要提及,以展示函数的对象特性。 最后,确保语言简洁,结构清晰,分点明确,例子恰当,帮助用户从多个角度理解一等函数的意义和应用。同时避免使用过于专业的术语,用通俗易懂的中文进行解释,确保不同层次的用户都能理解。</think>以下是关于 JavaScript 一等函数的详细解释: **核心概念** JavaScript 中的一等函数(First-class Functions)意味着函数与其他数据类型(如数值、字符串、对象)具有完全平等的地位,主要表现在以下方面: ### 1. 函数可赋值变量 ```javascript // 普通函数声明 function add(a, b) { return a + b } // 将函数赋值给变量 const multiply = function(a, b) { return a * b }; // 箭头函数表达式 const divide = (a, b) => a / b; console.log(typeof multiply); // "function" ``` ### 2. 函数可作参数传递(回调函数) ```javascript // 高阶函数:接收函数作为参数 function calculate(a, b, operation) { return operation(a, b); } // 传递不同函数实现不同运算 console.log(calculate(5, 3, add)); // 8 console.log(calculate(5, 3, multiply)); // 15 ``` ### 3. 函数可作为返回值 ```javascript // 工厂函数:返回新函数 function createGreeting(language) { return function(name) { const greetings = { en: `Hello ${name}!`, es: `¡Hola ${name}!`, zh: `${name},你好!` }; return greetings[language] || 'Unsupported language'; }; } const greetChinese = createGreeting('zh'); console.log(greetChinese('小明')); // "小明,你好!" ``` ### 4. 函数可存储于数据结构 ```javascript // 在数组中存储函数 const operations = [ (a, b) => a + b, (a, b) => a - b, (a, b) => a * b, (a, b) => a / b ]; // 在对象中存储方法 const mathUtils = { PI: 3.1415926, square: x => x * x, cube: x => x * x * x }; console.log(operations[0](2,3)); // 5 console.log(mathUtils.cube(3)); // 27 ``` ### 5. 函数拥有属性和方法 ```javascript function demoFunc(param1, param2) { console.log(demoFunc.caller); // 查看调用者 } // 添加自定义属性 demoFunc.metadata = { author: '张三', version: '1.0.0' }; console.log(demoFunc.name); // "demoFunc" console.log(demoFunc.length); // 2 (参数个数) console.log(demoFunc.metadata); // {author: "张三", version: "1.0.0"} ``` ### 6. 动态创建函数 ```javascript // 使用 Function 构造函数 const sum = new Function('a', 'b', 'return a + b'); console.log(sum(2, 3)); // 5 // 动态生成函数逻辑 const operationType = 'multiply'; const dynamicFunc = new Function('x', 'y', `return x ${operationType === 'multiply' ? '*' : '+'} y`); console.log(dynamicFunc(4, 5)); // 20 ``` **关键影响** 1. **高阶函数**:支持 map/filter/reduce 等函数式编程范式 ```javascript const numbers = [1, 2, 3]; const squares = numbers.map(x => x * x); // [1, 4, 9] ``` 2. **闭包应用**:函数可以记住并访问其词法作用域 ```javascript function counter() { let count = 0; return () => ++count; } const myCounter = counter(); console.log(myCounter()); // 1 console.log(myCounter()); // 2 ``` 3. **异步编程**:支撑回调模式、Promise、async/await ```javascript // 回调示例 setTimeout(() => console.log('2秒后执行'), 2000); // Promise 链 fetch('api/data') .then(response => response.json()) .then(data => processData(data)) .catch(error => handleError(error)); ``` 4. **装饰器模式**:增强函数功能 ```javascript function logDecorator(fn) { return function(...args) { console.log(`调用 ${fn.name},参数:${args}`); const result = fn(...args); console.log(`结果:${result}`); return result; }; } const decoratedAdd = logDecorator(add); decoratedAdd(2, 3); // 输出: // 调用 add,参数:2,3 // 结果:5 ``` **重要特性对比** | 特征 | 普通函数 | 箭头函数 | |--------------------|---------------|------------------| | `this` 绑定 | 动态绑定 | 继承外层上下文 | | `arguments` 对象 | 可用 | 不可用 | | 构造函数 | 可做构造函数 | 不可做构造函数 | | `prototype` 属性 | 有 | 无 | **实际应用场景** - 事件处理程序:`button.addEventListener('click', handler)` - 模块模式:通过立即执行函数封装私有变量 - 函数组合:`const composedFunc = compose(funcA, funcB, funcC)` - 策略模式:将不同算法封装为函数对象 一等函数特性使 JavaScript 具备强大的函数式编程能力,这种设计允许开发者: - 更灵活地组织代码结构 - 实现高阶抽象和代码复用 - 创建更简洁的异步处理流程 - 构建复杂的行为组合模式
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值