gzl的博客

  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

JavaScript的错误类型

发表于 2019-09-10 更新于 2020-02-24 分类于 JavaScript

EvalError

EvalError:eval() 的使用与定义不一致

RangeError

RangeError:数值越界

ReferenceError

ReferenceError:非法或不能识别的引用数值

SyntaxError

SyntaxError:发生语法解析错误

TypeError

TypeError:操作数类型错误

URIError

URIError:URI处理函数使用不当

JS深入理解异步

发表于 2019-09-07 更新于 2019-10-19 分类于 JavaScript

什么是异步

JS 为何会有异步

JS 是单线程的语言,所谓“单线程”就是一根筋,对于拿到的程序,一行一行的执行,上面的执行为完成,就傻傻的等着。例如:

1
2
3
let i, t = Date.now();
for (i = 0; i < 100000000; i++) {}
console.log(Date.now() - t) // 261 (chrome浏览器)

上面的程序花费 261ms 的时间执行完成,执行过程中就会有卡顿,其他的事儿就先撂一边不管了。

执行程序这样没有问题,但是对于 JS 最初使用的环境 ———— 浏览器客户端 ———— 就不一样了。因此在浏览器端运行的 js ,可能会有大量的网络请求,而一个网络资源啥时候返回,这个时间是不可预估的。这种情况也要傻傻的等着、卡顿着、啥都不做吗?———— 那肯定不行。

因此,JS 对于这种场景就设计了异步 ———— 即,发起一个网络请求,就先不管这边了,先干其他事儿,网络请求啥时候返回结果,到时候再说。这样就能保证一个网页的流程运行。

异步的实现原理

1
2
3
4
5
6
let ajax = $.ajax({
url: '/data/data1.json',
success: function () {
console.log('success')
}
})

上面代码中$.ajax()需要传入两个参数进去,url和success,其中url是请求的路由,success是一个函数。这个函数传递过去不会立即执行,而是等着请求成功之后才能执行。对于这种传递过去不执行,等出来结果之后再执行的函数,叫做callback,即回调函数,所谓”回调函数”(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

再看一段更加能说明回调函数的 nodejs 代码。和上面代码基本一样,唯一区别就是:上面代码时网络请求,而下面代码时 IO 操作。

1
2
3
4
let fs = require('fs')
fs.readFile('data1.json', (err, data) => {
console.log(data.toString())
})

从上面两个 demo 看来,实现异步的最核心原理,就是将callback作为参数传递给异步执行函数,当有结果返回之后再触发 callback执行,就是如此简单!

常用的异步操作

  • 网络请求,如ajax http.get
  • IO 操作,如readFile readdir
  • 定时函数,如setTimeout setInterval

事件绑定是不是也是异步操作?

异步和 event-loop

JavaScript 运行机制详解:再谈Event Loop

这一次,彻底弄懂 JavaScript 执行机制

事件绑定算不算异步?

事件绑定和异步操作的实现机制是一样的,那么事件绑定是不是就是异步操作呢?(声明一下,这里说的事件绑定是如下代码的形式)

1
2
3
$btn.on('click', function (e) {
console.log('你点击了按钮')
})

相同

从技术实现以及书写方法上来讲,他们是一样的。例如事件绑定和 IO 操作的写法基本相同。

1
2
3
4
5
6
$btn.on('click', function (e) {
console.log('你点击了按钮')
})
fs.readFile('data1.json', function (err, data) {
// 获取数据
})

最终执行的方式也基本一样,都会被放在 call-stack 中通过 event-loop 来调用。

不同

第一,event-loop 执行时,调用的源不一样。异步操作是系统自动调用,无论是setTimeout时间到了还是$.ajax请求返回了,系统会自动调用。而事件绑定就需要用户手动触发

第二,从设计上来将,事件绑定有着明显的“订阅-发布”的设计模式,而异步操作却没有。

其实,事件绑定在 js 中扮演着非常重要的角色,各个地方都会用到事件绑定的形式。例如 web 页面监控鼠标、键盘,以及 nodejs 中的 EventEmitter 应用非常广泛(特别是涉及到数据流时)。事件绑定被应用到非常广泛,却没有发生像异步操作带来的程序逻辑问题。

JS-EventLoop

发表于 2019-09-07 更新于 2020-02-13 分类于 JavaScript

javascript事件循环

当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。

EventLoop

导图要表达的内容用文字来表述的话:

  • 同步和异步任务分别进入不同的执行”场所”,所有同步任务进入主线程执行,形成一个执行栈(execution context stack)。异步的进入Event Table并注册函数。
  • 异步任务指定的事情完成时,Event Table会将这个函数移入Event Queue。
  • 主线程(执行栈)内的所有同步任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程(执行栈)执行。
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)。

例如:

1
2
3
4
5
6
7
8
9
let data = [];
$.ajax({
url: www.javascript.com,
data: data,
success: () => {
console.log('发送成功!');
}
})
console.log('代码执行结束');

上面是一段简易的ajax请求代码:

  • ajax进入Event Table,注册回调函数success。
  • 执行console.log('代码执行结束')。
  • ajax事件完成,回调函数success进入Event Queue。
  • 主线程从Event Queue读取回调函数success并执行。

定时器

setTimeout

1
2
3
4
5
6
7
8
9
10
11
12
function task() {
console.log("执行");
}

function sleep(num) {
for (let i = 0; i < num; i++) {}
}
setTimeout(() => {
task()
}, 3000)

sleep(10000000000)

把这段代码在chrome执行一下,却发现控制台执行task()需要的时间远远超过3秒

这时候我们需要重新理解setTimeout的定义。我们先说上述代码是怎么执行的:

  • task()进入Event Table并注册,计时开始。
  • 执行sleep函数,很慢,非常慢,计时仍在继续。
  • 3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep也太慢了吧,还没执行完,只好等着。
  • sleep终于执行完了,task()终于从Event Queue进入了主线程执行。

上述的流程走完,我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。

我们还经常遇到setTimeout(fn,0)这样的代码,0秒后执行又是什么意思呢?是不是可以立即执行呢?

答案是不会的,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。

setInterval

对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。

唯一需要注意的一点是,对于setInterval(fn,ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。这句话请读者仔细品味。

任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
console.log(1)

setTimeout(() => {
console.log(2)
}, 0)

Promise.resolve().then(() => {
console.log(3)
}).then(() => {
console.log(4)
})

console.log(5)
// 1、5、3、4、2

当遇到上面这种问题时,仅仅依靠上面的知识还是不够的。因为 setTimeout和Promise.then 都是异步的,怎么判断他们的执行顺序呢?

这就引出了宏任务与微任务。

这两个概念属于对异步任务的分类,不同的API注册的异步任务会依次进入自身对应的队列(Event Queue)中,然后等待Event Loop将它们依次压入执行栈中执行。比如setTimeout和setInterval会进入相同的Event Queue。

特别的:script代码执行到底部就相当于一次宏任务。第一次执行栈执行的代码就是script代码执行到底部。这样就能理解下面的“每次执行栈执行的代码就是一个宏任务”这个说法了。

EventLoop

宏任务

(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
2
3
4
5
6
7
8
9
10
11
12
setTimeout(function () {
console.log('setTimeout');
})

new Promise(function (resolve) {
console.log('promise');
resolve();
}).then(function () {
console.log('then');
})

console.log('console');

这段代码作为宏任务,进入主线程。

先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)

接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。

遇到console.log(),立即执行。

好啦,整体代码执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。

ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。

结束。

下图是它们的关系:

EventLoop

测试题

实例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
console.log('1');

setTimeout(function () {
console.log('2');
process.nextTick(function () {
console.log('3');
})
new Promise(function (resolve) {
console.log('4');
resolve();
}).then(function () {
console.log('5')
})
})
process.nextTick(function () {
console.log('6');
})
new Promise(function (resolve) {
console.log('7');
resolve();
}).then(function () {
console.log('8')
})

setTimeout(function () {
console.log('9');
process.nextTick(function () {
console.log('10');
})
new Promise(function (resolve) {
console.log('11');
resolve();
}).then(function () {
console.log('12')
})
})
// 1,7,6,8,2,4,3,5,9,11,10,12

第一轮事件循环流程分析如下:

  • 整体代码作为进入主线程,遇到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
  • 上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。
  • 我们发现了process1和then1两个微任务。
  • 执行process1,输出6。
  • 执行then1,输出8。

好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1宏任务开始:

  • 首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。
宏任务Event Queue 微任务Event Queue
setTimeout2 process2
then2
  • 第二轮事件循环宏任务结束,我们发现有process2和then2两个微任务可以执行。
  • 输出3。
  • 输出5。
  • 第二轮事件循环结束,第二轮输出2,4,3,5。
  • 第三轮事件循环开始,此时只剩setTimeout2了,执行。
  • 直接输出9。
  • 将process.nextTick()分发到微任务Event Queue中。记为process3。
  • 直接执行new Promise,输出11。
  • 将then分发到微任务Event Queue中,记为then3。
宏任务Event Queue 微任务Event Queue
process3
then3
  • 第三轮事件循环宏任务执行结束,执行两个微任务process3和then3。
  • 输出10。
  • 输出12。
  • 第三轮事件循环结束,第三轮输出9,11,10,12。

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)

实例2

1
2
3
4
5
6
7
8
9
10
11
12
13
new Promise(resolve => {
resolve(1);

Promise.resolve().then(() => {
// t2
console.log(2)
});
console.log(4)
}).then(t => {
// t1
console.log(t)
});
console.log(3);

这段代码的流程大致如下:

  1. 首先遇到 Promise 实例,构造函数首先执行,所以首先输出了 4。此时 microtask 的任务有 t2 和 t1
  2. 继续运行,输出 3。
  3. 执行所有的微任务,先后取出 t2 和 t1,分别输出 2 和 1
  4. 代码执行完毕

综上,上述代码的输出是:4321

为什么 t2 会先执行呢?理由如下:

对于下面的解释还是不太理解。。。

  • 根据 Promises/A+规范:

实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行

  • Promise.resolve 方法允许调用时不带参数,直接返回一个resolved 状态的 Promise 对象。立即 resolved 的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。

http://es6.ruanyifeng.com/#docs/promise#Promise-resolve

所以,t2 比 t1 会先进入 microtask 的 Promise 队列。

总结

javascript是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。

Event Loop(事件循环)是js实现异步的一种方法,也是js的执行机制。

参考

这一次,彻底弄懂 JavaScript 执行机制

从一道题浅说 JavaScript 的事件循环

1…131415…32

gzl

96 日志
14 分类
37 标签
© 2020 gzl
由 Hexo 强力驱动 v3.7.1
|
主题 – NexT.Pisces v7.2.0