Promise异步编程整理
1、单线程模型
单线程模型指的是,JavaScript 只在一个线程上运行。也就是说,JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待。 注意,JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上, JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合。 JavaScript 之所以采用单线程,而不是多线程,跟历史有关系。JavaScript 从诞生起就是单线程,原因是不想让浏览器变得太复杂, 因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。如果 JavaScript 同时有两个线程, 一个线程在网页 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?是不是还要有锁机制? 所以,为了避免复杂性,JavaScript 一开始就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
2、同步任务和异步任务
程序里面所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。 同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。 异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果), 该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。 举例来说,Ajax 操作可以当作同步任务处理,也可以当作异步任务处理,由开发者决定。如果是同步任务,主线程就等着 Ajax 操作返回结果,再往下执行; 如果是异步任务,主线程在发出 Ajax 请求以后,就直接往下执行,等到 Ajax 操作有了结果,主线程再执行对应的回调函数。
3、任务队列和事件循环
JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。 (实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。) 首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。 如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。 异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。 如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。 JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了, 引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。 维基百科的定义是:“事件循环是一个程序结构,用于等待和发送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”。
4、异步操作的模式
4.1回调函数
把f2写成f1的回调函数。
function f1(callback) {
// ...
callback();
}
function f2() {
// ...
}
f1(f2);
回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(coupling),使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。
4.2 事件监听
f1.on('done', f2);
function f1() {
setTimeout(function () {
// ...
f1.trigger('done');
}, 1000);
}
f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以“去耦合”(decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
4.3 发布/订阅
事件完全可以理解成“信号”,如果存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做“发布/订阅模式”(publish-subscribe pattern),又称“观察者模式”(observer pattern)。
f2向信号中心jQuery订阅done信号。
jQuery.subscribe('done', f2);
function f1() {
setTimeout(function () {
// ...
jQuery.publish('done');
}, 1000);
}
上面代码中,jQuery.publish('done')的意思是,f1执行完成后,向信号中心jQuery发布done信号,从而引发f2的执行。
f2完成执行后,可以取消订阅(unsubscribe)。
jQuery.unsubscribe('done', f2);
这种方法的性质与“事件监听”类似,但是明显优于后者。因为可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
5、Promise 对象的状态
Promise 对象通过自身的状态,来控制异步操作。Promise 实例具有三种状态。
异步操作未完成(pending)
异步操作成功(fulfilled)
异步操作失败(rejected)
上面三种状态里面,fulfilled和rejected合在一起称为resolved(已定型)。
这三种的状态的变化途径只有两种。
从“未完成”到“成功”
从“未完成”到“失败”
一旦状态发生变化,就凝固了,不会再有新的状态变化。这也是 Promise 这个名字的由来,它的英语意思是“承诺”,一旦承诺成效,就不得再改变了。这也意味着,Promise 实例的状态变化只可能发生一次。
因此,Promise 的最终结果只有两种。
异步操作成功,Promise 实例传回一个值(value),状态变为fulfilled。
异步操作失败,Promise 实例抛出一个错误(error),状态变为rejected。
6、Promise 构造函数
JavaScript 提供原生的Promise构造函数,用来生成 Promise 实例。
var promise = new Promise(function (resolve, reject) {
// ...
if (/* 异步操作成功 */){
resolve(value);
} else { /* 异步操作失败 */
reject(new Error());
}
});
上面代码中,Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。
resolve函数的作用是,将Promise实例的状态从“未完成”变为“成功”(即从pending变为fulfilled),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。reject函数的作用是,将Promise实例的状态从“未完成”变为“失败”(即从pending变为rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
下面是一个例子。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100)
上面代码中,timeout(100)返回一个 Promise 实例。100毫秒以后,该实例的状态会变为fulfilled。
7、then() 用法辨析
Promise 的用法,简单说就是一句话:使用then方法添加回调函数。但是,不同的写法有一些细微的差别,请看下面四种写法,它们的差别在哪里?
// 写法一
f1().then(function () {
return f2();
}); // 写法二
f1().then(function () {
f2();
}); // 写法三
f1().then(f2()); // 写法四
f1().then(f2);
为了便于讲解,下面这四种写法都再用then方法接一个回调函数f3。写法一的f3回调函数的参数,是f2函数的运行结果。
f1().then(function () {
return f2();
}).then(f3);
写法二的f3回调函数的参数是undefined。
f1().then(function () {
f2();
return;
}).then(f3);
写法三的f3回调函数的参数,是f2函数返回的函数的运行结果。
f1().then(f2())
.then(f3);
写法四与写法一只有一个差别,那就是f2会接收到f1()返回的结果。
f1().then(f2)
.then(f3);
8、Promise 优缺点
优点:让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它有一整套接口,可以实现许多强大的功能,比如同时执行多个异步操作,等到它们的状态都改变以后,再执行一个回调函数;再比如,为多个回调函数中抛出的错误,统一指定处理方法等等。
而且,Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状态。这意味着,无论何时为 Promise 实例添加回调函数,该函数都能正确执行。所以,你不用担心是否错过了某个事件或信号。如果是传统写法,通过监听事件来执行回调函数,一旦错过了事件,再添加回调函数是不会执行的。
缺点:编写的难度比传统写法高,而且阅读代码也不是一眼可以看懂。你只会看到一堆then,必须自己在then的回调函数里面理清逻辑。
9、微任务
Promise 的回调函数属于异步任务,会在同步任务之后执行。
new Promise(function (resolve, reject) {
resolve(1);
}).then(console.log);
console.log(2);
// 2
// 1
上面代码会先输出2,再输出1。因为console.log(2)是同步任务,而then的回调函数属于异步任务,一定晚于同步任务执行。
但是,Promise 的回调函数不是正常的异步任务,而是微任务(microtask)。它们的区别在于,正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着,微任务的执行时间一定早于正常任务。
setTimeout(function() {
console.log(1);
}, 0);
new Promise(function (resolve, reject) {
resolve(2);
}).then(console.log);
console.log(3);
// 3
// 2
// 1
上面代码的输出结果是321。这说明then的回调函数的执行时间,早于setTimeout(fn, 0)。因为then是本轮事件循环执行,setTimeout(fn, 0)在下一轮事件循环开始时执行。
Promise异步编程整理的更多相关文章
- ES6笔记(7)-- Promise异步编程
系列文章 -- ES6笔记系列 很久很久以前,在做Node.js聊天室,使用MongoDB数据服务的时候就遇到了多重回调嵌套导致代码混乱的问题. JS异步编程有利有弊,Promise的出现,改善了这一 ...
- promise异步编程的原理
一.起源 JavaScript中的异步由来已久,不论是定时函数,事件处理函数还是ajax异步加载都是异步编程的一种形式,我们现在以nodejs中异步读取文件为例来编写一个传统意义的异步函数: var ...
- async/await actor promise 异步编程
Python协程:从yield/send到async/await http://blog.guoyb.com/2016/07/03/python-coroutine/ Async/Await替代Pro ...
- Promise异步编程解决方案
Promise是ES6中新增的异步编程解决方案,体现在代码中它是一个对象,可以通过 Promise 构造函数来实例化. 其最基本的使用 new Promise(function(resolve,rej ...
- ES6入门八:Promise异步编程与模拟实现源码
Promise的基本使用入门: ——实例化promise对象与注册回调 ——宏任务与微任务的执行顺序 ——then方法的链式调用与抛出错误(throw new Error) ——链式调用的返回值与传值 ...
- 学习Promise异步编程
JavaScript引擎建立在单线程事件循环的概念上.单线程( Single-threaded )意味着同一时刻只能执行一段代码.所以引擎无须留意那些"可能"运行的代码.代码会被放 ...
- 【ES6】Generator+Promise异步编程
一.概念 首先我们要理解Generator和Promise的概念. Generator:意思是生成器,可以在函数内部通过yeild来控制语句的执行或暂停状态. *Foo(){ yeild consol ...
- Promise 异步编程
//1.解决异步回调问题 //1.1 如何同步异步请求 //如果几个异步操作之间并没有前后顺序之分,但需要等多个异步操作都完成后才能执行后续的任务,无法实现并行节约时间 const fs = requ ...
- 张鑫旭:Promise异步编程模式
参考文章: http://www.zhangxinxu.com/wordpress/2014/02/es6-javascript-promise-%E6%84%9F%E6%80%A7%E8%AE%A4 ...
随机推荐
- vs2017 修改项目名称
由于经常接到各种项目需求, 又不想重新搭建框架. 于是便想到直接修改项目名. 话不多说, 上图~ 1 重命名 解决方案 和 项目名称 2 重命名项目的应用程序名和命名空间 3 Ctrl+F 将旧的工 ...
- php不用递归完成无限分类,从表设计入手完整演示过程
无限分类是什么就不废话了,可以用递归实现,但是递归从数据库取东西用递归效率偏低,如果从表设计入手,就很容易做到网站导航的实现,下面是某论坛导航,如下图 网上无限分类大多不全面,今天我会从设计表开始, ...
- 应用于电力电子变压器的双向DC_DC变换器综述(学习笔记)
PET(Powerelectronictransformer):电力电子变压器 PET所具有的优点: (1)体积小.重量轻.无变压器油的污染: (2)具备功率因数调节能力,减小电网谐波污染: (3)能 ...
- Java的动态编译、动态加载、字节码操作
想起来之前做的一个项目:那时候是把需要的源代码通过文件流输出到一个.java文件里,然后调用sun的Comipler接口动态编译成.class文件,然后再用专门写的一个class loader加载这个 ...
- [LeetCode] Minimum Number of K Consecutive Bit Flips 连续K位翻转的最小次数
In an array A containing only 0s and 1s, a K-bit flip consists of choosing a (contiguous) subarray o ...
- 廖雪峰Python学习笔记——类和实例
Class MyList(list): __metaclass__ = ListMetaclass #它表示在创建MyList这个类时,必须通过 ListMetaclass这个元类的LIstMetac ...
- linux 中定时执行python脚本
一.让Python随Linux开机自动运行 准备好要自启的脚本auto.py 用root权限编辑以下文件 sudo vim /ect/rc.local 在exit 0上面编辑启动脚本的命令(编辑rc. ...
- Java并发编程之happens-before
happens-before是JMM最核心的概念,理解happens-before是理解JMM的关键. 一.JMM的设计 首先,让我们先分析一下JMM的设计意图.从JMM的设计者的角度,在设计JMM的 ...
- Servlet中相对路径与绝对路径
相对路径与绝对路径: 相对路径:相对路径指的是相对于当前文件所在目录的路径! http://localhost:8080/servlet01/ http://localhost:8080/servle ...
- POJ 1183
#include<iostream> #include<stdio.h> using namespace std; int main() { //freopen("a ...