欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

一、异步编程

程序员文章站 2022-05-08 11:28:56
...

同步模式与异步模式

概述

  • JavaScript单线程工作模式原因:

    运行浏览器的脚本语言需要进行页面的交互,交互必然要操作DOM,DOM操作必须要单线程,否则会导致线程工作冲突产生线程同步问题(比如一个线程修改了DOM一个线程删除了DOM,此时浏览器就不知道以哪个为准)。所以JS执行环境中负责执行代码的线程只有一个

  • 这种模式有优点也有缺点,优点就是简单,不会产生多线程冲突。缺点就是容易阻塞,耗时任务会阻塞后续代码。

  • JS中将代码的执行模式分成了两种,一种是同步模式一种是异步模式。

同步模式

  • 即代码依此执行,执行顺序等同于我们的书写顺序

  • 代码执行会将一个匿名函数压入栈进行开始调用,这个匿名函数包含着当前文档中的所有代码

  • 函数的声明、变量的声明没有入栈操作

案例:

一、异步编程

函数执行会进行压入栈操作,图中右侧call stack,调用结束(不存在闭包的前提下)后会弹出栈,同步模式下按代码的书写顺序执行,但容易导致阻塞。

异步模式

  • 不去等待当前这个任务执行结束再开始下一个任务,可以避免阻塞现象。但是代码的执行顺序比较混乱。

  • 意义:单线程的js可以去执行大量耗时的任务

事件循环与消息队列

  • setTimeout也会有压栈操作,并借助WebAPIs形成一个计时器,倒计时结束后就进入宏任务队列排队

  • EventLoop的作用就是监听消息队列中的任务,是同步队列和异步队列中间的枢纽。

一、异步编程

一、异步编程

同步和异步不是以我们写代码的方式来决定的,而是运行环境提供的API是以同步还是异步模式工作来决定的。耗时的任务总得去等,只不过不是我们js线程去等,而是让浏览器提供的API来为我们去等。

异步编程的几种方式

1. 回调函数

  • 由调用者定义,执行者执行的函数就是回调函数。

示例:

function foo (callback) {
    setTimeout(() => {
        callback()
    }, 3000);
}

foo(function() {
    console.log('这是回调函数');
})

回调函数嵌套层级过多将产生回调地狱,不利于代码的编写和阅读。

2. Promise

  • 为了解决回调地狱,引入Promise。

  • Promise有三种状态Pending、Fulfilled、Rejected,其中Fulfilled和Rejected是结果,一旦状态改变到达结果后就不会再变了。到达Fulfilled和Rejected后都会有相应的回调作为响应,分别是onFulfilled和onRejected。

Promise基本示例:

const promise = new Promise((resolve, reject) => {
    resolve('成功的回调')
    reject(new Error('失败的回调'))  // 这个不会执行,状态一旦改变就不会执行了
})

promise.then(value => {
    console.log(value)
}, error => {
    console.log(error);
})

promise案例——简版ajax

function ajax(url) {
    return new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest()
        xhr.open('GET', url)
        xhr.responseType = 'json'
        xhr.onload = function () {
            if (xhr.status === 200) {
                resolve(xhr.responseText)
            }else{
                reject(new Error(xhr.statusText))
            }
        }
        xhr.send()
    })
}

ajax('./api.json').then((value) =>{
    console.log(value);
})
  • xhr.responseType = ‘JSON’ 这是H5新增的方法,能接受返回的结果为JSON类型。

  • xhr.onload 相当于onreadystatuechange中的status ==400.

Promise链式调用

要时用链式调用来代替promise的嵌套调用

  • promise通过.then后返回一个全新的promise对象,新的promise对象的状态取决于上一个promise的状态

  • 如果不断地调用then方法,这里的每一个then方法都是上一个then方法返回的promise对象提供一个状态明确后的回调

  • 我们也可以在then方法中手动返回一个promise对象,这样一来下面的then的中接收到的就是这个promise对象的状态改变后的回调了,then中的回调函数的参数就是这个promise最终的执行结果,所以这样就可以通过链式调用then,并且then中不断return新的promise来实现多个异步任务链式调用执行来避免嵌套了。

  • 前面的then方法中回调函数的返回值会作为后面then方法回调的参数。

  • 如果上一个then中return的不是一个promise,那么下一个then中的回调参数中接收到的就是这个非promise值。

  • 如果上一个没有返回任何值,那么下一个then中的回调函数的参数接收的就是undefined

如下图示例:

一、异步编程

promise 异常处理

  • 在promise中抛出异常(比如调用了不存在的函数啥的或者手动抛出一个异常),这种情况下回触发then的第二函数参数回调,也就是onRejected回调来捕获异常。当然我们更常用catch方法来捕获异常。更符合链式调用的风格。

  • 但是两种错误的捕获有不同的地方,首先then的链式捕获只能捕获当前处理的promise对象中的错误。而.catch会捕获链条从一开始传递下来的异常。

对比案例:

let p_success = new Promise((resolve,reject) =>{
    resolve('成功')
})

let p_fail = new Promise((resolve,reject) =>{
    reject('失败')
})

// 用then来捕获失败
p_success
.then(function onFulfilled (value) {
    console.log('onFulfilled', value)   // onFulfilled 成功
    return p_fail
}, function onRejected (error) {
    console.log('onRejected', error)	// 捕获不到P_fail
})



// 用catch来捕获失败
p_success
.then(function onFulfilled (value) {
    console.log('onFulfilled', value)	// onFulfilled 成功
    return p_fail
})
.catch(function onRejected (error) {
    console.log('onRejected', error)	// onRejected 失败
})

Promise的静态方法

  1. promise.resolve(value)

    把一个值转换为一个状态为fulfilled的promise对象,并且’value’将会作为这个promise对象返回的值,.then中的回调函数接收到的值就是value。

    如:

    Promise.resolve('foo')
     .then((value) =>{
         console.log(value);  // foo
    })
    
    // 上面写法等价于下面的:
    
    new Promise(function(resolve, reject){
        resolve('foo')
    })
     .then((value) =>{
         console.log(value);  // foo
    })
    
    

    如果value是另一个promise对象,那么这个promise对象会被原样返回。

    let promise = new Promise(()=>{})
    let promise2 = Promise.resolve(promise)
    console.log(promise ===promise2)  // true
    

    如果一个对象,它具有then属性,并且属性值也是有onFulfilled和onRejected这种函数的格式,就可以通过promise.resolve方法转成标注promise对象

    Promise.resolve({
        then:(onFulfilled, onRejected) =>{
            onFulfilled('foo')
        }
    })
    .then(function(value){
        console.log(value)
    })
    
  2. Promise.reject(value)

    这个方法没啥特别的,就是作为失败的回调而已。

    Promise.reject('foo')
     .catch((value) =>{
         console.log(value);  // foo
    })
    
    // 等同下面写法
    
    new Promise(function(resolve, reject) {
        reject('foo')
    })
    .catch((value) => {
        console.log(value);
    })
    

Promise并行执行

  1. Promise.all()

    可以将多个promise对象集合成一个promise去管理,生成一个新的promise对象

    只有all中的多个promise对象中都完成了,并且都是fulfilled状态,新的promise对象p1才是fulfilled状态。其中一个失败,新的promise也失败。

    let p1 = Promise.all([
        ajax('/api1.json'),
        ajax('/api2.json')
    ])
    
    p1.then((value) =>{
        console.log(value);   // [Array(2), Array(2)]
    })
    .catch((err) =>{
        console.log(err);
    })
    

    案例:同时使用并行和串行

    // 注意:promise 新建后(new后)会立即执行内部代码
    ajax('/api.json')
    .then(value => {
        const urls = Object.values(value)   // 解析ajax的promise对象中的resolve的结果,
                                            // 结果是个对象,提取对象中所有属性的属性值构成一个数组
        const tasks = urls.map(url => ajax(url))   // 对每一个属性值也就是url地址都调用ajax方法,得到
                                                   // 有着访问不同地址的任务的promise对象,这个对象里面已经开始执行任务了,
                                                   // 如果有结果后就存在一个新的tasks数组中,只是需要用then来开启。
    
        return Promise.all(tasks)
    })
    .then(values =>{
        console.log(values);
    })
    
  2. Promise.race()

    他只会等待第一个任务的结束,并决定race的状态。

    案例:ajax超时控制:

    const request = ajax('/api.json')
    const timeout = new Promise((resolve, reject) =>{
        setTimeout(() =>{   
            reject(new Error('timeout'))
        ,500})
    })
    
    Promise.race([     // 如果request没有在500毫秒返回数据,那么就会走timeout这个,并且race不会再处理request。
        request,
        timeout
    ])
    .then(value => {
        console.log(value);
    })
    .catch(error =>{
        console.log(error);
    })
    

宏任务和微任务

异步任务可以分为微任务和宏任务,微任务的优先级大于宏任务。

常用的宏任务和微任务:

宏任务 微任务
setTimeout定时器 Promise中的then、catch
事件的绑定 await

3. Generator异步方案

promise是链式调用,可读性并没有传统代码高,引出Generator生成器函数

语法:

function* foo() {
    console.log('start')
    // yield 'foo'    // 函数体中可以随时使用yield方法向外部返回值(返回的值是yield后面的值,会被next方法的返回值接收到)
					  // 并且yield方法会使生成器暂停执行
    try {
        const res = yield 'foo' // res的值为 ‘res’,这里如果跟一个promise那就就可以把promise的执行结果返回出去给next
    } catch (e) {
        console.log(e);
    }

}

const generator = foo() // 调用函数并不会立即执行函数体

generator.next()   // 调用生成器的next方法才会开始执行generator生成器的函数体,碰到yield就暂停执行直到再次调用next

// const result = generator.next() // 这个next方法的返回值就是yield关键字返回的值

generator.next('res')  // 从上一个yield停止的地方开始执行,这个res参数将会传递到generator中作为yield的返回值

generator.throw(new Error('Generator error')) // 也可以让生成器函数接着向下执行,不过同时也会向generator生成器中抛出一个异常,然后生成器内部可以用try...catch捕获

// 每一个g.next()的返回值都是生成器开始(或恢复)执行碰到的yield后面的那个promise任务函数调用

使用generator管理异步流程:

核心思想就是使用yield暂停生成器的执行流程

function* main() {
    const user = yield ajax('/api.json')
    console.log(user);
    const post = yield ajax('/post.json')
    console.log(post);
    const get = yield ajax('/get.json')
    console.log(get);
}

const g = main()
const result = g.next()

result.value.then(data => {
    const result2 = g.next(data)
    if(result2.done) return
    result2.value.then(data => {
        const result3 = g.next(data)  // next方法返回的是下一个result对象,里面包含异步任务promise的解析结果
        if(result3.done) return
        result3.value.then(data => {
            g.next(data)
        })
    })
})

// 执行流程:

// 首先定义了一个构造器,然后再外部调用main生成了这个构造器,然后通过g.next()开始执行构造器
// 碰到yield先执行yield后面的异步任务函数调用,并将异步任务函数调用返回的promise结果传递到【当次执行(第一次执行)】的g.next()的返回值对象的value属性中
// g.next()的返回值是个对象,里面的value属性存的是yield返回过来的值,里面的done属性代表当前是否生成器中的所有任务都被迭代完成,这个对象被result接收到,result中的value属性再调用then方法就能拿到成功回调后的结果
// 最后在then中将得到的执行结果通过g.next(data)再回传到generator生成器中的yield的返回值user中,这样在生成器中就拿到了最终的结果,让程序接着向下执行。

// 每一个g.next()的返回值都是生成器开始(或恢复)执行碰到的【下一个】yield后面的那个promise任务函数调用,也就是说第二个异步任务的结果解析时result2.value.then中的g.next()的返回值就应该是result3,也就是第三个异步任务

用递归的方式来优化生成器代码:

function* main() {
    try{
        const user = yield ajax('/api.json')
        console.log(user);
        const post = yield ajax('/post.json')
        console.log(post);
        const get = yield ajax('/get.json')
        console.log(get);
    }catch(e){
        console.log(e)
    }
}

// 上面是生成器部分,下面是处理结果部分

function co(generator){
    const g = generator()
    function handleResult (result){
        if(result.done) return
        result.value.then(data =>{
            g.next(data)
            handleResult(g.next(data))  // next方法返回的是下一个result对象,里面包含异步任务promise的解析结果
        },error => {
            g.throw(error)   // 捕获异常
        })
        if (result.done) return
    }
    handleResult(g.next())
}

co(main)

4. Async/Await语法糖(ES2017)

和generator有着相似的用法,不同的是async会返回一个promise对象,并且await只能用在async中不可以用在外部.

await 表示等一下,代码就暂停到这里,不再向下执行了,它等什么呢?等后面的promise对象执行完毕,然后拿到promise resolve 的值并进行返回,返回值拿到之后,它继续向下执行。

function ajax(url) {
    return new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest()
        xhr.open('GET', url)
        xhr.responseType = 'json'
        xhr.onload = function () {
            if (xhr.status === 200) {
                resolve(xhr.responseText)
            }else{
                reject(new Error(xhr.statusText))
            }
        }
        xhr.send()
    })
}

async function main() {
    const user = await ajax('/api.json')
    console.log(user);
    const post = await ajax('/post.json')
    console.log(post);
    const get = await ajax('/get.json')
    console.log(get);
}

// async 会返回一个promise对象
const promise = main()
promise.then(() =>{
    console.log('all completed')
})

上一篇: 遗传与环境

下一篇: 出双腿