JS 基础 - 异步和单线程

单线程

js 是单线程语言,只能同时做一件事;
浏览器和 node.js 已支持 js 启动进程,如 web worker;
jsdom 渲染共同用一个线程,因为 js 可修改 dom 结构。

进程和线程的区别

进程:描述了 CPU 在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一 个程序。
线程:是进程中的更小单位,描述了执行一段指令所需的时间

JS 单线程的好处

  1. JS 运行的时候可能会阻止 UI 渲染,这说明了两个线程是互斥的。这其中的原因是因为 JS 可以修改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能安全的渲染 UI
  2. 得益于 JS 是单线程运行的,可以达到节省内存(在服务端中更容易体现),节约上下文切换时间(在服务端中更容易体现),没有锁的问题 。
    *[锁]:形象的来说就是当我读取一个数字 15 的时候,同时有两个操作对数字进行了加减,这时候结果就出现了错误。解决这 个问题也不难,只需要在读取的时候加锁,直到读取完毕之前都不能进行写入操作。

异步

异步出现背景:js 是单线程。
遇到等待(网络请求、定时任务)不能卡住,所以需要异步。异步是基于callback(回调函数)的形式来调用的。

同步和异步的区别

  1. 异步不会阻塞代码执行
  2. 同步会阻塞代码执行
// 异步
// 先执行同步任务,遇到异步任务就放一边,继续向下执行
// 到了某个时间点,发现异步任务还有一个任务要执行,然后就去执行它
// 执行它就通过回调函数的形式来调用,异步任务都要传一个回调函数 
// 回调函数的意思就是,先去执行同步任务,再到一个时间再去执行这个回调函数
// 异步的特点是不会阻塞后面代码的执行
console.log(100);

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

console.log(300);
// 结果: 100  300  200
// 同步 
// 会在等待的过程中卡住
// 后面的东西就不会执行,浏览器也不会渲染
console.log(100);

alert(200);

console.log(300);
// 结果:打印 100,弹出 200 ,关闭弹窗后才打印 300

异步应用场景

  1. 网络请求,如 ajax、图片加载;
  2. 定时任务,如 setTimeout
// ajax
console.log('start');

$.get('./data1.json', function(data1) {
	console.log(data1);
});

console.log('end');
// 先打印 start,然后去执行网络请求,就不管它了;
// 然后去打印 end;
// 什么时候网络请求执行完了就执行那个回调函数打印结果。
// 图片加载
console.log('start');

let img = document.createElement('img');

img.onload = funtion() {
	console.log('loaded');
};
img.src = '/xxx.png';

console.log('end');
// 先打印 start;
// 然后定义一个 img,赋值一个 onload 的回调函数;
// src 一旦赋值以后,图片就会触发加载,加载过程中就不管了,直接打印 end;
// 加载完成后触发 onload 事件,打印 loaded。
// 定时器
console.log(100);

setTimeout(function() {
	cosnole.log(200);
}, 1000);

console.log(300);
// 100、300、200

callback hell 和 Promise

回调地狱

// 获取第一份数据
$.get(url1, (data1) => {
	console.log(data1);
	
	// 获取第二份数据
	$.get(url2, (data2) => {
		console.log(data2);

		// 获取第三份数据
		$.get(url3, (data3) => {
			console.log(data3);

			// 还可能获取更多的数据
		});
	});
});

Promise

为了解决回调地狱。链式调用。

function getData(url) {
	return new Promise((resolve, reject) => {
		$.ajax({
			url,
			success(data) {
				resolve(data);
			},
			error(err) {
				reject(err);
			}
		});
	});
}

const url1 = '/data1.json';
const url2 = '/data2.json';
const url3 = '/data3.json';

// 以链式调用的形式解决回调地狱
getData(url1).then(data1 => {
	console.log(data1);
	
	return getData(url2);
}).then(data2 => {
	console.log(data2);
	
	return getData(url3);
}).then(data3 => {
	console.log(data3);
}).catch(err => console.log(err));

手写 Promise 加载一张图片

function loadImg(src) {
	return new Promise((resolve, reject) => {
		const img = document.createElement('img');
		
		img.onload = () => {
			resole(img);
		};
		
		img.onerror = () => {
			const err = new Error(`图片加载失败 ${src}`;
			reject(err));
		};
		
		img.src = src;
	})
}

const url = 'https://img.mukewang.com/xxx';

loadImg(url).then(img => {
	console.log(img.width);
	
	return img; // 这时候 img 会传到第二个 then 里面
}).then(img => {
	console.log(img.height);
}).catch(err => console.log(err));

异步进阶

event loop(事件循环/事件轮询)

背景

  1. js 是单线程运行的;
  2. 异步要基于回调来实现。

event loop 就是异步回调的实现原理

js 是如何执行的

  1. 从前到后,一行一行执行;
  2. 如果某一行执行报错,则停止下面代码的执行;
  3. 先把同步代码执行完,再执行异步;

event loop 过程

用图示演示下列代码的执行过程

// 回调怎么执行:就是event loop
console.log('hi');

setTimeout(function cb1(){
   console.log('bc1'); // cb1 就是 callback
}, 5000);

console.log('Bye');
  1. 下面演示 event loop 的过程
    在这里插入图片描述
  2. 执行第一行代码,推入调用栈
    在这里插入图片描述
  3. 打印出来,清空调用栈
    在这里插入图片描述
  4. 执行下一行代码 setTimeout
    在这里插入图片描述
  5. 发现是定时器,就在浏览器中创建一个定时器放着回调函数,清空调用栈
    在这里插入图片描述
  6. 继续执行下一行代码
    在这里插入图片描述
  7. 打印出来,清空调用栈
    在这里插入图片描述
  8. Call stack 空了,浏览器就会启动 Event loop,去 Callback Queue 里面取回调函数(像永动机一样)
  9. 等定时器时间到了,就会把回调函数推入 Callback Queue
    在这里插入图片描述
  10. 这时候 event loop 执行的时候,就能从 Callback Queue 队列中取出 cb1,放入调用栈
    在这里插入图片描述
  11. 这时回调函数会立即执行
    在这里插入图片描述
  12. 打印出 cb1,函数执行完毕,清空调用栈
    在这里插入图片描述
    在这里插入图片描述

总结 event loop 过程 1

  1. 同步代码,一行一行放在 Call Stack(调用栈)执行;
  2. 遇到异步,会先记录下,等待时机(定时、网络请求等);
  3. 时机到了,就移动到 Callback Queue(回调队列) 里面。

*[call Stack]:是一个存储函数调用的栈结构,遵循先进后出的原则。

总结 event loop 过程 2

  1. 如果 Call Stack 为空(即同步代码执行完成),Event Loop 开始工作;
  2. 轮询查找 Callback Queue,如果有,则移动到 Call Stack 执行;
  3. 然后继续轮询查找(像永动机一样)。

Node 中的 Event Loop 和浏览器中的是完全不相同的东西。

NodeEvent Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时 候,都会从对应的回调队队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
在这里插入图片描述

出处:https://coding.imooc.com/lesson/400.html

timers 阶段会执行 setTimeoutsetInterval 回调,并且是由 poll 阶段控制的。
同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行。

Promise 进阶

Promise 三个状态

pendingresolvedrejected

状态变化

pendingresolved 或者 pendingrejected,且变化不可逆。

// 默认是 pending 状态
const p1 = new Promise((resolve, reject) => {

});

console.log('p1', p1); // pending
const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve();
    }); // 模拟异步的过程
});

console.log('p3', p3); // 一开始是 pending

setTimeout(() => {
    console.log('p3-setTimeout', p3); // resolved
});

const p1 = Promise.resolve(100);

console.log('p1', p1); // resolved
const p4 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject();
    });
});

console.log('p4', p4);  // 一开始打印出来是 pending

setTimeout(() => {
    console.log('p4-setTimeout', p4);  // rejected
};

const p2 = Promise.reject('err');

console.log('p2', p2); // rejected

resolved 状态执行 then 回调函数;
rejected 状态执行 catch 回调函数。

then() 里面不报错,正常执行完成返回 resolved 状态,里面有报错则返回 rejected 状态;
catch() 里面不报错,正常执行完成返回 resolved 状态,里面有报错则返回 rejected 状态。

Promise.resolve().then() 正常执行完成,没有报错,会返回 Promise.resolve()
Promise.reject().catch() 正常执行完成,没有报错,也会返回 Promise.resolve()

const p1 = Promise.resolve(100);

console.log('p1', p1); // resolved

// resolved 状态执行 then 回调函数
p1.then((data) => {
	cosnole.log('data', data); // 执行
}).catch((err) => {
	cosnole.log('err', err); // 不执行
})
onst p2 = Promise.reject('err'); // rejected

console.log('p2', p2);

// rejected 状态执行 catch 回调函数
p2.then((data) => {
	cosnole.log('data', data); // 不执行
}).catch((err) => {
	cosnole.log('err', err); // 执行
})

async / await

也是为了解决回调地狱。
Promise then catch 是链式调用,是基于回调函数的形式。
async / await 是同步语法,彻底消灭回调函数。

function loadImg(src) {
	return new Promise((resolve, reject) => {
		const img = document.createElement('img');
		img.onload = () => {
			resole(img)
		};
		img.onerror = () => {
			const err = new Error(`图片加载失败 ${src}`;
			reject(err));
		};
		img.src = src;
	})
}

const url1 = 'https://img.mukewang.com/xxx';
const url2 = 'https://img.mukewang.com/xxx2';

// 用改 async / await 改写 promise 加载图片的示例
(async function(){
	// 同步的语法	

	// img1
	const img1 = await loadImg(url1);
	console.log(img1.width, img1.height);

	// img2
	const img2 = await loadImg(url2);
	console.log(img2.width, img2.height);
})();

async / await 和 promise 的关系

async / await 是消灭异步回调的终极武器,但和 Promise 并不互斥,反而相辅相成。

  1. 执行 async 函数返回 Promise 对象;
  2. await 相当于 Promisethen
  3. try {...} catch {...} 用来捕获异常,相当于 Promisecatch
  4. 如果执行 async 报错,就不会走 await 后续的代码。
// 执行 async 函数返回 Promise 对象
async function fun1(){
    return 100; // 相当于return Promise.resovle(100);
}

const res1 = fun1(); // 执行 async 函数返回 Promise 对象

console.log('res1', res1); // Promise 对象

res1.then(data => {
    console.log('data', data); // 100
});
// await 相当于 Promise 的 then
(async function(){
	const p1 = Promise.resolve(300);
	
	const data = await p1; // await 相当于 Promise 的 then
	
	console.log('data', data);
})();
// try {...} catch {...} 用来捕获异常
(async function(){
    // try-atch 只能捕获同步异常
	try {
		const p1 = Promise.reject('error');

		const data = await p1; // 执行 p1 报错,就不会执行 await,也不会返回 data,后面的代码都不会执行
		console.log('data', data);
	} catch(e) {
		console.log('err', e); // 捕获 p1 的异常
	}
})();

场景题

// 例题 1
async function async1(){
	console.log('async1 start'); // 2
	
	await async2(); // 会立刻执行 async2 // undefined  

	// await 后面的内容都可以看作是 callback 里的内容,即异步
	// 类似 event loop,   setTimeout(cb1)
	// setTimeout(() => { console.log('async1 end'); })
	// Promise.resolve().then(() => { console.log('async1 end'); })
	console.log('async1 end'); // 5
}

async function async2(){
	console.log('async2'); // 3
}

console.log('script start'); // 1

async1(); // 会立即执行 async1

console.log('script end'); // 4

// 输出结果:
// script start
// async1 start
// async2
// script end
// async1 end
// 例题 2
async function async1(){
	console.log('async1 start'); // 2
	
	await async2(); 

	// 下面三行都是异步回调 callback 的内容
	console.log('async1 end'); // 5
	
	await async3();

	// 下面一行是异步回调的内容
	console.log('async1 end2'); // 7
}

async function async2(){
	console.log('async2'); // 3
}

async function async3(){
	console.log('async3'); // 6
}

console.log('script start'); // 1

async1();

console.log('script end'); // 4

// 输出结果
// script start
// async1 start
// async2
// script end
// async1 end
// async3
// async1 end2

宏任务和微任务

dom 事件和 event loop

dom 事件也使用回调,基于 event loop,但是 dom 事件不是异步。

console.log('hi’); 

$(‘#btn’).click(function (e){ 
    console.log(‘button clicked'); 
}); 

console.log('Bye') 

event loop 和 dom 渲染

每次 Call Stack 清空(即每次轮询结束),即同步任务执行完成,都是 DOM 重新渲染的机会,DOM 结构如果有改变则重新渲染,然后再去触发下一次 Event loop

const $p1 = $('<p>一段文字</p>');
const $p2 = $('<p>一段文字</p>');
const $p3 = $('<p>一段文字</p>');

$('#container').append($p1).append($p2).append($p3);

console.log('length', $('#container').children().length);

alert('本次Call Stack调用结束,DOM结构已更新,但尚未触发渲染');
// alert 会阻断 js 执行,也会阻断 DOM 渲染,便于查看效果

宏任务和微任务的区别

宏任务

例如:setTimeoutsetIntervalAjaxDOM 事件、scriptsetImmediateI/OUI rendering

微任务

例如:Promiseasync / awaitprocess.nextTickMutationObserver

微任务和宏任务的区别

任务:是在 DOM 渲染之后触发,如 setTimeout
任务:是在 DOM 渲染之前触发,如 Promise

微任务执行的时机比宏任务要早
因为微任务是 ES6 语法规定的;宏任务是浏览器规定的。

const $p1 = $('<p>一段文字</p>');
const $p2 = $('<p>一段文字</p>');
const $p3 = $('<p>一段文字</p>');

$('#container').append($p1).append($p2).append($p3);

// 微任务:DOM 渲染之前触发
Promise.resolve().then(() => {
	console.log('length1', $('#container').children().length); // 3
	alert('Promise then'); // DOM渲染了吗?-- No
});

// 宏任务: DOM 渲染之后触发
setTimeout(() => {
	console.log('length2', $('#container').children().length); // 3
	alert('setTimeout'); // DOM渲染了吗?-- Yes
});

加入宏任务和微任务后的 event loop 过程

  1. Call Stack 清空;
  2. 执行当前的微任务(放在 micro task queue 里面等待时机);
  3. 尝试 DOM 渲染;
  4. 触发 Event Loop

手写 Promise

class MyPromise {
	state = 'pending'; // 状态: pending、fulfilled、rejected
	value = undefined; // 成功后的值
	reason = undefined; // 失败后的原因

	resolveCallbacks = []; // pending 状态下,存储成功的回调
	rejectCallbacks = []; // 失败的回调
    
    constructor(fn) {
        const resolveHandler = value => {
        	if (this.state === 'pending') {
        		this.state = 'fulfilled';
        		this.value = value;
        		this.resolveCallbacks.forEach(fn => fn(this.value));
        	}
        };

		const rejectHander = reason => {
			if (this.state === 'pending') {
        		this.state = 'rejected';
        		this.reason = reason;
        		this.rejectCallbacks.forEach(fn => fn(this.reason));
        	}
		};

		try {
			fn(resolveHandler, rejectHander);
		} catch(err) {
			rejectHander(err);
		}
	}
	
	then(fn1, fn2) {
		// 当 pending 状态下,fn1、fn2 会存储到 callbacks 中
		fn1 = typeof fn1 === 'function' ? fn1 : (v) => v;
		fn2 = typeof fn2 === 'function' ? fn2 : (e) => e;

		if (this.state === 'pending') {
			const p1 = new MyPromise((resolve, reject) => {
				this.resolveCallbacks.push(() => {
					try {
						const newValue = fn1(this.value);
	
						resolve(newValue);
					} catch (err) {
						reject(err);
					}
				});
				
				this.rejectCallbacks.push(() => {
					try {
						const newReason = fn2(this.reason);
	
						reject(newReason);
					} catch (err) {
						reject(err);
					}
				});
			});
			
			return p1;
		}

		if (this.state === 'fulfilled') {
			const p1 = new MyPromise((resolve, reject) => {
				try {
					const newValue = fn1(this.value);

					resolve(newValue);
				} catch (err) {
					reject(err);
				}
			});

			return p1;
		}

		if (this.state === 'rejected') {
			const p1 = new MyPromise((resolve, reject) => {
				try {
					const newReason = fn2(this.reason);

					reject(newReason);
				} catch (err) {
					reject(err);
				}
			});

			return p1;
		}
	}

	catch(fn) {
		// 就是 then 的一个语法糖,简单模式
		return this.then(null, fn);
	}
}

MyPromise.resolve = function (value) {
    return new MyPromise((resolve, reject) => resolve(value));
};

MyPromise.reject = function (reason) {
    return new MyPromise((resolve, reject) => reject(reason));
};

MyPromise.all = function (promiseList = []) {
    const p1 = new MyPromise((resolve, reject) => {
        const result = []; // 存储 promiseList 所有的结果
        const length = promiseList.length;
        let resolvedCount = 0;

        promiseList.forEach(p => {
            p.then(data => {
                result.push(data);

                // resolvedCount 必须在 then 里面做 ++
                // 不能用 index
                resolvedCount++;
                if (resolvedCount === length) {
                    // 已经遍历到了最后一个 promise
                    resolve(result);
                }
            }).catch(err => {
                reject(err);
            });
        });
    });
    
    return p1;
}

MyPromise.race = function (promiseList = []) {
    let resolved = false; // 标记
    const p1 = new Promise((resolve, reject) => {
        promiseList.forEach(p => {
            p.then(data => {
                if (!resolved) {
                    resolve(data);
                    resolved = true;
                }
            }).catch((err) => {
                reject(err);
            });
        });
    });
    
    return p1;
}

FQA

  1. 如何捕获 JS 中的异常?
// try-catch
try {

} catch (e) {
	console.error(e); // 手动捕获 catch
} finally {

}
// 自动捕获
window.onerror = function (nessage, source, lineNom, colNom, error) {
	// 对夸域的 js,如 CDN,不会有详细的报错信息
	// 对于压缩的js,还要配合 sourceMap 反查到未压缩代码的行、列
}
  1. 什么是 JSON
    • json 是一种数据格式,本质是一段字符串
    • json 格式和 js 对象结构一致,对 js 语言更友好
  2. 宏任务和微任务的区别。
    • 宏任务是浏览器规定的,例如:setTimeoutsetIntervalAjaxDOM 事件、scriptsetImmediateI/OUI rendering;宏任务是在 DOM 渲染之后触发;
    • 微任务是 es6 语法规定的,例如:Promiseasync / awaitprocess.nextTickMutationObserver
    • 微任务是在 DOM 渲染之前触发。
    • 微任务执行的时机比宏任务要早。
  3. 描述 event loop (事件循环/事件轮询)的过程。
    • 同步代码,一行一行放在 Call Stack(调用栈)执行;
    • 遇到异步,会先记录下,等待时机(定时、网络请求等);
    • 时机到了,就移动到 Callback Queue(回调队列) 里面。
    • 如果 Call Stack 为空(即同步代码执行完成),Event Loop 开始工作;
    • 轮询查找 Callback Queue,如果有,则移动到 Call Stack 执行;
    • 然后继续轮询查找(像永动机一样)。
    • 另外,每次call stack清空,即同步任务执行完成,都是 DOM 重新渲染的机会,DOM 结构如果有改变则重新渲染,然后再去触发下一次的 Event loop
  4. Promise 有哪三种状态?如何变化?
    pendingresolvedrejected
    pending变化为resolvedpending变化为rejected;变化不可逆
  5. 场景题:promise thencatch 的链接。
// 第一题
Promise.resolve().then(() => {
	console.log(1);
}).catch(() => {
	console.log(2);
}).then(() => {
	console.log(3);
});
// 以上输出结果?
// 第二题
Promise.resolve().then(() => {
	console.log(1);
	throw new Error('error1');
}).catch(() => {
	console.log(2);
}).then(() => {
	console.log(3);
});
// 以上输出结果?
// 第三题
Promise.resolve().then(() => {
	console.log(1);
	
	throw new Error('error1');
}).catch(() => {
	console.log(2);
}).catch(() => {
	console.log(3);
});
// 以上输出结果?
  1. async / await 语法题。
// 第一题
async function fn() {
	return 100;
}

(async function() {
	const a = fn(); // ??
	const b = await fn(); // ??
})();
// 第二题
(async function () {
	console.log('start');
	const a = await 100;
	console.log('a', a);
	const b = await Promise.resolve(200);
	console.log('b', b);
	const c = await Promise.reject(300);
	console.log('c', c);
	console.log('end');
})();
// 执行结果打印出那些内容?
  1. 场景题:promisesetTimeout 的顺序。
// promise 和 setTimeout 的顺序
console.log(100);

setTimeout(() => {
	console.log(200);
});

Promise.resolve().then(() => {
	console.log(300);
});

console.log(400);

// 以上输出结果?
// 外加 async / await 的顺序问题
async function async1() {
	console.log('async1 start');
	
	await async2();
	
	console.log('async1 end');
}

async function async2() {
	console.log('async2');
}

console.log('script start');

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

async1();

new Promise((resolve) => {
	console.log('promise1');
	resolve();
}).then(() => {
	console.log('promise2');
});

console.log('script end');

// 以上输出结果?
  • 23
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值