JavaScript的运行机制
运行栈
JavaScript的执行环境是 单线程
的,所谓单线程,就是每次都只能做一件事,后面的事必须等前面的执行完才可以进行。
console.log(1);
console.log(2);
console.log(3);
console.log(4);
// 1, 2, 3, 4
但是这有一个弊端,如果中途遇到某个操作长时间无法执行完成,那么后面的任务就必须排队等待,这严重影响了整个执行过程,会导致浏览器无响应。
为了解决这个问题,JavaScript将任务分为了 同步
和 异步
两种。
我们写的代码一般都是同步代码,如:console.log,变量赋值,for循环等。
哪些是异步操作?
- 定时器:setTimeout、setInterval
- 事件绑定
- ajax请求数据一般是异步操作,当然也可以同步
- promise
- saync await
- 读写文件
当遇到异步任务时,会将异步任务放到任务队列中,等到整个运行栈中的内容执行完后再去执行任务队列中的内容。
console.log(1);
new Promise((resolve) => {
resolve();
}).then(() => {
console.log(3);
})
setTimeout(() => {
console.log(2);
}, 0);
console.log(4);
// 1, 4, 3, 2
-
上面的代码中,首先输出1。
-
然后遇到
promise
, promise是一个异步操作,会将.then
中的内容放到任务队列
中。 -
setTimeout
也是一个异步操作, 虽然这里延迟时间是0毫秒,但是并不会马上执行。 -
然后继续执行输出4。
-
整个运行栈结束后,去执行任务队列中的任务,首先输出 3, 再输出 2
任务队列
上面我们知道了 任务队列
中存放的都是异步任务,他们按照放入的顺序进行排列,但是异步任务会被分为了两大类:
- 微任务:promise.then()、async await
- 宏任务:setTimeout、setInterval、事件绑定、ajax、读写文件
微任务
在 宏任务
之前执行,即使 微任务
在 宏任务
之后才被加入到任务队列中。
还是上面的例子,我们把 setTimeout
与 Promise
的顺序进行调整:
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
new Promise((resolve) => {
resolve();
}).then(() => {
console.log(3);
})
console.log(4);
// 1, 4, 3, 2
可以得到一样的结果,说明 Promise.then()
在 setTimeout
之前执行了。
事件轮询
上面说到,同步任务执行完成后,会去执行异步任务,那如果异步任务执行完成后,是不是整个过程就结束了?
当然不是,在执行异步任务时,异步任务中的代码同样可能包含新的同步任务与异步任务,过程还是一样的,按照顺序执行,遇到异步任务,会将异步任务放入到一个新的 任务队列
中。当任务队列中的所有任务执行完成后,又会重新去执行新的 任务队列
中的内容,即使 任务队列
中没有任务任务,这个过程也会无线的循环下去,这就是 事件轮询 。
console.log(1);
setTimeout(() => {
console.log(2);
}, 100);
new Promise((resolve) => {
resolve();
}).then(() => {
console.log(3);
new Promise((resolve) => {
console.log(4);
resolve();
}).then(() => {
console.log(5);
})
})
console.log(6);
// 1,6, 3,4,5,2
setImmediate与process.nextTick
setImmediate
和 process.nextTick
也是异步任务,为什么要单独把这个两个拿出来说呢,他们比较特殊,首先要知道一点,setImmediate
是宏任务, process.nextTick
是微任务(在node环境下才有)。
process.nextTick
会在所有的微任务之前执行,setImmediate
会在所有的宏任务最后执行。换句话说, process.nextTick
在所有异步任务前执行,setImmediate
在所有异步任务最后执行,
整个JavaScript的执行过程:
- 同步任务
- process.nextTick
- 微任务:promise、async await
- 宏任务:setTimeout、setInterval、事件绑定、ajax、读写文件
- setImmediate
setTimeout与 setInterval
这两个定时器我们经常会用到,setTimeout
用来延迟一定的时间后执行某些事情,setInterval
用来定时执行某些内容。
你有没有想过,定时器的时间是准的吗?
我们可以写个demo来验证一下:
for (let index = 0; index < 10000; index++) {
console.log('hello');
}
setTimeout(() => {
console.log('word');
}, 5);
先循环输出10000次 hello
,然后5毫秒后输出 word
。输出 10000次的 hello
再怎么也需要几十毫秒,那 word
是会插入到 1000 次的 hello
中输出呢还是在最后输出。结果是最后输出。
可能会有人提出了质疑,上面说过,JavaScript是单线程的,必须等上面的任务执行完成后才会执行下一个任务,在输出1000次 hello
结束前,根本还没有执行到 setTimeout
,也就是说还没有开始设置定时器,肯定会在最后才输出 word
。
改一下代码:
for (let index = 0; index < 10000; index++) {
new Promise((resolve) => {
resolve();
}).then(() => {
console.log('hello');
})
}
console.log('你好');
setTimeout(() => {
console.log('word');
}, 0);
// 你好
// hello
....
// hello
// word
输出结果在意料之中,来分析一下:
- for循环10000次,发现了10000个
Promise.then()
异步任务,将其依次放入到任务队列中 - 同步输出
你好
- 发现异步任务
setTimeout
,加入到任务队列中 。(重点来了,这里已经开始了定时器) - 执行
任务队列
中的任务,输出10000次hello
,然后输出word
通过上面的例子可以明确的得到结论:setTimeout
的定时是不准的。
那么 setInterval
呢?
当然也是不准的,他们的原理是这样的,在任务队列中,每当遇到 setTimeout
和 setInterval
时,会对当前时间与设置的时间做一个比较,如果未达到设定的阈值,则会将其放入到下一个任务队列中,如果达到了,则执行里面的内容,一直这样无限循环下去。
promise与async await
他们都是异步操作,先来说 promise
。
promise
第一点:promise
中的内容的同步的,promise.then()
中的内容才是异步的。
new Promise(() => {
console.log(1);
}).then(() => {
console.log(2);
})
console.log(3);
// 1, 3
上面的代码执行后1在3的前面输出。
第二点:只有调用了 resolve()
方法后才会将 promise.then()
中的内容加到任务队列中。
因此上面的代码并没有输出2,resolve()
中可以传入一个参数,该参数可以在 .then()
中拿到。
new Promise((resolve) => {
console.log(1);
resolve(2);
}).then((data) => {
console.log(data);
})
console.log(3);
// 1, 3, 2
async await
第一点:async函数返回的是一个 promise
对象
async function fun() {
await 'hello';
}
console.log(fun());
// Promise { <pending> }
第二点:await
后面的内容是异步任务,会阻塞后面代码的执行,当 await
的异步任务执行完成后,才会继续向下执行。
async function fun() {
console.log(1);
await new Promise((resolve) => {
console.log(2);
resolve();
}).then(() => {
console.log(3);
})
console.log(4);
}
// 1// 2// 3// 4
第三点:async
函数中的 return
后面的内容,并不是函数的返回值,
async function fun() {
console.log(1);
return 2;
}
const fn = fun();
console.log(fn);
// 1
// Promise { 2 }
第一点已经说到了, async
函数返回的是一个 promise
对象,而 return
后面的内容就相当于 promise.then()
中的内容。
async function fun() {
console.log(1);
return 2;
}
const fn = fun();
fn.then((data) => {
console.log(data);
})
// 1
// 2
如果将上面的例子改为同步函数,可以这样写:
function fun() {
return new Promise((resolve) => {
console.log(1);
resolve(2);
}).then((data) => {
console.log(data);
})
}
fun();
// 1// 2
通过对比可以发现, async
可以将 promise
简化。常用于异步请求数据,如果异步请求比较多的话,使用 async
可以大大提高代码的可读性,避免了多层嵌套。
async function fun() {
const data = await new Promise((resolve) => {
resolve({
name: '张三',
age: 18
})
}).then((res) => {
return res;
});
console.log(data);
}
fun();
// { name: '张三', age: 18 }
JavaScript的编译与执行
console.log(a);
// Uncaught ReferenceError: a is not defined
如果直接输出一个未定义的变量,则会报错:a is not defined
。
console.log(a);
var a = 1;console.log(a);
// undefined
// 1
如果在输出语句的后面定义呢,为什么就不会报错了,而是 undefined
,上面不是一直说 JavaScript
是从上至下按照运行栈依次执行吗。
JavaScript
代码在运行时会经历两个阶段:编译 和 执行,编译阶段往往在执行阶段的前几微秒甚至更短的时间内。
在编译的过程中有一个 变量提升
和 函数提升
的概念,在编译的过程中,先将标识符和函数声明给提升到其对应的作用域的顶端。
因此上面的例子中,第一个输出a时,是已经定义了的,但是还并没有执行赋值操作。第二个输出才会得到结果1。
函数提升
函数提升也是同样的道理,在编写代码时,我们可以在定义的函数上方使用函数。
fn();function fn() {
console.log(1);
}
// 1
上面的那句话其实并不严谨,有两种生命函数的方式:函数式声明 和 表达式声明。
如果是 表达式声明 的方式,只会提升声明,而不会提升函数表达式,本质上就是 变量提升
fn();
var fn = function() {
console.log(1);
}
// fn is not a function