写在前面:
第一遍学 Promise 时, 只是大概过了一遍, 感觉学的不够深入, 这一篇算是对之前的一个总结吧. Promise 在 ES6 中也属于一个较难理解的一部分; 所以在学习一个比较难理解的知识点时, 我们可以围绕这个知识点进行展开, 逐个去理解.
再探 Promise
理解一个知识点, 不妨先列出下面几个问题.
Promise 是用来干什么的?
Promise 是什么?
Promise 如何去创建, 使用?
Promise 的常用形式?
Promise 的使用有哪些注意点?
异步相关背景介绍
浏览器内核
首先聊一下浏览器, 一直对浏览器的结构比较好奇, 查了很多资料总结就有下面一点相关总结; 其中也借鉴其他人的一些东西.
浏览器是多进程的, 有主进程, GPU 加速进程, 渲染进程 (内核) 等, 一般新开一个 tab 页面就是新启动一个进程, CPU 就会给他分配资源; 但其中有一个核心进程 ==>渲染进程(浏览器内核), 是我们前端人员需要特别关注的, 它包括了多个线程...
GUI 渲染线程
负责渲染浏览器界面, 解析 html,CSS, 构建 DOM 树和 RenderObject 树, 布局和绘制等.
当界面需要重绘 (Repaint) 或由于某种操作引发回流 (reflow) 时, 该线程就会执行
注意, GUI 渲染线程与 JS 引擎线程是互斥的, 当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),
GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行.
JS 引擎线程
也称为 JS 内核, 负责处理 JavaScript 脚本程序.(例如 V8 引擎)JS 引擎线程负责解析 JavaScript 脚本, 运行代码.
JS 引擎一直等待着任务队列中任务的到来, 然后加以处理, 一个 Tab 页 (renderer 进程) 中无论什么时候都只有一个 JS 线程在运行 JS 程序
同样注意, GUI 渲染线程与 JS 引擎线程是互斥的, 所以如果 JS 执行的时间过长, 这样就会造成页面的渲染不连贯, 导致页面渲染加载阻塞.
不过 H5 中新增了 web Worker, 实现了多线程: JS 会新开线程来处理一些其他任务, 但不会影响 DOM 结构...
创建 Worker 时, JS 引擎向浏览器申请开一个子线程(子线程是浏览器开的, 完全受主线程控制, 而且不能操作 DOM)
JS 引擎线程与 worker 线程间通过特定的方式通信
(postMessage API, 需要通过序列化对象来与线程交互特定的数据)
事件触发线程
这个线程是归属于浏览器而不是 JS 引擎, 用来控制事件循环(可以理解, JS 引擎自己都忙不过来, 需要浏览器另开线程协助)
当 JS 引擎执行代码块如 setTimeOut 时(也可来自浏览器内核的其他线程, 如鼠标点击, Ajax 异步请求,
页面滚动等), 会将对应任务添加到事件线程中
当对应的事件符合触发条件被触发时, 该线程会把事件添加到待处理队列的队尾, 等待 JS 引擎的处理
注意,
由于 JS 的单线程关系, 所以这些待处理队列中的事件都得排队等待 JS 引擎处理(当 JS 引擎空闲时才会去执行)
定时触发器线程
即 setInterval 与 setTimeout 所在线程
浏览器定时计数器并不是由 JavaScript 引擎计数的,(因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确);
因此通过单独线程来计时并触发定时(计时完毕后, 添加到事件队列中, 等待 JS 引擎空闲后执行)
当然 setTimeout 中的延时参数也不一定准确
异步 HTTP 请求线程
在 XMLHttpRequest 在连接后是通过浏览器新开一个网络线程去请求
将检测到状态变更时, 如果设置有回调函数, 异步线程就产生状态变更事件,
将这个回调再放入事件队列中. 再由 JavaScript 引擎执行.
那么关于浏览器方面的背景知识就介绍到这里啦, 想要深入去了解, 可以去查相关资料...
事件队列和循环
大家都知道 JavaScript 引擎是单线程的工作模式, 即同一时间只能跑一段代码, 还要按顺序自上而下执行; 但是碰到 I/O 操作, 定时器, 事件监听函数等这些耗时操作; JS 引擎不会等待它们有结果了才去之下它们后面的代码, 而是会将它们扔进任务 (事件) 队列中, 等待同步代码执行栈空了之后, 再去任务队列将任务一个个取出来执行任务所对应的回调函数, 执行完毕后会一直等待新的任务到来; 如此循环...
几个类型的回调
同步回调函数
我们可以利用了函数的执行栈顺序, 函数作为参数放到另一个函数中调用, 谁在后面调用谁就先被放在函数执行栈栈顶
异步回调函数
事先在外面定义好一个 callback; 将回调函数作为某个函数的参数, 利用函数的作用域将函数中异步任务得到的结果存在回调函数的形参中, 然后在函数体末尾调用...
定时器
setTimeout 的作用是在间隔一定的时间后, 将回调函数插入任务队列中, 等栈中的同步任务都执行完毕后, 再执行, 当然这个时间不一定准确...
Promise 是用来干什么的?
看阮老师的 ES6 出门上说 Promise 是 JS 异步编程的一种解决方案. 举个例子, Ajax 的回调问题, 如果下一个 Ajax 请求要用到上一个 Ajax 请求中的结果, 那么往往就会导致多个回调嵌套的问题, 那么 Promise 就可以解决这种代码上的嵌套问题, 是我们的代码变得更优美, 更利于维护; 我暂时先对 Promise 的理解就是: 处理异步任务, 保存异步结果状态, 异步代码同步化...
Promise 是什么?
Promise 它就是一个对象, 相当于一个容器, 里面存的就是一个异步操作的结果; 我们可以是从中获取异步操作结果的相关信息.
Promise 对象代表一个未完成, 但预计将来会完成的操作.
它有以下三种状态:
pending: 初始值, 不是 fulfilled, 也不是 rejected
fulfilled: 代表操作成功
rejected: 代表操作失败
Promise 有两种状态改变的方式, 既可以从 pending 转变为 fulfilled, 也可以从 pending 转变为 rejected. 一旦状态改变, 就「凝固」了, 会一直保持这个状态, 不会再发生变化. 当状态发生变化, promise.then 绑定的函数就会被调用.
注意: Promise 一旦新建就会「立即执行」(它属于 microtask), 无法取消. 这也是它的缺点之一.
Promise 的创建和使用?
1. 创建 promise 对象
- //1. 使用 new Promise(func)的形式
- //2. 快捷语法: Promise.resolve(func) || Promise.reject(func)
- // 参数 1: 一般是一个处理异步任务的函数
- // 返回值: 一个 promise 实例对象
- Promise.resolve('foo')
- // 等价于, 不过参数类型不一样执行的操作也会有所不同
- new Promise(resolve => resolve('foo'))
2. 在函数 func 中 放异步处理代码
- // 传入两个参数: 回调函数 resolve, reject 分别去保存异步处理的结果
- // 成功: 使用 resolve(结果)
- // 失败: 使用 reject(原因)
3. 调用实例的 then(func1) 或者 catch(err)
首先 then 方法是异步执行, 对上面的异步结果进行处理的函数
参数: 传回调函数, 一个两个都行, 前者是成功状态的回调, 后者是失败的回调
Promise 常用的场景?
promise 一般的使用套路就是:
1. 先定义一个函数, 函数内部使用 new Promise()的方式来返回一个 promise 对象, resolve 用来保存 异步处理成功的结果
reject 用来保存 异常处理的结果
2. 然后函数调用, 传参
3. 链式语法点出 then 方法, then 中的回调用来处理异步结果
4. 有错误就点出 catch 方法, 也可以用 then(null, function() {})代替 catch
5.then 的回调中也可 return 一个值, 会被包装成一个新的 promise, 因此可以继续调用 then 方法
应用场景: 在 Ajax 中使用, 解决异步嵌套问题
- function Ajax(url) {
- return new Promise((resolve, reject) => {
- let xhr = new XMLHttpRequest();
- // 请求类型, 地址, 异步
- xhr.open('get', url, true);
- xhr.send();
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4 && xhr.status === 200) {
- try {
- // 处理响应内容, 将内容丢到成功状态的回调
- resolve(JSON.parse(xhr.responseText))
- } catch (e) {
- // 捕获错误, 丢到失败状态的回调
- reject(e)
- }
- }
- }
- });
- }
- // 调用 封装的 Ajax 函数
- let url = 'http://127.0.0.1:3000/xxoo'; // 自己本地开的一个服务
- Ajax(url)
- .then(res => console.log(res)) // 输出 {code: 0, msg: 'hello cors'}
- .catch(err => console.log(err))
- ```
其他场景
- // 实现串行任务管道; 即当前任务的输出可以作为下一个任务的输入, 形成一条数据管道;
- // 比如: 比如从 url1 获取参数 userId, 拿到后再从 url2 获取第三方 openId, 最后再从 url3 货取 orderList, 然后把结果展示给用户, 类似的逻辑都是任务管道:
- new Promise(function(resolve, reject) {
- resolve(1);
- })
- .then(function(res) {
- return new Promise(function(resolve, reject) {
- resolve(res + 1);
- });
- })
- .then(function(res) {
- return new Promise(function(resolve, reject) {
- resolve(res + 1);
- });
- })
- .then(function(res) {
- console.log(res); // 3
- });
promise 的好处
在异步执行的流程中, 使用 Promise 可以把 执行代码 和 处理结果 的代码清晰地分离
这样我们便可以 把执行代码 和 结果处理 分成不同的模块来写, 易于维护
减少异步回调的嵌套, 比如 Ajax 回调, 我们可以依次调用 then 方法即可, 还可以控制回调的顺序
多个异步任务是为了容错去访问用同一资源时, 可以使用 Promise.race([promise 实例...])
多个异步任务并行执行时, 比如 Ajax 访问两个接口, 可以用 Promise.all([promise 实例...])
promose 使用的注意事项
Promise 构造函数内的同步代码立即执行
回调函数参数 resolve 异步执行, 将结果作为参数传给 then 方法中的回调函数
resolve 只有第一次执行有效, 状态不能二次改变
then 和 catch 如果有 return, 返回的是一个全新的 promise 对象, 可以链式调用
Promise 构造函数只会执行一次, promise 实例会保存 resolve 的状态,
以后这个实例每次调用 then 都是返回一个这个状态, 若链式调用 then, 下一个则会打印 undefined, res 没有值...
then 中返回任意一个非 promise 的值都会被包裹成 promise 对象
.then 或 .catch 返回的值不能是 promise 本身, 否则会造成死循环
.then 或者 .catch 的参数期望是函数, 传入非函数则会发生值穿透.
.then 可以接收两个参数, 第一个是处理成功的函数, 第二个是处理错误的函数..catch 是 .then 第二个参数的简便写法, 但是它们用法上有一点需要注意:.then 的第二个处理错误的函数捕获不了第一个处理成功的函数抛出的错误, 而后续的 .catch 可以捕获之前的错误.
来源: https://segmentfault.com/a/1190000018527530