03月14, 2018

谈谈js异步史

js异步的发展历程,还是比较有意思的。

在说这个主题之前,我觉得可以先思考一下,什么叫同步,什么叫异步,还有什么叫阻塞,什么叫非阻塞。

也许有些人觉得这概念比较简单,但在我看来,很容易混淆吧。

同步异步跟阻塞非阻塞的区别在于,同步异步是指被调用者,阻塞非阻塞是指调用者。

js的异步史,我觉得从三个node框架可以简单地看出一些端倪。express、koa1、koa2,这三个对应callback的时代、生成器generator yeild的时代、async await的时代。

下面展开说一下。

回调

这是远古时代的做法,比如我们用node读取一个文件,中规中矩的写法如下:

const fs = require("fs");
fs.readFile("文件Path", "utf8", function(err, data){
       if (err) { /* 打印错误信息 */ }
       // 没有出错信息的话,就可以直接拿到data,即文件的内容
})

如果需求是,读取文字中的内容,作为下一次读取文件的路径,那么就要写嵌套callback了。

它有几个问题:

  • 无法return
  • 不能try catch(在外部)
  • 回调地狱 效率非常低(串行)、难看、难以维护

说到try catch,莫名想到了一个问题,怎么让setTimeout里面的代码捕捉到异常。

之前好像看到过一个方案,重写setTimeout,然后在其里面上报异常。

事件发布订阅

这个算是callback的改良版,虽然好不到什么地方去。

比如说我们有这样一个需求,通过一份数据渲染一个模板,模板跟数据都在文件中。

let fs = require('fs');
let EventEmitter = require('events');
let eve = new EventEmitter();
let html = {};
eve.on('ready',function(key,value){
  html[key] = value;
  if(Object.keys(html).length==2){
    console.log(html);
  }
});
function render(){
  fs.readFile('template.txt','utf8',function(err,template){
    eve.emit('ready','template',template);
  })
  fs.readFile('data.txt','utf8',function(err,data){
    eve.emit('ready','data',data);
  })
}
render();

但其实本质上和callback没多大区别,因为callback里面也可以这样来写:

let fs = require('fs');

let after = function(times,callback){
  let result = {};
  return function(key,value){
    result[key] = value;
    if(Object.keys(result).length==times){
      callback(result);
    }
  }
}
let done = after(2,function(result){
  console.log(result);
});

function render(){
  fs.readFile('template.txt','utf8',function(err,template){
    done('template',template);
  })
  fs.readFile('data.txt','utf8',function(err,data){
    done('data',data);
  })
}
render()

只是说使用event的发布订阅这种方式,能让代码看起来,稍微优雅一些。

像react组件间的通讯也可以通过事件来实现。现在有了webpack,node内部实现的一些库都可以放到前端进行使用,比如说util模块之类的,不过我个人不是太推荐(因为这个也涉及到node版本,如果不同的版本有方法的删减,那就坑了)。

Promise

我个人觉得Promise和生成器Generator的出现,是一个异步编程的分水岭。

它解决了回调地狱的问题,能够让我们写代码时,一直不断地去then或者catch。

出去面试,好像很多地方都会问如何写一个Promise。

我们根据Promises/A+规范其实可以写一个简单的Promise版本出来:

// A promise must be in one of three states: pending, fulfilled, or rejected.
const PENDING = "pending",
    FULFILLED = "fulfilled",
    REJECTED = "rejected";


class Promise {
    constructor(executor) {
        let self = this;
        self.status = PENDING;
        self.value = undefined;
        self.onResolvedCallbacks = [];
        self.onRejectedCallbacks = [];
        function resolve(value) {
            if (value instanceof Promise) {
                return value.then(resolve, reject)
            }
            if (self.status == PENDING) {
                self.value = value;
                self.status = FULFILLED;
                self.onResolvedCallbacks.forEach(item => item(value));
            }
        }

        function reject(value) {
            if (self.status == PENDING) {
                self.value = value;
                self.status = REJECTED;
                self.onRejectedCallbacks.forEach(item => item(value));
            }
        }

        try {
            executor(resolve, reject);
        } catch (e) {
            reject(e);
        }
    }

    then(onFulfilled, onRejected) {
        let self = this;
        onFulfilled = typeof onFulfilled == 'function' ? onFulfilled : function (value) {
            return value
        };
        onRejected = typeof onRejected == 'function' ? onRejected : function (value) {
            throw value
        };
        let promise2;
        if (self.status == FULFILLED) {
            promise2 = new Promise(function (resolve, reject) {
                setTimeout(function () {
                    try {
                        let x = onFulfilled(self.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });

            });
        }
        if (self.status == REJECTED) {
            promise2 = new Promise(function (resolve, reject) {
                setTimeout(function () {
                    try {
                        let x = onRejected(self.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
            });
        }
        if (self.status == PENDING) {
            promise2 = new Promise(function (resolve, reject) {
                self.onResolvedCallbacks.push(function (value) {
                    setTimeout(function () {
                        try {
                            let x = onFulfilled(value);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    });
                });
                self.onRejectedCallbacks.push(function (value) {
                    setTimeout(function () {
                        try {
                            let x = onRejected(value);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    });
                });
            });
        }
        return promise2;
    }
}

function resolvePromise(promise2, x, resolve, reject) {
    if (x === promise2) {
        // throw new TypeError("循环引用");
        return reject(new TypeError("循环引用"));
    }
    let called;
    if (x != null && ((typeof x === 'function') || (typeof x === 'object'))) {
        try {
            let then = x.then;
            if (typeof then === "function") {
                then.call(x, function (y) {
                    if (called) return;
                    called = true;
                    resolvePromise(promise2, y, resolve, reject);
                }, function (err) {
                    if (called) return;
                    called = true;
                    reject(err);
                });
            } else {
                resolve(x);
            }
        } catch (e) {
            if (called) return;
            called = true;
            reject(e);
        }
    } else {
        resolve(x);
    }
}
module.exports = Promise;

这里有几个点解释一下:

  • 关于resolve的value,如果是promise,则继续then,直到出来结果为止

alt

考虑的代码写法为:

let promise = new Promise(function(resolve, reject) {
    resolve(new Promise(function(resolve, reject) {
        setTimeout(() => {
            resolve(100);
        }, 1000);
    }))
})

promise.then((data) => {
    console.log(data);
})
  • 关于promise.then可以穿透

alt

alt

也就是说我们可以这样书写代码:

let p = new Promise((resolve, reject) => {
     resolve(100);
});

p.then().then((data) => {
     console.log(data);
})

因此需要判断一下传入的onFulfilled和onRejected参数

alt

  • 关于resolvePromise方法中,called变量

这个是因为有可能别人的promise既调用了resolve,又 调用了reject,所以需要这个别人来控制一下。

alt

  • 关于onFulfilled和onRejected使用了setTimeout

在promiseA+文档中有指出:

alt

上面实现的promise版本,其实是有问题的,在这一块。浏览中原生的Promise是微任务,它会优先于setTimeout(宏任务)执行。

console.log(1);
setTimeout(()=>{
    console.log(2);
});
new Promise(function(resolve, reject) {
     console.log(3);
     resolve(100);
}).then((data) => {
     console.log(4)
})
console.log(5);

打印结果为:1 3 5 4 2

其实这里有几个面试的发散点:

  • 说一下event loop
  • 说一下vue中的nextTick的实现方式(MutationObserver、MessageChannel)

另外这里简单扯一下promiseA+的测试:

npm i -g promises-aplus-tests
promises-aplus-tests 你的promise文件

不过在运行promises-aplus-tests脚本前,要在你写的Promise里面挂一个静态属性:

Promise.deferred = Promise.defer = function () {
    let defer = {};
    defer.promise = new Promise(function (resolve, reject) {
        defer.resolve = resolve;
        defer.reject = reject;
    })
    return defer;
}

因为在它的源码中有使用到:

alt

在测试中做的事就是跑这些用例(大概有800多个)

alt

不过上面实现的Promise是阉割版的,它只是简单还原了PromiseA+的规范。

像实例上的catch,静态方法esolve、reject、all、race这些都需要在上面代码的基础上做一些扩展。

生成器Generator

刚接触es6那会儿,我觉得最难理解的就是这个了,什么yield的,很头疼。

其实本质上就相当于起到了暂停的功能,就相当于我们开车上高速从A到B,如果路程长的话,只能中间到服务区休息会儿,再出发。即函数不再是一次执行到底,而是分段执行。

用法这一块大家可以参考:ES6 Generator介绍

看完文章可以思考下面的代码运行的结果是啥:

function *go () {
    console.log(1);
    // 此处的b是供外界输入值进来的
    // 这一行实现输入与输出,本次的输出放在yield后面,下次的输入放在yield前面
    let b = yield "a"; 
    console.log(2);
    let c = yield b;
    console.log(3);
    return c;
}

let it = go();
let r = it.next();
console.log(r);
r = it.next("XXX");
console.log(r);
r = it.next("c的值");
console.log(r);

co

这个库有意思了,它就相当于自执行一段Generator。

const fs = require("fs");
const co = require("co");

function readFile(filename) {
    return new Promise((resolve, reject) => {
        fs.readFile(filename, "utf8", (err, data) => {
            if (err) return reject(err);
            resolve(data);
        })
    })
}

function *read() {
    let a = yield readFile("./1.txt");
    console.log(a);
    let b = yield readFile("./2.txt");
    console.log(b);
    return b;
}

co(read).then((data) => {
    console.log("data: ", data);
})

co的源码实现其实比较简单:

function co(gen) {
    let it = gen(); // 我们要让我们的生成器不断地执行
     return new Promise((resolve, reject) => {
        ~function next(lastVal) {
            let { value, done } = it.next(lastVal);
            if (done) {
                resolve(value);
            } else {
                value.then(next, reject);
            }
        }();
    })
}

看到这里,相信大家也有点清楚async await的语法糖大概是怎么回事了。

async await语法糖

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

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

可以说async await就是建立在Generator和co以及promise的基础上实现的。

当然上面readFile方法写的略麻烦,完全可以考虑用util这个模块的promisify,将方法改造为promise。

本文链接:www.my-fe.pub/post/talk-about-js-async.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。