# 异步介绍

# 同步 vs 异步

先看下面的 demo,根据程序阅读起来表达的意思,应该是先打印100,1秒钟之后打印200,最后打印300。但是实际运行根本不是那么回事。

console.log(100)
setTimeout(function () {
    console.log(200)
}, 1000)
console.log(300)

1
2
3
4
5
6

再对比以下程序。先打印100,再弹出200(等待用户确认),最后打印300。这个运行效果就符合预期要求。

console.log(100)
alert(200)  // 1秒钟之后点击确认
console.log(300)

1
2
3
4
  • 第一个示例中间的步骤根本没有阻塞接下来程序的运行
  • 第二个示例却阻塞了后面程序的运行。
  • 前面这种表现就叫做 异步(后面这个叫做 同步 ),即不会阻塞后面程序的运行

# 异步和单线程

  • 在JavaScript的世界中,所有代码都是单线程执行的。同一时间只能做一件事,不能“一心二用”。
  • 由于这个“缺陷”,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。

相关补充

涉及面试题:进程与线程区别?JS 单线程带来的好处?

  • 进程--进程是指一个具有一定独立功能的程序在一个数据集合上的一次动态执行的过程。
  • 线程--是进程中的更小单位,
  • 把这些概念拿到浏览器中来说,当你打开一个 Tab 页时,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。

单线程的好处

  • 举个例子,如果 UI 线程、JS 线程同时工作,而JS 可以修改 DOM,就可能导致不能安全的渲染 UI。(都修改DOM就冲突了)
进程

进程是指一个具有一定独立功能的程序在一个数据集合上的一次动态执行的过程。

进程的组成:进程包括了正在运行的一个程序的所有状态信息。

  • 代码
  • 数据
  • 状态寄存器
  • 通用寄存器
  • 进程占用资源系统

进程的特点:

  • 动态性
  • 并发性
  • 独立性
  • 制约性

进程与程序的联系

  • 进程是操作系统处于执行状态程序的抽象
    • 程序 = 文件(静态的可执行文件)
    • 进程 = 执行中的程序 = 程序 + 执行状态
  • 同一个程序的多次执行过程对应为不同进程
    • 如命令 "ls" 的多次执行对应多个进程
  • 进程执行需要的资源
    • 内存:保存代码和数据
    • CPU:执行指令

# JS异步解决方案

# 回调函数

按照 MDN 的描述:回调函数是作为参数传给另一个函数的函数,然后通过在外部函数内部调用该回调函数以完成某种操作。

// 方法 用于请求数据(模拟)
function f(cb) {
	setTimeout(function() {
		cb && cb();
	}, 1000);
}
f(function() {
	console.log(1);
	f(function() {
		console.log(2);
		f(function() {
			console.log(3);
			f(function() {
				console.log(4);
				f(function() {
					console.log(5);
					f(function() {
						console.log(6);
					});
				});
			});
		});
	});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

回调地狱的根本问题就是:

  • 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
  • 嵌套函数一多,就很难处理错误
  • 不能使用 try catch 捕获错误,
  • 不能直接 return。

# 同步回调与异步回调

同步回调

  1. 理解: 立即执行, 完全执行完了才结束, 不会放入回调队列中
  2. 例子: 数组遍历相关的回调函数 / Promise 的 excutor 函数 异步回调
  3. 理解: 不会立即执行, 会放入回调队列中将来执行
  4. 例子: 定时器回调 / ajax 回调 / Promise 的成功|失败的回调
const arr = [1, 2, 3] 
arr.forEach(item => console.log(item)) // 同步回调, 不会放入回调队列, 而是 立即执行 
console.log('forEatch()之后') 
setTimeout(() => { // 异步回调, 会放入回调队列, 所有同步执行完后才可能执行 
	console.log('timout 回调') 
}, 0) 
console.log('setTimeout 之后')
1
2
3
4
5
6
7

# Promise

上面的例子用 Promise 来写代码:

// 方法 用于请求数据(模拟)
function f() {
	return new Promise(resolve => {
		setTimeout(function() {
			resolve();
		}, 1000);
	})
}

f()
	.then(function() {
		console.log(1);
		return f();
	})
	.then(function() {
		console.log(2);
		return f();
	})
	.then(function() {
		console.log(4);
		return f();
	})
	.then(function() {
		console.log(3);
		return f();
	})
	.then(function() {
		console.log(5);
		return f();
	})
	.then(function() {
		console.log(6);
	});
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

显而易见,Promise 以链式调用的方式避免了大量的嵌套,也符合人的线性思维方式,大大方便了异步编程。

# co + Generator 方式

利用协程完成 Generator 函数,用 co 库让代码依次执行完,同时以同步的方式书写,也让异步操作按顺序执行。

# async/await方式

# MDN 文档

MDN-async (opens new window)

MDN-await (opens new window)

# async 函数

  1. 函数的返回值为 promise 对象
  2. promise 对象的结果由 async 函数执行的返回值决定

# await 表达式

  1. await 右侧的表达式一般为 promise 对象, 但也可以是其它的值
  2. 如果表达式是 promise 对象, await 返回的是 promise 成功的值
  3. 如果表达式是其它值, 直接将此值作为 await 的返回值

# 注意

  1. await 必须写在 async 函数中, 但 async 函数中可以没有 await
  2. 如果 await 的 promise 失败了, 就会抛出异常, 需要通过 try...catch 捕获处理
// async函数的返回值是一个promise对象
// async函数返回的promise的结果由函数执行的结果决定
async function fn1() {
	return 1
	// throw 2
	// return Promise.reject(3)
	// return Promise.resolve(3)
	/* return new Promise((resolve, reject) => {
	  setTimeout(() => {
	    resolve(4)
	  }, 1000);
	}) */
}

const result = fn1()
// console.log(result)
result.then(
	value => {
		console.log('onResolved()', value)
	},
	reason => {
		console.log('onRejected()', reason)
	}
)

function fn2() {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			// resolve(5)
			reject(6)
		}, 1000);
	})
}

function fn4() {
	return 6
}

async function fn3() {
	try {
		// const value = await fn2() // await右侧表达为promise, 得到的结果就是promise成功的value
		const value = await fn1()
		console.log('value', value)
	} catch (error) {
		console.log('得到失败的结果', error)
	}

	// const value = await fn4() // await右侧表达不是promise, 得到的结果就是它本身
	// console.log('value', value)
}
fn3()
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51

# 优缺点

  • 优点:await 将异步代码改造成了同步代码
  • 缺点:因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。

# 其他面试题

# 手写Promise

放到下一篇文章进行详细介绍

# 手写-用 Promise 加载一张图片

function loadImg(src) {
	return new Promise((resolve, reject) => {
		const img = document.createElement('img')
		img.onload = () => {
			console.log('done')
			resolve(img)
		}
		img.onerror = () => {
			const err = new Error(`图片加载失败 ${src}`)
			reject(err)
		}
		img.src = src
	})
}
const url = 'http://www.imooc.com/static/img/index/logo_new.png'
loadImg(url).then(img => {
	console.log(img.width)
}).catch(e => {
	console.log(e)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 前端使用异步的场景有哪些?

  • 网络请求,如 Ajax <img>加载
  • 定时任务 setTimeout setInterval
// ajax
console.log('start');
$.get('./data1.json', function(data1) {
	console.log(data1);
})
console.log('end');

// 图片加载
console.log('start');
let img = document.createElement('img');
img.onload = function() {
	console.log('loaded')
}
img.src = '/xxx.png';
console.log('end');


// setTimeout
console.log(100);
setTimeout(function() {
	console.log(200)
}, 1000)
console.log(300);

// setInterval
console.log(100);
setInterval(function() {
	console.log(200)
}, 1000)
console.log(300);
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
上次更新: 2021年10月30日星期六晚上8点03分