阅读 143

前端必知的异步与Promise

回顾一下 什么是同步和异步:

 同步:“你当兵去吧!我不交别的男朋友!我一直等着你回来娶我!”
    --专注做一个任务,等得到这个任务的结果后在执行其它任务,叫做同步;
    
 异步:“你当兵去吧!你回来之前我会先找个其它男朋友,等你回来后再跟你聊我们的事”
    --把任务分成2段,先执行一段,转而执行其它任务,等做好了准备,再回过头执行第二段,这种不连续的执行,就叫做异步;
    
 一辈子很长,按我们人类的思路当然一辈子只对一段感情从一而终啦!
 但是JavaScript却提倡使用异步方法执行(不要这么反人类吧!)
    优点:因为可以利用时间队列机制解决无阻塞的运行应用! 
    缺点:异步操作会形成大量函数异步嵌套,难以维护问题...
    对,你没听错,就是这么反人类,但我们也不是吃素的,总会有*代表月亮消灭你!
    
复制代码

异步的发展流程

文章开始先让我们看下异步的发展流程就是这个样子:
    callback -> event -> Promise -> yield & co -> async await
让我们按照发现问题-解决问题-再发现问题-再解决问题的方式,来试试如何一步一步使代码变得优雅起来!
复制代码
买家:“小哥哥 给我来一个判断类型的方法”
卖家:“小姐姐 需求能满足,不过,我有高阶函数,要不要看看!”
买家:“what!?( ⊙ o ⊙ ) ” 高阶函数什么鬼
复制代码
函数作为一等公民,可以作为 参数 和 返回值 这种形式就是高阶函数
高阶函数至少满足以两个条件: 
    参数:接受一个或多个函数作为输入
    返回值:输出一个函数
复制代码
预置函数
let isType = function(type,obj){
    return Object.prototype.toString.call(obj) ===`[object ${type}]`
}
console.log(isType('Object',{}));
console.log(isType('Number',123));
console.log(isType('String','我最美'));
console.log(isType('String','hello'));

执行结果没毛病,都是true;--喜欢拿去!
但是问题来了,每次调用isType方法都需要传入类型,好麻烦啊!反人类!我要智能点的。

卖家说:针对此需求,我们做了升级哦!可以先批量产出可供调用的函数,您请看:

    let isType = function(type){
        return function(obj){
            return Object.prototype.toString.call(obj) ===`[object ${type}]`;
        }
    }
    let isObject = isType('Object');
    let isString = isType('String');
    console.log(isObject({}));
    console.log(isString('我是美女'));
    
执行结果没毛病,都是true;而且,函数作为参数传递,并且函数作为返回值,必然高阶了!  --喜欢拿去!
    
当达到某个条件时 执行callback: 条件:执行3次,打印-请你吃饭 
    function after(times,cb){
        return function(){
            if(--times === 0){
                cb();
            }
        }
    }
    let eat = after(3,function(){console.log('请你吃饭')}); //这里函数当参数传是典型的callback回调
    eat();
    eat();
    eat();
执行结果没毛病,打印了请你吃饭,用了回调又高阶;--喜欢拿去!
复制代码

callback

回调函数:
    就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。
    既然今天异步主场,给大家介绍一个常见的node中的异步方法readFile:可以用来读取文件
    
例如这样:
    fs.readFile(filename, function (err, data) {
      if (err) throw err;
      console.log(data);
    });
    错误优先:在node中,回调函数的第一个参数是错误对象(error-first callbacks)

用回调函数来解决异步问题:
function read(callback){
    setTimeout(function(){
        let result = '我是美女';
        callback(result);
    })
}
read(function(data){
    console.log(data);
});
1)函数read执行时传了一个参数(参数是个函数)
2)setTimeout执行,这里没有写执行时间,系统默认是4秒
3)定义了一个result变量,callback函数执行,打印data 
    callback没有()是参数,后面加上()就是函数执行
复制代码

回调的问题

* 异步不支持try/catch,回调函数是在下一个事件环中取出,所以一般在回调函数的第一个参* 数预置错误对象(错误优先)
* 回调地狱问题,异步多级依赖的情况下嵌套非常深,代码难以阅读的维护
* 多个异步在某一时刻获取所有异步的结果
* 结果不能通过return返回

综上所述,项目中也是频频遇到,解决起来很头疼。怎么办呢!?嗳~~!咱们的主角Promise诞生了!
复制代码

Promise是个啥?

中译英了一下,Promise:承诺、许诺 
                            ————“你承诺我当兵回来会娶我!”
在程序中的意思就是:承诺我过一段时间后会给你一个结果。 什么时候会用到过一段时间?
    答案是:异步操作
        异步是指可能比较长时间才有结果的才做,例如网络请求、读取本地文件等...
复制代码

Promise解决了什么问题?

1) 解决-回调地狱 - 嵌套问题
2) 解决-并发异步,再同一时刻内获取并发的结果 
3) 链式调用 (像jquery) A.b().c()...
    每个promise上都有一个then方法
    then方法种有两个参数:成功的函数,失败的函数
        用来指定 Promise 对象状态改变时确定执行的操作
        .then(resolve,reject)
            参1 resolve 代表第一个函数(onFulfilled)
            参2 reject  代表第二个函数(onRejected)
复制代码

Promise的三个状态

* Pending   Promise对象实例创建时候的初始状态   ———— 装傻,什么也不做
* Fulfilled 可以理解为成功的状态                ———— 成功
* Rejected  可以理解为失败的状态                ———— 失败
怎么理解这三个状态呢?比如上面说到了“你承诺我当兵回来要娶我!”
    1)我先要“等待”他当兵回来,做娶我的事情
    2)如果娶了表示“成功”;如果没取表示“失败”;
        当然也有可能一直让我等待一辈子...好可怜!
复制代码

动手来写一个Promise吧!

let promise = new Promise((resolve, reject) => {
    console.log('hello');
}); 
console.log('美女'); // 1) hello 2) 美女 

new Promise(()=>{}) 这里有一个参数:叫做执行器
    1)是个回调函数
        参1 resolve 代表第一个函数(onFulfilled)
        参2 reject  代表第二个函数(onRejected)
    2)会立即执行;
如上所示,先打印了执行器里的hello,再打印的美女,说明这个执行器是同步的;
复制代码

解决回调地狱

需求:读取第一个文件成功后,将值作为下一个读文件的入口内容,读文件,传文件...;
// 1.txt => 2.txt
// 2.txt => 哈哈哈 我是帅锅
let fs = require('fs');
function read(){
    fs.readFile('./1.txt','utf8',function(err,data){
        if(err) return console.log(err);
        fs.readFile(data,'utf8',function(err,data){
            if(err) return console.log(err);
            console.log(data); // 我很帅
        })
    })
}
read();
实现:
    1) 调用read函数
    2) 异步读取1.txt文件,要求用utf8编码规范,执行回调函数
    4) 错误优先,如果拿到的是个错误的值,调用并返回并打印错误值
    5) 如果data无异常,将data作为下一个读文件的内容,继续2)流程,以此类推
 
//改写Promise形式
let fs = require('fs');
function read(file){
    return new Promise(function(resolve,reject){
        fs.readFile(file,'utf8',function(err,data){
            if(err) return reject(err);
            resolve(data);
        })
    })
}
read('./1.txt').then(function(data){
    return read(data);
}).then(function(data){
    console.log(data)
}).catch(function(err){
    console.log(err)
});

1) 引入node的读取文件模块:fs模块
2) 调用read('./1.txt')函数并参(文件路径)
3) 遇到new Promise构造函数会立即执行
    . 异步读取1.txt文件,要求用utf8编码规范,执行回调函数
        错误优先,如果报错了,调用错误的函数,传入错误的原因
        否则调用成功函数;
4) 返回promise的实例,才可以进行链式操作
    * 同一个promise的实例可以多次.then().then().then().... 
    * 当成功后会将then中的成功方法按顺序执行
5) .then 时会调用成功方法,返回并从新从2)过程开始执行
    *注意的是,这里read(data) 的data已经变成第一次读取的结果("./2.txt")作为形参传入
6) 经过2次的回调,最终打印出了 “哈哈哈 我是帅锅”
7) catch捕获错误,写在了最后,上面有err会走err 没有会走catch

无论有多少层回调都不用再一层嵌套一层!
复制代码

并发异步,再同一时刻内获取并发的结果

// 1.txt => template
// 2.txt => data
let fs = require('fs');
let result = {}
function out(key,data) {
    result[key] = data;
    if(Object.keys(result).length === 2){
        console.log(result)
    }
}
fs.readFile('./1.txt', 'utf8', function (err, data) {
    if (err) return console.log(err);
    out('template',data);
})
fs.readFile('./2.txt', 'utf8', function (err, data) {
    if (err) return console.log(err);
    out('data',data);
});
// 这种方式并不好,要声明全局的对象,并且成功的数量也是写死的,我们可以使用偏函数来进行改写


let fs = require('fs');
let result = {}
function after(times,cb) {
    let result = {}
    return function(key,data){
        result[key] = data;
        if(Object.keys(result).length === times){
            cb(result)
        }
    }
}
let out = after(2,function(data){
    console.log(data)
})
fs.readFile('./1.txt', 'utf8', function (err, data) {
    if (err) return console.log(err);
    out('template',data);
})
fs.readFile('./2.txt', 'utf8', function (err, data) {
    if (err) return console.log(err);
    out('data',data);
});

最后我们可以采用Promise.all方法来进行简化
let fs = require('fs');
function read(file){
    return new Promise(function(resolve,reject){
        fs.readFile(file,'utf8',function(err,data){
            if(err) return reject(err);
            resolve(data);
        })
    })
}
Promise.all([read('1.txt'),read('2.txt')]).then(([template,data])=>{
    console.log({template,data})
});
// 不管两个promise谁先完成,Promise.all 方法会按照数组里面的顺序将结果返回
复制代码

Promise.race

接受一个数组,数组内都是Promise实例,返回一个Promise实例,这个Promise实例的状态转移取决于参数的Promise实例的状态变化。
当参数中任何一个实例处于resolve状态时,返回的Promise实例会变为resolve状态。
如果参数中任意一个实例处于reject状态,返回的Promise实例变为reject状态。

Promise.race([read('1.txt'),read('2.txt')]).then(data=>{
    console.log({template,data})
},(err)=>{
    console.log(err)
});
复制代码

Promise.reject

返回一个Promise实例,这个实例处于reject状态
Promise.reject('失败').then(data=>{ 
   console.log(data); 
},err=>{ 
    console.log(err); 
})
复制代码
文章分类
前端
文章标签