JS异步编程的几种方式

1,283 阅读8分钟

本文难点:手写一个符合Promise/A+规范的Promise

一、回调函数

  • 把B函数被作为参数传递到A函数里,在A函数执行完后再执行B
  • 优点:回调函数是异步编程最基本的方法,简单、容易理解和部署。
    function f1(callback){
        setTimeout(function () {
            // f1的任务代码
            callback();
        }, 1000);
    }
  • 缺点:不利于代码的阅读和维护,比如回调地狱;
    let fs = require('fs');
    fs.readFile('1.txt','utf8',function(err,data){
        fs.readFile(data,'utf8',function(err,data){
            fs.readFile(data,'utf8',function(err,data){
                console.log(data);
            });
        });
    });
  • 注意:注意区分同步回调和异步回调

拓展:高阶函数

1、接收一个或多个函数作为参数
    //after函数
    function after(times,callback){
        return function(){
            times--;
            if(times == 0){
                callback();
            }
        }
    }
    let fn = after(3,function(){
        console.log("调用三次后再执行")
    })
    fn()
    fn()
    fn()
2、输出另一个函数
    //检测数据类型方法
    function isType(type) { // [object String]
        return function (content) {
            let t = Object.prototype.toString
                .call(content).replace(/\[object\s|\]/g, '');
            return t === type
        }
    }
    let types = ['String','Undefined','Function','Number'];
    let util = {}; // util.isString isUndefined
    types.forEach(t=>{
        util['is'+t] = isType(t);
    });
    console.log(util.isString('hello'));

二、事件的绑定、监听、委托

  • 直接绑定事件-只能绑定一次
    //onclick、onmouseover、onmouseout、onmousedown、onmouseup、ondblclick、onkeydown、onkeypress、onkeyup等
    document.getElementById("btn").onclick = function(){
        console.log("hello world!");
    }
  • 事件监听- addEventListener() 或 attachEvent()
    //可绑定多个事件
    document.getElementById("btn").addEventListener("click",hello);
    function hello(){
        console.log("hello world!");
    }
  • 事件委托-事件委托就是利用冒泡的原理,把事件加到父元素或祖先元素上,触发执行效果
    var btn = document.getElementById("btn");
    document.onclick = function(e){
        e = e || window.event;
        var target = e.target || e.srcElement;
        if(target == btn){
        console.log("委托成功")
    }
  • 注意:浏览器兼容性

三、发布订阅

  • 发布订阅模式:基于一个主题/事件通道,希望接收通知的对象(称为subscriber)通过自定义事件订阅主题,被激活事件的对象(称为publisher)通过发布主题事件的方式被通知。
  • 观察者模式:一个对象(称为subject)维持一系列依赖于它的对象(称为observer),将有关状态的任何变更自动通知给它们(观察者)。
    //需求:读取两次文件后输出结果
    let fs = require("fs");
    let event = {
        arr: [],
        result: [],
        on(fn) {
            this.arr.push(fn)
        },
        emit(data) {
            this.result.push(data);
            this.arr.forEach(fn => fn(this.result)); //执行订阅的内容
        }
    }
    event.on(function (data) { //订阅
        if (data.length === 2) {
            console.log(2, data); //输出
        }
    })
    fs.readFile("1.txt", "utf-8", function (err,data) {
        event.emit(data); //发布
    })
    fs.readFile("2.txt", "utf-8", function (err,data) {
        event.emit(data); //发布
    })
    

四、promise系列

  • promise对象是commonJS工作组提出的一种规范,一种模式,目的是为了异步编程提供统一接口。 new Promise 返回一个 promise对象 接收一个excutor执行函数作为参数, excutor有两个函数类型形参resolve reject,promise对象初始化状态为 pending,当调用resolve(成功),会由pending => fulfilled,当调用reject(失败),会由pending => rejected
    let fs = require("fs");
    let promise = new Promise(function (resolve, reject) {
        fs.readFile("1.txt","utf-8",function (err,data) {
            if(err){
                reject(err);
            }
            resolve(data)
        })
    })
    promise.then((data)=>{
        console.log(data)
    },(err)=>{
        console.log(err)
    })
  • 优点 1、可解决回调地狱(恶魔金字塔) 2、回调函数写成了链式写法 f1().then(f2).then(f3); 3、可“同步”异步执行的结果
  • 其他方法 Promise.resolve Promise.reject Promise.all Promise.race等方法适用于不同场景下的异步编程开发。

拓展:手写一个符合Promise/A+规范的Promise

  • 基本实现:执行器中为异步函数,resolve时执行then中回调函数,且then的回调函数能获取到resolve(data)到结果data
  • then的链式调用:then函数需要返回一个新的promise2, 才能使用点then2, 将then中回调函数A(同步或异步))放入promise2的执行器中同步执行,A函数执行完毕后调用resolve或reject,下一个then2执行时,then2中回调函数会放入事件池,当promise2中接收的参数resolve或reject执行时并传入参数data,就将本次then中回调函数的返回结果传递到了下一个then2中
  • 注意:
    • 只能从pending => resolve/reject
    • 执行器执行异常处理-reject
    • 上一个then是rejected,不影响本次then
    • then中回调函数的返回值return 可能是一个常量 可能是undefined,也可能是异步promise
    • 若返回值仍是promise,则一直执行直到得到一个常量值再返回
    • 若then中什么都没有,值可以穿透
    /**
     * promise/A+规范:
     * 在某些应用场景下,我们需要知道异步请求的数据在什么时候返回的,然后进行下一步处理
     * 如果我们在异步操作回调里面仍是异步操作的时候,就形成了异步回调的嵌套
     * 由此,Promise/a+规范应运而生。
     */
    function Promise(executor) {
        let self = this;
        self.status = "pending"; //初始状态
        self.data = undefined; //成功的值
        self.err = undefined; //失败原因
        // new Promise的时候可能会有异步操作,需要保存成功和失败的回调
        self.onResolveCallbacks = []; //成功回调池
        self.onRejectCallbacks = []; //失败回调池
        //pending=>resolved
        function resolve(data) {
            if (self.status == "pending") {
                self.status = "resolved"; //改变状态
                self.data = data; //接收数据
                self.onResolveCallbacks.forEach(fn => fn()); //resolve执行时-执行回调事件池(then的时候存入的)
            }
        }
        //pending=>rejected
        function reject(err) {
            if (self.status == "pending") {
                self.status = "rejected"; //改变状态
                self.err = err; //接收错误信息
                self.onRejectCallbacks.forEach(fn => fn()); //reject执行时-执行回调事件池(then的时候存入的)
            }
        }
        try {
            executor(resolve, reject); //默认new Promise时,执行器-同步执行
            //但resolve reject函数没有立即执行,仅作为参数传入执行器函数,异步操作回来后再调用resolve reject
        } catch (e) {
            //如果执行器执行异常,直接reject-失败态
            reject(e)
        }
    }
    /**
     * resolvePromise:
     * 当resolve/reject的时候执行事件池->resolvePromise函数执行
     */
    function resolvePromise(promise2, x, resolve, reject) {
        //promise2和函数之后后返回的结果是同一个对象
        if (promise2 == x) {
            return reject(new TypeError("Chaining cycle"));
        }
        let called;
        //x可能是一个普通值,也可能是一个promise
        if (x != null && (typeof x == "function" || typeof x == "object")) {
            //外部的promise,可能报异常
            try {
                let then = x.then;
                // x可能还是一个promise 那么就让这个promise执行即可
                // 这里的逻辑不单单是自己的 还有别人的 别人的promise 可能既会调用成功 也会调用失败
                if (typeof then == "function") {
                    //执行promise
                    then.call(x, val => { //改变this指向,返回promise执行结果
                        if (called) return; //防止多次被调用
                        called = true;
                        //递归-执行的结果可能还是一个promise,那就循环的其解析,直到得到普通值为止
                        resolvePromise(promise2, val, resolve, reject)
                    }, err => { // promise的失败结果
                        if (called) return;
                        called = true;
                        reject(err)
                    })
                } else {
                    resolve(x)
                }
            } catch (e) {
                if (called) return;
                called = true;
                reject(e)
            }
    
        } else {
            //x 是一个常量
            resolve(x)
        }
    }
    /**
     * then:
     * then调用的时候 都是异步调用 (原生的then的成功或者失败是一个微任务)-用setTimeout模仿
     * 成功和失败的回调 是可选参数
     */
    Promise.prototype.then = function (onFulFilled, onRejected) { 
        //值得穿透-then里面什么都不写时
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val;
        onRejected = typeof onRejected === 'function' ? onRejected : err => {
            throw err
        }
        let self = this;
        let promise2;
        promise2 = new Promise((resolve, reject) => {
            if (self.status == "pending") {         
                //本次的promise执行为异步代码-走then的时候,状态仍为pending
                //执行器executor中有异步操作->then函数执行时,此时Promise的状态仍为pending
                //所以then函数的主要作用之一是:将回调函数(onFulFilled和onRejected)存入事件池,等异步操作完成后通过resolve和reject执行
                //then函数要返回一个新的promise,便于链式操作
                self.onResolveCallbacks.push(() => { //resolve执行时,调动事件池-执行本次回调并得到结果x
                    setTimeout(() => {
                        try{
                            let x = onFulFilled(self.data); //本次执行结果
                            // resolve(x);//x若是一个常量
                            //本次结果x可能仍为promise-要得到最终态数据并返回
                            resolvePromise(promise2, x, resolve, reject);
                            //传参:
                            //本次的promise2对象、本次执行结果x,本次promise2的resolve,reject-->得到最终结果时调用
                        }catch(e){
                            // 当执行成功回调的时候 可能会出现异常,那就用这个异常作为promise2的错误的结果
                            reject(e);
                        }
                    }, 0);
                })
                self.onRejectCallbacks.push(() => { 
                    //reject执行时,调动事件池-执行本次回调并得到结果x
                    setTimeout(() => {
                        try {
                            let x = onRejected(self.err);
                            resolvePromise(promise2, x, resolve, reject)
                        } catch (e) {
                            reject(e)
                        }
                    }, 0);
                })
            }
            if (self.status == "resolved") {
                //本次的promise执行为同步代码-直接resolve(data)了,并将数据传到then中的回调函数onFulFilled(self.data)
                setTimeout(() => {
                    try {
                        //resolve执行后-onFulFilled再执行
                        let x = onFulFilled(self.data); //仍不确定执行结果x是常量还是promise,
                        //如果是常量则直接传给下一个promise2的resolve(x)即可,
                        //如果是promise,则需要继续调用promise直到得到最终结果为常量在返回给下一个promise2的resolve(x)
                        //下一个promise2的resolve(x)接收本次执行结果x,在本次中执行这个resolve(x),
                        //如此,即可将本次执行结果传递给下一个then函数的回调
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                }, 0);
            }
            if (self.status == "rejected") {
                setTimeout(() => {
                    try {
                        let x = onRejected(self.err);
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                }, 0);
            }
        })
        return promise2
    }

五、gennerator

  • 先上代码
    //gennerator+co
    let bluebird = require("bluebird");//第三方库
    let fs = require("fs");
    let read = bluebird.promisify(fs.readFile);//promise化
    function * gen() {//生成器
        let r1 = yield read('1async/1.txt', 'utf8');
        let r2 = yield read(r1, 'utf8');
        let r3 = yield read(r2, 'utf8');
        return r3;
    }
    function co(it) {
        return new Promise(function (resolve,reject) {
            function next(data) {
                let {value,done} = it.next(data);//迭代
                if (!done) {
                    value.then(data=>{
                        next(data);
                    },reject)
                }else{
                    resolve(value);
                }
            }
            next();
        })
    }
    co(gen()).then(data=>{
        console.log(data)
    })
  • gen和普通函数的区别在于,不是直接执行的,生成器返回的是一个gen对象,我们要通过这个对象来执行里面的逻辑
  • gen的中常常用yield的关键字隔开,gen中如有有yield关键字,那么,gen中的代码不会一次被执行完成,而是会根据yield关键字,一段一段的执行完成
  • gen的每次执行返回的都会返回一个next对象,value的的数值就是yield代码执行后返回的数值,done代表之后,是否还有要执行的逻辑,false代表还有,反之则无

六、async+aweit

  • 优雅的代码同步写法
  • 可以try + catch
  • 可以使用promise的形式
  • async+aweit = gennerator+co
    let bluebird = require("bluebird");//第三方库
    let fs = require("fs");
    let read = bluebird.promisify(fs.readFile);//promise化
    async function gen() {
        try{
            let r1 = await read('1async/1.txt', 'utf8');
            let r2 = await read(r1, 'utf8');
            let r3 = await read(r2, 'utf8');
            return r3;
        }catch(e){
            throw(e)
        }
        
    }
    gen().then(data=>{
        console.log(data)
    },err=>{
        console.log(err)
    })

结语

欢迎批评指正!