EvalError
EvalError:eval() 的使用与定义不一致
RangeError
RangeError:数值越界
ReferenceError
ReferenceError:非法或不能识别的引用数值
SyntaxError
SyntaxError:发生语法解析错误
TypeError
TypeError:操作数类型错误
URIError
URIError:URI处理函数使用不当
JS 是单线程的语言,所谓“单线程”就是一根筋,对于拿到的程序,一行一行的执行,上面的执行为完成,就傻傻的等着。例如:
1 | let i, t = Date.now(); |
上面的程序花费 261ms 的时间执行完成,执行过程中就会有卡顿,其他的事儿就先撂一边不管了。
执行程序这样没有问题,但是对于 JS 最初使用的环境 ———— 浏览器客户端 ———— 就不一样了。因此在浏览器端运行的 js ,可能会有大量的网络请求,而一个网络资源啥时候返回,这个时间是不可预估的。这种情况也要傻傻的等着、卡顿着、啥都不做吗?———— 那肯定不行。
因此,JS 对于这种场景就设计了异步 ———— 即,发起一个网络请求,就先不管这边了,先干其他事儿,网络请求啥时候返回结果,到时候再说。这样就能保证一个网页的流程运行。
1 | let ajax = $.ajax({ |
上面代码中$.ajax()
需要传入两个参数进去,url
和success
,其中url
是请求的路由,success
是一个函数。这个函数传递过去不会立即执行,而是等着请求成功之后才能执行。对于这种传递过去不执行,等出来结果之后再执行的函数,叫做callback,即回调函数,所谓”回调函数”(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
再看一段更加能说明回调函数的 nodejs 代码。和上面代码基本一样,唯一区别就是:上面代码时网络请求,而下面代码时 IO 操作。
1 | let fs = require('fs') |
从上面两个 demo 看来,实现异步的最核心原理,就是将callback作为参数传递给异步执行函数,当有结果返回之后再触发 callback执行,就是如此简单!
ajax
http.get
readFile
readdir
setTimeout
setInterval
事件绑定是不是也是异步操作?
JavaScript 运行机制详解:再谈Event Loop
事件绑定和异步操作的实现机制是一样的,那么事件绑定是不是就是异步操作呢?(声明一下,这里说的事件绑定是如下代码的形式)
1 | $btn.on('click', function (e) { |
从技术实现以及书写方法上来讲,他们是一样的。例如事件绑定和 IO 操作的写法基本相同。
1 | $btn.on('click', function (e) { |
最终执行的方式也基本一样,都会被放在 call-stack 中通过 event-loop 来调用。
第一,event-loop 执行时,调用的源不一样。异步操作是系统自动调用,无论是setTimeout
时间到了还是$.ajax
请求返回了,系统会自动调用。而事件绑定就需要用户手动触发
第二,从设计上来将,事件绑定有着明显的“订阅-发布”的设计模式,而异步操作却没有。
其实,事件绑定在 js 中扮演着非常重要的角色,各个地方都会用到事件绑定的形式。例如 web 页面监控鼠标、键盘,以及 nodejs 中的 EventEmitter
应用非常广泛(特别是涉及到数据流时)。事件绑定被应用到非常广泛,却没有发生像异步操作带来的程序逻辑问题。
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。
导图要表达的内容用文字来表述的话:
例如:
1 | let data = []; |
上面是一段简易的ajax
请求代码:
success
。console.log('代码执行结束')
。success
进入Event Queue。success
并执行。1 | function task() { |
把这段代码在chrome执行一下,却发现控制台执行task()
需要的时间远远超过3秒
这时候我们需要重新理解setTimeout
的定义。我们先说上述代码是怎么执行的:
task()
进入Event Table并注册,计时开始。sleep
函数,很慢,非常慢,计时仍在继续。timeout
完成,task()
进入Event Queue,但是sleep
也太慢了吧,还没执行完,只好等着。sleep
终于执行完了,task()
终于从Event Queue进入了主线程执行。上述的流程走完,我们知道setTimeout
这个函数,是经过指定时间后,把要执行的任务(本例中为task()
)加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。
我们还经常遇到setTimeout(fn,0)
这样的代码,0秒后执行又是什么意思呢?是不是可以立即执行呢?
答案是不会的,setTimeout(fn,0)
的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。
对于执行顺序来说,setInterval
会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。
唯一需要注意的一点是,对于setInterval(fn,ms)
来说,我们已经知道不是每过ms
秒会执行一次fn
,而是每过ms
秒,会有fn
进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。这句话请读者仔细品味。
1 | console.log(1) |
当遇到上面这种问题时,仅仅依靠上面的知识还是不够的。因为 setTimeout
和Promise.then
都是异步的,怎么判断他们的执行顺序呢?
这就引出了宏任务与微任务。
这两个概念属于对异步任务的分类,不同的API注册的异步任务会依次进入自身对应的队列(Event Queue)中,然后等待Event Loop将它们依次压入执行栈中执行。比如setTimeout
和setInterval
会进入相同的Event Queue。
特别的:script代码执行到底部就相当于一次宏任务。第一次执行栈执行的代码就是script代码执行到底部。这样就能理解下面的“每次执行栈执行的代码就是一个宏任务”这个说法了。
(macro)task(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:
1 | (macro)task->渲染->(macro)task->... |
(macro)task主要包含:setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)
(micro)task(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。
所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个(macro)task执行完后,就会将在它执行期间产生的所有(micro)task都执行完毕(在渲染前)。
(micro)task主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)
事件循环的顺序,决定js代码的执行顺序。进入整体代码后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。
例如:
1 | setTimeout(function () { |
这段代码作为宏任务,进入主线程。
先遇到setTimeout
,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)
接下来遇到了Promise
,new Promise
立即执行,then
函数分发到微任务Event Queue。
遇到console.log()
,立即执行。
好啦,整体代码执行结束,看看有哪些微任务?我们发现了then
在微任务Event Queue里面,执行。
ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout
对应的回调函数,立即执行。
结束。
下图是它们的关系:
1 | console.log('1'); |
第一轮事件循环流程分析如下:
console.log
,输出1。setTimeout
,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1
。process.nextTick()
,其回调函数被分发到微任务Event Queue中。我们记为process1
。Promise
,new Promise
直接执行,输出7。then
被分发到微任务Event Queue中。我们记为then1
。setTimeout
,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2
。宏任务Event Queue | 微任务Event Queue |
---|---|
setTimeout1 | process1 |
setTimeout2 | then1 |
process1
和then1
两个微任务。process1
,输出6。then1
,输出8。好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1
宏任务开始:
process.nextTick()
,同样将其分发到微任务Event Queue中,记为process2
。new Promise
立即执行输出4,then
也分发到微任务Event Queue中,记为then2
。宏任务Event Queue | 微任务Event Queue |
---|---|
setTimeout2 | process2 |
then2 |
process2
和then2
两个微任务可以执行。process.nextTick()
分发到微任务Event Queue中。记为process3
。new Promise
,输出11。then
分发到微任务Event Queue中,记为then3
。宏任务Event Queue | 微任务Event Queue |
---|---|
process3 | |
then3 |
process3
和then3
。整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)
1 | new Promise(resolve => { |
这段代码的流程大致如下:
Promise
实例,构造函数首先执行,所以首先输出了 4。此时 microtask 的任务有 t2
和 t1
t2
和 t1
,分别输出 2 和 1综上,上述代码的输出是:4321
为什么 t2
会先执行呢?理由如下:
对于下面的解释还是不太理解。。。
实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在
then
方法被调用的那一轮事件循环之后的新执行栈中执行
Promise.resolve
方法允许调用时不带参数,直接返回一个resolved
状态的 Promise
对象。立即 resolved
的 Promise
对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。所以,t2
比 t1
会先进入 microtask 的 Promise
队列。
javascript是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。
Event Loop(事件循环)是js实现异步的一种方法,也是js的执行机制。
参考