>  기사  >  웹 프론트엔드  >  es6의 Promise에 대한 간략한 분석(예제 포함)

es6의 Promise에 대한 간략한 분석(예제 포함)

不言
不言앞으로
2018-10-17 14:36:242274검색

이 기사는 es6의 Promise에 대한 간략한 분석을 제공합니다(예제 포함). 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.

Promise의 기본 사용에 대해서는 Ruan Yifeng 선생님의 "ECMAScript 6 소개"를 읽어보세요.

다른 얘기 좀 해보자.

callback

우리가 Promise에 대해 이야기할 때 보통 콜백이나 콜백 지옥으로 시작하는데, 콜백을 사용하면 어떤 나쁜 일이 일어날까요? 장소는 어디인가요?

1. 콜백 중첩

콜백을 사용하면 다음 형식으로 비즈니스 코드를 작성할 수 있습니다.

doA( function(){
    doB();

    doC( function(){
        doD();
    } )

    doE();
} );

doF();
#🎜 🎜#물론 이것은 단순화된 형태입니다. 조금만 생각해보면 실행 순서는 다음과 같다고 판단할 수 있습니다.

doA()
doF()
doB()
doC()
doE()
doD()
그러나 실제 프로젝트에서는 문제를 해결하기 위해 코드가 더 지저분해집니다. 문제가 발생하면 보기 흉한 콘텐츠를 많이 건너뛰고 기능 간에 끊임없이 이동해야 하므로 문제 해결이 기하급수적으로 더 어려워집니다.

물론 이런 문제가 발생하는 이유는 이러한 중첩 작성 방식이 선형적 사고방식에 어긋나기 때문에 실제 실행 순서를 생각하는데 더 많은 에너지를 소비해야 하기 때문입니다. 이 사고 과정에서는 단지 붉은 청어일 뿐입니다.

물론 선형적 사고 방식에 반대하는 것이 최악은 아닙니다. 실제로 위의 예와 같이 다양한 논리적 판단도 코드에 추가하게 됩니다. doC()가 완료된 후에 완료됩니다. doC()가 실행에 실패하면 어떻게 되나요? doC()를 다시 시도하시겠습니까? 아니면 다른 오류 처리 기능으로 직접 이동하시겠습니까? 이러한 판단을 프로세스에 추가하면 코드가 유지 관리 및 업데이트하기에는 너무 복잡해집니다.

2. 제어 반전

일반적으로 코드를 작성할 때는 물론 자체 코드를 제어할 수 있지만 콜백을 사용하면 , 이 콜백 함수가 실제로 계속 실행될 수 있는지 여부는 콜백을 사용하는 API에 따라 다릅니다. 예:

// 回调函数是否被执行取决于 buy 模块
import {buy} from './buy.js';

buy(itemData, function(res) {
    console.log(res)
});
우리가 자주 사용하는 fetch API의 경우 일반적으로 문제가 없지만, 만약 그렇다면 타사 API를 사용하시나요?

타사 API를 호출하면 오류로 인해 여러 번 전달한 콜백 함수를 상대방이 실행하게 되나요?

이러한 문제를 방지하기 위해 콜백 함수에 판단을 추가할 수 있는데, 오류로 인해 콜백 함수가 실행되지 않는다면 어떻게 될까요?

이 콜백 함수가 때로는 동기적으로, 때로는 비동기적으로 실행된다면 어떨까요?

다음 상황을 요약해 보겠습니다.

  1. 콜백 함수가 여러 번 실행됩니다

  2. #🎜 🎜 #콜백함수가 실행되지 않습니다
  3. 콜백함수가 동기적으로 실행되는 경우도 있고 비동기적으로 실행되는 경우도 있습니다
  4. 이러한 상황에서는 콜백 함수에서 일부 처리를 해야 할 수도 있고, 콜백 함수가 실행될 때마다 일부 처리를 해야 하므로 반복되는 코드가 많이 발생합니다.

콜백 지옥

먼저 콜백 지옥의 간단한 예를 살펴보겠습니다.

이제 디렉토리에서 가장 큰 파일을 찾으려면 다음 처리 단계를 따라야 합니다.

    fs.readdir사용 > 디렉토리에서 파일 목록을 가져옵니다.
  1. fs.readdir 获取目录中的文件列表;

  2. 循环遍历文件,使用 fs.stat 获取文件信息

  3. 比较找出最大文件;

  4. 以最大文件的文件名为参数调用回调。

代码为:

var fs = require('fs');
var path = require('path');

function findLargest(dir, cb) {
    // 读取目录下的所有文件
    fs.readdir(dir, function(er, files) {
        if (er) return cb(er);

        var counter = files.length;
        var errored = false;
        var stats = [];

        files.forEach(function(file, index) {
            // 读取文件信息
            fs.stat(path.join(dir, file), function(er, stat) {

                if (errored) return;

                if (er) {
                    errored = true;
                    return cb(er);
                }

                stats[index] = stat;

                // 事先算好有多少个文件,读完 1 个文件信息,计数减 1,当为 0 时,说明读取完毕,此时执行最终的比较操作
                if (--counter == 0) {

                    var largest = stats
                        .filter(function(stat) { return stat.isFile() })
                        .reduce(function(prev, next) {
                            if (prev.size > next.size) return prev
                            return next
                        })

                    cb(null, files[stats.indexOf(largest)])
                }
            })
        })
    })
}

使用方式为:

// 查找当前目录最大的文件
findLargest('./', function(er, filename) {
    if (er) return console.error(er)
    console.log('largest file was:', filename)
});

你可以将以上代码复制到一个比如 index.js 文件,然后执行 node index.js 就可以打印出最大的文件的名称。

看完这个例子,我们再来聊聊回调地狱的其他问题:

1.难以复用

回调的顺序确定下来之后,想对其中的某些环节进行复用也很困难,牵一发而动全身。

举个例子,如果你想对 fs.stat

파일을 반복하고 fs.stat를 사용하여 파일 정보를 가져옵니다

비교하여 가장 큰 파일 찾기

콜백은 가장 큰 파일의 파일 이름을 매개변수로 호출됩니다. .

#🎜🎜##🎜🎜#코드는 다음과 같습니다. #🎜🎜#
request(url, function(err, res, body) {
    if (err) handleError(err);
    fs.writeFile('1.txt', body, function(err) {
        request(url2, function(err, res, body) {
            if (err) handleError(err)
        })
    })
});
#🎜🎜#사용법은 다음과 같습니다. #🎜🎜#
request(url)
.then(function(result) {
    return writeFileAsynv('1.txt', result)
})
.then(function(result) {
    return request(url2)
})
.catch(function(e){
    handleError(e)
});
#🎜🎜#위 코드를 예를 들어 index.js 파일을 실행한 다음 node index.js를 실행하여 가장 큰 파일의 이름을 출력합니다. #🎜🎜##🎜🎜#이 예제를 읽은 후 콜백 지옥의 다른 문제에 대해 이야기해 보겠습니다. #🎜🎜##🎜🎜##🎜🎜#1. 재사용하기 어려움#🎜🎜##🎜🎜##🎜 🎜 #콜백 순서가 결정된 후에는 일부 링크를 재사용하기 어려워 전체 본문에 영향을 미치게 됩니다. #🎜🎜##🎜🎜#예를 들어 fs.stat에서 파일 정보를 읽는 데 코드를 재사용하려는 경우 외부 변수가 콜백에서 참조되기 때문에 이후에도 계속 추출됩니다. 추출. 외부 코드를 수정해야 합니다. #🎜🎜##🎜🎜##🎜🎜#2.스택 정보가 끊어졌습니다#🎜🎜##🎜🎜##🎜🎜#우리는 함수가 실행될 때 실행 컨텍스트 스택을 유지한다는 것을 알고 있습니다. will 함수를 생성하는 실행 컨텍스트가 스택에 푸시됩니다. 함수 실행이 완료되면 실행 컨텍스트가 스택에서 팝됩니다. #🎜🎜##🎜🎜#함수 A에서 함수 B가 호출되면 JavaScript는 먼저 함수 A의 실행 컨텍스트를 스택에 푸시한 다음 함수 B의 실행 컨텍스트를 스택에 푸시합니다. 함수 B의 실행 컨텍스트가 스택에서 팝됩니다. 함수 A의 실행이 완료되면 함수 A의 실행 컨텍스트가 스택에서 팝됩니다. #🎜🎜##🎜🎜#이것의 장점은 코드 실행을 중단하면 전체 스택 정보를 검색하고 여기에서 원하는 정보를 얻을 수 있다는 것입니다. #🎜🎜#

可是异步回调函数并非如此,比如执行 fs.readdir 的时候,其实是将回调函数加入任务队列中,代码继续执行,直至主线程完成后,才会从任务队列中选择已经完成的任务,并将其加入栈中,此时栈中只有这一个执行上下文,如果回调报错,也无法获取调用该异步操作时的栈中的信息,不容易判定哪里出现了错误。

此外,因为是异步的缘故,使用 try catch 语句也无法直接捕获错误。

(不过 Promise 并没有解决这个问题)

3.借助外层变量

当多个异步计算同时进行,比如这里遍历读取文件信息,由于无法预期完成顺序,必须借助外层作用域的变量,比如这里的 count、errored、stats 等,不仅写起来麻烦,而且如果你忽略了文件读取错误时的情况,不记录错误状态,就会接着读取其他文件,造成无谓的浪费。此外外层的变量,也可能被其它同一作用域的函数访问并且修改,容易造成误操作。

之所以单独讲讲回调地狱,其实是想说嵌套和缩进只是回调地狱的一个梗而已,它导致的问题远非嵌套导致的可读性降低而已。

Promise

Promise 使得以上绝大部分的问题都得到了解决。

1. 嵌套问题

举个例子:

request(url, function(err, res, body) {
    if (err) handleError(err);
    fs.writeFile('1.txt', body, function(err) {
        request(url2, function(err, res, body) {
            if (err) handleError(err)
        })
    })
});

使用 Promise 后:

request(url)
.then(function(result) {
    return writeFileAsynv('1.txt', result)
})
.then(function(result) {
    return request(url2)
})
.catch(function(e){
    handleError(e)
});

而对于读取最大文件的那个例子,我们使用 promise 可以简化为:

var fs = require('fs');
var path = require('path');

var readDir = function(dir) {
    return new Promise(function(resolve, reject) {
        fs.readdir(dir, function(err, files) {
            if (err) reject(err);
            resolve(files)
        })
    })
}

var stat = function(path) {
    return new Promise(function(resolve, reject) {
        fs.stat(path, function(err, stat) {
            if (err) reject(err)
            resolve(stat)
        })
    })
}

function findLargest(dir) {
    return readDir(dir)
        .then(function(files) {
            let promises = files.map(file => stat(path.join(dir, file)))
            return Promise.all(promises).then(function(stats) {
                return { stats, files }
            })
        })
        .then(data => {

            let largest = data.stats
                .filter(function(stat) { return stat.isFile() })
                .reduce((prev, next) => {
                    if (prev.size > next.size) return prev
                    return next
                })

            return data.files[data.stats.indexOf(largest)]
        })

}

2. 控制反转再反转

前面我们讲到使用第三方回调 API 的时候,可能会遇到如下问题:

  1. 回调函数执行多次

  2. 回调函数没有执行

  3. 回调函数有时同步执行有时异步执行

对于第一个问题,Promise 只能 resolve 一次,剩下的调用都会被忽略。

对于第二个问题,我们可以使用 Promise.race 函数来解决:

function timeoutPromise(delay) {
    return new Promise( function(resolve,reject){
        setTimeout( function(){
            reject( "Timeout!" );
        }, delay );
    } );
}

Promise.race( [
    foo(),
    timeoutPromise( 3000 )
] )
.then(function(){}, function(err){});

对于第三个问题,为什么有的时候会同步执行有的时候回异步执行呢?

我们来看个例子:

var cache = {...};
function downloadFile(url) {
      if(cache.has(url)) {
            // 如果存在cache,这里为同步调用
           return Promise.resolve(cache.get(url));
      }
     return fetch(url).then(file => cache.set(url, file)); // 这里为异步调用
}
console.log('1');
getValue.then(() => console.log('2'));
console.log('3');

在这个例子中,有 cahce 的情况下,打印结果为 1 2 3,在没有 cache 的时候,打印结果为 1 3 2。

然而如果将这种同步和异步混用的代码作为内部实现,只暴露接口给外部调用,调用方由于无法判断是到底是异步还是同步状态,影响程序的可维护性和可测试性。

简单来说就是同步和异步共存的情况无法保证程序逻辑的一致性。

然而 Promise 解决了这个问题,我们来看个例子:

var promise = new Promise(function (resolve){
    resolve();
    console.log(1);
});
promise.then(function(){
    console.log(2);
});
console.log(3);

// 1 3 2

即使 promise 对象立刻进入 resolved 状态,即同步调用 resolve 函数,then 函数中指定的方法依然是异步进行的。

PromiseA+ 规范也有明确的规定:

实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。

Promise 反模式

1.Promise 嵌套

// bad
loadSomething().then(function(something) {
    loadAnotherthing().then(function(another) {
        DoSomethingOnThem(something, another);
    });
});
// good
Promise.all([loadSomething(), loadAnotherthing()])
.then(function ([something, another]) {
    DoSomethingOnThem(...[something, another]);
});

2.断开的 Promise 链

// bad
function anAsyncCall() {
    var promise = doSomethingAsync();
    promise.then(function() {
        somethingComplicated();
    });

    return promise;
}
// good
function anAsyncCall() {
    var promise = doSomethingAsync();
    return promise.then(function() {
        somethingComplicated()
    });
}

3.混乱的集合

// bad
function workMyCollection(arr) {
    var resultArr = [];
    function _recursive(idx) {
        if (idx >= resultArr.length) return resultArr;

        return doSomethingAsync(arr[idx]).then(function(res) {
            resultArr.push(res);
            return _recursive(idx + 1);
        });
    }

    return _recursive(0);
}

你可以写成:

function workMyCollection(arr) {
    return Promise.all(arr.map(function(item) {
        return doSomethingAsync(item);
    }));
}

如果你非要以队列的形式执行,你可以写成:

function workMyCollection(arr) {
    return arr.reduce(function(promise, item) {
        return promise.then(function(result) {
            return doSomethingAsyncWithResult(item, result);
        });
    }, Promise.resolve());
}

4.catch

// bad
somethingAync.then(function() {
    return somethingElseAsync();
}, function(err) {
    handleMyError(err);
});

如果 somethingElseAsync 抛出错误,是无法被捕获的。你可以写成:

// good
somethingAsync
.then(function() {
    return somethingElseAsync()
})
.then(null, function(err) {
    handleMyError(err);
});
// good
somethingAsync()
.then(function() {
    return somethingElseAsync();
})
.catch(function(err) {
    handleMyError(err);
});

红绿灯问题

题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用 Promse 实现)

三个亮灯函数已经存在:

function red(){
    console.log('red');
}
function green(){
    console.log('green');
}
function yellow(){
    console.log('yellow');
}

利用 then 和递归实现:

function red(){
    console.log('red');
}
function green(){
    console.log('green');
}
function yellow(){
    console.log('yellow');
}

var light = function(timmer, cb){
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            cb();
            resolve();
        }, timmer);
    });
};

var step = function() {
    Promise.resolve().then(function(){
        return light(3000, red);
    }).then(function(){
        return light(2000, green);
    }).then(function(){
        return light(1000, yellow);
    }).then(function(){
        step();
    });
}

step();

promisify

有的时候,我们需要将 callback 语法的 API 改造成 Promise 语法,为此我们需要一个 promisify 的方法。

因为 callback 语法传参比较明确,最后一个参数传入回调函数,回调函数的第一个参数是一个错误信息,如果没有错误,就是 null,所以我们可以直接写出一个简单的 promisify 方法:

function promisify(original) {
    return function (...args) {
        return new Promise((resolve, reject) => {
            args.push(function callback(err, ...values) {
                if (err) {
                    return reject(err);
                }
                return resolve(...values)
            });
            original.call(this, ...args);
        });
    };
}

Promise 的局限性

1. 错误被吃掉

首先我们要理解,什么是错误被吃掉,是指错误信息不被打印吗?

并不是,举个例子:

throw new Error('error');
console.log(233333);

在这种情况下,因为 throw error 的缘故,代码被阻断执行,并不会打印 233333,再举个例子:

const promise = new Promise(null);
console.log(233333);

以上代码依然会被阻断执行,这是因为如果通过无效的方式使用 Promise,并且出现了一个错误阻碍了正常 Promise 的构造,结果会得到一个立刻跑出的异常,而不是一个被拒绝的 Promise。

然而再举个例子:

let promise = new Promise(() => {
    throw new Error('error')
});
console.log(2333333);

这次会正常的打印 233333,说明 Promise 内部的错误不会影响到 Promise 外部的代码,而这种情况我们就通常称为 “吃掉错误”。

其实这并不是 Promise 独有的局限性,try..catch 也是这样,同样会捕获一个异常并简单的吃掉错误。

而正是因为错误被吃掉,Promise 链中的错误很容易被忽略掉,这也是为什么会一般推荐在 Promise 链的最后添加一个 catch 函数,因为对于一个没有错误处理函数的 Promise 链,任何错误都会在链中被传播下去,直到你注册了错误处理函数。

2. 单一值

Promise 只能有一个完成值或一个拒绝原因,然而在真实使用的时候,往往需要传递多个值,一般做法都是构造一个对象或数组,然后再传递,then 中获得这个值后,又会进行取值赋值的操作,每次封装和解封都无疑让代码变得笨重。

说真的,并没有什么好的方法,建议是使用 ES6 的解构赋值:

Promise.all([Promise.resolve(1), Promise.resolve(2)])
.then(([x, y]) => {
    console.log(x, y);
});

3. 无法取消

Promise 一旦新建它就会立即执行,无法中途取消。

4. 无法得知 pending 状态

当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。



위 내용은 es6의 Promise에 대한 간략한 분석(예제 포함)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 segmentfault.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제