异步、回调和Promise

JS 异步编程模型

同步、异步、轮询和回调

如果能直接拿到结果

  • 那就是同步
  • 比如你在医院挂号,你拿到号才会离开窗口
  • 同步任务可能消耗 10 毫秒,也可能需要 3 秒
  • 总之不拿到结果你是不会离开的

如果不能直接拿到结果

  • 那就是异步
  • 比如你在餐厅等位置,你拿到号可以去逛街
  • 什么时候才能真正吃饭呢?
  • 你可以每 10 分钟去餐厅问一下(轮询)
  • 你也可以扫码用微信接收通知(回调)

异步举例

以 AJAX 为例

  • request.send()之后,并不能直接得到response

    不信console.log(request.response)试试

  • 必须等到readyState变为4后,浏览器request.onreadystatechange函数
  • 我们才能得到request.response
  • 这跟餐厅给你发送微信提醒的过程是类似的

回调 callback

  • 你写给自己用的函数,不是回调
  • 你写给别人用的函数,就是回调
  • request.onreadystatechange就是我写给浏览器调用的
  • 意思就是你(浏览器)回头调用一下这个函数

    在中文里,’回头’ 也有 ‘将来’ 的意思, 如 ‘我回头请你吃饭’

回调举例

把函数 1 给另一个函数 2

function f1() {}
function f2(fn) {
  fn();
}
f2(f1);

分析

  1. Q: 我调用f1没有?A: 没有调用
  2. Q: 我把f1传给f2(别人)了没有?A: 传了
  3. Q: f2调用f1了没有?A: f2调用了f1
  4. Q: 那么,f1是不是我写给f2调用的函数?A: 是

所以,f1是回调

抬杠

  • Q: 如果f2没有调用f1呢?
  • A: f2有病啊?它不调用f1那它为什么要接收fn参数

异步和回调的关系

关联

  • 异步任务需要在得到结果时通知 JS 来拿结果
  • 怎么通知呢?
  • 可以让 JS 留一个函数地址(电话号码)给浏览器
  • 异步任务完成时浏览器调用该函数地址即可(拨打电话)
  • 同时把结果作为参数传给该函数(电话里说可以来吃了)
  • 这个函数是我写给浏览器调用的,所以是回调函数

回调就是我们先把一个函数留给它,等它完成了它就会调这个函数,调的时候它会把结果放到函数的参数里,这样就可以实现异步的通知。

区别

  • 异步任务需要用到回调函数来通知结果

但它不一定非要用回调函数,它也可以用轮询

  • 回调函数不一定只用在异步任务里,回调可以用到同步任务里

array.forEach(n=>console.log(n))就是同步回调

异步不一定要用到回调,回调也不一定要用到异步里。异步和回调他们两个只是合作关系。

判断同步异步

判断一个函数的返回值是否处于

  • setTimeout
  • AJAX(即XMLHttpRequest)
  • AddEventListener

如果一个函数的返回值处于这三个东西内部,那么这个函数就是异步函数

如果还有其他 API 是异步的,之后的博客会另行说明

等一下

Q: 我听说 AJAX 可以设置为同步的

A: 傻*前端才把 AJAX 设置为同步的,因为这样做会使请求期间页面卡住

总结

  • 异步任务不能拿到结果
  • 于是我们传一个回调给异步任务
  • 异步任务完成时调用回调
  • 调用的时候把结果作为参数

扩展阅读: 高频网红面试题’1’,’2',‘3’.map(parseInt) 原理解析 - 掘金

A JavaScript Optional Argument Hazard


Promise(前端解决异步问题的统一方案)

进一步思考: 如果异步任务有两个结果成功或失败,怎么办?

两个结果

  • 方法一: 回调接收两个参数
fs.readFile("./1.txt", (error, data) => {
  if (error) {
    console.log("失败");
    return;
  }
  console.log(data.toString()); // 成功
});
  • 方法二: 搞两个回调
ajax(
  "get",
  "/1.json",
  (data) => {},
  (error) => {}
);
// 前面函数是成功回调,后面函数是失败回调

ajax("get", "/1.json", {
  success: () => {},
  fail: () => {},
});
// 接收一个对象,对象有两个key表示成功和失败

这些方法的不足

  1. 不规范,成功回调和失败回调的名称五花八门,比如 success+error、success+fail、done+fail
  2. 容易出现回调地狱(callback hell),代码变得看不懂
  3. 很难进行错误处理

A guide to writing asynchronous JavaScript programs: Callback Hell

怎么解决回调问题

  • 1976 年,Daniel P.Friedman 和 David Wise 提出 Promise 思想
  • 后人基于此发明了 Future、Delay、Deferred 等
  • 前端结合 Promise 和 JS,制定了Promise/A+ 规范

该规范详细描述了 Promise 的原理和使用方法

以 AJAX 的封装为例

来解释 Promise 的用法

ajax = (method, url, options) => {
  const { success, fail } = options; // 析构赋值
  // const success = options.success
  // const fail = options.fail
  const request = new XMLHttpRequest();
  request.open(method, url);
  request.onreadystatechange = () => {
    if (request.readyState === 4) {
      //成功就调用success,失败就调用fail
      if (request.status < 400) {
        success.call(null, request.response);
      } else if (request.status >= 400) {
        fail.call(null, request, request.status);
      }
    }
  };
  request.send();
};

ajax("get", "/xxx", {
  success(response) {},
  fail: (request, status) => {},
}); // 左边是 function 缩写,右边是箭头函数

Promise 说这代码太傻了

因为这代码不规范、容易出现回调地狱和很难进行错误处理。所以太傻了。

让我们改成 Promise 写法:

// 先改一下调用的姿势
ajax("get", "/xxx", {
  success(response) {},
  fail: (request, status) => {},
});
// 上面用到了两个回调,还是用了success和fail

// 改成Promise写法
ajax("get", "/xxx").then(
  (response) => {},
  (request) => {}
);

虽然也是回调,但是不需要记 success 和 fail 了,then 的第一个参数就是 success,then 的第二个参数就是 fail

Q: 请问ajax()返回了个啥?

A: 返回了一个含有 .then() 方法的对象

那么如何得到这个含有 .then() 的对象呢?

那就要改造 ajax 的源码了:

// 与之前的版本相比,改动的三行已经标出。
ajax = (method, url, options) => {
  // 改动
  return new Promise((resolve, reject) => {
    const { success, fail } = options;
    const request = new XMLHttpRequest();
    request.open(method, url);
    request.onreadystatechange = () => {
      if (request.readyState === 4) {
        //成功就调用success,失败就调用fail
        if (request.status < 400) {
          resolve.call(null, request.response); // 改动
        } else if (request.status >= 400) {
          reject.call(null, request); // 改动
        }
      }
    };
    request.send();
  });
};

把这个形式背下来:return new Promise((resolve,reject)=>{})

小结

第一步

  • return new Promise((resolve,reject)=>{ ... })
  • 任务成功则调用resolve(result)
  • 任务失败则调用reject(error)
  • resolvereject会再去调用成功和失败函数

第二步

  • 使用.then(success, fail)传入成功和失败函数

Promise 的其他用法: 使用 Promise | MDN

我们封装的 ajax 的缺点

  • post 无法上传数据

request.send(这里可以上传数据)

  • 不能设置请求头

request.setRequestHeader(key, value)

  • 解决办法

花时间把ajax写到完美(不推荐)

使用jQuery.ajax(这个可以)

使用axios(这个库比jQuery逼格高)

jQuery.ajax

  • 已经非常完美

jQuery.ajax() - jQuery 中文文档

  • 封装优点

支持更多形式的参数,Promise 等超多功能

Q: 需要掌握jQuery.ajax吗?

A: 不用,时代变了,现在用axios就好了:)

axios

  • 目前最新的 AJAX 库(显然它抄袭了 jQuery 的封装思路)

axios 的用法: Axios Cheat Sheet

  • 代码示例
axios.get("/5.json").then((response) => console.log(response));

axios 高级用法

JSON 自动处理

  • axios如果发现响应的Content-Typejson,就会自动调用JSON.parse

所以说正确设置 Content-Type是好习惯

请求拦截器

  • 你可以在所有请求里加些东西,比如加查询参数

具体用法: axios Interceptors - GitHub

响应拦截器

  • 你可以在所有响应里加些东西,甚至改内容

可以生成不同实例(对象)

  • 不同的实例可以设置不同的配置,用于复杂场景

扩展阅读:

Promise 的 MDN 文档

axios 中文文档

实用工具: BootCDN - 开源项目免费 CDN 加速服务

comments powered by Disqus