menu 光风霁月。
JS 异步编程
451 浏览 | 2020-05-21 | 阅读时间: 约 2 分钟 | 分类: Javascript 原理 | 标签: Javascript,异步
请注意,本文编写于 242 天前,最后修改于 241 天前,其中某些信息可能已经过时。

前言

单线程是JavaScript的程序运行方式

JS 对异步编程的支持方式分为三个阶段(也就是异步的发展历程):

  • 回调(callback) 阶段
  • 承诺(promise)阶段
  • 生成器(generator)阶段
  • async await

异步编程技术的主要使用场景:

  • 网络请求(Ajax)
  • 文件系统操作(读写文件)
  • 刻意的时间延迟功能(警告)

scope(范围)和异步执行

function countdown() {
    let i;
    console.log("Countdown:");
    for (i = 5; i >=0 ; i--) {
        setTimeout(function() {
            console.log(i === 0 ? "GO!" : i);
        }, (5 - i) * 1000);
    }
}
countdown();
function countdown() {
    console.log("Countdown:");
    for (let i = 5; i >= 0; i--) {
         setTimeout(function() {
            console.log(i === 0 ? "GO!" : i);
        }, (5 - i) * 1000);
    }
}
countdown();

仔细观察以上两段代码,唯一不同的地方就是关于 i 的声明位置,但是两段代码的执行结果是完全不同的。

原因如下:

  • 第一个函数在打印 i 的时候,只有一个共享的变量 i, i 早已经被赋值为 -1
  • 第二个函数在打印 i 的时候,每个回调函数都有一个 ii 的值各不相同

因此要牢牢记住这句话:注意回调函数的作用域,回调函数可以访问闭包内的所有内容。

Node 错误优先回调:

const fs = require('fs');
const fname = 'abc.txt';
fs.readFile(fname, (err, data) => {
    if (err) return console.error('error reading file');
    console.log(`${fname} contents: ${data}`);
})

回调函数中,所要做的第一件事就是判断 err 是否为真!然后再 do something

如果在写一个使用回调的接口,强烈建议坚持遵守错误优先的约定!

回调地狱

const fs = require('fs');
fs.readFile('a.txt', (err, dataA) => {
    if (err) console.error(err);
    fs.readFile('b.txt', (err, dataB) => {
        if(err) console.error(err);
        fs.readFile('c.txt', (err, dataC) => {
            if(err) console.error(err);
            setTimeout(() => {
                fs.writeFile('d.txt', dataA + dataB + dataC, (err) => {
                    if (err) console.error(err);                    
                });
            }, 60 * 1000);
        });
    });
});

回调嵌套的太多导致思路混乱不清晰,如果再加上一些 try catch 语句的话,就会更加爆炸

function readSketchyFile() {
    try {
        fs.readFile('abc.txt', (err, data) => {
            if (err) throw err;
        });
    } catch(err) {
        console.log('something bad happened')
    }
}

try catch 只会捕捉同一个函数内(在本例中是 readSketchyFile )的异常,而异常的抛出确是在匿名回调函数中产生的,因此程序运行仍然会崩溃。

Promise (承诺)

创建 promise

function launch() {
    return new Promise((resolve, reject) => {
        if (true) return;
        console.log('Lift off');
        setTimeout(() => {
            resolve('In orbit!')
        }, 2 * 1000)
    })
}

使用 promise

launch()
    .then(() => {
        console.log('resolve 的结果');
     })
    .catch(() => {
         console.log('reject 的结果');
     });

事件

const EventEmitter = require('events').EventEmitter;

class Countdown extends EventEmitter {
    constructor(seconds, superstitious) {
        super();
        this.seconds = seconds;
        this.superstitious = !!superstitious;
    }
    go() {
        const countdown = this;
        const timeoutIds = [];
        return new Promise((resolve, reject) => {
            for (let i = countdown.seconds; i >= 0; i--) {
                timeoutIds.push(setTimeout(() => {
                    if (countdown.superstitious && i === 13) {
                        timeoutIds.forEach(clearTimeout);
                        return reject(new Error("DEFINITELY NOT COUNTING THAT"))
                    }
                    countdown.emit('tick', i);
                    if (i === 0) {
                        resolve();
                    };
                }, (countdown.seconds - i) * 1000));
            }
        });
    }
}

function launch() {
    return new Promise((resolve, reject) => {
        if (Math.random() < 0.5) return;
        console.log('Lift off');
        setTimeout(() => {
            resolve('In orbit!')
        }, 2 * 1000)
    })
}

function addTimeout(fn, timeout) {
    if (timeout === undefined) timeout = 1000;
    return () => {
        return new Promise((resolve, reject) => {
            const tid = setTimeout(reject, timeout, new Error('Promise timed out'));
            fn()
                .then(() => {
                    clearTimeout(tid);
                    resolve();
                })
                .catch(() => {
                    clearTimeout(tid);
                    reject();
                });
        });
    }
}

const c = new Countdown(5)
    .on('tick', i => console.log(i + '...')); 

c.go()
    .then(addTimeout(launch, 4 * 1000))
    .then((msg) => {
        console.log(msg);
    })
    .catch((err) => {
        console.error('Houston, we have a problem' + err.message);
    });

promise 链式调用

当一个promise被满足时,可以立即调用另一个返回promise的函数,不用在每一步都捕捉错误

需要注意的是:

  • .then() 中的参数必须是函数
  • .then((err) => { console.log(err) }) 参数 err 是当前 promise 实例 resolve 出的
  • .catch() 如果错误在任何一环发生,promise链都会停止,调到 catch 中

避免不被处理的promise

promise可以简化异步代码,同时确保回调函数不被多次调用,但却不能避免那些因为promise没有被处理而产生的问题( 既没有调用 resolve, 也没有调用 reject )

function launch() {
    return new Promise((resolve, reject) => {
        if (Math.random() < 0.5) return;
        console.log('Lift off');
        setTimeout(() => {
            resolve('In orbit!')
        }, 2 * 1000)
    })
}

只有在 随机数 >= 0.5 时 resolve 才被调用

解决办法:给 promise 一个特定的超时,在以上代码中可以得到展示。

异步错误的捕获

try {
    setTimeout(() => {
        throw new Error('FAIL');
    }, 1000);
} catch(err) {
    console.log(err);
}

上述代码在运行时的错误不会被捕获,已经脱离了 try catch 的上下文环境。

解决办法:

① 异步代码内部捕获错误

setTimeout(() => {
    try {
        throw new Error("FAIL");
    } catch (err) {
        console.log(err);
    }
})

② 在回调函数内部直接捕获错误

var p1 = () => {
    return new Promise((resolve, reject) => {
        throw new Error('p1_同步_err');         //代码1
        setTimeout(() => {
            console.log('p1执行');
            resolve(true);
            // throw new Error('p1_异步_err');  //代码2
            // reject('p1_rej');                //代码3
        }, 1000);
    });
}

var p2 = () => {
    return new Promise((resolve, reject) => {
        // throw new Error('p2_同步_err');      //代码4
        setTimeout(() => {
            console.log('p2执行');
            // throw new Error('p2_异步_err');  //代码5
            // reject('p2_rej');                //代码6
        }, 1000);
    });
}

p1()
    .then()
    .catch((err) => {
        console.log('我错了' + err);
    })

执行结果:

  • 代码1 代码4 两处的异常可以被catch到
  • 代码2 代码5 两处的异常不可以被catch到
  • 代码3 代码6 两处的异常可以被catch到

执行要求:

  • 代码1-6只能执行其中一个
  • 如果要捕获 代码3 处的异常必须使 resolve(); 语句失效

结论:

  • 如果是同步执行,throw出去的错误可以捕获,而异步执行的不可以
  • 如果是异步执行,throw出去的错误不可以被捕获,只能通过reject来传递。

使用async和await

async function doSomething() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            throw new Error("fail");
        }, 1000);
    });
}

async function main() {
    try {
        await doSomething();
    } catch (e) {
        console.log(e.message);
    }
}

main();
知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

发表评论

email
web

全部评论 (暂无评论)

info 还没有任何评论,你来说两句呐!