# 温习异步

我们知道 Javascript 语言的执行环境是 单线程。也就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。

在开始之前请大家思考下,我们在前面的环节中大量使用了 async/await,那么它究竟是什么?

# 什么是异步?

异步指的是每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。这种模式虽然实现起来比较简单,执行环境相对单纯,但是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段 Javascript 代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

# 回调实现异步

小明在看电视,但他肚子饿了。同步的做法是先吃饭后看电视。异步的做法是一边吃饭一边看电视。实现异步的本质是回调,我们使用 nodejs 的 fs 这个 API 接口来看一下异步与同步的区别:

const fs = require('fs')

// 读取文件
let txt1 = fs.readFileSync('./key.txt')
console.log(txt1.toString(), '1')

let txt2 = fs.readFile('./key.txt', (err, txt3) => {
  console.log(txt3.toString(), '2')
})

console.log(txt2, '3')

运行结果如下, readFileSync 是一个同步方法,所以当我们打印 txt1 的时候立刻有返回值。而当我们打印 txt2 的时候可以看到返回值是一个 undefined,这说明没有返回值,而数据在 txt3 的回调里打印出来的。

在 NodeJs 中这种 API 太多了,异步是不能直接返回值的,当我们的项目大时又或者随着项目的不断迭代,久而久之嵌套就会越来越多,这个时候代码是你不愿意去维护也没法维护的,这还牵扯出来一个比较好听的名字,叫回调地狱,我们后面再讲该如何优化。

# 异步函数

我们来动手创建一个异步函数,在这里我们来通过 setTimeout 来模拟异步任务, setTimeout 也是回调,这种回调错误不好处理,默认第一个参数为错误,如果有第一个参数,则说明发生了错误,那么我们就要进行相应的处理。

function asynchronous(text, fn) {
  setTimeout(() => {
    fn(null, '-' + text + '-')
  }, 3000)
}

asynchronous('hello nodejs', (err, text) => {
  if(err) console.log(err)
  console.log(text)
})

// 3s  hello nodejs

# 事件实现异步

事件这个词汇相信大家都不陌生,在学习编程的道路上大家或多或少都接触过前端。在浏览器和页面交互时有各种事件,我们用的比较多的就是点击事件,比如当用户点击某个 HTML 元素时执行一段代码。而在 NodeJs 中,也有一个 events 的事件模块,事件的本质就是发布订阅设计模式。之所以能异步,还是因为回调。

class Evente {
    private map: any = {}

    // 监听器
    public on(name: string, fn: Function) {
        if (this.map[name]) {
            this.map[name].push(fn)
            return this
        }
        this.map[name] = [fn]
        return this
      }

    // 发射器
    public emit(name: string, ...args: any[]) {
        if (this.map[name]) {
            this.map[name].forEach((fn: Function) => {
                fn(...args)
            })
        }
        return this
    }
}

我们以 name 作为 key 放到 map 里,也就是事件名称。当触发 emit 方法的时候,获得对应的回调进行调用:

const evente = new Evente()

evente.on('click', (error: string, result: any) => {
    if (error) return console.log(`error: ${error}`)
    console.log(`result: ${result}`)
})
.emit('click', '执行错误!')
.emit('click', null, '执行成功!')

NodeJs 里不少模块都是基于事件模块构建的,因为事件的里面还是有回调,对于处理错误而言,还是把第一个参数作为错误传递。

我们来使用事件模式重新完成我们一开始的例子:

const evente = new Evente()

evente.on('readFile', (error: string, result: any) => {
    if (error) return console.log(`error: ${error}`)
    console.log(`result: ${result}`)
})
fs.readFile('./key.txt', (err: any, data: any) => {
    evente.emit('readFile', err, data)
})

好像变的更麻烦了一样,对于简单的异步我们没有必要使用事件来完成异步,在这里只是为大家做演示。

# 观察者模式实现异步

观察者模式或者说订阅模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。

function observableCreate(fn: Function) {
    let ret = false
    return ({
        next,
        complete,
        error,
    }: any) => {
        // 通知:传递给观察者的实际数据
        function nextFn(...args: any[]) {
            if (ret) return
            next(...args)
        }
        // 通知:按理来说这步不会发送任何值
        function completeFn(...args: any[]) {
            complete(...args)
            ret = true
        }
        // 通知:发送一个错误或异常
        function errorFn(...args: any[]) {
            error(...args)
        }
        fn({
            next: nextFn, complete: completeFn, error: errorFn
        })
        return () => (ret = true)
    }
}

当订阅下面代码中的 Observable 的时候会立即(同步地)推送值1、2、3,然后1秒后会推送值4、5,再然后是完成流,因为 complete 调用之后,把 ret 设置为 true,complate 的字面意思就是完成,完成了之后 next 就不会再生效了,有错误的时候用 error 发出即可。

const observerable = observableCreate((observer: any) => {
    try {
        observer.next(1)
        observer.next(2)
        observer.next(3)
        setTimeout(() => {
            observer.next(4)
            observer.complete('完成!')
        }, 1000)
    }catch(err) {
        observer.error(err)  // 如果捕获到异常会发送一个错误
    }
})

要调用 observerable 并看到这些值,我们需要订阅 observerable:

const subject  = {
    next: (value: any) => console.log(value, 'next'),
    complete: (value: any) => console.log(value, 'complate'),
    error: (value: any) => console.log(value, 'error')
}

const unsubscribe = observerable(subject)

# Primise 实现异步

Primise 是 ES6 的新特性,为了处理异步而生,本质还是回调。只不过 Promise 比传统的回调和事件更合理且更强大。ES6 将其写入了语言标准,统一了用法,所以我们通常会使用 Promise 来实现异步编程。

// Promise 接受一个函数作为参数,该函数的两个参数分别的是 resolve:成功,reject:失败
const promise = new Promise((resolve, reject) => {
  if (/* 条件满足 */) return resolve(result)
   reject(error)
})

Promise 实例生成后,可用then 和 catch 方法分别指定两种状态回调参数:

promise.then(console.log)
.catch(console.log)

# async/await 大杀器

相信大家也注意到了我们在实战环节中,都是使用的 async/await , ES7 标准引入了 async 函数,使得异步操作变得更加方便,async 其实本质是Generator函数的语法糖。async/await 应该是目前最简单的异步解决方案了,我们先来看个例子:

const sleep = (time: number) => {
    return new Promise(function (resolve: any, reject: any) {
        setTimeout(() => {
            resolve();
        }, time);
    })
};

const start = async() => {
    console.log('start');
    await sleep(3000);
    console.log('end');
};

start();

async 表示 是一个异步函数,await 只能用在这个函数里面。await 表示在这里等待 Promise 返回结果了,再继续执行。await 后面跟着的应该是一个 Promise 对象。

阅读全文