异步的发展历程总结

365 阅读7分钟

1. 异步

所谓"异步" 简单说就是一个任务分成两段, 先执行第一段, 然后转而执行其他任务, 等做好了准备, 再回过头来执行第二段.

高阶函数

函数既可以作为参数,也可以作为返回值

高阶函数的英文名叫 Higher-Order Function,熟悉 React 的朋友应该知道高阶组件 Higher-Order Component。没错,React 的高阶组件本质上就是高阶函数。

那么,什么是高阶函数呢?

高阶函数源自于函数式编程,是函数式编程的基本技术。

那么,JS作为一门“一切皆为对象”的语言,是如何拥有函数式编程的能力呢?

是因为在JS中函数是一等公民,即函数可以被赋值给变量,被变量引用,这便使得函数可以作为参数,在其他函数间相互传递:

函数当一个参数传递

/** 
 * 数值转换
 * @param {Number} val 要被处理的数值
 * @param {Function} fn 处理输入的val
 * @return {Number || String}
 */
const toConvert = function(val, fn) {
    return fn(val);
};

const addUnitW = function(val) {
    return val + 'W';
};

toConvert(123.1, Math.ceil); // 124
toConvert(123.1, addUnitW); // "123.1W"

函数当一个返回值

// 判断一个变量的类型

function isType(type) {
  return function (param) {
    return Object.prototype.toString.call(param) == `[object ${type}]`
  }
}
let isString = isType('String')
let isArray = isType('Array')
console.log(isString('nan')); // true
console.log(isArray({}));  // false

函数即做参数又做返回值

假设有这样一个需求, 有个函数需要连续调用三次才可以执行, 你可以提前思考一下怎么写

function eat() {
  console.log("吃完了");
}
function after(time, fn) {
  let count = 0
  return function () {
    if(count++ === time) {
      fn()
    }
  }
}
let newEat = after(3, eat)
newEat()
newEat()
newEat()

异步编程的语法目标, 就是怎么样让它更像同步编程一样

  • 回调函数
  • 事件监听
  • 发布订阅
  • Promise/A+ 和 生成器函数
  • async/await

回调

所谓回调函数, 就是把任务的第二段单独写在一个函数里面, 等到重新执行这个任务的时候, 就直接调用这个函数

fs.readFile('某个文件', function(err, data) {
  if(err) throw err
  console.log(data)
})

回调函数的问题:

  1. 无法捕获错误 try catch
  2. 不能return
  3. 回调地狱 (如先读取A接口的数据,再根据A接口返回的数据去读取B接口的数据再根据B的返回的内容读取C..)

事件监听

发布订阅

// 利用发布订阅的模式, 读取html模板和数据, 当两者都存在时输出
const Event = require('events')
const fs = require('fs')

let eve = new Event()
let html = {}

eve.on('ready', function (key, value) {
  html[key] = value
  // 当两个结果都有了
  if(Object.keys(html).length == 2) {
    console.log(html);
  }
})  // 注册

fs.readFile('./template.txt','utf8', function (err, template) {
  eve.emit('ready', 'data', template)
})
fs.readFile('./data.txt', 'utf8', function (err, data) {
  eve.emit('ready', 'data', data)
})


/********************************************/
// 以上这样方式简写的形式可以用一个"哨兵函数来监听"

const fs = require('fs')
/*
let html = {}
// 哨兵函数
function done (key, value) {
  html[key] = value
  if(Object.keys(html).length == 2) {
    console.log(html);
  }
}
**/
// 给哨兵函数封装一下
/**********************************************/
function reader (length, fn) {
  let html = {}
  return function (key, value) {
    html[key] = value
    if(Object.keys(html).length == length) {
      fn(html)
    }
  }
}
let done = reader(2, function (html) {
  console.log(html)
})
/**********************************************/
fs.readFile('./data.txt','utf8', function (err, template) {
  done('template', template)
})
fs.readFile('./demo.txt', 'utf8', function (err, data) {
  done('data', data)
})

生成器

生成器是一个函数, 可以用来生成迭代器

Generator 函数是 ES6 提供的一种异步编程解决方案,整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。

function* gen() {
    let a = yield 111;
    console.log(a);
    let b = yield 222;
    console.log(b);
    let c = yield 333;
    console.log(c);
    let d = yield 444;
    console.log(d);
}
let t = gen();
//next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值
t.next(1); //第一次调用next函数时,传递的参数无效
t.next(2); //a输出2;
t.next(3); //b输出3; 
t.next(4); //c输出4;
t.next(5); //d输出5;

为了让大家更好的理解上面代码是如何执行的,我画了一张图,分别对应每一次的next方法调用:

Promise

手写promise

Promise.all的原理

function gen(times, fn) {
  return function (i, data) {
    if(i+1 == times) {
      fn(data)
    }
  }
}

Promise.all = function (promises) {
  return new Promise(function (resolve, reject) {
    let done = gen(promises.length, resolve)
    for (let i = 0; i < promises.length; i++) {
      promises[i].then(function (data) {
        done(i, data)
      }, reject)
    }
  })
}

Promise.race的原理:
Promise.race = function (promises) {
  return new Promise(function (resolve, reject) {
    for (let i = 0; i < promises.length; i++) {
      promises[i].then(resolve, reject)
    }
  })
}

Promise 的业界实现都有哪些?

q(早期angular项目中用过) bluebird 这两个库

let fs = require('fs');
let bluebird = require('bluebird'); // bluebird中的一个方法, 可以让异步转化为promise
let read = bluebird.promisify(fs.readFile);

read('1.txt','utf-8').then(function(data){
    console.log(data);
})

***********实现原理*************
function promisify(fn) { 
    return function (...args) {
        return new Promise(function (resolve, reject) {
            fn(...args, function (err, data) {
                if (err) reject(err);
                resolve(data);
            })
        })
    }
}
function *read () {
    console.log('开始');
    let a = yield readFile('1.txt');
    console.log(a)
    let b = yield readFile('2.txt');
    console.log(=b)
    let c = yield readFile('3.txt');
    console.log(c)
    return c;
}

function co(it){
    return new Promise((resolve,reject)=>{
        function next(data){
            let { value,done } = it.next(data)
            if(!done){
                value.then((data)=>{
                    next(data)
                },reject)
            }else{
                resolve(value)
            }
        }
        next()
    })
}

手写promise简单例子

// 完整代码 也顺便带大家理顺一下
function Promise(executor) {
    let self = this;
    self.value = undefined;  // 成功的值
    self.reason = undefined;  // 失败的值
    self.status = 'pending'; // 目前promise的状态pending
    self.onResolvedCallbacks = []; // 可能new Promise的时候会存在异步操作,把成功和失败的回调保存起来
    self.onRejectedCallbacks = [];
    function resolve(value) { // 把状态更改为成功
        if (self.status === 'pending') { // 只有在pending的状态才能转为成功态
            self.value = value;
            self.status = 'resolved';
            self.onResolvedCallbacks.forEach(fn => fn()); // 把new Promise时异步操作,存在的成功回调保存起来
        }
    }
    function reject(reason) {  // 把状态更改为失败
        if (self.status === 'pending') { // 只有在pending的状态才能转为失败态
            self.reason = reason;
            self.status = 'rejected';
            self.onRejectedCallbacks.forEach(fn => fn()); // 把new Promise时异步操作,存在的失败回调保存起来
        }
    }
    try {
        // 在new Promise的时候,立即执行的函数,称为执行器
        executor(resolve, reject);
    } catch (e) { // 如果执行executor抛出错误,则会走失败reject
        reject(e);
    }
}

// 这个函数为核心,所有的promise都遵循这个规范
// 主要是处理then中返回的值x和promise2的关系
function resolvePromise(promise2,x,resolve,reject){
    // 当promise2和then返回的值x为同一个对象时,变成了自己等自己,会陷入死循环
    if(promise2 === x){
        return reject(new TypeError('Chaining cycle'));
    }
    let called;
    // x可能是一个promise也可能是一个普通值
    if(x!==null && (typeof x=== 'object' || typeof x === 'function')){
        try{
            let then = x.then; 
            if(typeof then === 'function'){
                then.call(x,y=>{ 
                    if(called) return; 
                    called = true;
                    resolvePromise(promise2,y,resolve,reject);
                },err=>{ 
                    if(called) return;
                    called = true;
                    reject(err);
                });
            }else{
                resolve(x);
            }
        }catch(e){
            if(called) return;
            called = true;
            reject(e);
        }
    }else{ 
        resolve(x);
    }
}

// then调用的时候,都是属于异步,是一个微任务
// 微任务会比宏任务先执行
// onFulfilled为成功的回调,onRejected为失败的回调
Promise.prototype.then = function (onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function'?onFulfilled:val=>val;
    onRejected = typeof onRejected === 'function'?onRejected: err=>{throw err}
    let self = this;
    let promise2;
    // 上面讲了,promise和jquery的区别,promise不能单纯返回自身,
    // 而是每次都是返回一个新的promise,才可以实现链式调用,
    // 因为同一个promise的pending resolve reject只能更改一次
    promise2 = new Promise((resolve, reject) => {
        if (self.status === 'resolved') {
            // 为什么要加setTimeout?
            // 首先是promiseA+规范要求的
            // 其次是大家写的代码,有的是同步,有的是异步
            // 所以为了更加统一,就使用为setTimeout变为异步了,保持一致性
            setTimeout(()=>{
                try { // 上面executor虽然使用try catch捕捉错误
                      // 但是在异步中,不一定能够捕捉,所以在这里
                      // 用try catch捕捉
                    let x = onFulfilled(self.value);
                    // 在then中,返回值可能是一个promise,所以
                    // 需要resolvePromise对返回值进行判断
                    resolvePromise(promise2,x,resolve,reject);
                } catch (e) {
                    reject(e);
                }
            },0)
        }
        if (self.status === 'rejected') {
            setTimeout(()=>{
                try {
                    let x = onRejected(self.reason);
                    resolvePromise(promise2,x,resolve,reject);
                } catch (e) {
                    reject(e);
                }
            },0)
        }
        if (self.status === 'pending') {
            self.onResolvedCallbacks.push(() => {
                setTimeout(()=>{
                    try {
                        let x = onFulfilled(self.value);
                        resolvePromise(promise2,x,resolve,reject);
                    } catch (e) {
                        reject(e);
                    }
                },0)
            });
            self.onRejectedCallbacks.push(() => {
                setTimeout(()=>{
                    try {
                        let x = onRejected(self.reason);
                        resolvePromise(promise2,x,resolve,reject);
                    } catch (e) {
                        reject(e);
                    }
                },0)
            });
        }
    });
    return promise2
}
Promise.defer = Promise.deferred = function(){
    let dfd = {};
    dfd.promise = new Promise((resolve,reject)=>{
        dfd.resolve = resolve;
        dfd.reject = reject;
    })
    return dfd;
}
module.exports = Promise;

异步的终极方案 async await

async await解决了异步问题:

  1. 可以让代码像同步

  2. 可以使用try catch

  3. 可以使用promise

  4. 如果let r1 = await 后面等待的是promise,那么会把promise的结果赋值给前面的r1,如果let r1 = await 后面等待的是普通值,那么就会把这个普通值赋值给前面的r1

设为 Flex 布局以后,子元素的 float 、 clear 和 vertical-align 属性将失效

async函数的实现, 就是将Generator函数和自动执行器,包装到一个函数中

async function read() {
    let template = await readFile('./template.txt');
    let data = await readFile('data.txt');
    return template + '+' + data
}

//等同于
function read () {
    return co(function *() {
        let template = yield readFile('template.txt');
        let data = yield readFile('data.txt');
        return template + '+' + data
    })
}