JavaScript 同步与异步

Posted 知其黑、受其白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript 同步与异步相关的知识,希望对你有一定的参考价值。

阅读目录

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

  • 读写文件

当遇到异步任务时,会将异步任务放到任务队列中,等到整个运行栈中的内容执行完后再去执行任务队列中的内容。

<script type="text/javascript">
	console.log(1)

	new Promise((resolve) => 
		resolve();
	).then(() => 
		console.log(3);
	)

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

	console.log(4)
	// 1, 4, 3, 2
	
	new Promise((resolve) => 
		resolve(5);
	).then((data) => 
		console.log(data);
	)
</script>


上面的代码中,首先输出 1。

然后遇到 promise, promise 是一个异步操作,会将 .then 中的内容放到 任务队列中。

setTimeout 也是一个异步操作, 虽然这里延迟时间是 0 毫秒,但是并不会马上执行。

然后继续执行输出 4。

整个运行栈结束后,去执行任务队列中的任务,首先输出 3, 再输出 2。

任务队列

上面我们知道了任务队列中存放的都是异步任务,他们按照放入的顺序进行排列,但是异步任务会被分为了两大类:

微任务:promise.then()、async await

宏任务:setTimeout、setInterval、事件绑定、ajax、读写文件

微任务在宏任务之前执行,即使微任务在宏任务之后才被加入到任务队列中。

还是上面的例子,我们把 setTimeout 与 Promise 的顺序进行调整:

<script type="text/javascript">
	console.log(1);

	setTimeout(() => 
		console.log(2);
	, 0);

	new Promise((resolve)=>
		resolve();
	).then(()=>
		console.log(3);
	);

	console.log(4);
	// 1, 4, 3, 2
</script>

可以得到一样的结果,说明 Promise.then() 在 setTimeout 之前执行了。

事件轮询

上面说到,同步任务执行完成后,会去执行异步任务,那如果异步任务执行完成后,是不是整个过程就结束了?

当然不是,在执行异步任务时,异步任务中的代码同样可能包含新的同步任务与异步任务,过程还是一样的,按照顺序执行,遇到异步任务,会将异步任务放入到一个新的 任务队列 中。

当任务队列中的所有任务执行完成后,又会重新去执行新的 任务队列 中的内容,即使 任务队列 中没有任务任务,这个过程也会无线的循环下去,这就是 事件轮询 。

<script type="text/javascript">
	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
</script>

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 来验证一下:

<script type="text/javascript">
for (let index = 0; index < 10000; index++) 
	console.log('hello');  

setTimeout(()=>
	console.log("word");
,5);
</script>

先循环输出 10000 次 hello,然后 5 毫秒后输出 word。输出 10000 次的 hello 再怎么也需要几十毫秒,那 word 是会插入到 1000 次的 hello 中输出呢还是在最后输出。

结果是最后输出。

可能会有人提出了质疑,上面说过,JavaScript 是单线程的,必须等上面的任务执行完成后才会执行下一个任务,在输出1000次 hello 结束前,根本还没有执行到 setTimeout ,也就是说还没有开始设置定时器,肯定会在最后才输出 word。

改一下代码:

<script type="text/javascript">
for (let index = 0; index < 10000; index++) 
	new Promise((resolve) => 
		resolve();
	).then(() => 
		console.log('hello');
	)


console.log('你好');

setTimeout(() => 
	console.log('word');
, 0);
// 你好
// ....hello
// word
</script>

输出结果在意料之中,来分析一下:

  • for 循环10000次,发现了 10000 个 Promise.then() 异步任务,将其依次放入到任务队列中。

  • 同步输出 你好。

  • 发现异步任务 setTimeout ,加入到任务队列中 。(重点来了,这里已经开始了定时器)

  • 执行 任务队列 中的任务,输出10000次 hello ,然后输出 word。

通过上面的例子可以明确的得到结论:setTimeout 的定时是不准的。

那么 setInterval 呢 ?

当然也是不准的,他们的原理是这样的,在任务队列中,每当遇到 setTimeout 和 setInterval 时,会对当前时间与设置的时间做一个比较,如果未达到设定的阈值,则会将其放入到下一个任务队列中,如果达到了,则执行里面的内容,一直这样无限循环下去。

promise 与 async await

他们都是异步操作,先来说 promise。

promise

第一点:promise 中的内容是同步的,promise.then() 中的内容才是异步的。

<script type="text/javascript">
	new Promise(() => 
		console.log(1);
	).then(() => 
		console.log(2);
	)
	console.log(3);
	// 1, 3
</script>

上面的代码执行后 1 在 3 的前面输出。

第二点:只有调用了 resolve() 方法后才会将 promise.then() 中的内容加到任务队列中。

因此上面的代码并没有输出 2,resolve() 中可以传入一个参数,该参数可以在 .then() 中拿到。

<script type="text/javascript">
	new Promise((resolve) => 
		console.log(1);
		resolve(2);
	).then((data) => 
		console.log(data);
	)
	console.log(3);
	// 1, 3, 2
</script>

async await

第一点:async 函数返回的是一个 promise 对象

<script type="text/javascript">
async function fun() 
  await 'hello';

console.log(fun());
// Promise  <pending> 
</script>


第二点:await 后面的内容是异步任务,会阻塞后面代码的执行,当 await 的异步任务执行完成后,才会继续向下执行。

<script type="text/javascript">
async function fun() 
	console.log(1);
	await new Promise((resolve) => 
		console.log(2);
		resolve();
	).then(() => 
		console.log(3);
	)
	console.log(4);


console.log(fun());

// 1
// 2
// 3
// 4
</script>


第三点:async 函数中的 return 后面的内容,并不是函数的返回值。

<script type="text/javascript">
async function fun() 
  console.log(1);
  return 2;


const fn = fun();
console.log(fn);

// 1
// Promise  2 
</script>

第一点已经说到了, async 函数返回的是一个 promise 对象,而 return 后面的内容就相当于 promise.then() 中的内容。

<script type="text/javascript">
async function fun() 
  console.log(1);
  return 2;


const fn = fun();
fn.then((data) => 
  console.log(data);
)

// 1
// 2
</script>

如果将上面的例子改为同步函数,可以这样写:

<script type="text/javascript">
function fun() 
  return new Promise((resolve) => 
    console.log(1);
    resolve(2);
  ).then((data) => 
    console.log(data);
  )


fun();

// 1
// 2
</script>

通过对比可以发现, async 可以将 promise 简化。
常用于异步请求数据,如果异步请求比较多的话,使用 async 可以大大提高代码的可读性,避免了多层嵌套。

<script type="text/javascript">
async function fun() 
  const data = await new Promise((resolve) => 
    resolve(
      name: '张三',
      age: 18
    )
  ).then((res) => 
    return res;
  );
  console.log(data);


fun();

//  name: '张三', age: 18 
</script>

JavaScript 的编译与执行

如果直接输出一个未定义的变量,则会报错:a is not defined。

console.log(a);
// Uncaught ReferenceError: 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

以上是关于JavaScript 同步与异步的主要内容,如果未能解决你的问题,请参考以下文章

Promise前期准备---同步回调与异步回调

javascript同步和异步的区别与实现方式

js事件循环运行机制

js事件循环运行机制

js中的异步与同步,解决由异步引起的问题

JavaScript 系列八:同步与异步