【ES6】异步编程Ajax、Callback Hell与Promise、Generator的完全指北


前言

本文首先介绍了异步操作的前置知识及存在的问题。第二、三节分别介绍了ES6引入的两种异步编程解决方案Promise和Generator。


一、异步操作前置知识

详细解释参看:JS运行机制详解 动图详解Event Loop

1. JS是单线程的

作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。

2. 同步任务 异步任务

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

异步执行的运行机制:

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

下图是主线程和任务队列的示意图:
在这里插入图片描述

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

const a = 2
const b = 3
console.log(a + b) // 同步任务
// 异步任务
setTimeout(() => { // 延迟1s执行
    console.log(a + b)
}, 1000)

// 前后端数据分离   前端 <->  后端  ajax
console.log(1)
setTimeout(() => {
    console.log(2)
}, 1000)
console.log(3)
// 1 3 2

console.log(1)
//不管延迟时间是多少,它都是异步任务,必须等主线程任务执行完成之后再执行
setTimeout(() => {
    console.log(2)
}, 0)
console.log(3)
//1 3 2

// 伪代码
setTimeout(()=>{
    task() // 表示一个任务
}, 2000) // 异步任务,首先进入Event table,等2s之后再进入Event Queue
// 虽然异步任务2s就准备好了,但是还必须等到同步任务执行完成
sleep(5000) // 表示一个很复杂的同步任务,会先于异步任务执行

3. Ajax原理

Ajax相当于在用户和服务器之间加了一个中间层,使用户操作与服务器响应异步化。并不是所有的用户请求都提交给服务器,像一些数据验证和数据处理等都交给Ajax引擎自己来做,只有确定需要从服务器读取新数据时再由Ajax引擎代为向服务器提交请求。

Ajax的原理简单来说通过XmlHttpRequest对象来向服务器发送异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面。这其中最关键的一步就是从服务器获得请求数据

XMLHttpRequest是ajax的核心机制,它是在IE5中首先引入的,是一种支持异步请求的技术。简单的说,也就是JavaScript可以及时向服务器提出请求和处理响应,而不阻塞用户。达到无刷新的效果。
在这里插入图片描述
在实现的时候,要考虑兼容性问题

// 封装成一个函数
// ajax的第二个参数是一个回调函数
function ajax(url, callback) {
    // 1、创建XMLHttpRequest对象
    var xmlhttp
    if (window.XMLHttpRequest) {
        xmlhttp = new XMLHttpRequest()
    } else { // 兼容早期浏览器
        xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')
    }
    // 2、发送请求
    xmlhttp.open('GET', url, true) // 指定发送请求的操作
    xmlhttp.send() // 发送
    // 3、服务端响应
    // 监听onreadystatechange 方法
    xmlhttp.onreadystatechange = function () {
    // 事件处理函数
        if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
            var obj = JSON.parse(xmlhttp.responseText)
            // console.log(obj)
            callback(obj) // 将响应得到的数据传给回调,回调函数是自己定义并传入的
        } 
    }
}

var url = 'http://musicapi.xiecheng.live/personalized'
ajax(url, res => {
     console.log(res)
})

4. Callback Hell

回调函数可规范调用的顺序,但是当代码层层嵌套越写越深,代码的可维护性、可读性都会降低,就会造成Callback Hell,下面将介绍ES6对异步操作的解决方案Promise来解决这类问题。

// 1 -> 2 -> 3
// callback hell
ajax('static/a.json', res => {
    console.log(res)
    ajax('static/b.json', res => {
        console.log(res)
        ajax('static/c.json', res => {
            console.log(res)
        })
    })
})

二、Promise (※面试必考)

1. 原理

所谓 Promise ,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可 以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise 对象有以下两个特点。

  1. 对象的状态不受外界影响
    Promise 对象代表一个异步操作,有三种状态: pending (进行中)、 fulfilled (已成功)和 rejected (已失败)。 只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是“承诺”,表 示其他手段无法改变。
  2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
    Promise 对象的状态改变,只有两种可能:从 pending 变为 fulfilled从 pending变为 rejected
    只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
    有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外, Promise 对象提供统一的接口,使得控制异步操作更加容易。
  3. Promise 也有一些缺点。
    ① 无法取消 Promise ,一旦新建它就会立即执行,无法中途取消。
    ② 如果不设置回调函数, Promise 内部抛出的错误, 不会反应到外部。
    ③ 当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
    如果某些事件不断地反复发生,一般来说,使用 Stream 模式是比部署 Promise 更好的选择。

2. Promise的用法

Promise的精髓在于对于异步的状态管理

// 状态管理 
// resolve 成功
// reject 失败
let p = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('kakaDorothy')
        // resolve()
        // reject()
        // if(){
        //     resolve()
        // }else{
        //     reject()
        // }
        // resolve('成功') // 传参到then里面
        reject('失败')
    }, 1000)
}).then(res => { // then可传入两个函数作为参数,第一个为必须得参数,第二个可省略
	// 异步操作成功之后进行的操作
    console.log(res)
}, err => {
    console.log(err)
})

3. Promise执行顺序

注意,如果Promise内部没有写任何异步操作,那么它是会立即执行的。then方法相当于promise的回调函数(它的微任务),待promise内的函数执行完成便执行。

let p = new Promise((resolve, reject) => {
    console.log(1)
    resolve() // 成功->可调用then方法进一步操作
})
console.log(2)
p.then(res => { // 想要调用then方法,在promise对象中的resolve或者reject方法是一定要写的
    console.log(3)
})
//1 2 3

4. Promise的三种状态

在这里插入图片描述

let p1 = new Promise((resolve, reject) => {
    resolve(1)
})
let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(2) // 模拟成功的状态
    }, 1000)
})
let p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject(3) // 模拟失败的状态
    }, 1000)
})
console.log(p1) // resolved
console.log(p2) // pending
console.log(p3) // pending

// 待状态转换完成之后再输出
setTimeout(() => {
    console.log(p2)
}, 2000) // resolved
setTimeout(() => {
    console.log(p3)
}, 2000) // rejected

p1.then(res => {
    console.log(res) // 1
})
p2.then(res => {
    console.log(res) // 2
})
p3.catch(err => {
    console.log(err) // 3
})

Promise状态形成之后就无法再改变


let p = new Promise((resolve, reject) => {
    reject(2)
    resolve(1)
})
p.then(res => {
    console.log(res)
}).catch(err => {
    console.log(err)
})
// 只输出2,promise状态形成之后就无法再改变

5. 改造回调深渊Callback Hell

封装Ajax请求

// 传入成功回调与失败回调
function ajax(url, successCallback, failCallback) {
    // 1、创建XMLHttpRequest对象
    var xmlhttp
    if (window.XMLHttpRequest) {
        xmlhttp = new XMLHttpRequest()
    } else { // 兼容早期浏览器
        xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')
    }
    // 2、发送请求
    xmlhttp.open('GET', url, true)
    xmlhttp.send()
    // 3、服务端响应
    xmlhttp.onreadystatechange = function () {
        if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
            var obj = JSON.parse(xmlhttp.responseText)
            // console.log(obj)
            successCallback && successCallback(obj)
        } else if (xmlhttp.readyState === 4 && xmlhttp.status === 404) {
            failCallback && failCallback(xmlhttp.statusText)
        }
    }
}

对于重复的方法,将它封装成一个函数,每次调用此方法时,返回一个新的Promise对象。

function getPromise(url) {
    return new Promise((resolve, reject) => {		
    // 在Promise内部调用ajax函数
        ajax(url, res => {
            resolve(res)
        }, err => {
            reject(err)
        })
    })
}

调用getPromise方法(注意需要使用return,否则链式操作可能不起效)

// resolved成功示例
getPromise('static/a.json')
    .then(res => {
        console.log(res)
        return getPromise('static/b.json')
    }).then(res => {
        console.log(res)
        return getPromise('static/c.json')
    }).then(res => {
        console.log(res)
    })
 
 // static文件夹下不存在aa.json,会进入rejected状态 
getPromise('static/aa.json')
    .then(res => {
        console.log(res)
        return getPromise('static/b.json')// aa.json读取成功时向b.json发出请求
    }, err => {
        console.log(err) // Not Found
        return getPromise('static/b.json') // aa.json读取失败时也向b.json发出请求
    }).then(res => {
        console.log(res) // 输出b.json的内容
        return getPromise('static/c.json')
    }).then(res => {
        console.log(res) // 输出c.json的内容
    })

// 在最后加上catch方法,如出现错误便会抛出异常,而不会进入内部继续输出
getPromise('static/aa.json')
    .then(res => {
        console.log(res)
        return getPromise('static/b.json')
    }).then(res => {
        console.log(res)
        return getPromise('static/c.json')
    }).then(res => {
        console.log(res)
    }).catch(err => {
        console.log(err) // Not Found
    })

6. Promise静态方法

  • Promise.resolve() 表示成功的状态
  • Promise.reject() 表示失败的状态
  • Promise.all() 传入一个数组作为参数,数组内的每一个内容都对应一个Promise对象
  • Promise.race() 将多个 Promise 实例,包装成一个新的 Promise 实例

6.1 Promise.resolve()、Promise.reject()

// Promise.resolve() 
let p1 = Promise.resolve('success')
// console.log(p1)
p1.then(res => {
    console.log(res)
})

// Promise.reject() 
let p2 = Promise.reject('fail')
console.log(p2)
p2.catch(err => {
    console.log(err)
})

function foo(flag) {
    if (flag) {
        return new Promise(resolve => {
            // 异步操作
            resolve('success')
        })
    } else {
        // return 'fail' // 因为下面是在then方法中调用此返回字符串方法 所以报错表示not a function
        return Promise.reject('fail') // 能够返回一个Promise对象
    }
}
foo(false).then(res => {
    console.log(res)
}, err => {
    console.log(err)
})

6.2 Promise.all()

Promise.all 方法接受一个数组作为参数, p1 、 p2 、 p3 都是 Promise 实例,如果不是,就会先调用 Promise.resolve 方法, 将参数转为 Promise 实例,再进一步处理。( Promise.all 方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)
p 的状态由 p1 、 p2 、 p3 决定,分成两种情况:

  • 只有 p1 、 p2 、 p3 的状态都变成 fulfilled , p 的状态才会变成 fulfilled ,此时 p1 、 p2 、 p3 的返回值组成一个数组,传递给 p 的回调函数。
  • 只要 p1 、 p2 、 p3 之中有一个被 rejected , p 的状态就变成 rejected ,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。
let p1 = new Promise((resolve, reject) => {
	// 模拟异步操作
    setTimeout(() => {
        console.log(1)
        resolve('1成功')
    }, 2000)
})
let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log(2)
        // resolve('2成功')
        reject('2失败')
    }, 1000)
})
let p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log(3)
        resolve('3成功')
    }, 3000)
})

// 传入数组作为参数, 数组内放入Promise对象
Promise.all([p1, p2, p3]).then(res => {
	// 需要等数组内的Promise对象都执行完成之后再执行此时的then方法
    console.log(res)
}, err => {
	// 如果出现错误 便直接进入err错误函数 而不会执行前面的res
    console.log(err)
})

6.3 Promise.race()

只要 p1 、 p2 、 p3 之中有一个实例率先改变状态, p 的状态就跟着改变,即只要其中一个promise对象状态转换完成了(无论是成功or失败),便认为整体的p完成了。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数。
Promise.race 方法的参数与 Promise.all 方法一样,如果不是 Promise 实例,就会先调用Promise.resolve 方法,将参数转为 Promise 实例,再进一步处理。

Promise.race([p1, p2, p3]).then(res => {
    console.log(res)
}, err => {
    console.log(err) 
})
// 结果为 2失败

6.4 应用场景

const imgArr = ['1.jpg', '2.jpg', '3.jpg']
let promiseArr = []
imgArr.forEach(item => {
    promiseArr.push(new Promise((resolve, reject) => {
        // 图片上传的操作
        resolve()
    }))
})

// 控制图片全部上传成功
Promise.all(promiseArr).then(res => {
    // 插入数据库的操作
    console.log('图片全部上传完成')
})

// 加载图片
function getImg() {
    return new Promise((resolve, reject) => {
        let img = new Image()
        img.onload = function () {
            resolve(img)
        }
        img.src = 'http://www.xxx.com/xx.jpg'
    })
}

// 定义定时器,判断两秒内是否成功
function timeout() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('图片请求超时')
        }, 2000)
    })
}

// 如果图片为加载成功而定时器超时了->返回超时信息
// 如果两秒内图片加载成功-> 返回图片信息
// 这两个Promise对象中任意一个完成了 整个对象便算完成了
Promise.race([getImg(), timeout()]).then(res => {
    console.log(res)
}).catch(err => {
    console.log(err)
})

三、Generator

1. 原理

  • 从语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
    执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

  • 形式上,Generator 函数是一个普通函数,但是有两个特征。一是, function 关键字与函数名之间有一个星号;二是,函数体内部使用 yield 表达式, 定义不同的内部状态( yield 在英语里的意思就是“产出”)。

  • Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象

  • 必须调用遍历器对象的 next 方法(next方法可以传递参数),使得指针移向下一个状态。也就是说,每次调用 next 方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式(或 return 语句)为止。换言之,Generator 函数是分段执行的, yield 表达式是暂停执行的标记,而 next 方法可以恢复执行。

2. Generator的用法

Generator是可以暂停的,需要调用next方法手动执行, yield指令只能在生成器内部使用。

// 普通函数
function foo() {
    for (let i = 0; i < 3; i++) {
        console.log(i)
    }
}
foo()

// Generator
function* foo() {
    for (let i = 0; i < 3; i++) {
        yield i
    }
}
console.log(foo()) // 输出一个Generator对象 但是并不会输出 因为它需要我们手动输出
let f = foo()
console.log(f.next())
console.log(f.next())
console.log(f.next())
console.log(f.next())

// yield指令只能在生成器内部使用
function* gen(args) {
    args.forEach(item => {
        yield item + 1
    })
}

3. next()、yield()

由于 Generator 函数返回的遍历器对象,只有调用 next 方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。 yield 表达式就是暂停标志
遍历器对象的 next 方法的运行逻辑:

  1. 遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值,作为返回的对象的 value 属性值(暂停时的返回值)。
  2. 下一次调用 next 方法时(next传递的参数会作为暂停时yield处的返回值。),再继续往下执行,直到遇到下一个 yield 表达式。
  3. 如果没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value属性值(最后的返回值)。
  4. 如果该函数没有 return 语句,则返回的对象的 value 属性值为 undefined 。 需要注意的是, yield 表达式后面的表达式,只有当调用 next 方法、内部指针指向该语句时才会执行 ,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

yield 表达式与 return 语句既有相似之处,也有区别。

  • 相似之处:都能返回紧跟在语句后面的那个表达式的值。
  • 区别之处:每次遇到 yield ,函数暂停执行,下一次再从该位置继续向后执行,而 return 语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return 语句,但是可以执行多次(或者说多个)yield 表达式。正常函数只能返回一个值,因为只能执行一次 return ;Generator 函数可以返回一系列的值,因为可以有任意多 个 yield 。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历(“生成器”)。 Generator 函数可以不用 yield 表达式,这时就变成了一个单纯的暂缓执行函数。

function* gen(x) {
    let y = 2 * (yield(x + 1))
    let z = yield(y / 3)
    return x + y + z
}
let g = gen(5)
console.log(g.next()) // 6
console.log(g.next()) // NaN
console.log(g.next()) // NaN

let g = gen(5)
console.log(g.next()) // 6
console.log(g.next(12)) // y=24  8
console.log(g.next(13)) // z=13 x=5 42

// 计数器
function* count(x = 1) {
    while (true) {
    	// x为7的倍数时才暂停
        if (x % 7 === 0) {
            yield x
        }
        x++
    }
}
let n = count()
console.log(n.next().value) // 7
console.log(n.next().value) // 14
console.log(n.next().value) // 21
console.log(n.next().value) // 28
console.log(n.next().value) // 35

4. Generator异步状态管理

同样以ajax为例:

function ajax(url, callback) {
    // 1、创建XMLHttpRequest对象
    var xmlhttp
    if (window.XMLHttpRequest) {
        xmlhttp = new XMLHttpRequest()
    } else { // 兼容早期浏览器
        xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')
    }
    // 2、发送请求
    xmlhttp.open('GET', url, true)
    xmlhttp.send()
    // 3、服务端响应
    xmlhttp.onreadystatechange = function () {
        if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
            var obj = JSON.parse(xmlhttp.responseText)
            // console.log(obj)
            callback(obj)
        }
    }
}
// 封装请求方法,调用ajax
function request(url) {
    ajax(url, res => {
        getData.next(res) // 调用next,使Generator对象继续执行
    })
}

//Generator函数
function* gen() {
    let res1 = yield request('static/a.json')
    console.log(res1) // 返回第一次请求的结果
    let res2 = yield request('static/b.json')
    console.log(res2) // 返回第二次请求的结果
    let res3 = yield request('static/c.json')
    console.log(res3) // 返回第三次请求的结果
}
let getData = gen()
getData.next()

参考链接:

http://es6.ruanyifeng.com/#docs/generator
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
https://juejin.cn/post/6969028296893792286

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值