单线程
js
是单线程语言,只能同时做一件事;
浏览器和 node.js
已支持 js
启动进程,如 web worker
;
js
和 dom
渲染共同用一个线程,因为 js
可修改 dom
结构。
进程和线程的区别
进程:描述了 CPU
在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一 个程序。
线程:是进程中的更小单位,描述了执行一段指令所需的时间。
JS 单线程的好处
- 在
JS
运行的时候可能会阻止UI
渲染,这说明了两个线程是互斥的。这其中的原因是因为JS
可以修改DOM
,如果在JS
执行的时候UI
线程还在工作,就可能导致不能安全的渲染UI
。 - 得益于
JS
是单线程运行的,可以达到节省内存(在服务端中更容易体现),节约上下文切换时间(在服务端中更容易体现),没有锁的问题 。
*[锁]:形象的来说就是当我读取一个数字15
的时候,同时有两个操作对数字进行了加减,这时候结果就出现了错误。解决这 个问题也不难,只需要在读取的时候加锁,直到读取完毕之前都不能进行写入操作。
异步
异步出现背景:js
是单线程。
遇到等待(网络请求、定时任务)不能卡住,所以需要异步。异步是基于callback
(回调函数)的形式来调用的。
同步和异步的区别
- 异步不会阻塞代码执行
- 同步会阻塞代码执行
// 异步
// 先执行同步任务,遇到异步任务就放一边,继续向下执行
// 到了某个时间点,发现异步任务还有一个任务要执行,然后就去执行它
// 执行它就通过回调函数的形式来调用,异步任务都要传一个回调函数
// 回调函数的意思就是,先去执行同步任务,再到一个时间再去执行这个回调函数
// 异步的特点是不会阻塞后面代码的执行
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
异步应用场景
- 网络请求,如
ajax
、图片加载; - 定时任务,如
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(事件循环/事件轮询)
背景
js
是单线程运行的;- 异步要基于回调来实现。
event loop
就是异步回调的实现原理。
js 是如何执行的
- 从前到后,一行一行执行;
- 如果某一行执行报错,则停止下面代码的执行;
- 先把同步代码执行完,再执行异步;
event loop 过程
用图示演示下列代码的执行过程
// 回调怎么执行:就是event loop
console.log('hi');
setTimeout(function cb1(){
console.log('bc1'); // cb1 就是 callback
}, 5000);
console.log('Bye');
- 下面演示
event loop
的过程
- 执行第一行代码,推入调用栈
- 打印出来,清空调用栈
- 执行下一行代码
setTimeout
- 发现是定时器,就在浏览器中创建一个定时器放着回调函数,清空调用栈
- 继续执行下一行代码
- 打印出来,清空调用栈
- Call stack 空了,浏览器就会启动
Event loop
,去Callback Queue
里面取回调函数(像永动机一样) - 等定时器时间到了,就会把回调函数推入
Callback Queue
- 这时候
event loop
执行的时候,就能从Callback Queue
队列中取出cb1
,放入调用栈
- 这时回调函数会立即执行
- 打印出
cb1
,函数执行完毕,清空调用栈
总结 event loop 过程 1
- 同步代码,一行一行放在
Call Stack
(调用栈)执行; - 遇到异步,会先记录下,等待时机(定时、网络请求等);
- 时机到了,就移动到
Callback Queue
(回调队列) 里面。
*[call Stack]:是一个存储函数调用的栈结构,遵循先进后出的原则。
总结 event loop 过程 2
- 如果
Call Stack
为空(即同步代码执行完成),Event Loop
开始工作; - 轮询查找
Callback Queue
,如果有,则移动到Call Stack
执行; - 然后继续轮询查找(像永动机一样)。
Node 中的 Event Loop 和浏览器中的是完全不相同的东西。
Node
的 Event Loop
分为 6
个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时 候,都会从对应的回调队队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
出处:https://coding.imooc.com/lesson/400.html
timers
阶段会执行 setTimeout
和 setInterval
回调,并且是由 poll
阶段控制的。
同样,在 Node
中定时器指定的时间也不是准确时间,只能是尽快执行。
Promise 进阶
Promise 三个状态
pending
、resolved
、rejected
状态变化
pending
→ resolved
或者 pending
→ rejected
,且变化不可逆。
// 默认是 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
并不互斥,反而相辅相成。
- 执行
async
函数返回Promise
对象; await
相当于Promise
的then
;try {...} catch {...}
用来捕获异常,相当于Promise
的catch
;- 如果执行
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 渲染,便于查看效果
宏任务和微任务的区别
宏任务
例如:setTimeout
、setInterval
、Ajax
、DOM
事件、script
、setImmediate
、 I/O
、 UI rendering
微任务
例如:Promise
、async / await
、process.nextTick
、MutationObserver
微任务和宏任务的区别
宏任务:是在 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 过程
Call Stack
清空;- 执行当前的微任务(放在
micro task queue
里面等待时机); - 尝试
DOM
渲染; - 触发
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
- 如何捕获
JS
中的异常?
// try-catch
try {
} catch (e) {
console.error(e); // 手动捕获 catch
} finally {
}
// 自动捕获
window.onerror = function (nessage, source, lineNom, colNom, error) {
// 对夸域的 js,如 CDN,不会有详细的报错信息
// 对于压缩的js,还要配合 sourceMap 反查到未压缩代码的行、列
}
- 什么是
JSON
?json
是一种数据格式,本质是一段字符串json
格式和js
对象结构一致,对js
语言更友好
- 宏任务和微任务的区别。
- 宏任务是浏览器规定的,例如:
setTimeout
、setInterval
、Ajax
、DOM
事件、script
、setImmediate
、I/O
、UI rendering
;宏任务是在DOM
渲染之后触发; - 微任务是
es6
语法规定的,例如:Promise
、async / await
、process.nextTick
、MutationObserver
; - 微任务是在
DOM
渲染之前触发。 - 微任务执行的时机比宏任务要早。
- 宏任务是浏览器规定的,例如:
- 描述
event loop
(事件循环/事件轮询)的过程。- 同步代码,一行一行放在
Call Stack
(调用栈)执行; - 遇到异步,会先记录下,等待时机(定时、网络请求等);
- 时机到了,就移动到
Callback Queue
(回调队列) 里面。 - 如果
Call Stack
为空(即同步代码执行完成),Event Loop
开始工作; - 轮询查找
Callback Queue
,如果有,则移动到Call Stack
执行; - 然后继续轮询查找(像永动机一样)。
- 另外,每次call stack清空,即同步任务执行完成,都是
DOM
重新渲染的机会,DOM
结构如果有改变则重新渲染,然后再去触发下一次的Event loop
。
- 同步代码,一行一行放在
Promise
有哪三种状态?如何变化?
pending
、resolved
、rejected
pending
变化为resolved
;pending
变化为rejected
;变化不可逆- 场景题:
promise then
和catch
的链接。
// 第一题
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);
});
// 以上输出结果?
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');
})();
// 执行结果打印出那些内容?
- 场景题:
promise
和setTimeout
的顺序。
// 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');
// 以上输出结果?