资讯专栏INFORMATION COLUMN

浅谈 Promise

李涛 / 2154人阅读

摘要:解决异步编程有两种主要方式事件模型和回调函数。将异步操作以同步操作的流程表达出来,避免了层层嵌套回调函数。方法是的别名,相当于函数的第一个参数传入,第二个参数传入发生错误时的回调函数。

JavaScript 解决异步编程有两种主要方式:事件模型和回调函数。但是随着业务越来越复杂,这两种方式已经不能满足开发者的需求了,Promise 可以解决这方面的问题。

为了更好的理解 Promise 是如何工作的,我们先来了解一下传统的异步编程的方式。

异步编程的方式 1. 事件模型:
let button = document.getElementId("my-btn");
button.onclick = function() {
    console.log("Hello");
}

任务的执行不取决于代码的顺序,而取决于某个事件是否发生。上面代码中,console.log("Hello") 直到 button 被点击后才会被执行。当 button 被点击,赋值给 onclick 的函数就被添加到作业队列的尾部,并在队列前部所有任务结束之后再执行。

事件模型适用于处理简单的交互,若将多个独立的异步调用连接在一起,必须跟踪每个事件的事件目标,会使程序更加复杂,运行流程会变得很不清晰。

2. 回调函数

我们来看一下比较经典的使用 jsonp 解决跨域问题的示例:

function callback (res) {
  document.getElementById("d1").innerHTML = res.result.address
  console.log("Your public IP address is: ", res)
}
function jsonp (lat, lng) {
  let src = `https://apis.map.qq.com/ws/geocoder/v1/?location=${lat},${lng}&key=yourKey&output=jsonp&callback=callback`
  let script = document.createElement("script")
  script.setAttribute("type","text/javascript")
  script.src = src;
  document.body.appendChild(script)
}
jsonp(39.92, 116.43)

初看这种模式运作得相当好,简单、容易理解,但你可能会迅速的发现这样的模式不利于代码的阅读和维护,各个部分之间耦合度太高,容易陷入回调地狱。就像这样:

method1(function(err, result) {
  if (err) {
    throw err
  }

  method2(function(err, result) {
    if (err) {
      throw err
    }

    method3(function(err, result) {
      if (err) {
        throw err
      }

      method4(function(err, result) {
        if (err) {
          throw err
        }

        method5(result)
      })
    })
  })
})

Promise 能大幅度改善这种情况。我们来看下Promise 是如何实现的:

let promise = new Promise((resolve, reject) => {
  // ... method 1 some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
})
promise.then((value) => {
  // method 2 some code
}).then((value) => {
  // method 3 some code
}).then((value) => {
  // method 4 some code
}).then((value) => {
  // method 5 some code
}).catch((err) => {
  // some err code
})

是不是清晰很多?是不是很神奇?接下来一起来学习一下 Promise 吧!

Promise 相关知识 1. Promise 的基础知识

我们先来看下 Promise 的方法有哪些:

Promise.Prototype.then()
Promise.Prototype.catch()
Promise.Prototype.finally()
Promise.all()
Promise.race()
Promise.resolve()
Promise.reject()
Promise.try()

Promise 函数的执行,都是依赖于状态的改变,这三种状态要记牢哦:

Pending:进行中
Fulfilled:已成功
Rejected:已失败

Promise 优点:

1)对象的状态不受外界影响。
2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。
3)将异步操作以同步操作的流程表达出来,避免了层层嵌套回调函数。
4)提供统一的接口,使得控制异步操作更加容易。

Promise 缺点:

1)无法取消 Promise,一旦新建它就会立即执行,无法中途取消。
2)如果不设置回调函数,Promise 内部抛出的错误,不会反映到外部。
3)当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

了解了 Promise 的方法,3种状态以及特点和优缺点之后,接下来我们来看一下 Promise 是怎么使用的。

2. Promise 的基本用法

我们来创造一个读取文件的 Promise 实例:

const fs = require("fs")
const path = require("path")

function readFile (filename) {
  return new Promise (function (resolve, reject) {
    // reject(new Error("err"))
    //触发异步操作
    fs.readFile(filename, {encoding: "utf8"}, function (err, contents) {

      // 检查错误
      if (err) {
        reject(err)
        return
      }

      //读取成功
      resolve(contents)
    })
  })
}
let promise = readFile(path.resolve(__dirname, "../json/a.json"))

上述实例中 resolve 函数的作用是,将 Promise 对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;

reject 函数的作用是,将 Promise 对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

2.1 Promise.Prototype.then()

Promise 实例生成以后,就可以用 then 方法来分别指定 resolved 状态和 rejected 状态的回调函数了。

promise.then(function (contents) {
  // 完成
  console.log(contents)
  return(contents)
}, function (err) {
  // 失败
  console.log(err.message)
})

我们可以看到,then 方法接受两个回调函数作为参数;
第一个回调函数在 Promise 对象的状态变为 resolved 时调用;
第二个回调函数在 Promise 对象的状态变为 rejected 时调用;
其中,第二个函数是可选的。这两个函数都接受 Promise 对象传出的值作为参数。

Promise.then 方法每次调用,都返回一个新的 Promise 对象,所以支持链式写法。

let taskA = (value) => {
  console.log("Task A")
  console.log(value)
  return value
}

let taskB = (value) => {
  console.log("Task B")
  console.log(value)
}

promise
.then(taskA)
.then(taskB)
.catch((err) => {
  console.log(err.message)
})
2.2 Promise.Prototype.catch()

Promise.prototype.catch 方法是 .then(null, rejection) 的别名,相当于 then 函数的第一个参数传入 null,第二个参数传入发生错误时的回调函数。

promise.then(function(value) {
  // 成功
  console.log(value)
}).catch(function (err) {
  // 失败
  console.log(err.message)
})
2.3 Promise.Prototype.finally()

finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的,目前大部分浏览器还不支持,不过可以自己实现。

finally 方法的实现:

Promise.prototype.finally = function (callback) {
  let P = this.constructor
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  )
}

finally 方法的使用:

promise
.then((contents) => {
  console.log(contents)
  return contents
})
.catch((err) => {
  console.log(err.message)
})
.finally(() => {
    console.log("finally")
})
2.4 Promise.all()

Promise.all 方法可以将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

新的 Promise p 的状态由 p1、p2、p3 决定,只有当 p1、p2、p3 的状态都变成了 fulfilled,p 的状态才会变成 fulfilled;只要 p1、p2、p3 之中有一个被 rejected,p 的状态就变成了 rejected。

注意,如果作为参数的 Promise 实例,自己定义了 catch 方法,那么它一旦被 rejected,并不会触发Promise.all()的 catch 方法的。

如果 p2 有自己的 catch 方法,就不会调用 Promise.all() 的 catch 方法。

const p1 = new Promise((resolve, reject) => {
  resolve("hello");
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error("报错了");
})
.then(result => result)
.catch(e => e);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]

如果 p2 没有自己的 catch 方法,就会调用 Promise.all() 的 catch 方法。

const p1 = new Promise((resolve, reject) => {
  resolve("hello");
})
.then(result => result);

const p2 = new Promise((resolve, reject) => {
  throw new Error("报错了");
})
.then(result => result);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 报错了
2.5 Promise.race()

Promise.race 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);

新的Promise p,只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数。

function timerPromisefy(delay) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(delay)
    }, delay)
  })
}

Promise.race([
  timerPromisefy(10),
  timerPromisefy(20),
  timerPromisefy(30)
]).then(function (values) {
  console.log(values) // 10
})
2.6 Promise.resolve()

有时需要将现有对象转为 Promise 对象,Promise.resolve 方法就起到这个作用,返回一个 fulfilled 状态的 Promise 对象。

const promise = Promise.resolve("hello");
promise.then(function(value){
    console.log(value);
});

// 相当于
const promise = new Promise(resolve => {
   resolve("hello");
});
promise.then((value) => {
  console.log(value)
})
2.7 Promise.reject()

Promise.reject(reason) 方法也会返回一个新的 Promise 实例,该实例的状态为 rejected。

const p = Promise.reject("出错了");
p.then(null, (value) => {
  console.log(value)
})

// 等同于
const p = new Promise((resolve, reject) => reject("出错了"))
p.then(null, (value) => {
  console.log(value)
})
2.8 Promise.try()

让同步函数同步执行,异步函数异步执行。

const f = () => console.log("now");
Promise.try(f);
console.log("next");
// now
// next
应用 异步加载图片

实现方法:

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    const image = new Image()

    image.onload = function() {
      resolve(image)
    }

    image.onerror = function() {
      reject(new Error("Could not load image at " + url))
    }

    image.src = url
  })
}

调用:

loadImageAsync("图片路径").then((value) => {
  document.getElementById("d1").appendChild(value)
}).catch((err) => {
  console.log(err.message)
})
异步加载 js

实现方法:

let loadScript = function () {
  return function _loadScript(url, callBack) {
    return new Promise(function (resolve) {
      let script = document.createElement("script")
      script.type = "text/javascript"
      if (script.readyState) {
        // 兼容IE的script加载事件
        script.onreadystatechange = function () {
          // loaded : 下载完毕 complete: 数据准备完毕。这两个状态ie可能同时出现或者只出现一个
          if (script.readyState === "loaded" || script.readyState === "complete") {
            // 防止加载两次
            script.onreadystatechange = null
            callBack()
            // 把函数传递下去,保证能顺序加载js
            resolve(_loadScript)
          }
        }
      } else {
        script.onload = function () {
          callBack()
          resolve(_loadScript)
        }
      }
      script.src = url
      document.head.appendChild(script)
    })
  }
}()

调用:

loadScript("http://code.jquery.com/jquery-3.2.1.min.js ", () => {})
    .then(() => {
      $("#d1").on("click", () => {alert(1)})
    }).catch((err) => {
      console.log(err)
    })
request 请求的封装
import axios from "./axios"
import qs from "qs"

const config = {
  time: +new Date() + "",
  timeout: 6000,
  headers: {
    "Content-Type": "application/x-www-form-urlencoded",
    time: new Date().getTime()
  }
}

function checkResponse (response, notice) {
  return new Promise((resolve, reject) => {
    let code = Number(response.code)
    if (code === 0 || code === 200 || code === 2000 || code === 1 || code === 2 || code === "0" || code === 109) {
      resolve(response)
    } else {
      if (notice) {
        // 提示信息
        console.log("response-notice", notice)
      }
      reject(response)
    }
  })
}

function fixURL (url, type) {
  let result = ""
  switch (type) {
    case "r":
      result += `/api/v2${url}`
      break
  }
  return result
}

/**
 * Requests a URL, returning a promise.
 *
 * @param  {object} [options]         The options we want to pass to axios
 * @param  {string} [options.url]     请求的url地址(必须)
 * @param  {string} [options.method]  请求方式, get or post,默认post
 * @param  {object} [options.data]    请求参数
 * @param  {number} [options.timeout] 请求超时时间
 * @param  {boolean} [options.notice] 请求失败是否显示提示,默认true
 * @return {object}                   promise对象
 */
function request (options = {}) {
  let {
    url,
    method,
    data,
    timeout,
    headers,
    type,
    notice
  } = options

  method = method || "post"
  data = data || {}
  type = type || "t"
  timeout = timeout || config.timeout
  headers = Object.assign({}, config.headers, headers)
  notice = notice === undefined ? true : notice

  let result = {}
  if (method === "get") {
    result = new Promise((resolve, reject) => {
      axios({
        method: "get",
        url: fixURL(url, type),
        params: data,
        timeout,
        headers
      })
        .then((res) => {
          checkResponse(res.data, notice).then((data) => {
            resolve(data)
          })
            .catch((data) => {
              reject(data)
            })
        })
        .catch((data) => {
          reject(data)
        })
    })
  } else if (method === "post") {
    result = new Promise((resolve, reject) => {
      axios({
        method: "post",
        url: fixURL(url, type),
        data: headers["Content-Type"] === "application/x-www-form-urlencoded" ? qs.stringify(data) : data,
        timeout,
        headers
      })
        .then((res) => {
          checkResponse(res.data, notice).then((data) => {
            resolve(data)
          })
            .catch((data) => {
              reject(data)
            })
        })
        .catch((data) => {
          reject(data)
        })
    })
  }
  return result
}

export default request
附 Promise 代码实现
// 判断变量否为function
const isFunction = variable => typeof variable === "function"
// 定义Promise的三种状态常量
const PENDING = "PENDING"
const FULFILLED = "FULFILLED"
const REJECTED = "REJECTED"

class MyPromise {
  constructor (handle) {
    if (!isFunction(handle)) {
      throw new Error("MyPromise must accept a function as a parameter")
    }
    // 添加状态
    this._status = PENDING
    // 添加状态
    this._value = undefined
    // 添加成功回调函数队列
    this._fulfilledQueues = []
    // 添加失败回调函数队列
    this._rejectedQueues = []
    // 执行handle
    try {
      handle(this._resolve.bind(this), this._reject.bind(this)) 
    } catch (err) {
      this._reject(err)
    }
  }
  // 添加resovle时执行的函数
  _resolve (val) {
    const run = () => {
      if (this._status !== PENDING) return
      // 依次执行成功队列中的函数,并清空队列
      const runFulfilled = (value) => {
        let cb;
        while (cb = this._fulfilledQueues.shift()) {
          cb(value)
        }
      }
      // 依次执行失败队列中的函数,并清空队列
      const runRejected = (error) => {
        let cb;
        while (cb = this._rejectedQueues.shift()) {
          cb(error)
        }
      }
      /* 如果resolve的参数为Promise对象,则必须等待该Promise对象状态改变后,
        当前Promsie的状态才会改变,且状态取决于参数Promsie对象的状态
      */
      if (val instanceof MyPromise) {
        val.then(value => {
          this._value = value
          this._status = FULFILLED
          runFulfilled(value)
        }, err => {
          this._value = err
          this._status = REJECTED
          runRejected(err)
        })
      } else {
        this._value = val
        this._status = FULFILLED
        runFulfilled(val)
      }
    }
    // 为了支持同步的Promise,这里采用异步调用
    setTimeout(run, 0)
  }
  // 添加reject时执行的函数
  _reject (err) { 
    if (this._status !== PENDING) return
    // 依次执行失败队列中的函数,并清空队列
    const run = () => {
      this._status = REJECTED
      this._value = err
      let cb;
      while (cb = this._rejectedQueues.shift()) {
        cb(err)
      }
    }
    // 为了支持同步的Promise,这里采用异步调用
    setTimeout(run, 0)
  }
  // 添加then方法
  then (onFulfilled, onRejected) {
    const { _value, _status } = this
    // 返回一个新的Promise对象
    return new MyPromise((onFulfilledNext, onRejectedNext) => {
      // 封装一个成功时执行的函数
      let fulfilled = value => {
        try {
          if (!isFunction(onFulfilled)) {
            onFulfilledNext(value)
          } else {
            let res =  onFulfilled(value);
            if (res instanceof MyPromise) {
              // 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
              res.then(onFulfilledNext, onRejectedNext)
            } else {
              //否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
              onFulfilledNext(res)
            }
          }
        } catch (err) {
          // 如果函数执行出错,新的Promise对象的状态为失败
          onRejectedNext(err)
        }
      }
      // 封装一个失败时执行的函数
      let rejected = error => {
        try {
          if (!isFunction(onRejected)) {
            onRejectedNext(error)
          } else {
              let res = onRejected(error);
              if (res instanceof MyPromise) {
                // 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
                res.then(onFulfilledNext, onRejectedNext)
              } else {
                //否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
                onFulfilledNext(res)
              }
          }
        } catch (err) {
          // 如果函数执行出错,新的Promise对象的状态为失败
          onRejectedNext(err)
        }
      }
      switch (_status) {
        // 当状态为pending时,将then方法回调函数加入执行队列等待执行
        case PENDING:
          this._fulfilledQueues.push(fulfilled)
          this._rejectedQueues.push(rejected)
          break
        // 当状态已经改变时,立即执行对应的回调函数
        case FULFILLED:
          fulfilled(_value)
          break
        case REJECTED:
          rejected(_value)
          break
      }
    })
  }
  // 添加catch方法
  catch (onRejected) {
    return this.then(undefined, onRejected)
  }
  // 添加静态resolve方法
  static resolve (value) {
    // 如果参数是MyPromise实例,直接返回这个实例
    if (value instanceof MyPromise) return value
    return new MyPromise(resolve => resolve(value))
  }
  // 添加静态reject方法
  static reject (value) {
    return new MyPromise((resolve ,reject) => reject(value))
  }
  // 添加静态all方法
  static all (list) {
    return new MyPromise((resolve, reject) => {
      /**
       * 返回值的集合
       */
      let values = []
      let count = 0
      for (let [i, p] of list.entries()) {
        // 数组参数如果不是MyPromise实例,先调用MyPromise.resolve
        this.resolve(p).then(res => {
          values[i] = res
          count++
          // 所有状态都变成fulfilled时返回的MyPromise状态就变成fulfilled
          if (count === list.length) resolve(values)
        }, err => {
          // 有一个被rejected时返回的MyPromise状态就变成rejected
          reject(err)
        })
      }
    })
  }
  // 添加静态race方法
  static race (list) {
    return new MyPromise((resolve, reject) => {
      for (let p of list) {
        // 只要有一个实例率先改变状态,新的MyPromise的状态就跟着改变
        this.resolve(p).then(res => {
          resolve(res)
        }, err => {
          reject(err)
        })
      }
    })
  }
  finally (cb) {
    return this.then(
      value  => MyPromise.resolve(cb()).then(() => value),
      reason => MyPromise.resolve(cb()).then(() => { throw reason })
    );
  }
}

参考文章
Promise 官网
ECMAScript 6 入门
Promise 源码详解
Promise 实现原理(附源码)
es6-promise-try npm
JavaScript Promise:简介

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/99301.html

相关文章

  • 浅谈Javascript中Promise对象的实现

    摘要:我们可以进行适当的改进,把回调函数写到外面即使是改写成这样,代码还是不够直观,但是如果有了对象,代码就可以写得非常清晰,一目了然,请看这样函数就不用写在的回调中了目前的标准中还未支持对象,那么我们就自己动手,丰衣足食吧。 本文同步自我得博客:http://www.joeray61.com 很多做前端的朋友应该都听说过Promise(或者Deferred)对象,今天我就讲一下我对Prom...

    caiyongji 评论0 收藏0
  • 浅谈Promise之参照Promise/A+规范实现Promise

    摘要:在需要多个操作的时候,会导致多个回调函数嵌套,导致代码不够直观,就是常说的回调地狱,通常通过来解决本意是承诺,在程序中的意思就是承诺我过一段时间后会给你一个结果。 在需要多个操作的时候,会导致多个回调函数嵌套,导致代码不够直观,就是常说的回调地狱,通常通过promise来解决Promise本意是承诺,在程序中的意思就是承诺我过一段时间后会给你一个结果。 什么时候会用到过一段时间?答案是...

    roundstones 评论0 收藏0
  • 浅谈ES6原生Promise

    摘要:如果有错误,则到的第二个回调函数中,对错误进行处理。假设第一个的第一个回调没有返回一个对象,那么第二个的调用者还是原来的对象,只不过其的值变成了第一个中第一个回调函数的返回值。 ES6标准出炉之前,一个幽灵,回调的幽灵,游荡在JavaScript世界。 正所谓: 世界本没有回调,写的人多了,也就有了})})})})})。 Promise的兴起,是因为异步方法调用中,往往会出现回调函数一...

    yedf 评论0 收藏0
  • 浅谈JavaScript中的事件循环机制

    摘要:事件循环背景是一门单线程非阻塞的脚本语言,单线程意味着,代码在执行的任何时候,都只有一个主线程来处理所有的任务。在意识到该问题之际,新特性中的可以让成为一门多线程语言,但实际开发中使用存在着诸多限制。这个地方被称为执行栈。 事件循环(Event Loop) 背景 JavaScript是一门单线程非阻塞的脚本语言,单线程意味着,JavaScript代码在执行的任何时候,都只有一个主线程来...

    Pluser 评论0 收藏0
  • 浅谈异步编程

    摘要:等到一段时间后,车到了,小红打电话给小明车到了发布,小明在接到电话之后,要做一些准备订阅时定义的回调函数。工作中的异步通过事件实例去做调控,简单的代码示例是一个事件实例,负责发布订阅者的内部实现订阅发布另一端程序干一些事情。 浅谈异步编程 引子 页面渲染与setTimeout(); 同步与异步------我的理解 任务在当次事件循环中阻塞后续任务进行的(指的是耗时较多,这个多少,暂时还...

    laoLiueizo 评论0 收藏0
  • 浅谈promise和js执行机制(一)

    摘要:如果改变已经发生了,你再对对象添加回调函数,也会立即得到这个结果。其次,如果不设置回调函数,内部抛出的错误,不会反应到外部。作为一个入门级前端,今天是一个非常值得纪念的日子,因为这是我第一次在论坛上发表帖子,作为起步。虽然我觉得自己水平还是十分的有限,对一些细节的理解还不是很透彻,但是还是要迈出这一步,不管是给别的新手作为学习参考,还是自己以后回顾,总觉得需要把自己的成长记录下来,希望自己以...

    haitiancoder 评论0 收藏0

发表评论

0条评论

李涛

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<