js是单线程的,在浏览器中js的执行栈跟渲染线程是相互阻塞的。
单线程模式最大的优势就是更安全,更简单
缺点也很明确,就是如果中间有一个特别耗时的任务,其他的任务就要等待很长的时间,出现假死的情况。
为了解决这种问题,js有两种任务的执行模式:同步模式(Synchronous)和异步模式(Asynchronous)。
指的是代码的任务依次执行,后一个任务必须等待前一个任务结束才能开始执行。
程序的执行顺序和代码的编写顺序是完全一致的。
在单线程模式下,大多数任务都会以同步模式执行。
缺点:因为是类似于排队的按次序执行,如果一旦某一个操作执行特别耗时,就会让页面卡顿,卡死,所以需要异步模式。
异步模式 不会去等待这个任务的结束才开始下一个任务,都是开启过后就立即往后执行下一个任务。耗时函数的后续逻辑会通过回调函数的方式定义。任务完成后,就会调用回调函数。
异步模式对于单线程的JavaScript 非常重要,如果没有这种模式,单线程的JavaScript 就无法同时处理大量的耗时任务。
而单线程下异步模式的最大难点在于代码执行的顺序混乱。
异步代码的执行方式简单来说:
这里要强调,js是单线程的,浏览器不是单线程的,有一些API是有单独的线程去做的。
回调函数:由调用者定义,交给执行者执行的函数
// callback就是回调函数
// 就是把函数作为参数传递,缺点是不利于阅读,执行顺序混乱。
function foo(callback) {
setTimeout(function(){
callback()
}, 3000)
}
foo(function() {
console.log('这就是一个回调函数')
console.log('调用者定义这个函数,执行者执行这个函数')
console.log('其实就是调用者告诉执行者异步任务结束后应该做什么')
})
回调的多重嵌套,会导致代码可读低、编写费劲、容易出错,故而被称为 callback hell。
为了避免这个问题。CommonJS社区提出了Promise的规范,ES6中称为语言规范。
Promise是一个对象,用来表述一个异步任务执行之后是成功还是失败,它有多个状态:
返回resolve
const promise = new Promise((resolve, reject) => {
resolve(100)
})
promise.then((value) => {
console.log('resolved', value) // resolve 100
},(error) => {
console.log('rejected', error)
})
返回reject
const promise = new Promise((resolve, reject) => {
reject(new Error('promise rejected'))
})
promise.then((value) => {
console.log('resolved', value)
},(error) => {
console.log('rejected', error)
// rejected Error: promise rejected
// at E:\professer\lagou\Promise\promise-example.js:4:10
// at new Promise ()
})
即便promise中没有任何的异步操作,then方法的回调函数仍然会进入到事件队列中排队。
// promise假设是一个Promise实例
// then中的回调return了一个Promise实例
promise.then(()=>{
return new Promise((resolve,reject)=>{
// some code
resolve("实参A")
} )
}).then( (接收实参A)=>{
作为上一个then中的回调 } )
// then中回调return的不是promise实例
promise.then(()=>{
return 123 })
.then(val=>{
console.log(val) // 123 })
也就是then中的第二个回调函数
如果then中没有传入第二个回调 那么异常会进入catch的回调处理
promise中如果有异常,都会调用reject方法,还可以使用.catch()
使用.catch方法更为常见,因为更加符合链式调用
还可以在全局对象上注册一个unhandledrejection事件,处理那些代码中没有被手动捕获的promise异常,当然并不推荐使用
// 浏览器
window.addEventListener('unhandledrejection', event => {
const {
reason, promise } = event
console.log(reason, promise)
//reason => Promise 失败原因,一般是一个错误对象
//promise => 出现异常的Promise对象
event.preventDefault()
}, false)
// node
process.on('unhandledRejection', (reason, promise) => {
console.log(reason, promise)
//reason => Promise 失败原因,一般是一个错误对象
//promise => 出现异常的Promise对象
})
Promise.resolve('foo')
.then(function (value) {
console.log(value)
})
new Promise(function (resolve, reject) {
resolve('foo')
})
// 如果传入的是一个 Promise 对象,Promise.resolve 方法原样返回
var promise = ajax('/api/users.json')
var promise2 = Promise.resolve(promise)
console.log(promise === promise2)
// 如果传入的是带有一个跟 Promise 一样的 then 方法的对象,
// Promise.resolve 会将这个对象作为 Promise 执行
Promise.resolve({
then: function (onFulfilled, onRejected) {
onFulfilled('foo')
}
})
.then(function (value) {
console.log(value)
})
Promise.reject()方法返回一个带有拒绝原因的Promise对象
// Promise.reject 传入任何值,都会作为这个 Promise 失败的理由
Promise.reject(new Error('rejected'))
.catch(function (error) {
console.log(error)
})
Promise.reject('anything')
.catch(function (error) {
console.log(error)
})
如何简单理解宏任务和微任务
比如:你在银行排队办理业务
那么队列中的每一个人都可以看做是一个宏任务
当排到你的时候 你告诉柜员你需要 办卡,存钱,转账,这些就是微任务。柜员不会让你办完一个业务就重新排一次队 而是一次性把你的微任务全处理完。然后才会轮到下一个人,也就是下一个宏任务。
(macro) task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
宏任务包括
script(整体代码,同步代码)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 环境)
(micro)task,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。
微任务包括
Promise.then
Object.observe
MutaionObserver
process.nextTick(Node.js 环境)
判断代码执行结果
// 微任务
console.log('global start')
// setTimeout 的回调是 宏任务,进入回调队列排队
setTimeout(() => {
console.log('setTimeout')
}, 0)
// Promise 的回调是 微任务,本轮调用末尾直接执行
Promise.resolve()
.then(() => {
console.log('promise')
})
.then(() => {
console.log('promise 2')
})
.then(() => {
console.log('promise 3')
})
console.log('global end')
结果是:
// 同步代码看做是一次宏任务,执行结束后 执行该宏任务阶段产生的所有微任务 也就是promise中的then, 然后再执行下一个宏任务
global start
global end
promise
promise 2
promise 3
setTimeout
Generator 函数可以暂停执行和恢复执行。除此之外,它还有两个特性:函数体内外的数据交换和错误处理机制。
next方法不传递参数返回一个对象,next方法传入参数把参数传给函数内部
next 方法返回值的 value 属性,是 Generator 函数向外输出数据;next 方法还可以接受参数,这是向 Generator 函数体内输入数据。
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
上面代码中,第一个 next 方法的 value 属性,返回表达式 x + 2 的值(3)。第二个 next 方法带有参数2,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量 y 接收。因此,这一步的 value 属性,返回的就是2(变量 y 的值)。
Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出错了');
// 出错了
上面代码的最后一行,Generator 函数体外,使用指针对象的 throw 方法抛出的错误,可以被函数体内的 try … catch 代码块捕获。
Generator 配合 Promise 的异步方案在开发中 可以使用co这个库
当然, **“大人,时代变了”**我们现在用 async / await
这个大家已经用的比较多了 就不再赘述了
基本用法
async function main () {
try {
const users = await ajax('/api/users.json')
console.log(users)
const posts = await ajax('/api/posts.json')
console.log(posts)
const urls = await ajax('/api/urls.json')
console.log(urls)
} catch (e) {
console.log(e)
}
}
异常处理