浏览器工作原理(19) - 使用promise, 告别回调函数

上一篇文章 我们了解了微任务是如何工作的,今天主要来学习一下微任务的一个实现 — Promise,BOM和DOM中大多数的API都是建立在Promise上, 可以说Promise已经成为现代前端的一把利刃

我们先从JavaScript引入Promise的动机谈起,了解一下为什么引入promise,它又能解决什么问题呢?

异步编程:代码逻辑不连贯

页面的事件是通过循环系统来维护的,页面中的任务都是执行在主线程之上的,而那些比较耗时的任务,比如下载网络文件,获取用户设备信息等,这些任务都不会放在主线程,而是放在主线程之外的进程或线程来完成,这样会提高效率。

在这里插入图片描述
上图是一个 异步回调 的示意图,页面主线程发起一个耗时的任务,并将任务交给其他进程来处理,页面主线程会继续执行消息队列中的任务,等该进程处理完这个任务后,会将该任务添加到渲染进程的消息队列中,等待循环系统来处理。

web页面的单线程架构决定了异步回调,异步回调又影响了我们的编程方式。假设有一个XMLHTTPRequest任务:


//执行状态
function onResolve(response){console.log(response) }
function onReject(error){console.log(error) }

let xhr = new XMLHttpRequest()
xhr.ontimeout = function(e) { onReject(e)}
xhr.onerror = function(e) { onReject(e) }
xhr.onreadystatechange = function () { onResolve(xhr.response) }

//设置请求类型,请求URL,是否同步信息
let URL = 'https://time.geekbang.com'
xhr.open('Get', URL, true);

//设置参数
xhr.timeout = 3000 //设置xhr请求的超时时间
xhr.responseType = "text" //设置响应返回的数据格式
xhr.setRequestHeader("X_TEST","time.geekbang")

//发出请求
xhr.send();

这段传统的五步请求方法并没有什么问题,可以正常获取结果,但是回调函数太多,造成逻辑不连贯,那么该如何解决这个问题呢?

封装异步代码,让处理流程变得线性

有没有这样一种方式,可以让我们只关注输入和输出,而减少中间过程的代码?

在这里插入图片描述
从图中可以看出:我们把输入之后的步骤完全封装起来,只在乎输入和输出,不管中间的XMLHttpRequest过程。

我们把输入的请求信息全部保存在一个request对象中,里面包含了请求方式,请求地址,请求头,引用地址,同步还是异步,安全设置等信息


//makeRequest用来构造request对象
function makeRequest(request_url) {
    let request = {
        method: 'Get',
        url: request_url,
        headers: '',
        body: '',
        credentials: false,
        sync: true,
        responseType: 'text',
        referrer: ''
    }
    return request
}

然后封装请求过程, 将所有的请求细节封装在一个XFetch函数中,


//[in] request,请求信息,请求头,延时值,返回类型等
//[out] resolve, 执行成功,回调该函数
//[out] reject  执行失败,回调该函数
function XFetch(request, resolve, reject) {
    let xhr = new XMLHttpRequest()
    xhr.ontimeout = function (e) { reject(e) }
    xhr.onerror = function (e) { reject(e) }
    xhr.onreadystatechange = function () {
        if (xhr.status = 200)
            resolve(xhr.response)
    }
    xhr.open(request.method, URL, request.sync);
    xhr.timeout = request.timeout;
    xhr.responseType = request.responseType;
    //补充其他请求信息
    //...
    xhr.send();
}

现在我们有了参数生成函数和封装请求参数,我们的业务代码可以如下实现:


XFetch(makeRequest('https://time.geekbang.org'),
    function resolve(data) {
        console.log(data)
    }, function reject(e) {
        console.log(e)
    })
新的问题:回调地狱

上面的业务代码已经很方便了,也比较线性化,如果遇到很复杂的场景,那么又会怎么样呢?参考下面的代码:


XFetch(makeRequest('https://time.geekbang.org/?category'),
      function resolve(response) {
          console.log(response)
          XFetch(makeRequest('https://time.geekbang.org/column'),
              function resolve(response) {
                  console.log(response)
                  XFetch(makeRequest('https://time.geekbang.org')
                      function resolve(response) {
                          console.log(response)
                      }, function reject(e) {
                          console.log(e)
                      })
              }, function reject(e) {
                  console.log(e)
              })
      }, function reject(e) {
          console.log(e)
      })

这段代码的意思基本上一个请求会依赖另一个请求的结果,如果嵌套比较多,就会写出上面的代码,造成这样的结果有两个原因:

  • 第一是嵌套调用,下面的任务依赖上个任务的请求结果,并在上个任务的回调函数内部执行新的业务逻辑,这样当嵌套层次多了之后,代码的可读性就变得非常差了。
  • 第二是任务的不确定性,执行每个任务都有两种可能的结果(成功或者失败),所以体现在代码中就需要对每个任务的执行结果做两次判断,这种对每个任务都要进行一次额外的错误处理的方式,明显增加了代码的混乱程度。

那么该如何解决呢?

  • 第一是消灭嵌套调用
  • 第二是合并多个任务的错误处理
Promise:上面解决方案的具体实现

首先我们先使用Promise重构一下Xfetch函数:


function XFetch(request) {
  function executor(resolve, reject) {
      let xhr = new XMLHttpRequest()
      xhr.open('GET', request.url, true)
      xhr.ontimeout = function (e) { reject(e) }
      xhr.onerror = function (e) { reject(e) }
      xhr.onreadystatechange = function () {
          if (this.readyState === 4) {
              if (this.status === 200) {
                  resolve(this.responseText, this)
              } else {
                  let error = {
                      code: this.status,
                      response: this.response
                  }
                  reject(error, this)
              }
          }
      }
      xhr.send()
  }
  return new Promise(executor)
}

然后我们用一下重构之后的方法重新业务逻辑:


var x1 = XFetch(makeRequest('https://time.geekbang.org/?category'))
var x2 = x1.then(value => {
    console.log(value)
    return XFetch(makeRequest('https://www.geekbang.org/column'))
})
var x3 = x2.then(value => {
    console.log(value)
    return XFetch(makeRequest('https://time.geekbang.org'))
})
x3.catch(error => {
    console.log(error)
})

接下来分析一下具体实现:

1、 在实现XFetch的时候我们引入了Promise,调用XFetch就会返回一个promise对象

2、构建promise的时候,传入一个excutor回调函数,主要业务流程都放在这个函数里面

3、如果excutor里运行成功了,就会执行then里的resolve方法,如果失败,就会reject

4、在excutor函数中调用resolve函数,会触发then里的回调函数,如果reject,则会调用catch方法

Promise主要通过下面两步解决了嵌套回调的问题:

首先,Promise实现了回调函数的延时绑定,具体实现就是先创建Promise对象x1,通过Promise的构造函数excutor来执行业务代码逻辑,然后使用x1.then来设置回调函数,示例代码如下:

//创建Promise对象x1,并在executor函数中执行业务逻辑
function executor(resolve, reject){
    resolve(100)
}
let x1 = new Promise(executor)


//x1延迟绑定回调函数onResolve
function onResolve(value){
    console.log(value)
}
x1.then(onResolve)
其次,需要将回调函数onResolve的返回值穿透到最外层

们会根据 onResolve 函数的传入值来决定创建什么类型的 Promise 任务,创建好的 Promise 对象需要返回到最外层,这样就可以摆脱嵌套循环了。你可以先看下面的代码:

在这里插入图片描述
那接下来我们再来看看 Promise 是怎么处理异常的?


function executor(resolve, reject) {
    let rand = Math.random();
    console.log(1)
    console.log(rand)
    if (rand > 0.5)
        resolve()
    else
        reject()
}
var p0 = new Promise(executor);

var p1 = p0.then((value) => {
    console.log("succeed-1")
    return new Promise(executor)
})

var p3 = p1.then((value) => {
    console.log("succeed-2")
    return new Promise(executor)
})

var p4 = p3.then((value) => {
    console.log("succeed-3")
    return new Promise(executor)
})

p4.catch((error) => {
    console.log("error")
})
console.log(2)

这段代码有四个 Promise 对象:p0~p4。无论哪个对象里面抛出异常,都可以通过最后一个对象 p4.catch 来捕获异常,通过这种方式可以将所有 Promise 对象的错误合并到一个函数来处理,这样就解决了每个任务都需要单独处理异常的问题。

之所以可以使用最后一个对象来捕获所有异常,是因为 Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被 onReject 函数处理或 catch 语句捕获为止

Promise 与微任务

看下面代码:

function executor(resolve, reject) {
    resolve(100)
}
let demo = new Promise(executor)

function onResolve(value){
    console.log(value)
}
demo.then(onResolve)

对于上面这段代码,我们需要重点关注下它的执行顺序:

1、首先执行 new Promise 时,Promise 的构造函数会被执行

2、 Promise 的构造函数会调用 Promise 的参数 executor 函数。然后在 executor 中执行了 resolve,resolve的执行会引起then函数里onResolve函数的执行

口说无凭,接下来看一下具体实现,通过模拟一个Promise来看一下实现:


function Bromise(executor) {
    var onResolve_ = null
    var onReject_ = null
     //模拟实现resolve和then,暂不支持rejcet
    this.then = function (onResolve, onReject) {
        onResolve_ = onResolve
    };
    function resolve(value) {
          //setTimeout(()=>{
            onResolve_(value)
           // },0)
    }
    executor(resolve, null);
}

观察上面这段代码,我们实现了自己的构造函数、resolve、then 方法。接下来我们使用 Bromise 来实现我们的业务代码,实现后的代码如下所示:


function executor(resolve, reject) {
    resolve(100)
}
//将Promise改成我们自己的Bromsie
let demo = new Bromise(executor)

function onResolve(value){
    console.log(value)
}
demo.then(onResolve)

执行这段代码,我们发现执行出错,输出的内容是


Uncaught TypeError: onResolve_ is not a function
    at resolve (<anonymous>:10:13)
    at executor (<anonymous>:17:5)
    at new Bromise (<anonymous>:13:5)
    at <anonymous>:19:12

之所以出现这个错误,是由于 Bromise 的延迟绑定导致的,在调用到 onResolve_ 函数的时候,Bromise.then 还没有执行,所以执行上述代码的时候,当然会报“onResolve_ is not a function“的错误了。

也正是因为此,我们要改造 Bromise 中的 resolve 方法,让 resolve 延迟调用 onResolve_。

要让 resolve 中的 onResolve_ 函数延后执行,可以在 resolve 函数里面加上一个定时器,让其延时执行 onResolve_ 函数,你可以参考下面改造后的代码:


function resolve(value) {
          setTimeout(()=>{
              onResolve_(value)
            },0)
    }

上面采用了定时器来推迟 onResolve 的执行,不过使用定时器的效率并不是太高,好在我们有微任务,所以 Promise 又把这个定时器改造成了微任务了,这样既可以让 onResolve_ 延时被调用,又提升了代码的执行效率。这就是 Promise 中使用微任务的原由了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值