Promise ——解决JS异步问题的好帮手

159 阅读13分钟

我们知道,任何一门编程语言,代码都是从上往下执行的。有些代码不耗时执行,而有些代码需要耗时执行。当这些代码从上往下执行时,就会带来一些问题。究竟是不耗时的代码先执行,还是耗时的代码先执行呢?

今天,我们就来探讨一下这个问题。在下文中,我们统一使用定时器模拟需要耗时的代码。

1.异步

我们先来看一段代码:

let a = 1

console.log(a);

setTimeout(() => {
    console.log(a);
}, 1000)

我们定义一个a为1,然后输出a,又写了一个定时器模拟耗时执行的代码,在1秒过后再次输出a。

这段代码输出结果应该是先输出一个1,等1秒过后,再次输出1。这很容易接受,因为代码是从上往下执行的。

let a = 1
console.log(a, 2);

setTimeout(() => {
    a = 2
    console.log(a, 6);
}, 1000)

console.log(a, 9);

我们在定时器里将a的值改为了2,在定时器的下面也输出一下a。这段代码就变成了一段不耗时代码、一段耗时代码、一段不耗时代码。它的执行顺序会是什么呢?是依然从上往下执行,碰到耗时1秒的代码先将耗时代码执行完毕再执行下面的代码,还是碰到耗时的代码先搁置,先运行完不耗时的代码再运行耗时的代码呢?

我们输出a的时候顺便输出它的行数,这样看的更清楚。如果依然从上往下执行,那就是2、6、9的顺序输出。如果是耗时代码先搁置,那就是2、9、6的顺序输出。我们来看一下输出结果。

image.png

我们发现是2、9、6的顺序。这说明在JS中,当碰到需要耗时执行的代码时,它会先搁置,先执行完不耗时的代码,再来执行耗时的代码。

为什么要这样设计呢?为什么不能碰到耗时的代码,我就按顺序执行,执行完耗时的代码再执行下面的代码。

因为这样设计的话代码执行效率会大大降低。假如在这段耗时代码的后面还有几千行代码,那后面几千行代码就会被你活活拖累一段时间,而后面要执行的代码可能和你这段耗时的代码没太大关系,那执行效率不就变慢了。

而又因为JS是一门单线程语言,它不能像Java一样碰到耗时的代码,可以一边执行耗时代码一边执行不耗时的代码。它一次性只能干一件事情,所以当碰到耗时的代码时,它会先将耗时的代码挂起,它会维护一个队列来存放耗时代码,先去执行不耗时的代码,再来执行耗时的代码。

那这就会带来一个问题。如果我就是想让耗时的代码先执行呢?在实际开发过程中,有些代码的耗时是无法避免的,比如前端向后端发送一个http请求,它就得先执行,但因为JS会将耗时的代码先挂起之后再去执行,那就造成了代码的不同步执行,这就是代码的异步执行了。

所以我们得去解决这个异步问题。

2.回调函数解决异步

在es6之前,代码异步执行的问题就存在,当时的人们就是用回调函数解决异步问题的。我们来看一下回调函数是怎么解决异步的。

function a() {
    setTimeout(() => {
        console.log('a 执行完毕');
    }, 1000)
}

function b() {
    console.log('b 执行完毕');
}

a()
b()

我们定义一个函数a,里面用一个定时器模拟需要耗时执行的代码,如果函数a被调用了,它会在1秒钟后输出'a 执行完毕'。我们还定义了一个函数b,它输出'b 执行完毕'。然后我们先调用a再调用b,按照JS的特性,它会异步执行。那就会先输出'b 执行完毕'再输出'a 执行完毕'。

image.png

那如果我就是要让函数a先执行呢?这在实际开发中太常见了。因为有些代码就是不可避免的需要耗时执行,但它又得先执行。那我们可以用回调函数来解决。

我们直接将函数b的调用放到函数a中console.log('a 执行完毕')的后面,就行了。

function a() {
    setTimeout(() => {
        console.log('a 执行完毕');
        b()
    }, 1000)
}

function b() {
    console.log('b 执行完毕');
}

a()

我们人为的让a执行完毕后再去调用b,这样只有等我a执行完了你b才能调用。但我们一般这样写,给函数a定义一个形参cb,然后调用的时候将b作为实参传进来调用。

function a(cb) {
    setTimeout(() => {
        console.log('a 执行完毕');
        cb()
    }, 1000)
}

function b() {
    console.log('b 执行完毕');
}

a(b)

image.png

这样我们就让a的执行在b的前面。这就叫回调,回过头来调用。

这种方法能很好的解决异步的问题,但是它会有个问题,当碰到很多个函数需要互相依赖调用时,嵌套关系就会变得很复杂。

比如,我们有四个耗时的函数a、b、c、d。我希望函数b的执行必须依赖函数a的执行结果,函数c的执行必须依赖函数b的执行结果,又希望函数d的执行必须依赖函数c的执行结果。那我们就只能这样写:

function a(cb, cb2, cb3) {
    setTimeout(() => {
        console.log('a 执行完毕');
        cb(cb2, cb3)
    }, 1000)
}

function b(cb, cb3) {
    setTimeout(() => {
        console.log('b 执行完毕');
        cb(cb3)
    }, 1500)
}

function c(cb) {
    setTimeout(() => {
        console.log('c 执行完毕');
        cb()
    }, 500)
}

function d() {
    console.log('d 执行完毕');
}

a(b,c,d)

函数之间的嵌套关系就会变得非常复杂。这只是四个函数之间互相等待,要是有100个函数之间互相等待,那嵌套关系简直不敢想象。业内把这一现象称作回调地狱

后来为了应对在这一现象,es6新增了一个解决异步的方法——promise。

3.Promise

我们来看看promise解决异步问题的语法。

我们依旧用定时器模拟需要耗时执行的代码。

我们先来定义一个相亲函数xq,需要耗时2秒执行:

function xq() {
    setTimeout(() => {
            console.log('章总相亲了');
        }, 2000)
}

再来定义一个结结婚函数marry,需要耗时1秒执行:

function marry() {
    setTimeout(() => {
            console.log('章总结婚了');
        }, 1000)        
}

正常去调用这两个函数的话marry函数就会先执行,因为它耗时更短。

image.png

那这样就不是我们想要的结果,应该是先相亲再结婚才对。我们应该要让函数xq先执行。

除了上一种提到的回调函数解决外,还可以用promise方法解决,我们来看一下它是怎么使用的。

在xq函数里我们return 一个new Promise,它接收一个函数体,函数里面有两个形参:resolve和reject。然后我们把这个xq函数的过程,放到这个函数体里去。

function xq() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('章总相亲了');
        }, 2000)
    })
}

Promise显然是一个构造函数,我们用new去调用它,能得到一个实例对象,然后我们返回了它。那我们调用了函数xq,xq()就是一个实例对象,在这个对象上有一个方法then,这个then可以接收一个回调函数,我们可以将marry放到then里面来使用。

xq().then(() => {
    marry()
})

但是这样还不够,我们还要在xq函数里resolve调用一下。resolve和reject就像两个开关,一个是执行成功,一个是执行失败。我们人为的去调用resolve,那整个promise就是成功的状态;人为的去调用reject,那整个promise就是失败的状态。只有在成功的状态下,then方法才会走。

整段代码:

function xq() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('章总相亲了');
            resolve()
        }, 2000)
    })
}

function marry() {
    setTimeout(() => {
        console.log('章总结婚了');
    }, 1000)
}

xq().then(() => {
    marry()
})

image.png

这样就成功先相亲再结婚了。我们解决了代码异步问题。

这个resolve还有一个作用,它里面可以放一个值‘相亲顺利’,resolve出来的值可以被then拿到,我们可以输出这个值。

function xq() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('章总相亲了');
            resolve('相亲顺利')
        }, 2000)
    })
}

function marry() {
    setTimeout(() => {
        console.log('章总结婚了');
    }, 1000)
}

xq().then((res) => {
    console.log(res);
    marry()
})

image.png

了解完语法后,我们再来加一个函数baby,我们想让它在marry函数的后面执行。

function baby() {
    console.log('小章出生了');
}

因为marry函数需要耗时执行,而baby函数不用,这两个函数就会异步执行。所以我们得在marry函数里也加一个promise。

function marry() {
    return new Promise(() => {
        setTimeout(() => {
            console.log('章总结婚了');
        }, 1000)
        resolve()
    })
}

此时我们就可以在marry()上调用then方法了。

xq().then((res) => {
    console.log(res);
    marry().then(() => {
        baby()
    })
})

还有另外一种写法,看起来更简洁。我们可以让then并列。

function xq() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('章总相亲了');
            resolve('相亲顺利')
        }, 2000)
    })
}

function marry() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('章总结婚了');
            resolve()
        }, 1000)     
    })
}

function baby() {
    console.log('小章出生了');
}

xq()
    .then((res) => {
        console.log(res);
        return marry()
    })
    .then(() => {
        baby()
    })

我们在第一个then里return marry(),于是返回一个对象,我们就可以then后面再接then。

现在我们来看一下输出结果:

image.png

成功达到了我们想要的顺序。

我们再来看一下promise中关于reject的语法。刚刚我们看了resolve,它可以让整个promise执行成功,被then方法拿到。

function a() {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log('a');
            resolve('a 执行完毕')
        }, 1000)
    })
}

a()
    .then(res => {
        console.log(res);
    })

image.png

我们还可以用reject让整个promise执行失败,它会被catch方法拿到。

function a() {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log('a');
            // resolve('a 执行完毕')
            reject('a 执行失败')
        }, 1000)
    })
}

a()
    .then(res => {
        console.log(res);
    })
    .catch(err => {
        console.log(err);
    })

image.png

这就是promise的一些语法,接下来我们来看一个promise的真实场景应用。

4.Promise的应用场景

我们来模拟一个前端向后端发送http请求要求数据的场景。

我们提前准备一个接口地址:'mock.mengxuegu.com/mock/65a915…' ,这是后端给前端提供的一个地址,当我们用前端代码往这个地址发送请求,我们就能请求到地址中的数据。

我希望能将地址中的数据展示到页面上。我们准备一个ul,到时候我们就将数据放到ul中的li中一行一行展示。给ul取一个id=“ul”。

<body>
    <ul id="ul"></ul>

</body>

然后我们来写一段JS。在JS中,有专门一个方法能向后端发送http请求。

<body>
    <ul id="ul"></ul>
    
    <script>
        let xhr = new XMLHttpRequest();    
    </script>
</body>

XMLHttpRequest 是JS内定的一个构造函数,我们new一下它得到了一个发送http请求的实例对象xhr。

然后xhr调用open方法,然后发送一个‘GET’请求,第二个参数就放我们的接口地址,第三个参数写一个‘true’,将这段代码变成异步执行。

<body>
    <ul id="ul"></ul>
    
    <script>
        let xhr = new XMLHttpRequest(); 
        xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
        
    </script>
</body>

然后我们调用send方法,发射这个请求。

<body>
    <ul id="ul"></ul>
    
    <script>
        let xhr = new XMLHttpRequest(); 
        xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
        xhr.send();
    </script>
</body>

我们发送了这个请求,后端就会给我们返回数据,那我们就要接收它。接收用onreadystatechange 方法,监听状态的变更,它为一个函数体。

<body>
    <ul id="ul"></ul>
    
    <script>
        let xhr = new XMLHttpRequest(); 
        xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
        xhr.send();
        xhr.onreadystatechange = function () {
        
        }
    </script>
</body>

在这个函数里我们去判断一下。

<body>
    <ul id="ul"></ul>
    
    <script>
        let xhr = new XMLHttpRequest(); 
        xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
        xhr.send();
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4 && xhr.status == 200) {
            
            }
        }
    </script>
</body>

readyState 代表的是状态,status 是http的状态码。这个在这里先不细聊。

我们在里面输出xhr.responseText。responseText是后端传来的数据的字符串格式,它是JSON字符串。我们将它转换成JSON对象。

<body>
    <ul id="ul"></ul>
    
    <script>
        let xhr = new XMLHttpRequest(); 
        xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
        xhr.send();
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4 && xhr.status == 200) {
                console.log(JSON.parse(xhr.responseText));
            }
        }
    </script>
</body>

我们现在写的就是发送请求获取数据的代码,所以我们写一个函数getData将它们包裹起来,分门别类。

<body>
    <ul id="ul"></ul>
    
    <script>
        function getData() {
            let xhr = new XMLHttpRequest(); 
            xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
            xhr.send();
            xhr.onreadystatechange = function () {
                if (xhr.readyState == 4 && xhr.status == 200) {
                    console.log(JSON.parse(xhr.responseText));
                }
            }
        }
    </script>
</body>

然后我们再写一个展示数据的函数showList。里面放一个参数data。

<body>
    <ul id="ul"></ul>
    
    <script>
        function getData() {
            let xhr = new XMLHttpRequest(); 
            xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
            xhr.send();
            xhr.onreadystatechange = function () {
                if (xhr.readyState == 4 && xhr.status == 200) {
                    console.log(JSON.parse(xhr.responseText));
                }
            }
        }
        
        function showList(data) {
            
        }
    </script>
</body>

此时在写showList函数之前,有一个问题。如果我们要去使用这两个函数,应该先调用getData再调用showList。但getData里面的代码一定是耗时的,因为它向后端发送了http请求,所以会造成代码的异步执行。它会先执行showList再执行getData,所以在这里,我们可以用promise去解决异步。

我们这样写,在函数getData里面return new Promise:

<body>
    <ul id="ul"></ul>
    
    <script>
       function getData() {
            return new Promise((resolve, reject) => {
                let xhr = new XMLHttpRequest();
                xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
                xhr.send();
                xhr.onreadystatechange = function () {
                    if (xhr.readyState == 4 && xhr.status == 200) {
                        console.log(JSON.parse(xhr.responseText));
                        resolve(JSON.parse(xhr.responseText).movieList)
                    }
                }
            })
        }
        
        function showList(data) {
            
        }
    </script>
</body>

然后我们将得到的这个JSON对象里面的属性movieList通过resolve返回出去,它到时候就会被return new Promise生成的实例对象中的then方法拿到。然后我们再将这个数据传给showList方法使用。

<body>
    <ul id="ul"></ul>
    
    <script>
       function getData() {
            return new Promise((resolve, reject) => {
                let xhr = new XMLHttpRequest();
                xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
                xhr.send();
                xhr.onreadystatechange = function () {
                    if (xhr.readyState == 4 && xhr.status == 200) {
                        console.log(JSON.parse(xhr.responseText));
                        resolve(JSON.parse(xhr.responseText).movieList)
                    }
                }
            })
        }
        
        function showList(data) {
            
        }
        
         getData()
            .then((res) => {
                showList(res)
            })
    </script>
</body>

此时res存放的就是JSON对象里的属性movieList,也就是我们想要的数据,我们在then方法里再去调用showList方法,并将res作为参数传给它。此时showList里的形参data就接收到了这个数据。我们再去showList方法中对这串数据进行操作。

我们对这个data数据进行遍历,这个数据是一个数组类型,这个数组里面存了12个对象,我们用foreach去遍历这个数组,里面接收一个回调函数,参数item代表数组里的每一个对象。

<body>
    <ul id="ul"></ul>
    
    <script>
       function getData() {
            return new Promise((resolve, reject) => {
                let xhr = new XMLHttpRequest();
                xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
                xhr.send();
                xhr.onreadystatechange = function () {
                    if (xhr.readyState == 4 && xhr.status == 200) {
                        console.log(JSON.parse(xhr.responseText));
                        resolve(JSON.parse(xhr.responseText).movieList)
                    }
                }
            })
        }
        
        function showList(data) {
            data.forEach(item => {
            
            }
        }
        
         getData()
            .then((res) => {
                showList(res)
            })
    </script>
</body>

然后我们需要去创建li标签,并将对象里的nm属性赋值给li,并将这个li添加到ul里去。

<body>
    <ul id="ul"></ul>
    
    <script>
       function getData() {
            return new Promise((resolve, reject) => {
                let xhr = new XMLHttpRequest();
                xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
                xhr.send();
                xhr.onreadystatechange = function () {
                    if (xhr.readyState == 4 && xhr.status == 200) {
                        console.log(JSON.parse(xhr.responseText));
                        resolve(JSON.parse(xhr.responseText).movieList)
                    }
                }
            })
        }
        
       function showList(data) {
            data.forEach(item => {
                let li = document.createElement('li')
                li.innerText = item.nm
                document.getElementById('ul').appendChild(li)
            });
        }
        
         getData()
            .then((res) => {
                showList(res)
            })
    </script>
</body>

这样我们就完成了封装接口的代码。我们到浏览器看看是否展示了数据:

image.png

成功展示了数据。

5.总结

在这篇文章中,我们了解了异步是一个什么样的概念,并且学到了两种解决异步的方法:回调函数和Promise。在实际开发中我们更倾向于使用Promise,因为它更清晰简洁。