浅谈 Javascript 异步编程
单线程 JavaScript
目前主流的 JavaScrip 环境都是以单线程模式执行的 javaScript 代码
采用单线程的原因
JS 在最初只是一门运行在浏览器端的脚本语言,负责处理页面动态交互,而实现页面交互的核心是 DOM 操作,这也就决定了必须是单线程,否则就会出现复杂的线程同步问题。
假如当操作 Dom 的时候如果同时多个线程工作,在一个线程中删除一个 DOM,而另一个线程删除一个 DOM,浏览器就不知道到底要按照那个线程的工作结果为准。因此 JavaScript 被设计成单线程模式,也是这门语言最为核心的特性之一
什么是单线程
JS 执行环境中负责执行代码的线程只有一个。通俗的说,单线程就是一个人同一时间只做一个任务,如果有多个任务就必须要排队执行,只有当前任务做完才回去做下一个任务。在下面代码中
document.write('task 1')
document.write('task 2')
document.write('task 3')
document.write('task 4')
document.write('task 5')
当 js 引擎加载的时候,因为 JavaScript 引擎 只有一个线程在处理代码。所以从上到下依次同步执行
单线程的优缺点
- 优点: 简答,直观
- 缺点: 在执行一些耗时任务就会耗时很长,后面的任务就会排队等候当前任务的结束,导致整个程序的执行出现拖延,出现页面'假死'
console.log('foo')
for (let i = 0; i < 100000; i++) {
console.log('耗时操作 ')
}
console.log('等待耗时操作结束')
为了解决 JavaScript 这种阻塞耗时的问题,将任务的执行模式分成两种
- 同步执行
- 异步执行
同步模式和异步模式
什么是同步模式
同步模式就是 js 代码依次执行代码,后面的代码排队等候。程序的执行顺序与我们代码的执行顺序完全一致。简单点,同步不是'同时',而是'排队'
console.log('global begin')
function bar () {
console.log('bar task')
}
function foo () {
console.log('foo task')
bar()
}
foo()
console.log('global end')
什么是异步模式
异步模式就是代码执行到异步任务的时候下达一个指令,单独开启一个线程(并非 js 线程)去执行异步任务,js 线程继续按照同步模式往下执行,并在本轮执行结束后,加载异步任务的回调函数。
-
本质: 运行环境提供的 API 是以异步模式工作的,异步编程也是建立在此基础上,如果运行环境的 API 都是同步的,所谓的异步编程就是'耍流浪'
-
优点:程序不会因为某个耗时任务而卡死,不会阻塞代码,进而提高用户体验
-
缺点:代码执行顺序混乱,回调地狱
console.log('global begin')
setTimeout(function timer1 () {
console.log('timer1 invoke')
}, 1800)
setTimeout(function timer2 () {
console.log('timer2 invoke')
setTimeout(function inner () {
console.log('inner invoke')
}, 1000)
}, 1000)
console.log('global end')
EventLoop
当调用栈(正在执行的工作表)结束本轮执行,事件循环就会监听到,同时监听消息队列(待办工作表),如果消息队列有异步回调,就会从消息队列取出第一个回调函数压入到调用栈,以此循环直到所有任务被执行完。
消息队列
当异步任务执行完成就会将异步任务的回调函数放到消息队列中等待下一轮事件循环
总结
异步执行过程回调函数
所有异步编程的根基
定义: 由调用者定义,交给执行者执行的函数
理解:回调函数就是一件想要做的事情,但是你并不知道这件事情所依赖的任务什么时候完成。所以最好的办法就是把这件事的步骤放到函数中,交给任务的执行者去执行,因为执行者是知道这件事情什么时候完成的。
// 回调函数
function foo (callback) {
setTimeout(function () {
callback()
}, 3000)
}
foo(function () {
console.log('这就是一个回调函数')
console.log('调用者定义这个函数,执行者执行这个函数')
console.log('其实就是调用者告诉执行者异步任务结束后应该做什么')
})
-
缺点:不利于阅读 执行顺序混乱
-
其他异步方式:事件机制,发布订阅
Promise
一种更优的异步编解决方案
CommonJS 社区提出了 Promise 规范,主要解决回调地狱问题,在 ES2015 中被标准化
// 回调地狱,只是示例,不能运行
$.get('/url1', function (data1) {
$.get('/url2', data1, function (data2) {
$.get('/url3', data2, function (data3) {
$.get('/url4', data3, function (data4) {
$.get('/url5', data4, function (data5) {
$.get('/url6', data5, function (data6) {
$.get('/url7', data6, function (data7) {
// 略微夸张了一点点
})
})
})
})
})
})
})
Promise 的基本使用
一张图看懂// Promise 基本示例
const promise = new Promise(function (resolve, reject) {
// 这里用于“兑现”承诺
// resolve(100) // 承诺达成
reject(new Error('promise rejected')) // 承诺失败
})
promise.then(function (value) {
// 即便没有异步操作,then 方法中传入的回调仍然会被放入队列,等待下一轮执行
console.log('resolved', value)
}, function (error) {
console.log('rejected', error)
})
console.log('end')
常见误区
// Promise 常见误区
function ajax (url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'json'
xhr.onload = function () {
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
// 嵌套使用 Promise 是最常见的误区
ajax('/api/urls.json').then(function (urls) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {
// 这种方式偏离的promise的设计初心,还是会有回调'地狱'的问题
})
})
})
})
})
链式调用
Promise的then或者catch方法每次返回新的promise对象
// Promise 链式调用
function ajax (url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'json'
xhr.onload = function () {
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
// var promise = ajax('/api/users.json')
// var promise2 = promise.then(
// function onFulfilled (value) {
// console.log('onFulfilled', value)
// },
// function onRejected (error) {
// console.log('onRejected', error)
// }
// )
// console.log(promise2 === promise)
ajax('/api/users.json')
.then(function (value) {
console.log(1111)
return ajax('/api/urls.json')
}) // => Promise
.then(function (value) {
console.log(2222)
console.log(value)
return ajax('/api/urls.json')
}) // => Promise
.then(function (value) {
console.log(3333)
return ajax('/api/urls.json')
}) // => Promise
.then(function (value) {
console.log(4444)
return 'foo'
}) // => Promise
.then(function (value) {
console.log(5555)
console.log(value)
})
异常处理
// Promise 异常处理
function ajax (url) {
return new Promise(function (resolve, reject) {
// foo()
// throw new Error()
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'json'
xhr.onload = function () {
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
- 回调处理方式 onRejected只是给当前 Promise 对象注册的失败回调
ajax('/api/users11.json') // 当前返回的promise
.then(function onFulfilled (value) {
console.log('onFulfilled', value)
}, function onRejected (error) {
console.log('onRejected', error)
})
- 使用 catch 注册失败回调是更常见的、因为 Promise 链条上的任何一个异常都会被一直向后传递,直至被捕获,catch相当于给整个 Promise 链条注册失败回调
ajax('/api/users11.json')
.then(function onFulfilled (value) {
console.log('onFulfilled', value)
})
.catch(function onRejected (error) {
console.log('onRejected', error)
})
Node.js 中使用以下方式
process.on('unhandledRejection', (reason, promise) => {
console.log(reason, promise)
// reason => Promise 失败原因,一般是一个错误对象
// promise => 出现异常的 Promise 对象
})
执行时序
- 宏任务和微任务
表示异步任务的两种事件队列,JS 引擎会将所有任务按照类别分到这两个队列中,首先在宏任务任务的队列中取出第一个任务,执行完毕后取出队列中的所有微任务顺序执行;之后再取宏任务,周而复始,直至两个队列的任务都取完。宏任务包括 setTimeout,setInterval、I/O 等,微任务有 Promise,process.nextTick, MutationObserver - Promise 是微任务,而微任务在本轮同步代码完成之后立即执行。
console.log(1)
setTimeout(()=>{ // 宏任务
console.log(2)
},0)
Promise.resovle().then(()=>{
console.log(3)
})
console.og(4) // 1 4 3 2
Generator
Promise 的链式调用还是没有达到传统同步代码的可读性。Generator 就是为了让异步代码采用同步方式去编写。
// generator生成器函数
function* foo() {
try {
let res = yield 'hello';
console.log(res);
} catch (e) {
console.log(e);
}
}
// 生成 generator 对象
const generator = foo();
console.log(generator.next('f'));
console.log(generator.throw('err'));
- 特性
1 调用foo()方法不会直接执行foo函数
2 调用generator.next()方法,返回yield后面的值
3 yield只是暂停生成器函数的执行,直到下次调用generator.next()
4 下次执行next会在生成器函数中上次的yield接收到这个参数
5 第一次调用next('XXX')传参不会在生成器函数被yield返回
利用生成器完成更优的异步编程体验
// const ajax = value => {
return new Promise((resovle, rej) => {
setTimeout(() => {
resovle(value);
}, 1000);
});
};
function* mian() {
const name = yield ajax('guolihang');
console.log(name); // 彻底消灭Promise的回调 近乎于同步的编程
const age = yield ajax('18');
console.log(age);
console.log(2334);
}
const result = g.next();
result.value.then(name => {
if (result.done) return;
const result2 = g.next(name);
result2.value.then(age => {
console.log(result2.done);
if (result2.done) return;
g.next(age);
// .... 采用递归
});
});
采用递归的方式实现生成器函数执行器
co库在15年之前就是这样分装 async/await 的实现机制也是类似
function co(generator) {
const g = generator();
function handleResult(result) {
if (result.done) return;
result.value.then(
value => {
handleResult(g.next(value));
},
err => {
g.throw(err);
},
);
}
handleResult(g.next());
}
co(mian);
async/await
async await 是对 Generator 的封装,内部提供了生成器函数的执行器方法,并帮我们自动调用。
async/await的内部实现 参考 阮一峰ES6
// async await 的内部实现方式
async function fn(args) {
// ...
}
function fn(args) {
return spawn(function* () {
// ...
});
}
function spawn(genF) {
return new Promise(function (resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch (e) {
return reject(e);
}
if (next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(
function (v) {
step(function () {
return gen.next(v);
});
},
function (e) {
step(function () {
return gen.throw(e);
});
}
);
}
step(function () {
return gen.next(undefined);
});
});
}
学习完再加餐撸个面试题,美滋滋~~
- 阅读下面代码,只考虑浏览器或者node环境下输出的结果
console.log('AAAA');
setTimeout(() => console.log('BBBB'), 1000); // t1
const start = new Date();
while (new Date() - start < 3000) {}
console.log('CCCC');
setTimeout(() => console.log('DDDD'), 0); // t2
new Promise((resolve, reject) => {
console.log('EEEE');
foo.bar(100);
})
.then(() => console.log('FFFF'))
.then(() => console.log('GGGG'))
.catch(() => console.log('HHHH'));
console.log('IIII');
结果
AAAA => CCCC => EEEE => IIII => HHHH => BBBB => DDDD
分析:
这道题重点考察了js异步编程,宏任务,微任务。
-
第一行同步打印:AAAA
-
第二行开启定时器,这是个异步任务且是个宏任务
-
第四行是个while语句,需要等待3秒后才能执行下面的代码,这里有个问题,就是3秒后上一个计时器1的提交时间已经过了,但是线程上的任务还没有执行结束,所以暂时不能打印结果,所以它排在宏任务的最前面了。
-
第五行输出CCCC
-
第六行又开启一个计时器2 (称呼) ,它提交的时间是0秒(其实每个浏览器器有默认最小时间的,暂时忽略) ,但是之前的1任务还没有执行,还在等待,所以2就排在t1的后面。(t2排在t1后面的原因是while造成的)都还需要等待,因为线程上的任务还没执行完毕。
-
第七行new Promise将执行promise函数, 它参数是一个回调函数,这个回调函数内的代码是同步的,它的异步核心在于resolve和reject, 同时这个异步任务在任务队列中属于微任务,是优先于宏任务执行的,(不管宏任务有多急,反正我是VIP),所以先直接打印输出同步代码EEE 。.第九行中的代码是个不存在的对象,这个错误要抛给reject这个状态,也就是catch去处理,但是它是异步的且是微任务,只有等到线程上的任务执行完毕,立马执行它,不管宏任务(计时器, ajax等)等待多久了。
-
第十四行,这是线程上的最后一个任务,打印输出IIII
我们先找出线程上的同步代码,将结果依次排列出来, AAA CCC EEEE IIII
然后我们再找出所有异步任务中的微任务把結果打印出来HHHH
最后我们再找出异步中的所有宏任务,这里t1排在前面,t2排在后面(这个原因是while造成的) ,输出结果顺序是BBBB DDDD
所以综上结果是AAAA => CCCC => EEEE => IIII => HHHH => BBBB => DDDD