JavaScript之多线程和Event-Loop

引子几乎在每一本JS相关的书籍中,都会说JS是单线程的,JS是通过事件队列(Event Loop)的方式来实现异步回调的。 对很多初学JS的人来说,根本搞不清楚单线程的JS为什么拥有异步的能力,所以,我试图从进程、线程的角度来解释这个问题。 CPU说到CPU和进程、线程,对计算机操作系统有过学习和了解的同学应该比较熟悉。 计算机的核心是CPU,它承担了所有的计算任务。 它就像一座工厂,时刻在运行。 假定工厂的电力有限,一次只能供给一个车间使用。 也就是说,一个车间开工的时候,其他车间都必须停工。 背后的含义就是,单个CPU一次只能运行一个任务。 进程就好比工厂的车间,它代表CPU所能处理的单个任务。 进程之间相互独立,任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。 CPU使用时间片轮转进度算法来实现同时运行多个进程。 CPU、进程、线程之间的关系从上文我们已经简单了解了CPU、进程、线程,简单汇总一下。 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)不同进程之间也可以通信,不过代价较大单线程与多线程,都是指在一个进程内的单和多浏览器是多进程的我们已经知道了CPU、进程、线程之间的关系,对于计算机来说,每一个应用程序都是一个进程, 而每一个应用程序都会分别有很多的功能模块,这些功能模块实际上是通过子进程来实现的。 对于这种子进程的扩展方式,我们可以称这个应用程序是多进程的。 而对于浏览器来说,浏览器就是多进程的,我在Chrome浏览器中打开了多个tab,然后打开windows控制管理器: 如上图,我们可以看到一个Chrome浏览器启动了好多个进程。总结一下: 浏览器是多进程的每一个Tab页,就是一个独立的进程浏览器包含了哪些进程主进程 协调控制其他子进程(创建、销毁)浏览器界面显示,用户交互,前进、后退、收藏将渲染进程得到的内存中的Bitmap,绘制到用户界面上处理不可见操作,网络请求,文件访问等第三方插件进程 每种类型的插件对应一个进程,仅当使用该插件时才创建GPU进程 用于3D绘制等渲染进程,就是我们说的浏览器内核 负责页面渲染,脚本执行,事件处理等每个tab页一个渲染进程那么浏览器中包含了这么多的进程,那么对于普通的前端操作来说,最重要的是什么呢? 答案是渲染进程,也就是我们常说的浏览器内核 浏览器内核(渲染进程)从前文我们得知,进程和线程是一对多的关系,也就是说一个进程包含了多条线程。 而对于渲染进程来说,它当然也是多线程的了,接下来我们来看一下渲染进程包含哪些线程。 GUI渲染线程 负责渲染页面,布局和绘制页面需要重绘和回流时,该线程就会执行与js引擎线程互斥,防止渲染结果不可预期JS引擎线程 负责处理解析和执行javascript脚本程序只有一个JS引擎线程(单线程)与GUI渲染线程互斥,防止渲染结果不可预期事件触发线程 用来控制事件循环(鼠标点击、setTimeout、ajax等)当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中定时触发器线程 setInterval与setTimeout所在的线程定时任务并不是由JS引擎计时的,是由定时触发线程来计时的计时完毕后,通知事件触发线程异步http请求线程 浏览器有一个单独的线程用于处理AJAX请求当请求完成时,若有回调函数,通知事件触发线程当我们了解了渲染进程包含的这些线程后,我们思考两个问题: 为什么 javascript 是单线程的为什么 GUI 渲染线程为什么与 JS 引擎线程互斥为什么 javascript 是单线程的首先是历史原因,在创建 javascript 这门语言时,多进程多线程的架构并不流行,硬件支持并不好。 其次是因为多线程的复杂性,多线程操作需要加锁,编码的复杂性会增高。 而且,如果同时操作 DOM ,在多线程不加锁的情况下,最终会导致 DOM 渲染的结果不可预期。 为什么 GUI 渲染线程与 JS 引擎线程互斥这是由于 JS 是可以操作 DOM 的,如果同时修改元素属性并同时渲染界面(即 JS线程和UI线程同时运行), 那么渲染线程前后获得的元素就可能不一致了。 因此,为了防止渲染出现不可预期的结果,浏览器设定 GUI渲染线程和JS引擎线程为互斥关系, 当JS引擎线程执行时GUI渲染线程会被挂起,GUI更新则会被保存在一个队列中等待JS引擎线程空闲时立即被执行。 ...

November 2, 2019 · 1 min · jiezi

Nodejs-中的事件循环计时器和processnextTick

前言本篇文章翻译自 Node.js 官网的同名文章也算是经典老物了, 不过官网的文章也随着 Node.js 的演化在修改, 这篇文章最后的编辑时间是 2019年9月10日请注意时效性, 地址在文章的最后有给出. 首次翻译英语水平有限, 错误之处还请多多指教. 什么是事件循环事件循环允许node.js执行非阻塞I/O操作. 虽然 JavaScript 是单线程的, 但是事件循环会尽可能的将操作转移到系统内核中来完成. 现代的操作系统内核都是多线程的, 它们可以在后台处理多种操作. 一旦这些操作完成, 系统内核会通知 Node.js 以便将事件回调放入轮询队列中等待执行. (我们会在随后的内容讨论它们的具体工作细节) 解析事件循环当 Node.js 启动的时候, 他会初始化事件循环, 处理输入的脚本内容 (或者进入 REPL), 脚本可能会调用异步接口, 设置定时器, 或者调用 process.nextTick(), 然后开始处理事件循环(eventloop). 下面的简图中展示了事件循环的操作流程: ┌───────────────────────┐┌─>│ timers ││ └──────────┬────────────┘│ ┌──────────┴────────────┐│ │ I/O callbacks ││ └──────────┬────────────┘│ ┌──────────┴────────────┐│ │ idle, prepare ││ └──────────┬────────────┘ ┌───────────────┐│ ┌──────────┴────────────┐ │ incoming: ││ │ poll │<─────┤ connections, ││ └──────────┬────────────┘ │ data, etc. ││ ┌──────────┴────────────┐ └───────────────┘│ │ check ││ └──────────┬────────────┘│ ┌──────────┴────────────┐└──┤ close callbacks │ └───────────────────────┘每一个方框代表了事件循环中不同的阶段(所有阶段执行完成算是一次事件循环).每一个阶段都有一个由回调组成的 FIFO 队列被用于执行. 虽然不同的队列执行方式不同, 总的来看, 当事件循环进入该阶段后会执行该阶段对应的操作, 然后调用对应的回调直到队列耗尽或者达到了回调执行上限. 在到达上述情况后事件循环进入下一阶段, 然后继续这样的流程. ...

September 11, 2019 · 4 min · jiezi

Java并发16-CompletionService批量执行异步任务

我们思考下这个场景:从三个电商询价,然后保存在自己的数据库里。通过之前所学,我们可能这么实现。 // 创建线程池ExecutorService executor = Executors.newFixedThreadPool(3);// 异步向电商 S1 询价Future<Integer> f1 = executor.submit( ()->getPriceByS1());// 异步向电商 S2 询价Future<Integer> f2 = executor.submit( ()->getPriceByS2());// 异步向电商 S3 询价Future<Integer> f3 = executor.submit( ()->getPriceByS3()); // 获取电商 S1 报价并保存r=f1.get();executor.execute(()->save(r)); // 获取电商 S2 报价并保存r=f2.get();executor.execute(()->save(r)); // 获取电商 S3 报价并保存 r=f3.get();executor.execute(()->save(r));上面的这个方案本身没有太大问题,但是有个地方的处理需要你注意,那就是如果获取电商 S1 报价的耗时很长,那么即便获取电商 S2 报价的耗时很短,也无法让保存 S2 报价的操作先执行,因为这个主线程都阻塞在了 f1.get(),那我们如何解决了? 我们可以增加一个阻塞队列,获取到 S1、S2、S3 的报价都进入阻塞队列,然后在主线程中消费阻塞队列,这样就能保证先获取到的报价先保存到数据库了。下面的示例代码展示了如何利用阻塞队列实现先获取到的报价先保存到数据库。 // 创建阻塞队列BlockingQueue<Integer> bq = new LinkedBlockingQueue<>();// 电商 S1 报价异步进入阻塞队列 executor.execute(()-> bq.put(f1.get()));// 电商 S2 报价异步进入阻塞队列 executor.execute(()-> bq.put(f2.get()));// 电商 S3 报价异步进入阻塞队列 executor.execute(()-> bq.put(f3.get()));// 异步保存所有报价 for (int i=0; i<3; i++) { Integer r = bq.take(); executor.execute(()->save(r));} 利用 CompletionService 实现询价系统不过在实际项目中,并不建议你这样做,因为 Java SDK 并发包里已经提供了设计精良的 CompletionService。利用 CompletionService 能让代码更简练。 ...

June 27, 2019 · 2 min · jiezi

Java并发15-CompletableFuture-异步编程

前面我们不止一次提到,用多线程优化性能,其实不过就是将串行操作变成并行操作。如果仔细观察,你还会发现在串行转换成并行的过程中,一定会涉及到异步化,例如下面的示例代码,现在是串行的,为了提升性能,我们得把它们并行化。 // 以下两个方法都是耗时操作doBizA();doBizB();//创建两个子线程去执行就可以了,两个操作已经被异步化了。new Thread(()->doBizA()) .start();new Thread(()->doBizB()) .start(); 异步化,是并行方案得以实施的基础,更深入地讲其实就是:利用多线程优化性能这个核心方案得以实施的基础。Java 在 1.8 版本提供了 CompletableFuture 来支持异步编程。 CompletableFuture 的核心优势为了领略 CompletableFuture 异步编程的优势,这里我们用 CompletableFuture 重新实现前面曾提及的烧水泡茶程序。首先还是需要先完成分工方案,在下面的程序中,我们分了 3 个任务:任务 1 负责洗水壶、烧开水,任务 2 负责洗茶壶、洗茶杯和拿茶叶,任务 3 负责泡茶。其中任务 3 要等待任务 1 和任务 2 都完成后才能开始。这个分工如下图所示。 烧水泡茶分工方案 // 任务 1:洗水壶 -> 烧开水CompletableFuture<Void> f1 = CompletableFuture.runAsync(()->{ System.out.println("T1: 洗水壶..."); sleep(1, TimeUnit.SECONDS); System.out.println("T1: 烧开水..."); sleep(15, TimeUnit.SECONDS);});// 任务 2:洗茶壶 -> 洗茶杯 -> 拿茶叶CompletableFuture<String> f2 = CompletableFuture.supplyAsync(()->{ System.out.println("T2: 洗茶壶..."); sleep(1, TimeUnit.SECONDS); System.out.println("T2: 洗茶杯..."); sleep(2, TimeUnit.SECONDS); System.out.println("T2: 拿茶叶..."); sleep(1, TimeUnit.SECONDS); return " 龙井 ";});// 任务 3:任务 1 和任务 2 完成后执行:泡茶CompletableFuture<String> f3 = f1.thenCombine(f2, (__, tf)->{ System.out.println("T1: 拿到茶叶:" + tf); System.out.println("T1: 泡茶..."); return " 上茶:" + tf; });// 等待任务 3 执行结果System.out.println(f3.join());void sleep(int t, TimeUnit u) { try { u.sleep(t); }catch(InterruptedException e){}}// 一次执行结果:T1: 洗水壶...T2: 洗茶壶...T1: 烧开水...T2: 洗茶杯...T2: 拿茶叶...T1: 拿到茶叶: 龙井T1: 泡茶...上茶: 龙井从整体上来看,我们会发现 ...

June 25, 2019 · 3 min · jiezi

js异步从入门到放弃实践篇-常见写法面试题解析

前文该系列下的前几篇文章分别对不同的几种异步方案原理进行解析,本文将介绍一些实际场景和一些常见的面试题。(积累不太够,后面想到再补) 正文流程调度(schedule)流程调度,最常见的就是继发和并发(或者说串行和并行)两种类型,在日常工作里都很常见。接下来结合实际场景进行说明: 1. 串行执行一系列异步操作,每一步依赖前一步的结果串行执行的关键是,将每一个异步任务放到前一个异步任务的回调函数里执行。 场景:一串连续的动画,每个动画必须等待前一个动画完全执行完,并且如果某个动画执行失败,则不继续执行下一个动画。代码: // 这里假定一共要执行5个动画 // getAnimation 函数模拟执行动画 接收参数i表述动画编号 返回一个promose const getAnimation = (i) => new Promise((resolve, reject) => { setTimeout(()=>{ // 随机返回true或者false const isSuccess = Math.random() > 0.5 console.log(`第${i}个动画执行`) if(isSuccess){ return resolve(isSuccess) } return reject(isSuccess) },1000) }) // 1.promise实现 核心就是嵌套代码 const serialScheduleByPromise = () => { let p = Promise.resolve(true) const tasks = [] for(let i=0;i < 5; i++){ p = p.then(isSuccess=>{ if(isSuccess){ return getAnimation(i+1) } }).catch((err)=>{ return console.log(`执行失败`) }) } } serialScheduleByPromise() // 2.async/await实现 const serialScheduleByAsync = async () => { try{ for(let i=0;i < 5; i++){ await getAnimation(i+1) }}catch(e){ console.log(`执行失败`) } } serialScheduleByAsync()async/await的语法虽然没有单独解析,但是本质就是前一篇介绍的带自动执行器的generator而已,因此不再赘述可以看到,async的写法代码更简洁,而且逻辑更清晰,可读性更强。 ...

June 13, 2019 · 2 min · jiezi

从js来聊聊异步编程

文章的目的揭开go的 gorouter,c#的 async/await等 使用同步的写法写异步代码的神秘面纱 , 证明其本质就是一个语法糖 为什么使用js来讲异步编程因为js可以通过编程语言自己的语法特性,实现async/await语法 js异步最底层写法promiseconst promise = new Promise(function(resolve, reject) { xxxxx.异步IO操作((res)=>{ if(res成功){ resolve(res) }else{ reject(res) } })});promise出入的回调函数有一定的要求 resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数(处理返回的结果)。promise.then(function(value) { // success}, function(error) { // failure});引申-注意: promise对象在js中非常特殊,比如下面的例子const p1 = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('fail')), 3000)})const p2 = new Promise(function (resolve, reject) { setTimeout(() => resolve(p1), 1000)})p2 .then(result => console.log(result)) .catch(error => console.log(error))这个的结果是failt 因为 p2中resolve返回一个promise对象,这个操作将会导致p2的状态升级成p1的状态(标准)promise的then链式写法promise then方法将会返回一个promise,所以js支持链式异步 ...

May 31, 2019 · 3 min · jiezi

原生的-Linux-异步文件操作iouring-尝鲜体验

Linux异步IO的历史异步IO一直是 Linux 系统的痛。Linux 很早就有 POSIX AIO 这套异步IO实现,但它是在用户空间自己开用户线程模拟的,效率极其低下。后来在 Linux 2.6 引入了真正的内核级别支持的异步IO实现(Linux aio),但是它只支持 Direct IO,只支持磁盘文件读写,而且对文件大小还有限制,总之各种麻烦。到目前为止(2019年5月),libuv 还是在用pthread+preadv的形式实现异步IO。 随着 Linux 5.1 的发布,Linux 终于有了自己好用的异步IO实现,并且支持大多数文件类型(磁盘文件、socket,管道等),这个就是本文的主角:io_uring IOCP于IO多路复用模型 epoll 不同,io_uring 的思想更类似于 Windows 上的 IOCP。用快递来举例:同步模型就是你从在电商平台下单前,就在你家楼下一直等,直到快递公司把货送到楼下,你再把东西带上楼。epoll 类似于你下单,快递公司送到楼下,通知你可以去楼下取货了,这时你下楼把东西带上来。虽然还是需要用户下楼取货(有一段同步读写的时间),但是由于不需要等快递在路上的时间,效率已经有非常大的提升。但是,epoll不适用于磁盘IO,因为磁盘文件总是可读的。 而 IOCP 就是一步到位,直接送货上门,连下楼取的动作都不需要。整个过程完全是非阻塞的。 io_uring 的简单使用io_uring 是一套系统调用接口,虽然总共就3个系统调用,但实际使用却非常复杂。这里直接介绍封装过便于用户使用的 liburing。 在尝试前请首先确认自己的 Linux 内核版本在 5.1 以上(uname -r)。liburing 需要自己编译(之后可能会被各大Linux发行版以软件包的形式收录),git clone 后直接 ./configure && sudo make install 就好了。 io_uring 结构初始化liburing 提供了自己的核心结构 io_uring,它内部封装了 io_uring 自己的文件描述符(fd)以及其他与内核通信所需变量。 struct io_uring { struct io_uring_sq sq; struct io_uring_cq cq; int ring_fd;};使用之前需要先初始化,使用 io_uring_queue_init 初始化此结构。 ...

May 27, 2019 · 2 min · jiezi

js异步从入门到放弃四-Generator-封装异步任务

在之前的文章介绍了传统异步的实现方案,本文将介绍ES6中的一种全新的异步方案--Generator函数。 generator简介简单介绍一下generator的原理和语法,(更详细内容请看ECMAScript 6 入门,本文只介绍和异步相关的核心内容) 基本语法通过一个简单的例子来了解generator函数 function* MyGenerator() { yield 'yield的含义是:执行此处时,暂停执行当前函数' yield '暂停之后的函数可以用next方法继续执行' return '遇到return之后会真正结束,done会变成true'}const gen = MyGenerator() //执行后返回的是一个指针,可以利用该指针执行next方法console.log(gen.next()) // {value: "yield的含义是:执行此处时,暂停执行当前函数“, done: false}console.log(gen.next())// {value: "暂停之后的函数可以用next方法继续执行", done: false}console.log(gen.next())// {value: "遇到return之后会真正结束,done会变成true", done: true}数据交互数据交互指的是可以通过yield把当前执行的结果传出,也可以使用next把外部参数传入 function* MyGenerator(){ const result = yield '传出的数据' return result } const gen = MyGenerator() console.log(gen.next())// generatorAndAsync2.html:19 {value: "传出的数据", done: false} console.log(gen.next('传入的数据'))// {value: "传入的数据", done: true}交互的参数类型当然也可以是函数: function sayHi(){ console.log('hi'); } function* MyGenerator(){ const result = yield sayHi() return } const gen = MyGenerator() const result = gen.next()// {value: sayHi, done: false} result.value() // 正常输出'hi'具备以上最核心的两个特性之后,generator就可以进行异步操作封装。 ...

May 26, 2019 · 2 min · jiezi

细说JS异步发展历程

知其然知其所以然,首先了解三个概念: 1.什么是同步? 所谓同步,就是在发出一个"调用"时,在没有得到结果之前,该“调用”就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由“调用者”主动等待这个“调用”的结果。此调用执行完之前,阻塞之后的代码执行。 2.什么是异步? "调用"在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在"调用"发出后,"被调用者"通过状态、通知来通知调用者,或通过回调函数处理这个调用。异步调用发出后,不影响后面代码的执行。 3.JavaScript 中为什么需要异步? 首先我们知道JavaScript是单线程的(即使新增了webworker,但是本质上JS还是单线程)。同步代码意味着什么呢?意味着有可能会阻塞,当我们有一个任务需要时间较长时,如果使用同步方式,那么就会阻塞之后的代码执行。而异步则不会,我们不会等待异步代码的之后,继续执行异步任务之后的代码。 概念了解完了,我们就要进入今天的正题了。首先大家思考一下:平时在工作中,主要使用了哪些异步解决方案,这些异步方案有什么优缺点? 异步最早的解决方案是回调函数,如事件的回调,setInterval/setTimeout中的回调。但是回调函数有一个很常见的问题,就是回调地狱的问题(稍后会举例说明); 为了解决回调地狱的问题,社区提出了Promise解决方案,ES6将其写进了语言标准。Promise一定程度上解决了回调地狱的问题,但是Promise也存在一些问题,如错误不能被try catch,而且使用Promise的链式调用,其实并没有从根本上解决回调地狱的问题,只是换了一种写法。 ES6中引入 Generator 函数,Generator是一种异步编程解决方案,Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权,Generator 函数可以看出是异步任务的容器,需要暂停的地方,都用yield语句注明。但是 Generator 使用起来较为复杂。 ES7又提出了新的异步解决方案:async/await,async是 Generator 函数的语法糖,async/await 使得异步代码看起来像同步代码,异步编程发展的目标就是让异步逻辑的代码看起来像同步一样。 回调函数 ---> Promise ---> Generator ---> async/await.1.回调函数: callback//node读取文件fs.readFile(xxx, 'utf-8', function(err, data) { //code});回调函数的使用场景(包括但不限于): 事件回调Node APIsetTimeout/setInterval中的回调函数ajax 请求回调函数的优点: 简单。回调函数的缺点: 异步回调嵌套会导致代码难以维护,并且不方便统一处理错误,不能 try catch 和 回调地狱(如先读取A文本内容,再根据A文本内容读取B再根据B的内容读取C...)。 fs.readFile(A, 'utf-8', function(err, data) { fs.readFile(B, 'utf-8', function(err, data) { fs.readFile(C, 'utf-8', function(err, data) { fs.readFile(D, 'utf-8', function(err, data) { //.... }); }); });});2.PromisePromise 一定程度上解决了回调地狱的问题,Promise 最早由社区提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。 ...

May 22, 2019 · 3 min · jiezi

总结异步编程的六种方式

异步编程众所周知 JavaScript 是单线程工作,也就是只有一个脚本执行完成后才能执行下一个脚本,两个脚本不能同时执行,如果某个脚本耗时很长,后面的脚本都必须排队等着,会拖延整个程序的执行。那么如何让程序像人类一样可以多线程工作呢?以下为几种异步编程方式的总结,希望与君共勉。 回调函数事件监听发布订阅模式PromiseGenerator (ES6)async (ES7)异步编程传统的解决方案:回调函数和事件监听 初始示例:假设有两个函数, f1 和 f2,f1 是一个需要一定时间的函数。 function f1() { setTimeout(function(){ console.log('先执行 f1') },1000)}function f2() { console.log('再执行 f2')}回调函数因为 f1 是一个需要一定时间的函数,所以可以将 f2 写成 f1 的回调函数,将同步操作变成异步操作,f1 不会阻塞程序的运行,f2 也无需空空等待,例如 JQuery 的 ajax。 回调函数的demo: function f1(f2){ setTimeout(function(){ console.log('先执行 f1') },1000) f2()}function f2() { console.log('再执行 f2')}效果如下: 总结:回调函数易于实现、便于理解,但是多次回调会导致代码高度耦合 事件监听脚本的执行不取决代码的顺序,而取决于某一个事件是否发生。 事件监听的demo $(document).ready(function(){ console.log('DOM 已经 ready')});发布订阅模式发布/订阅模式是利用一个消息中心,发布者发布一个消息给消息中心,订阅者从消息中心订阅该消息,。类似于 vue 的父子组件之间的传值。 发布订阅模式的 demo //订阅done事件$('#app').on('done',function(data){ console.log(data)})//发布事件$('#app').trigger('done,'haha')PromisePromise 实际就是一个对象, 从它可以获得异步操作的消息,Promise 对象有三种状态,pending(进行中)、fulfilled(已成功)和rejected(已失败)。Promise 的状态一旦改变之后,就不会在发生任何变化,将回调函数变成了链式调用。 Promise 封装异步请求demo export default function getMethods (url){ return new Promise(function(resolve, reject){ axios.get(url).then(res => { resolve(res) }).catch(err =>{ reject(err) }) })}getMethods('/api/xxx').then(res => { console.log(res)}, err => { console.log(err)})GeneratorGenerator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,使用该对象的 next() 方法,可以遍历 Generator 函数内部的每一个状态,直到 return 语句。 ...

May 15, 2019 · 1 min · jiezi

ES6Async与异步编程11

单线程是Javascript语言最本质的特性之一,Javascript引擎在运行js代码的时候,同一个时间只能执行单个任务。 这种模式的好处是实现起来比较简单,执行环境相对单纯。 坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。 所以异步编程对JavaScript语言太重要。 有些小伙伴可能还不太理解"异步"。 所谓的"异步",就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。 例如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步。 相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。 讲的通俗点: 朱自清的《背影》中,父亲对朱自清说 :“我买几个橘子去。你就在此地,不要走动。” 朱自清没有走动,等着买完橘子的父亲一起吃橘子,就叫同步。 如果朱自清没有等父亲,独自走了,那就不能和父亲一起吃橘子,就叫异步。 1、异步编程 我们就以用户注册这个特别常见的场景为例,讲讲异步编程。 第一步,验证用户是否注册 第二步,没有注册,发送验证码 第三步,填写验证码、密码,检验验证码是否正确 这个过程是有一定的顺序的,你必须保证上一步完成,才能顺利进行下一步。 1.1 回调函数 function testRegister(){} // 验证用户是否注册function sendMessage(){} // 给手机发送验证码xfunction testMessage(){} // 检验验证码是否正确function doRegister(){ //开始注册 testRegister(data){ if(data===false){ //已注册 }else{ //未注册 sendMessage(data){ if(data===true){ //发送验证码成功 testMessage(data){ if(data===true){ //验证码正确 }else{ //验证码不正确 } } } } } }}代码中就已经有许多问题,比如杂乱的 if 判断语句 、层层嵌套的函数,造成代码的可读性差,难于维护。 另外,如果在层层回调函数中出现异常,调试起来是非常让人奔溃的 —— 由于 try-catch 无法捕获异步的异常,我们只能不断不断的写 debugger 去追踪,简直步步惊心。 这种层层嵌套被称为回调地狱。 1.2 Promise方式 Promise就是为了解决回调地狱问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。采用Promise,连续读取多个文件,写法如下。 ...

May 15, 2019 · 2 min · jiezi

ES6--浅析Promise内部结构

一、前言什么是promise?promsie的核心是什么?promise如何解决回调地狱的?等问题1、什么是promise?promise是表示异步操作的最终结果;可以用来解决回调地狱和并发IO操作的问题A promise represents the eventual result of an asynchronous operation.2、promise 的核心是什么?promise的核心就是链式调用3、采用什么方法可以实现链式调用?通过使用then的方法,then方法是用来注册在这个Promise状态确定后的回调,很明显,then方法需要写在原型链上。4、promise是如何解决回调地狱的问题?(1)如果一个promise返回的是一个promise,会把这个promise传递结果传递到下一次的then中;(2)如果一个promise返回的是一个普通的值,会把这个普通值作为下一次then的成功回调结果;(3)如果当前promise失败了,会走下一个then的回调函数;(4)如果then不返回值,就会有一个默认值为undefined,作为普通值,会作为下一个then的成功回调;(5)catch是错误没有处理的情况才会执行;(6)then中可以不写东西二、promise的标准解读1、只有一个then方法,没有catch,race,all等方法,甚至没有构造函数;Promise标准中仅指定了Promise对象的then方法的行为,其它一切我们常见的方法/函数都并没有指定,包括catch,race,all等常用方法,甚至也没有指定该如何构造出一个Promise对象,另外then也没有一般实现中(Q, $q等)所支持的第三个参数,一般称onProgress2、then方法返回一个新的Promise;Promise的then方法返回一个新的Promise,而不是返回this,此处在下文会有更多解释promise2 = promise1.then(alert)promise2 != promise1 // true3、不同Promise的实现需要可以相互调用(interoperable)4、Promise的初始状态为pending,它可以由此状态转换为fulfilled(本文为了一致把此状态叫做resolved)或者rejected,一旦状态确定,就不可以再次转换为其它状态,状态确定的过程称为settle三、实现一个promise1、构造函数因为标准并没有指定如何构造一个Promise对象,所以我们同样以目前一般Promise实现中通用的方法来构造一个Promise对象,也是ES6原生Promise里所使用的方式,即:// Promise构造函数接收一个executor函数,executor函数执行完同步或异步操作后,调用它的两个参数resolve和rejectvar promise = new Promise(function(resolve, reject) { /* 如果操作成功,调用resolve并传入value 如果操作失败,调用reject并传入reason */})我们先实现构造函数的框架如下:function Promise(executor) { var self = this self.status = ‘pending’ // Promise当前的状态 self.data = undefined // Promise的值 self.onResolvedCallback = [] // Promise resolve时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面 self.onRejectedCallback = [] // Promise reject时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面 executor(resolve, reject) // 执行executor并传入相应的参数}上面的代码基本实现了Promise构造函数的主体,但目前还有两个问题:1、我们给executor函数传了两个参数:resolve和reject,这两个参数目前还没有定义2、executor有可能会出错(throw),类似下面这样,而如果executor出错,Promise应该被其throw出的值reject:new Promise(function(resolve, reject) { throw 2})所以我们需要在构造函数里定义resolve和reject这两个函数:function Promise(executor) { var self = this self.status = ‘pending’ // Promise当前的状态 self.data = undefined // Promise的值 self.onResolvedCallback = [] // Promise resolve时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面 self.onRejectedCallback = [] // Promise reject时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面 function resolve(value) { // TODO } function reject(reason) { // TODO } try { // 考虑到执行executor的过程中有可能出错,所以我们用try/catch块给包起来,并且在出错后以catch到的值reject掉这个Promise executor(resolve, reject) // 执行executor } catch(e) { reject(e) }}有人可能会问,resolve和reject这两个函数能不能不定义在构造函数里呢?考虑到我们在executor函数里是以resolve(value),reject(reason)的形式调用的这两个函数,而不是以resolve.call(promise, value),reject.call(promise, reason)这种形式调用的,所以这两个函数在调用时的内部也必然有一个隐含的this,也就是说,要么这两个函数是经过bind后传给了executor,要么它们定义在构造函数的内部,使用self来访问所属的Promise对象。所以如果我们想把这两个函数定义在构造函数的外部,确实是可以这么写的:function resolve() { // TODO}function reject() { // TODO}function Promise(executor) { try { executor(resolve.bind(this), reject.bind(this)) } catch(e) { reject.bind(this)(e) }}但是众所周知,bind也会返回一个新的函数,这么一来还是相当于每个Promise对象都有一对属于自己的resolve和reject函数,就跟写在构造函数内部没什么区别了,所以我们就直接把这两个函数定义在构造函数里面了。不过话说回来,如果浏览器对bind的所优化,使用后一种形式应该可以提升一下内存使用效率。另外我们这里的实现并没有考虑隐藏this上的变量,这使得这个Promise的状态可以在executor函数外部被改变,在一个靠谱的实现里,构造出的Promise对象的状态和最终结果应当是无法从外部更改的。接下来,我们实现resolve和reject这两个函数function Promise(executor) { // … function resolve(value) { if (self.status === ‘pending’) { self.status = ‘resolved’ self.data = value for(var i = 0; i < self.onResolvedCallback.length; i++) { self.onResolvedCallbacki } } } function reject(reason) { if (self.status === ‘pending’) { self.status = ‘rejected’ self.data = reason for(var i = 0; i < self.onRejectedCallback.length; i++) { self.onRejectedCallbacki } } } // …}基本上就是在判断状态为pending之后把状态改为相应的值,并把对应的value和reason存在self的data属性上面,之后执行相应的回调函数,逻辑很简单,这里就不多解释了。2、then方法then方法是用来注册这个promise确定状态后的回调,then方法是需要写在原型链上。自然约束:then方法会返回一个Promise,关于这一点,Promise/A+标准并没有要求返回的这个Promise是一个新的对象,但在Promise/A标准中,明确规定了then要返回一个新的对象,目前的Promise实现中then几乎都是返回一个新的Promise(https://promisesaplus.com/dif…,所以在我们的实现中,也让then返回一个新的Promise对象。下面我们来实现then方法:// then方法接收两个参数,onResolved,onRejected,分别为Promise成功或失败后的回调Promise.prototype.then = function(onResolved, onRejected) { var self = this var promise2 // 根据标准,如果then的参数不是function,则我们需要忽略它,此处以如下方式处理 onResolved = typeof onResolved === ‘function’ ? onResolved : function(v) {} onRejected = typeof onRejected === ‘function’ ? onRejected : function(r) {} if (self.status === ‘resolved’) { return promise2 = new Promise(function(resolve, reject) { }) } if (self.status === ‘rejected’) { return promise2 = new Promise(function(resolve, reject) { }) } if (self.status === ‘pending’) { return promise2 = new Promise(function(resolve, reject) { }) }}Promise总共有三种可能的状态,我们分三个if块来处理,在里面分别都返回一个new Promise。根据标准,我们知道,对于如下代码,promise2的值取决于then里面函数的返回值:promise2 = promise1.then(function(value) { return 4}, function(reason) { throw new Error(‘sth went wrong’)})如果promise1被resolve了,promise2的将被4 resolve,如果promise1被reject了,promise2将被new Error(‘sth went wrong’) reject,更多复杂的情况不再详述。3、完整的promisetry { module.exports = Promise} catch (e) {}function Promise(executor) { var self = this self.status = ‘pending’ self.onResolvedCallback = [] self.onRejectedCallback = [] function resolve(value) { if (value instanceof Promise) { return value.then(resolve, reject) } setTimeout(function() { // 异步执行所有的回调函数 if (self.status === ‘pending’) { self.status = ‘resolved’ self.data = value for (var i = 0; i < self.onResolvedCallback.length; i++) { self.onResolvedCallbacki } } }) } function reject(reason) { setTimeout(function() { // 异步执行所有的回调函数 if (self.status === ‘pending’) { self.status = ‘rejected’ self.data = reason for (var i = 0; i < self.onRejectedCallback.length; i++) { self.onRejectedCallbacki } } }) } try { executor(resolve, reject) } catch (reason) { reject(reason) }}function resolvePromise(promise2, x, resolve, reject) { var then var thenCalledOrThrow = false if (promise2 === x) { return reject(new TypeError(‘Chaining cycle detected for promise!’)) } if (x instanceof Promise) { if (x.status === ‘pending’) { //because x could resolved by a Promise Object x.then(function(v) { resolvePromise(promise2, v, resolve, reject) }, reject) } else { //but if it is resolved, it will never resolved by a Promise Object but a static value; x.then(resolve, reject) } return } if ((x !== null) && ((typeof x === ‘object’) || (typeof x === ‘function’))) { try { then = x.then //because x.then could be a getter if (typeof then === ‘function’) { then.call(x, function rs(y) { if (thenCalledOrThrow) return thenCalledOrThrow = true return resolvePromise(promise2, y, resolve, reject) }, function rj(r) { if (thenCalledOrThrow) return thenCalledOrThrow = true return reject(r) }) } else { resolve(x) } } catch (e) { if (thenCalledOrThrow) return thenCalledOrThrow = true return reject(e) } } else { resolve(x) }}Promise.prototype.then = function(onResolved, onRejected) { var self = this var promise2 onResolved = typeof onResolved === ‘function’ ? onResolved : function(v) { return v } onRejected = typeof onRejected === ‘function’ ? onRejected : function(r) { throw r } if (self.status === ‘resolved’) { return promise2 = new Promise(function(resolve, reject) { setTimeout(function() { // 异步执行onResolved try { var x = onResolved(self.data) resolvePromise(promise2, x, resolve, reject) } catch (reason) { reject(reason) } }) }) } if (self.status === ‘rejected’) { return promise2 = new Promise(function(resolve, reject) { setTimeout(function() { // 异步执行onRejected try { var x = onRejected(self.data) resolvePromise(promise2, x, resolve, reject) } catch (reason) { reject(reason) } }) }) } if (self.status === ‘pending’) { // 这里之所以没有异步执行,是因为这些函数必然会被resolve或reject调用,而resolve或reject函数里的内容已是异步执行,构造函数里的定义 return promise2 = new Promise(function(resolve, reject) { self.onResolvedCallback.push(function(value) { try { var x = onResolved(value) resolvePromise(promise2, x, resolve, reject) } catch (r) { reject(r) } }) self.onRejectedCallback.push(function(reason) { try { var x = onRejected(reason) resolvePromise(promise2, x, resolve, reject) } catch (r) { reject(r) } }) }) }}Promise.prototype.catch = function(onRejected) { return this.then(null, onRejected)}Promise.deferred = Promise.defer = function() { var dfd = {} dfd.promise = new Promise(function(resolve, reject) { dfd.resolve = resolve dfd.reject = reject }) return dfd}四、promise的常用方法是如何实现1、Promise.resolve / Promise.reject 实现// 原生的Promise.resolve使用Promise.resolve(‘hello swr’).then((data)=>{ // 直接把成功的值传递给下一个then console.log(data) // hello swr})// 那么Promise.resolve内部是怎么实现的呢?Promise.resolve = function(value){ return new Promise((resolve,reject)=>{ // 在内部new一个Promise对象 resolve(value) })}// 同理,Promise.reject内部也是类似实现的Promise.reject = function(reason){ return new Promise((resolve,reject)=>{ reject(reason) })}2、catch的实现// 原生Promise的catch使用Promise.reject(‘hello swr’).catch((e)=>{ console.log(e) // hello swr})// 上面这段代码相当于下面这段代码Promise.reject(‘hello swr’).then(null,(e)=>{ // then里直接走了失败的回调 console.log(e) // hello swr})// 内部实现Promise.prototype.catch = function(onRejected){ return this.then(null,onRejected) // 相当于then里的成功回调只传个null}3、promise.all的实现同时执行多个异步,并且返回一个新的promise,成功的值是一个数组,该数组的成员的顺序是传参给promise.all的顺序// 原生Promise.all的使用// 假设1.txt内容为hello 2.txt内容为swrlet fs = require(‘fs’)function read(filePath,encoding){ return new Promise((resolve,reject)=>{ fs.readFile(filePath,encoding,(err,data)=>{ if(err) reject(err) resolve(data) }) })}Promise.all([read(’./1.txt’,‘utf8’),read(’./2.txt’,‘utf8’)]).then((data)=>{ console.log(data) // 全部读取成功后返回 [‘hello’,‘swr’] // 需要注意的是,当其中某个失败的话,则会走失败的回调函数})promise.all内部实现Promise.all = function(promises){ // promises 是一个数组 return new Promise((resolve,reject)=>{ let arr = [] let i = 0 function processData(index,data){ arr[index] = data // 5.我们能用arr.length === promises.length来判断请求是否全部完成吗? // 答案是不行的,假设arr[2] = ‘hello swr’ // 那么打印这个arr,将是[empty × 2, “hello swr”], // 此时数组长度也是为3,而数组arr[0] arr[1]则为空 // 那么换成以下的办法 if(++i === promises.length){ // 6.利用i自增来判断是否都成功执行 resolve(arr) // 此时arr 为[‘hello’,‘swr’] } } for(let i = 0;i < promises.length;i++){ // 1.在此处遍历执行 promises[i].then((data)=>{ // 2.data是成功后返回的结果 processData(i,data) // 4.因为Promise.all最终返回的是一个数组成员按照顺序排序的数组 // 而且异步执行,返回并不一定按照顺序 // 所以需要传当前的i },reject) // 3.如果其中有一个失败的话,则调用reject } })}4、promise.race方法实现,同时执行多个异步,然后那个快,就用那个的结果,race是赛跑// 原生Promise.race的使用// 一个成功就走成功的回调,一个失败就走失败的回调Promise.race([read(’./1.txt’,‘utf8’),read(’./2.txt’,‘utf8’)]).then((data)=>{ console.log(data) // 可能返回 ‘hello’ 也可能返回 ‘swr’ 看哪个返回快就用哪个作为结果})// 内部实现Promise.race = function(promises){ // promises 是一个数组 return new Promise((resolve,reject)=>{ for(let i = 0;i < promises.length;i++){ promises[i].then(resolve,reject) // 和上面Promise.all有点类似 } })}5、promise.defer = promise.deferred这个语法糖怎么理解呢?这个语法糖可以简化一些操作,比如:let fs = require(‘fs’)// 写法一:function read(filePath,encoding){ // 这里的new Promise依然是传递了一个executor回调函数 // 我们该怎样减少回调函数嵌套呢? return new Promise((resolve,reject)=>{ fs.readFile(filePath,encoding,(err,data)=>{ if(err) reject(err) resolve(data) }) })}// 写法二:// 这样的写法减少了一层回调函数的嵌套function read(filePath,encoding){ let dfd = Promise.defer() fs.readFile(filePath,encoding,(err,data)=>{ if(err) dfd.reject(err) dfd.resolve(data) }) return dfd.promise}read(’./1.txt’,‘utf8’).then((data)=>{ console.log(data)})五、promise的链式调用promise的核心在于:链式调用。promise主要解决两个问题:(1)回调地狱(2)并发的异步IO操作,同一时间内把这个结果拿到,比如有两个异步io操作,当这2个获取完毕后,才执行相应的代码1、回调地狱怎么解决那么我们来看下面的代码,并且改为promise。// 回调函数let fs = require(‘fs’)fs.readFile(’./a.txt’,‘utf8’,(err,data)=>{ // 往fs.readFile方法传递了第三个为函数的参数 if(err){ console.log(err) return } console.log(data)})// 改写为Promiselet fs = require(‘fs’)function read(filePath,encoding){ return new Promise((resolve,reject)=>{ fs.readFile(filePath,encoding,(err,data)=>{ if(err) reject(err) resolve() }) })}read(’./a.txt’,‘utf8’).then((data)=>{ // 在这里则不再需要传回调函数进去,而是采用then来达到链式调用 console.log(data)},(err)=>{ console.log(err)})这样看好像Promise也没什么优势,那么接下来我们对比一下// 假设有3个文件// - 1.txt 文本内容为'2.txt’// - 2.txt 文本内容为'3.txt’// - 3.txt 文本内容为’hello swr’// 用回调函数fs.readFile(’./1.txt’,‘utf8’,(err,data)=>{ fs.readFile(data,‘utf8’,(err,data)=>{ fs.readFile(data,‘utf8’,(err,data)=>{ console.log(data) // hello swr }) })})// 用Promiseread(’./1.txt’,‘utf8’).then((data)=>{ // 1.如果一个promise执行完后,返回的还是一个promise, // 会把这个promise的执行结果会传递给下一次then中 return read(data,‘utf8’)}).then((data)=>{ return read(data,‘utf8’)}).then((data)=>{ // 2.如果在then中返回的不是一个promise, // 而是一个普通值,会将这个普通值作为下次then的成功的结果 return data.split(’’).reverse().join(’’)}).then((data)=>{ console.log(data) // rws olleh // 3.如果当前then中失败了,会走下一个then的失败回调 throw new Error(‘出错’)}).then(null,(err)=>{ console.log(err) // Error:出错 报错了 // 4.如果在then中不返回值,虽然没有显式返回, // 但是默认是返回undefined,是属于普通值,依然会把这个普通值传到 // 下一个then的成功回调中}).then((data)=>{ console.log(data) // undefined})从上面可以看得出,改写为Promise的代码,更好阅读和维护,从用Promise方式可以得出结论:(1)如果一个promise执行完后,返回的是一个promise,会将这个promise的执行结果传递给下一个then回调成功中;(2)如果在then中返回的不是一个promise,而是一个普通的值,会将这个普通的值传到下一个then成功回调中;(3)如果当时then中失败了,会走下一个then的回调失败;(4)如果then不返回值,但是默认是返回undefined的,属于普通值,会将这个普通值传到下一个then成功回调中。如果在then中抛出错误,会怎么样呢?情况1:会被下一个then中的失败回调捕获// 情景一,会被下一个then中的失败回调捕获read(’./1.txt’,‘utf8’).then((data)=>{ throw new Error(‘出错了’)}).then(null,(err)=>{ console.log(err) // Error:出错了 报错})情况2:没有被失败回调捕获,抛出错误最终会变成异常read(’./1.txt’,‘utf8’).then((data)=>{ throw new Error(‘出错了’)})情况3:如果没有被失败的回调捕获,那么最终会被catch捕获到read(’./1.txt’,‘utf8’).then((data)=>{ throw new Error(‘出错了’)}).then((data)=>{ }).catch((err)=>{ console.log(err) // Error:出错了 报错})情况4:如果被失败的回调捕获,那么就不会被catch捕获到read(’./1.txt’,‘utf8’).then((data)=>{ throw new Error(‘出错了’)}).then(null,(err)=>{ console.log(err) // Error:出错了 报错}).catch((err)=>{ console.log(err) // 不会执行到这里})(5)catch是错误没有处理的情况下才会执行(6)then回调中可以不写六、关于promise的其他问题1、性能问题可能各位看官会觉得奇怪,Promise能有什么性能问题呢?并没有大量的计算啊,几乎都是处理逻辑的代码。理论上说,不能叫做“性能问题”,而只是有可能出现的延迟问题。什么意思呢,记得刚刚我们说需要把4块代码包在setTimeout里吧,先考虑如下代码:var start = +new Date()function foo() { setTimeout(function() { console.log(‘setTimeout’) if((+new Date) - start < 1000) { foo() } })}foo()运行上面的代码,会打印出多少次’setTimeout’呢,各位可以自己试一下,不出意外的话,应该是250次左右,我刚刚运行了一次,是241次。这说明,上述代码中两次setTimeout运行的时间间隔约是4ms(另外,setInterval也是一样的),实事上,这正是浏览器两次Event Loop之间的时间间隔,相关标准各位可以自行查阅。另外,在Node中,这个时间间隔跟浏览器不一样,经过我的测试,是1ms。单单一个4ms的延迟可能在一般的web应用中并不会有什么问题,但是考虑极端情况,我们有20个Promise链式调用,加上代码运行的时间,那么这个链式调用的第一行代码跟最后一行代码的运行很可能会超过100ms,如果这之间没有对UI有任何更新的话,虽然本质上没有什么性能问题,但可能会造成一定的卡顿或者闪烁,虽然在web应用中这种情形并不常见,但是在Node应用中,确实是有可能出现这样的case的,所以一个能够应用于生产环境的实现有必要把这个延迟消除掉。在Node中,我们可以调用process.nextTick或者setImmediate(Q就是这么做的),在浏览器中具体如何做,已经超出了本文的讨论范围,总的来说,就是我们需要实现一个函数,行为跟setTimeout一样,但它需要异步且尽早的调用所有已经加入队列的函数,这里有一个实现。2、如何停止一个promise链?在一些场景里,我们会遇到一个较长的promise的链式调用,在某一步出现的错误让我们没有必要去运行链式调用后面所有的代码,类似于下面这样的(此处省略then/catch里的函数):new Promise(function(resolve, reject) { resolve(42)}) .then(function(value) { // “Big ERROR!!!” }) .catch() .then() .then() .catch() .then()假设这个Big ERROR!!!的出现让我们完全没有必要运行后面所有的代码了,但链式调用的后面即有catch,也有then,无论我们是return还是throw,都不可避免的会进入某一个catch或then里面,那有没有办法让这个链式调用在Big ERROR!!!的后面就停掉,完全不去执行链式调用后面所有回调函数呢?从一个实现者的角度看问题时,确实找到了答案,就是在发生Big ERROR后return一个Promise,但这个Promise的executor函数什么也不做,这就意味着这个Promise将永远处于pending状态,由于then返回的Promise会直接取这个永远处于pending状态的Promise的状态,于是返回的这个Promise也将一直处于pending状态,后面的代码也就一直不会执行了,具体代码如下:new Promise(function(resolve, reject) { resolve(42)}) .then(function(value) { // “Big ERROR!!!” return new Promise(function(){}) }) .catch() .then() .then() .catch() .then()这种方式看起来有些山寨,它也确实解决了问题。但它引入的一个新问题就是链式调用后面的所有回调函数都无法被垃圾回收器回收(在一个靠谱的实现里,Promise应该在执行完所有回调后删除对所有回调函数的引用以让它们能被回收,在前文的实现里,为了减少复杂度,并没有做这种处理),但如果我们不使用匿名函数,而是使用函数定义或者函数变量的话,在需要多次执行的Promise链中,这些函数也都只有一份在内存中,不被回收也是可以接受的。将返回一个什么也不做的Promise封装成一个有语义的函数,以增加代码的可读性Promise.cancel = Promise.stop = function() { return new Promise(function(){})}这么使用了:new Promise(function(resolve, reject) { resolve(42)}) .then(function(value) { // “Big ERROR!!!” return Promise.stop() }) .catch() .then() .then() .catch() .then()3、promise的链上返回的最后一个promise出错了怎么办?new Promise(function(resolve) { resolve(42)}) .then(function(value) { alter(value) })但运行这段代码的话你会发现什么现象也不会发生,既不会alert出42,也不会在控制台报错,怎么回事呢。细看最后一行,alert被打成了alter,那为什么控制台也没有报错呢,因为alter所在的函数是被包在try/catch块里的,alter这个变量找不到就直接抛错了,这个错就正好成了then返回的Promise的rejection reason。解决办法:(1)所有的promise链的最后都加上一个catch,这样出错后就会被捕获到,这样违背了DRY原则,而且繁琐;(2)借鉴Q的一个方法done,把这个方法加到promise链的最后,就能够处理捕获最后一个promise出现的错误,其实个catch的思路一样,这个是框架来实现的。(3)在一个Promise被reject的时候检查这个Promise的onRejectedCallback数组,如果它为空,则说明它的错误将没有函数处理,这个时候,我们需要把错误输出到控制台,让开发者可以发现。在Promise被reject但又没有callback时,把错误输出到控制台。4、出错时,使用throw new Error()还是使用return Promise.reject(new Error())呢?从性能和编码的舒适角度考虑:(1)性能方面:throw new Error()会使代码进入catch块里的逻辑(我们把多有的回调都包在try/catch里),传说throw多了会影响性能,因为一旦throw,代码就有可能跳转到不可预知的位置。而使用promise.reject(new Error()),则需要构造一个新的promise对象(包含2个数组,4个函数:resolve/reject,onResolved/onRejected),也会花费一定的时间和内存。因为onResolved/onRejected函数是直接被包在promise实现里的try里,出错后直接进入到这个try对应的catch块,代码的跳跃幅度相对较小,性能应该可以忽略不记。(2)编码的舒适度方面:出错用throw,正常用return,正常可以明显的区分出错和正常综上觉得还是promise里发现显式错误后,用throw抛出错误比较好,而不是显式的构造一个呗reject的promise对象。七、实践注意:1、不要把promise写成嵌套的结构// 错误的写法promise1.then(function(value) { promise1.then(function(value) { promise1.then(function(value) { }) })})2、链式promise要返回一个promise,而不是构造一个promise// 错误的写法Promise.resolve(1).then(function(){ Promise.resolve(2)}).then(function(){ Promise.resolve(3)})八、参考https://github.com/xieranmaya… ...

March 26, 2019 · 6 min · jiezi

红绿灯效果

本实现来自 winter《重学前端》第16节 JavaScript执行(一): Promise里的代码为什么比setTimeout先执行?中的最后的问题,使用实现一个红绿灯效果。<body> <style> .redgreenlight{ width: 100px; height: 100px; margin: 0 auto; border-radius: 50%; line-height: 100px; text-align: center; background-color: #eee; border: double 3px gray; } .btn{ display: inline-block; background-color: #ddd; transition: transform 200ms linear; border-radius: 5px; } .btn:hover{ background-color: #888; } .btn:active{ transform: scale(.9); } </style> 打开电源,红绿灯开始工作 <div class=“btn” id=“btn” onclick=“lightopen()">启动</div> <div class=“redgreenlight” id=“light”>红绿灯</div> <script> // 延时的promise包装 function wait(delay){ return new Promise((resolve, reject) => { setTimeout(resolve, delay) }) } // 同步函数promise包装 function asyncDeal(asyncfn){ return new Promise((resolve, reject) => { asyncfn() resolve() }) } // 业务目标 // 绿色 3 // 黄色 1 // 红色 2 // 启动队列循环 function lightopen(){ btn.onclick = null // promise队列执行 return Promise.resolve() .then(() => asyncDeal(() => { light.style.backgroundColor = “#00ff00” light.innerText = (‘绿灯三秒’) })) .then(() => wait(3000)) .then(() => asyncDeal(() => { light.style.backgroundColor = “#ff8800” light.innerText = (‘黄灯一秒’) })) .then(() => wait(1000)) .then(() => asyncDeal(() => { light.style.backgroundColor = “#ff0000” light.innerText = (‘红灯俩秒’) })) .then(() => wait(2000)) .then(() => lightopen()) } </script> </body> ...

March 14, 2019 · 1 min · jiezi

Promise的源码实现(符合Promise/A+规范)

Promise的源码实现/** * 1. new Promise时,需要传递一个 executor 执行器,执行器立刻执行 * 2. executor 接受两个参数,分别是 resolve 和 reject * 3. promise 只能从 pending 到 rejected, 或者从 pending 到 fulfilled * 4. promise 的状态一旦确认,就不会再改变 * 5. promise 都有 then 方法,then 接收两个参数,分别是 promise 成功的回调 onFulfilled, * 和 promise 失败的回调 onRejected * 6. 如果调用 then 时,promise已经成功,则执行 onFulfilled,并将promise的值作为参数传递进去。 * 如果promise已经失败,那么执行 onRejected, 并吧 promise 失败的原因作为参数传递进去。 * 如果promise的状态是pending,需要将onFulfilled和onRejected函数存放起来,等待状态确定后,再依次将对应的函数执行(发布订阅) * 7. then 的参数 onFulfilled 和 onRejected 可以缺省 * 8. promise 可以then多次,promise 的then 方法返回一个 promise * 9. 如果 then 返回的是一个结果,那么就会把这个结果作为参数,传递给下一个then的成功的回调(onFulfilled) * 10. 如果 then 中抛出了异常,那么就会把这个异常作为参数,传递给下一个then的失败的回调(onRejected) * 11.如果 then 返回的是一个promise,那么需要等这个promise,那么会等这个promise执行完,promise如果成功, * 就走下一个then的成功,如果失败,就走下一个then的失败 */const PENDING = ‘pending’;const FULFILLED = ‘fulfilled’;const REJECTED = ‘rejected’;function Promise(executor) { let self = this; self.status = PENDING; self.onFulfilled = [];//成功的回调 self.onRejected = []; //失败的回调 //PromiseA+ 2.1 function resolve(value) { if (self.status === PENDING) { self.status = FULFILLED; self.value = value; self.onFulfilled.forEach(fn => fn());//PromiseA+ 2.2.6.1 } } function reject(reason) { if (self.status === PENDING) { self.status = REJECTED; self.reason = reason; self.onRejected.forEach(fn => fn());//PromiseA+ 2.2.6.2 } } try { executor(resolve, reject); } catch (e) { reject(e); }}Promise.prototype.then = function (onFulfilled, onRejected) { //PromiseA+ 2.2.1 / PromiseA+ 2.2.5 / PromiseA+ 2.2.7.3 / PromiseA+ 2.2.7.4 onFulfilled = typeof onFulfilled === ‘function’ ? onFulfilled : value => value; onRejected = typeof onRejected === ‘function’ ? onRejected : reason => { throw reason }; let self = this; //PromiseA+ 2.2.7 let promise2 = new Promise((resolve, reject) => { if (self.status === FULFILLED) { //PromiseA+ 2.2.2 //PromiseA+ 2.2.4 — setTimeout setTimeout(() => { try { //PromiseA+ 2.2.7.1 let x = onFulfilled(self.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { //PromiseA+ 2.2.7.2 reject(e); } }); } else if (self.status === REJECTED) { //PromiseA+ 2.2.3 setTimeout(() => { try { let x = onRejected(self.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }); } else if (self.status === PENDING) { self.onFulfilled.push(() => { setTimeout(() => { try { let x = onFulfilled(self.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }); }); self.onRejected.push(() => { setTimeout(() => { try { let x = onRejected(self.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }); }); } }); return promise2;}function resolvePromise(promise2, x, resolve, reject) { let self = this; //PromiseA+ 2.3.1 if (promise2 === x) { reject(new TypeError(‘Chaining cycle’)); } if (x && typeof x === ‘object’ || typeof x === ‘function’) { let used; //PromiseA+2.3.3.3.3 只能调用一次 try { let then = x.then; if (typeof then === ‘function’) { //PromiseA+2.3.3 then.call(x, (y) => { //PromiseA+2.3.3.1 if (used) return; used = true; resolvePromise(promise2, y, resolve, reject); }, (r) => { //PromiseA+2.3.3.2 if (used) return; used = true; reject(r); }); }else{ //PromiseA+2.3.3.4 if (used) return; used = true; resolve(x); } } catch (e) { //PromiseA+ 2.3.3.2 if (used) return; used = true; reject(e); } } else { //PromiseA+ 2.3.3.4 resolve(x); }}module.exports = Promise;有专门的测试脚本可以测试所编写的代码是否符合PromiseA+的规范。首先,在promise实现的代码中,增加以下代码:Promise.defer = Promise.deferred = function () { let dfd = {}; dfd.promise = new Promise((resolve, reject) => { dfd.resolve = resolve; dfd.reject = reject; }); return dfd;}安装测试脚本:npm install -g promises-aplus-tests如果当前的promise源码的文件名为promise.js那么在对应的目录执行以下命令:promises-aplus-tests promise.jspromises-aplus-tests中共有872条测试用例。以上代码,可以完美通过所有用例。以下附上PromiseA+的规范(自己随便翻译了下)术语promise 是一个有then方法的对象或者是函数,行为遵循本规范thenable 是一个有then方法的对象或者是函数value 是promise状态成功时的值,包括 undefined/thenable或者是 promiseexception 是一个使用throw抛出的异常值reason 是promise状态失败时的值要求2.1 Promise StatesPromise 必须处于以下三个状态之一: pending, fulfilled 或者是 rejected2.1.1 如果promise在pending状态2.1.1.1 可以变成 fulfilled 或者是 rejected2.1.2 如果promise在fulfilled状态2.1.2.1 不会变成其它状态2.1.2.2 必须有一个value值2.1.3 如果promise在rejected状态2.1.3.1 不会变成其它状态2.1.3.2 必须有一个promise被reject的reason概括即是:promise的状态只能从pending变成fulfilled,或者从pending变成rejected.promise成功,有成功的value.promise失败的话,有失败的原因2.2 then方法promise必须提供一个then方法,来访问最终的结果promise的then方法接收两个参数promise.then(onFulfilled, onRejected)2.2.1 onFulfilled 和 onRejected 都是可选参数2.2.1.1 onFulfilled 必须是函数类型2.2.1.2 onRejected 必须是函数类型2.2.2 如果 onFulfilled 是函数:2.2.2.1 必须在promise变成 fulfilled 时,调用 onFulfilled,参数是promise的value2.2.2.2 在promise的状态不是 fulfilled 之前,不能调用2.2.2.3 onFulfilled 只能被调用一次2.2.3 如果 onRejected 是函数:2.2.3.1 必须在promise变成 rejected 时,调用 onRejected,参数是promise的reason2.2.3.2 在promise的状态不是 rejected 之前,不能调用2.2.3.3 onRejected 只能被调用一次2.2.4 onFulfilled 和 onRejected 应该是微任务2.2.5 onFulfilled 和 onRejected 必须作为函数被调用2.2.6 then方法可能被多次调用2.2.6.1 如果promise变成了 fulfilled态,所有的onFulfilled回调都需要按照then的顺序执行2.2.6.2 如果promise变成了 rejected态,所有的onRejected回调都需要按照then的顺序执行2.2.7 then必须返回一个promisepromise2 = promise1.then(onFulfilled, onRejected);2.2.7.1 onFulfilled 或 onRejected 执行的结果为x,调用 resolvePromise2.2.7.2 如果 onFulfilled 或者 onRejected 执行时抛出异常e,promise2需要被reject2.2.7.3 如果 onFulfilled 不是一个函数,promise2 以promise1的值fulfilled2.2.7.4 如果 onRejected 不是一个函数,promise2 以promise1的reason rejected2.3 resolvePromiseresolvePromise(promise2, x, resolve, reject)2.3.1 如果 promise2 和 x 相等,那么 reject promise with a TypeError2.3.2 如果 x 是一个 promsie2.3.2.1 如果x是pending态,那么promise必须要在pending,直到 x 变成 fulfilled or rejected.2.3.2.2 如果 x 被 fulfilled, fulfill promise with the same value.2.3.2.3 如果 x 被 rejected, reject promise with the same reason.2.3.3 如果 x 是一个 object 或者 是一个 function2.3.3.1 let then = x.then.2.3.3.2 如果 x.then 这步出错,那么 reject promise with e as the reason..2.3.3.3 如果 then 是一个函数,then.call(x, resolvePromiseFn, rejectPromise) 2.3.3.3.1 resolvePromiseFn 的 入参是 y, 执行 resolvePromise(promise2, y, resolve, reject); 2.3.3.3.2 rejectPromise 的 入参是 r, reject promise with r. 2.3.3.3.3 如果 resolvePromise 和 rejectPromise 都调用了,那么第一个调用优先,后面的调用忽略。 2.3.3.3.4 如果调用then抛出异常e 2.3.3.3.4.1 如果 resolvePromise 或 rejectPromise 已经被调用,那么忽略 2.3.3.3.4.3 否则,reject promise with e as the reason2.3.3.4 如果 then 不是一个function. fulfill promise with x.2.3.4 如果 x 不是一个 object 或者 function,fulfill promise with x. ...

March 8, 2019 · 4 min · jiezi

你与弄懂promise之间可能只差这篇文章(二)

点我看看~话不多说,上源码:// 核心方法!核心方法!核心方法!也是最难懂的!—-STARTconst resolvePromise = (promise2, x, resolve, reject) => { if (promise2 === x) { return reject(new TypeError(“A promise cannot be resolved with itself”)) } if (x && (typeof x === “object” || typeof x === “function”)) { try { let then = x.then; if (typeof then === “function”) { then.call(x, y => { resolve(y); resolvePromise(promise2, y, resolve, reject); }, r => { reject(r); }) } else { resolve(x); } } catch (e) { reject(e) } } else { resolve(x) }};// 核心方法!核心方法!核心方法!也是最难懂的!—-END// Commitment就是class Commitment { constructor (executor) { this.status = “PENDING” this.value = void(0); this.reason = void(0); this.onResolvedCallbacks = []; this.onRejectedCallbacks = []; const resolve = (value) => { if (this.status === “PENDING”) { this.status = “RESOLVED”; this.value = value; this.onResolvedCallbacks.forEach(cb => cb()) } } const reject = (reason) => { if (this.status === “PENDING”) { this.status = “REJECTED”; this.reason = reason; this.onRejectedCallbacks.forEach(cb => cb()) } } try { executor(resolve, reject) } catch (e) { reject(e) } } then (onResolvedFn, onRejectedFn) { let promise2, x; promise2 = new Promise((resolve, reject) => { if (this.status === “RESOLVED”) { setTimeout(() => { try { x = onResolvedFn(this.value); resolvePromise(promise2, x, resolve, reject); } catch(e) { reject(e) } }, 0) } if (this.status === “REJECTED”) { setTimeout(() => { try { x = onRejectedFn(this.reason); resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }, 0) } if (this.status === “PENDING”) { this.onResolvedCallbacks.push(() => { setTimeout(() => { try { x = onResolvedFn(this.value); resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }, 0) }); this.onRejectedCallbacks.push(() => { setTimeout(() => { try { x = onRejectedFn(this.reason); resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }, 0) }); } }); return promise2 } catch (onRejectedFn) { return this.then(null, onRejectedFn) } // 静态方法 static resolve (value) { return new Commitment((resolve, reject) => { resolve(value) }) } static reject (err) { return new Commitment((resolve, reject) => { reject(err) }) } static all () { let result = []; return new Commitment((resolve, reject) => { if (arguments.length === 0) { return reject(new TypeError(“undefined is not iterable (cannot read property Symbol(Symbol.iterator))”)) } else { let args = […arguments][0], result = []; let count = args.length, num = 0; for (let i = 0; i < count; i++) { let item = args[i]; try { let then = item.then; if (typeof then === “function”) { then.call(item, y => { num += 1; result[i] = y; if (num === count) { resolve(result) } }, r => { reject(r) }) } else { result[i] = item; if (num === count) { resolve(result) } } } catch (e) { reject(e) } } } }) } static race () { // 未完待续 }} ...

February 28, 2019 · 3 min · jiezi

Java 异步编程之:notify 和 wait 用法

最近看帖子,发现一道面试题:启动两个线程, 一个输出 1,3,5,7…99, 另一个输出 2,4,6,8…100 最后 STDOUT 中按序输出 1,2,3,4,5…100题目要求用 Java 的 wait + notify 机制来实现,重点考察对于多线程可见性的理解。wait 和 notify 简介wait 和 notify 均为 Object 的方法:Object.wait() —— 暂停一个线程Object.notify() —— 唤醒一个线程从以上的定义中,我们可以了解到以下事实:想要使用这两个方法,我们需要先有一个对象 Object。在多个线程之间,我们可以通过调用同一个对象的wait()和notify()来实现不同的线程间的可见。对象控制权(monitor)在使用 wait 和 notify 之前,我们需要先了解对象的控制权(monitor)。在 Java 中任何一个时刻,对象的控制权只能被一个线程拥有。如何理解控制权呢?请先看下面的简单代码:public class ThreadTest { public static void main(String[] args) { Object object = new Object(); new Thread(new Runnable() { @Override public void run() { try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); }}直接执行,我们将会得到以下异常:Exception in thread “Thread-0” java.lang.IllegalMonitorStateException at java.lang.Object.wait(Native Method) at java.lang.Object.wait(Object.java:502) at com.xiangyu.demo.ThreadTest$1.run(ThreadTest.java:10) at java.lang.Thread.run(Thread.java:748)出错的代码在:object.wait();。这里我们需要了解以下事实:无论是执行对象的 wait、notify 还是 notifyAll 方法,必须保证当前运行的线程取得了该对象的控制权(monitor)如果在没有控制权的线程里执行对象的以上三种方法,就会报 java.lang.IllegalMonitorStateException 异常。JVM 基于多线程,默认情况下不能保证运行时线程的时序性在上面的示例代码中,我们 new 了一个 Thread,但是对象 object 的控制权仍在主线程里。所以会报 java.lang.IllegalMonitorStateException 。我们可以通过同步锁来获得对象控制权,例如:synchronized 代码块。对以上的示例代码做改造:public class ThreadTest { public static void main(String[] args) { Object object = new Object(); new Thread(new Runnable() { @Override public void run() { synchronized (object){ // 修改处 try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); }}再次执行,代码不再报错。我们可以得到以下结论:调用对象的wait()和notify()方法,需要先取得对象的控制权可以使用synchronized (object)来取得对于 object 对象的控制权解题了解了对象控制权之后,我们就可以正常地使用 notify 和 wait 了,下面给出我的解题方法,供参考。public class ThreadTest { private final Object flag = new Object(); public static void main(String[] args) { ThreadTest threadTest = new ThreadTest(); ThreadA threadA = threadTest.new ThreadA(); threadA.start(); ThreadB threadB = threadTest.new ThreadB(); threadB.start(); } class ThreadA extends Thread { @Override public void run() { synchronized (flag) { for (int i = 0; i <= 100; i += 2) { flag.notify(); System.out.println(i); try { flag.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } class ThreadB extends Thread { @Override public void run() { synchronized (flag) { for (int i = 1; i < 100; i += 2) { flag.notify(); System.out.println(i); try { flag.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }}发散:notify()和notifyAll()这两个方法均为 native 方法,在JDK 1.8 中的关于notify()的JavaDoc如下:Wakes up a single thread that is waiting on this object’s monitor. If any threads are waiting on this object, one of them is chosen to be awakened.译为:唤醒此 object 控制权下的一个处于 wait 状态的线程。若有多个线程处于此 object 控制权下的 wait 状态,只有一个会被唤醒。也就是说,如果有多个线程在 wait 状态,我们并不知道哪个线程会被唤醒。在JDK 1.8 中的关于notifyAll()的JavaDoc如下:Wakes up all threads that are waiting on this object’s monitor.译为:唤醒所有处于此 object 控制权下的 wait 状态的线程。所以,我们需要根据实际的业务场景来考虑如何使用。 ...

February 3, 2019 · 2 min · jiezi

一篇文章了解前端异步编程方案演变

对于JS而言,异步编程我们可以采用回调函数,事件监听,发布订阅等方案,在ES6之后,又新添了Promise,Genertor,Async/Await的方案。本文将阐述从回调函数到Async/Await的演变历史,以及它们之间的关系。1. 异步编程的演变首先假设要渲染一个页面,只能异步的串行请求A,B,C,然后才能拿到页面的数据并请求页面针对于不同的异步编程方式,我们会得到如下的代码:1.1 回调函数// 假设request是一个异步函数request(A, function () { request(B, function () { request(C, function () { // 渲染页面 }) })})回调函数的嵌套是愈发深入的。在不断的回调中,request(A)回调函数中的其他逻辑会影响到request(B),request(C)中的逻辑,同理,request(B)中的其他逻辑也会影响到request(C)。在这个例子中,request(A)调用request(B),request(B)调用request(C),request(C)执行完毕返回,request(B)执行完毕返回,request(A)执行完毕返回。我们很快会对先后顺序产生混乱,从而很难直观的分析出异步回调的结果。这就被称为回调地狱。为了解决这种情况,ES6新增了Promise对象。1.2 Promise// 假设request是一个Promise函数request(A).then(function () { return request(B)}).then(function () { return request(C)}).then(function () { // 渲染页面})Promise对象用then函数来指定回调。所以,之前在1.1中回调函数的例子可以改为上文中的模样。可以看到,Promise并没有消除回调地狱,但是却通过then链将代码逻辑变得更加清晰了。在这个例子中,request(A)调用request(B),request(B)调用request(C),request(C)执行完毕返回。现在,request(A)中的内容只能通过显示声明的data来影响到request(C)——如果没有显示的在回调中声明,则影响不了request(C),换言之,每段回调被近乎独立的分割了。但是Promise本身还是有一堆的then,还是不能让我们像写同步代码一样写异步的代码,因此JS又引入了Generator。1.3 Generatorfunction* gen(){ var r1 = yield request(A) var r2 = yield request(B) var r3 = yield request(C) // 渲染页面};Generator是协程在ES6上的实现,协程是指一个线程上不同函数间执行权可以相互切换。如本例,先执行gen(),然后在遇到yield时暂停,执行权交给request(A),等到调用了next()方法,再将执行权还给gen()。通过协程,JS就实现了用同步的方式写异步的代码,但是Generator的使用要配合执行器,这自然是麻烦的。于是就有了Async/Await。Generator的自动执行器是co函数库,有兴趣的同学可以通过阅读《co 函数库的含义和用法》来进行了解。1.4 Async/Awaitasync function gen() { var r1 = await request(A) var r2 = await request(B) var r3 = await request(C) // 渲染页面}如果比较代码的话,1.4的代码只是把1.3的代码中* => async,yield变为await。但Async函数的实现,就是将 Generator函数和自动执行器,包装在一个函数里[1]。spawn就是自动执行器。async function fn(args){ // …}// 等同于function fn(args){ return spawn(function*() { // … }); }除此以外,Async函数比Generator函数有更好的延展性——yield接的是Promise函数/Thunk函数,但await还可以包括普通函数。对于普通函数,await表达式的运算结果就是它等到的东西。否则若await等到的是一个Promise函数,await就会协程到这个Promise函数上,直到它resolve或者reject,然后再协程回主函数上[2]。当然,Async函数也比Generator函数更加易读和易理解。2. 总结本文阐述了从回调函数到Async/Await的演变历史。Async函数作为换一个终极解决方案,尽管在并行异步处理上还要借助Promise.all(),但其他方面已经足够完美。参考文档《深入掌握 ECMAScript 6 异步编程》系列《理解JavaScript的 async/await》 ...

January 28, 2019 · 1 min · jiezi

javascript异步之Promise.all()、Promise.race()、Promise.finally()

同期异步系列文章推荐谈一谈javascript异步javascript异步中的回调javascript异步与promisejavascript异步之Promise.resolve()、Promise.reject()javascript异步之Promise then和catchjavascript异步之async(一)javascript异步之async(二)javascript异步实战javascript异步总结归档今天我们继续讨论promise网络上关于PromiseAPI使用的文章多如牛毛,为了保持javascript异步系列文章的完整性,现在对promise的API进行简单全面的介绍准备工作我在easy-mock添加了三个接口,备用依然使用axios进行ajax请求Promise.all()Promise.all()有点像“并行”我们看一个栗子<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>promise</title> <script src=“https://unpkg.com/axios/dist/axios.min.js"></script></head><body> <script> { const p1 = axios.get(‘https://easy-mock.com/mock/5b0525349ae34e7a89352191/example/promise1') .then(({ data }) => { console.log(‘p1成功啦’); return data.data }) const p2 = axios.get(‘https://easy-mock.com/mock/5b0525349ae34e7a89352191/example/promise2') .then(({ data }) => { console.log(‘p2成功啦’); return data.data }) const p3 = axios.get(‘https://easy-mock.com/mock/5b0525349ae34e7a89352191/example/mock') .then(({ data }) => { console.log(‘p3成功啦’); return data.data }) const p = Promise.all([p3, p1, p2]) .then(arr => { console.log(arr); console.log(‘Promise.all成功啦’); }) .catch(err=>{ console.log(err,‘Promise.all错啦’); }) } </script></body></html>我们知道axios返回的是一个promise对象,我们可以看下 console.log(p1);Promise.all就是用于将多个 Promise 实例,包装成一个新的 Promise 实例Promise.all,接收一个数组作为参数,数组的每一项都返回Promise实例我们重点看这段代码 const p = Promise.all([p3, p1, p2]) .then(arr => { console.log(arr); console.log(‘Promise.all成功啦’); }) .catch(err=>{ console.log(err,‘Promise.all错啦’); })p1,p2,p3都是返回promise实例,Promise.all不关心他们的执行顺序,如果他们都返回成功的状态,Promise.all则返回成功的状态,输出一个数组,是这三个p1,p2,p3的返回值,数组的顺序和他们的执行顺序无关,和他们作为参数排列的顺序有关我们看下输出为了是拉长接口三的返回时间我对接口三的数据进行了修改,返回值是长度1000-2000之间的随机数组,所以p3的执行要晚于p1和p2,但我们输出的arr,p3依然在前面,这给我们带来一个便利,返回值数组的顺序和方法的执行顺序无关,可以进行人为进行控制我们将p1做一下改动,使p1报错 const p1 = axios.get(‘https://easy-mock.com/mock/5b0525349ae34e7a89352191/example/promise1') .then(({ data }) => { console.log(‘p1成功啦’); return xxxx.data//将data.data修改为xxxx.data })如果有一个返回失败(reject),Promise.all则返回失败(reject)的状态,此时第一个被reject的实例的返回值,会传递给P的回调函数。三个promise实例参数之间是“与”的关系,全部成功,Promise.all就返回成功,有一个失败,Promise.all就返回失败换个角度说,一个promise的执行结果依赖于另外几个promise的执行结果,例如:几个ajax全部执行完了,才能渲染页面,几个ajax全部执行完了,才能做一些数据的计算操作,不关心执行顺序,只关心集体的执行结果Promise.race()Promise中的竞态,用法和Promise.all类似,对应参数的要求和Promise.all相同,传入一个数组作为参数,参数要返回一个Promise实例race就是竞争的意思,数组内的Promise实例,谁执行的快,就返回谁的执行结果,不管是成功还是失败const p = Promise.race([p3, p1, p2]) .then(res => { console.log(res); console.log(‘Promise.all成功啦’); }) .catch(err=>{ console.log(err,‘Promise.all错啦’); })通过输出我们发现p1是第一个完成的,所以p的返回结果就是p1的执行结果而且就算完成,但是 进程不会立即停止,还会继续执行下去。关于race的使用场景搜了一下,很多文章都说是用来解决网络超时的提示,类似于下面这样 const p3 = axios.get(‘https://easy-mock.com/mock/5b0525349ae34e7a89352191/example/mock') .then(({ data }) => { console.log(‘p3成功啦’); return data.data }) const p4 = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error(‘网络连接超时’)), 50) }) const p = Promise.race([p3, p4]) .then(res => console.log(res)) .catch(err => console.log(err));p3的ajax和50ms的定时器比较,看谁执行的快,如果超过了50ms,p3的ajax还没返回,就告知用户网络连接超时这里有个问题,就算提示超时了,p3还在继续执行,它并没有停下来,直到有状态返回个人观点:race可以用来为ajax请求的时长划定范围,如果ajax请求时长超过xxxms会执行某个方法,或者ajax请求时长不超过xxms会执行某个方法,总之,race的应用空间不是很大Promise.finally()finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。 const p = Promise.race([p3, p4]) .then(res => console.log(res)) .catch(err => console.log(err)) .finally(() => { console.log(“finally的执行与状态无关”) });当promise得到状态(不论成功或失败)后就会执行finally,原文链接参考链接Promise 对象Promise.prototype.finally ...

January 22, 2019 · 1 min · jiezi

javascript异步与promise

同期异步系列文章推荐谈一谈javascript异步javascript异步中的回调javascript异步之Promise.all()、Promise.race()、Promise.finally()javascript异步之Promise.resolve()、Promise.reject()javascript异步之Promise then和catchjavascript异步之async(一)javascript异步之async(二)javascript异步实战javascript异步总结归档我们说处理javascript异步最常用的方式就是通过回调函数,对于回调函数我们昨天对此做了介绍简单快速,我们一般使用嵌套回调或者链式回调,会产生以下问题当采用嵌套回调时,会导致层级太多,不利于维护所以我们又采用了链式回调,对嵌套回调进行拆分,拆分后的函数间耦合度很高,如果需要传递参数,函数之间的关联性会更高,而且要对参数进行校验以提高代码的健壮性如果将我们自己的回调函数传递给第三方插件或者库,就要考虑一些不可控因素调用回调过早调用回调过晚(或不被调用)调用回调次数过多或者过少promise的存在就是为了解决以上问题虽然我们日常写回调函数不会有这么严格的要求,但是如果不这样去写回调函数,就会存在隐患,当在团队协作的时候,显得编码规范显得尤为重要本文不重点介绍如何使用promise,重点介绍的是promise解决了哪些异步回调出现的问题。什么是promise我们来看一个场景,有助于我们了解promise设想一下这个场景,我去KFC,交给收银员10元,下单买一个汉堡,下单付款。到这里,我已经发出了一个请求(买汉堡),启动了一次交易。但是做汉堡需要时间,我不能马上得到这个汉堡,收银员给我一个收据来代替汉堡。到这里,收据就是一个承诺(promise),保证我最后能得到汉堡。所以我需要好好的保留的这个收据,对我来说,收据就是汉堡,虽然这张收据不能吃,我需要等待汉堡做好,等待收银员叫号通知我等待的过程中,我可以做些别的事情收银员终于叫到了我的号,我用收据换来了汉堡当然还有一种情况,当我去柜台取汉堡的时候,收银员告诉我汉堡卖光了,做汉堡的师傅受伤了等等原因,导致了我无法得到这个汉堡虽然我有收据(承诺),但是可能得到汉堡(成功),可能得不到汉堡(失败)我由等待汉堡变成了等到或者等不到,这个过程不可逆,上面很形象的介绍了promise,上面的等待汉堡和得到汉堡,汉堡卖光了,得不到汉堡,分别对应promise的三种状态三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)(一旦状态改变,就不会再变)回调函数调用过早调用过早就是将异步函数作为同步处理了,我们之前说过,javascript以单线程同步的方式执行主线程,遇到异步会将异步函数放入到任务队列中,当主线程执行完毕,会循环执行任务队列中的函数,也就是事件循环,直到任务队列为空。事件循环和任务队列事件循环就像是一个游乐场,玩过一个游戏后,你需要重新排到队尾才能再玩一次任务队列就是,在你玩过一个游戏后,可以插队接着玩我们看一个栗子 const promise = new Promise((resolve, reject) => { resolve(“成功啦”) }); promise.then(res => { console.log(res); console.log(“我是异步执行的”); }) console.log(‘我在主线程’);看下输出,重点看输出顺序//我在主线程//成功啦//我是异步执行的直接手动是promise的状态切为成功状态,console.log(“我是异步执行的”);这段代码也是异步执行的提供给then()的回调永远都是异步执行的,所以promise中不会出现回调函数过早执行的情况回调函数调用过晚或不被调用回调函数调用过晚回调函数调用过晚的处理原理和调用过早很类似,在promise的then()中存放着异步函数,所有的异步都存在于js的任务队列中,当js的主线程执行完毕后,会依次执行任务队列中的内容,不会出现执行过晚的情况回调函数不被调用我们用栗子说话 const promise = new Promise((resolve, reject) => resolve(‘成功啦’)) promise.then(s => console.log(s)); console.log(‘我在主线程’);成功状态的输出//我在主线程//成功啦成功状态下回调被调用继续看一下失败的回调 const promise = new Promise((resolve, reject) => reject(‘失败啦’)) promise.then(null, s => console.log(s)); console.log(‘我在主线程’);失败状态的输出//我在主线程//失败啦失败状态下回调被调用所以说,不管是失败还是成功,回调函数都会被调用回调函数调用次数过多或者过少调用次数过多我们之前说了promise有三种状态pending(进行中)、fulfilled(已成功)和rejected(已失败)状态一旦状态改变,就不会再变一个栗子 const promise = new Promise((resolve, reject) => { reject(‘失败啦’) resolve(‘成功啦’) }); promise.then(res => { console.log(我是异步执行的成功:${res}); },err=>{ console.log(我是异步执行的失败:${err}); }).catch(err => { console.log(err); }) console.log(‘我在主线程’);输出//我在主线程//我是异步执行的失败:失败啦当状态变为失败时,就不会再变为成功,成功的函数也不会执行,反之亦然调用次数过少回调函数正常是调用一次,过少=>0次=>回调函数不被调用,上面刚刚讨论过原文链接参考链接JavaScript Promise 迷你书Promise 对象ES6 系列之我们来聊聊 Promise ...

January 21, 2019 · 1 min · jiezi

javascript异步中的回调

同期异步系列文章推荐谈一谈javascript异步javascript异步与promisejavascript异步之Promise.all()、Promise.race()、Promise.finally()javascript异步之Promise.resolve()、Promise.reject()javascript异步之Promise then和catchjavascript异步之async(一)javascript异步之async(二)javascript异步实战javascript异步总结归档我们之前介绍了javascript异步的相关内容,我们知道javascript以同步,单线程的方式执行主线程代码,将异步内容放入事件队列中,当主线程内容执行完毕就会立即循环事件队列,直到事件队列为空,当用产生用户交互事件(鼠标点击,点击键盘,滚动屏幕等待),会将事件插入事件队列中,然后继续执行。处理异步逻辑最常用的方式是什么?没错这就是我们今天要说的—回调js回调函数如你所知,函数是对象,所以可以存储在变量中,所以函数还有以下身份:可以作为函数的参数可以在函数中创建可以在函数中返回当一个函数a以一个函数作为参数或者以一个函数作为返回值时,那么函数a就是高阶函数回调函数百度百科回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。维基百科在计算机程序设计中,回调函数,或简称回调(Callback 即call then back 被主函数调用运算后会返回主函数),是指通过函数参数传递到其它代码的,某一块可执行代码的引用。这一设计允许了底层代码调用在高层定义的子程序。回调函数,几乎每天我们都在用 setTimeout(() => { console.log(“这是回调函数”); }, 1000); const hero=[‘郭靖’,‘黄蓉’] hero.forEach(item=>{ console.log(item); })回调函数解决了哪些问题举一个简单的: let girlName = “裘千尺” function hr() { girlName = “黄蓉” console.log(我是${girlName}); } function gj() { console.log(${girlName}你好,我是郭靖,认识一下吧); } hr() gj()输出,重点看输出顺序//=>我是黄蓉//=>黄蓉你好,我是郭靖,认识一下吧上面的代码输出是没什么悬念的,不存在异步,都单线程同步执行,最后郭靖和黄蓉相识如果这时候黄蓉很忙,出现了异步,会怎么样? let girlName = “裘千尺” function hr() { setTimeout(() => { girlName = “黄蓉” console.log(‘我是黄蓉’); }, 0); } function gj() { console.log(${girlName}你好,我是郭靖,认识一下吧); } hr() gj()输出,重点看输出顺序//=>裘千尺你好,我是郭靖,认识一下吧//=>我是黄蓉虽然定时器是0ms,但是也导致了郭靖和黄蓉的擦肩而过,这不是我们期望的结果,hr函数存在异步,只有等主线程的内容走完,才能走异步函数所以最简单的办法就是使用回调函数解决这种问题,gj函数依赖于hr函数的执行结果,所以我们把gj作为hr的一个回调函数 let girlName = “裘千尺” function hr(callBack) { setTimeout(() => { girlName = “黄蓉” console.log(‘我是黄蓉’); callBack() }, 0); } function gj() { console.log(${girlName}你好,我是郭靖,认识一下吧); } hr(gj)输出,重点看输出顺序//=>我是黄蓉//=>黄蓉你好,我是郭靖,认识一下吧⚠️:当回调函数作为参数时,不要带后面的括号!我们只是传递函数的名称,不是传递函数的执行结果上面小栗子貌似的很简单,我们继续嵌套回调和链式回调我们把昨天的demo做一下升级引入了lodash:处理按钮点击防抖axios,集成了promis,但promise不是我们今天讨论的内容,我们只使用axios的ajax请求接口功能easy-mock:接口数据,用来实现ajax请求(数据是假的,但是请求是真的)嵌套回调<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>javascript回调</title> <script src=“https://unpkg.com/axios/dist/axios.min.js"></script> <script src=“https://cdn.bootcss.com/lodash.js/4.17.11/lodash.min.js"></script></head><body> <button>点击</button> <script> { const btn = document.querySelector(‘button’) btn.onclick = () => { _.debounce(() => { axios.get(‘https://easy-mock.com/mock/5b0525349ae34e7a89352191/example/mock') .then(data => { console.log(“ajax返回成功”); myData = data.data console.log(myData); }) .catch(error => { console.log(“ajax返回失败”); }) }, 500)() } } </script></body></html>仔细看代码,不难发现,这是一个典型的嵌套回调,我们分析一下第一层异步,用户交互,来自按钮的点击事件第二层异步,按钮去抖,来自lodash下debounce的500ms延时第三次异步,ajax请求,处理后台接口数据拿到数据后我们没有继续做处理,在实际工作中可能还存在异步,还会继续嵌套,会形成一个三角形的缩进区域再继续嵌套,就会形成所说的“回调地狱”,就是回调的层级太多了,代码维护成本会高很多上面的栗子最多算是入门毁掉地狱,我们看一下这个 function funA(callBack) { console.log(“A”); setTimeout(() => { callBack() }, 10); } function funB() { console.log(“B”); } function funC(callBack) { console.log(“C”); setTimeout(() => { callBack() }, 100); } function funD() { console.log(“D”); } function funE() { console.log(“E”); } function funF() { console.log(“F”); }//从这里开始执行 funA(() => { funB() funC(() => { funD() }) funE() }) funF()(这段代码,带回调的都是异步逻辑)你能很快的看出这段代码的执行顺序吗?顺序如下:A、F、B、C、E、D一般正常人不会这么嵌套多层,层级一多,就会考虑拆分链式回调 const btn = document.querySelector(‘button’) //监听按钮点击事件 btn.onclick = () => { debounceFun() } //去抖动 const debounceFun = _.debounce(() => { ajax() }, 500) //ajax 请求 const ajax = function () { axios.get(‘https://easy-mock.com/mock/5b0525349ae34e7a89352191/example/mock') .then(data => { console.log(“ajax返回成功”); myData = data.data console.log(myData); }) .catch(error => { console.log(“ajax返回失败”); }) }我相信很多人都会通过这种链式回调的方式处理异步回调,因为可读性比嵌套回调要搞,但是维护的成本可能要高很多上面的栗子,三个异步函数之间只有执行顺序上的关联,并没有数据上的关联,但是实际开发中的情况要比这个复杂,回调函数参数校验我们举一个简单的栗子 let girlName = “裘千尺” function hr(callBack) { setTimeout(() => { girlName = “黄蓉” console.log(‘我是黄蓉’); callBack(girlName) }, 0); } function gj(love) { console.log(${girlName}你好,我是郭靖,认识一下吧,我喜欢${love}); } hr(gj)gj作为hr的回调函数,并且hr将自己的一个变量传递给gj,gj在hr的回调中执行,仔细看这种写法并不严谨,如果gj并不只是一个function类型会怎么样?如果love的实参并不存在会怎么样?况且这只是一个简单的栗子所以回调函数中,参数的校验是很有必要的,回调函数链拉的越长,校验的条件就会越多,代码量就会越多,随之而来的问题就是可读性和可维护性就会降低。还是回调函数的校验但我们引用了第三方的插件或库的时候,有时候难免要出现异步回调的情况,一个栗子:xx支付,当用户发起支付后,我们将自己的一个回调函数,传递给xx支付,xx支付比较耗时,执行完之后,理论上它会去执行我们传递给他的回调函数,是的理论上是这样的,我们把回调的执行权交给了第三方,隐患随之而来第三方支付,多次调用我们的回调函数怎么办?第三方支付,不调用我们的回调函数怎么办?当我们把回调函数的执行权交给别人时,我们也要考虑各种场景可能会发生的问题总结一下:回调函数简单方便,但是坑也不少,用的时候需要多注意校验原文链接 ...

January 18, 2019 · 2 min · jiezi

周报2019-01-12

这周JS异步包括ES6 Promise写法以及ES7 Async/Await以及PromiseAPI的回顾demo,模拟异步获取token,我对什么时候Promise转换为Reject的理解还是不是很清楚React16的生命周期React所有的生命周期(包含已经废除和即将废除和新增的),学习了每个生命周期是什么时候触发以及具体是做什么用的,但是getDerivedStateFormProps 这个生命周期的应用场景以及发展趋势还不是很清楚,我写了一篇文章去总结React16 生命周期理解React Hooks学习了React 16.7-alpha.2版本中 Hooks的发展和作用,包括API的使用和自定义Hooks,掌握不熟练,暂时没有在项目中复杂的应用,我写了一个demo-hooks在这里微信小程序开始学习了,再不学就out了Ubuntu18.04的使用因为公司用Ubuntu,所以大概安装了Ubuntu上的前端开发环境,并且安装了仿MacOSX的主题,Ubuntu我不是很熟悉和喜欢,还是喜欢在Mac上开发,知乎上有Ubunut优雅使用指南,Ubuntu18.04安装mac os主题此文写得很详细了

January 12, 2019 · 1 min · jiezi

谈一谈javascript异步

从今天开始研究一下javascript的异步相关内容,感兴趣的请关注同期异步系列文章推荐javascript异步中的回调javascript异步与promisejavascript异步之Promise.all()、Promise.race()、Promise.finally()javascript异步之Promise.resolve()、Promise.reject()javascript异步之Promise then和catchjavascript异步之async(一)javascript异步之async(二)javascript异步实战javascript异步总结归档什么是js异步?我们知道JavaScript的单线程的,这与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。ajax的同步请求就会导致浏览器产生假死,因为它会锁定浏览器的UI(按钮,菜单,滚动条等),并阻塞所有用户的交互,jquery中的ajax有这样一个同步请求的功能,一定要慎用,尤其是在请求的数据量很大的时候,要避免使用同步请求。举几个栗子????感受一下异步后台接口使用easy-mock,官方地址:https://easy-mock.com/ajax使用axios,基本代码如下<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>javascript异步</title> <script src=“https://unpkg.com/axios/dist/axios.min.js"></script></head><body> <button>点击</button> <script> { let myData = null //ajax请求 function ajax() { axios.get(‘https://easy-mock.com/mock/5b0525349ae34e7a89352191/example/mock') .then(data => { console.log(“ajax返回成功”);// handle success myData = data.data console.log(myData); }) .catch(error => { // console.log(error); // handle error console.log(“ajax返回失败”); }) } } </script></body></html>我们通过添加一些js来看下效果,异步-定时器 console.log(myData); setTimeout(() => { console.log(‘定时器’); }, 2000); console.log(myData);输出,应该没什么悬念//null//null//定时器执行顺序:先执行第一个 console.log(myData);然后遇到了定时器,将定时器挂起(就是暂停了这个定时器)继续执行第二个 console.log(myData);没有可以执行的js代码,回头把挂起的任务继续执行下去继续看下一个栗子异步-ajax console.log(myData); ajax() console.log(myData);看下输出,依然没有悬念//null//null//ajax返回成功//{success: true, data: {…}}(这是接口返回的数据,我们不必关心返回的具体内容,只要知道返回了就好,陌上寒注)执行顺序和上面的定时器基本类似,不在此赘述。将两个栗子合并,我们看下 console.log(myData); ajax() setTimeout(() => { console.log(‘定时器’); }, 2000); console.log(myData);输出,//null//null//ajax返回成功//{success: true, data: {…}}//定时器发现问题了吗?两个异步函数相遇了,先执行谁?谁跑的快就先执行谁?也可以这么说,其实这引发了另外一个知识点,任务队列和事件循环两个 console.log(myData);是同步执行的,他们都在js的主线程上执行,在主线程之外还存在一个任务队列,任务队列中存放着需要异步执行的内容当主线程运行完毕之后,就会去执行任务队列中的任务(不断的重复扫描)直到任务队列清空观察这段代码 console.log(1); setTimeout(function () { console.log(2); }, 1000); console.log(3);输出:1,3,2,这没什么可解释的再看一段代码setTimeout(function(){console.log(1);}, 0);console.log(2);输出:2,1,为什么会这样?console.log(2);在主线程中,先执行,setTimeout(function(){console.log(1);}, 0);放在了任务队列中,只有在主线程执行完了才会去执行任务列队中的内容为什么主线程的任务执行完了后需要不断的扫描任务列队中的内容呢?看这段代码,有助于你的理解 console.log(myData); ajax() setTimeout(() => { console.log(‘定时器’); }, 2000); console.log(myData); const btn = document.querySelector(‘button’) btn.onclick = () => { console.log(“点击了”); }我们为button按钮添加了点击事件,在浏览器刷新的同时不停地对按钮进行点击操作(当然是手动点击)看下输出://null//null//(10次输出)点击了//ajax返回成功//{success: true, data: {…}}//定时器//点击了这样是不是可以理解为什么主线程要去循环扫描任务列队了?事件循环的每一轮称为一个tick(有没有联想到vue中的nextTick?)当产生用户交互(鼠标点击事件,页面滚动事件,窗口大小变化事件等等),ajax,定时器,计时器等,会向事件循环中的任务队列添加事件,然后等待执行,前端异步有哪些场景?定时任务:setTimeout,setInverval网络请求:ajax请求,img图片的动态加载事件绑定或者叫DOM事件,比如一个点击事件,我不知道它什么时候点,但是在它点击之前,我该干什么还是干什么。用addEventListener注册一个类型的事件的时候,浏览器会有一个单独的模块去接收这个东西,当事件被触发的时候,浏览器的某个模块,会把相应的函数扔到异步队列中,如果现在执行栈中是空的,就会直接执行这个函数。ES6中的Promise什么时候需要异步:在可能发生等待的情况等待过程中不能像alert一样阻塞程序的时候因此,所有的“等待的情况”都需要异步一句话总结就是需要等待但是又不能阻塞程序的时候需要使用异步异步和并行千万不要把异步和并行搞混了,异步是单线程的,并行是多线程的异步:主线程的任务以同步的方式执行完毕,才会去依次执行任务列队中的异步任务并行:两个或多个事件链随时间发展交替执行,以至于从更高的层次来看,就像是同时在运行(尽管在任意时刻只处理一个事件)原文链接参考链接关于js中的同步和异步异步和单线程——什么时候需要异步,前端使用异步的场景Javascript异步编程的4种方法 ...

January 10, 2019 · 1 min · jiezi

你与弄懂promise之间可能只差这篇文章(一)

promise诞生之前:因为JS引擎在执行js代码时只分配了一个线程去执行,所以Javascript是单线程的。由于有这个前置设定,前端er在书写代码时绕不开的一件事是就是—-如何处理异步,即处理“现在和稍后”关系的问题,事实上我们每一天都在与异步逻辑打交道。在promise出现之前,前端er基本上都是通过callback的方式来解决“稍后”的问题,例如有经典的“发布-订阅”模式,观察者模式,他们都运用了传入回调函数的高阶函数。vue2.x源码在实现数据双向绑定时就是运用的发布-订阅模式。我们先来看看三个例子。(例子均在node环境中运行, 其中name.txt中的内容是"kk", age.txt中的内容是10。)1 . 回调函数(callback)。fs读取文件的先后顺序是不固定的,我们无法判断哪个文件先读取完成。此例实现的是,在完全读取两个文件的内容之后进行某个操作(例如console个啥的)。let fs = require(‘fs’);let arr = [];let after = (times, cb) => { return (data) => { arr.push(data); if (–times === 0) { cb(arr) } }}let on = after(2, (arr) => { console.log(‘我是在全部读取了2个文件内容之后打印出来的, ‘, arr)})fs.readFile(’name.txt’, ‘utf8’, (err, data) => { on(data)})fs.readFile(‘age.txt’, ‘utf8’, (err, data) => { on(data)})结果: 我是在全部读取了2个文件内容之后打印出来的, [ ‘kk’, ‘10’ ]。说明: 这种写法的问题在于,需要依靠计数来执行回调函数里面的内容。我们先得这计算出有几个异步操作,然后统计出来在全部的异步操作完成后再执行回调。2 .发布-订阅模式。订阅的时候添加订阅者,发布的时候执行相应的订阅函数。此例实现的是,在特定的时候emit了某事件,订阅了该事件的回调函数继而执行。class EventEmitter { constructor () { this.subs = {} } on (eventName, cb) { if (!this.subs[eventName]) { this.subs[eventName] = [] } this.subs[eventName].push((…args) => cb(…args)) } emit (eventName, …args) { if (this.subs[eventName]) { this.subs[eventName].forEach(cb => cb(…args)) } else { throw Error(没有订阅${eventName}这个事件) } }}const event = new EventEmitter();let fs = require(‘fs’);event.on(‘kk-event’, (…args) => { fs.readFile(’name.txt’, ‘utf8’, (err, data) => { console.log(‘data1’, data, …args) })})event.on(‘kk-event’, (…args) => { fs.readFile(‘age.txt’, ‘utf8’, (err, data) => { console.log(‘data2’, data, …args) })})event.emit(‘kk-event’, 123, 456)结果:data1 kk 123 456data2 10 123 4563 . 观察者模式。它与发布-订阅两者本质是一样的,只不过观察者模式在写法上强调观察者和被观察者之间的关系,而发布-订阅模式则没有这样的关系。此例实现的是,在被观察者的状态发生变化后,观察者执行自己的update方法进行更新。 class Subject { constructor() { this.observers = []; this.state = ‘’; // 假设观察者观察的是被观察者的state } setState (status) { // 当state变化时出发观察者的update方法 this.state = status; this.notify(); } attach (observer) { this.observers.push(observer) // 与发布-订阅不同的是,这里添加的是一个个观察者实例,这就将被观察者和观察者之间关联了起来 } notify () { this.observers.forEach(observe => observe.update()) // 在被观察者状态变化时,调用更新的是观察者的update方法 }}class Observer { constructor (name, target) { this.name = name; this.target = target; } update () { console.log(通知${this.name},被观察者状态变化,所以观察者${this.name}跟着变化) }}let fs = require(‘fs’);let subject = new Subject();let observer1 = new Observer(‘kk1’, subject);let observer2 = new Observer(‘kk2’, subject);subject.attach(observer1);subject.attach(observer2);subject.setState(‘B’);结果:通知kk1,被观察者状态变化,所以观察者kk1跟着变化通知kk2,被观察者状态变化,所以观察者kk2跟着变化 ...

January 8, 2019 · 2 min · jiezi

解析Angularjs的$http异步删除数据及实例

这篇文章主要介绍了Angularjs的$http异步删除数据详解及实例的相关资料,这里提供实现思路及实现具体的方法,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。Angularjs的$http异步删除数据详解及实例有人会说删除这东西有什么可讲的,写个删除的service,controller调用一下不就完了。嗯…看起来是这样,但是具体实现起来真的有这么简单吗?首先有以下几个坑怎么确定数据是否删除成功?怎么同步视图的数据库的内容?1.思路1.实现方式一删除数据库中对应的内容,然后将$scope中的对应的内容splice2.实现方式二删除数据库中对应的内容,然后再reload一下数据(也就是再调用一次查询方法,这种消耗可想而知,并且还要保证先删除数据再查询)2.具体实现方式删除数据的service:用异步,返回promiseservice(‘deleteBlogService’,//删除博客 [’$rootScope’, ‘$http’, ‘$q’, function ($rootScope, $http, $q) { var result = {}; result.operate = function (blogId) { var deferred = $q.defer(); $http({ headers: { ‘Content-Type’: ‘application/x-www-form-urlencoded;charset=UTF-8’ },//欢迎加入前端全栈开发交流圈一起学习交流:864305860 url: $rootScope.$baseUrl + “/admin/blog/deleteBlogById”, method: ‘GET’, dataType: ‘json’, params: { id: blogId } }) .success(function (data) { deferred.resolve(data); console.log(“删除成功!”); }) .error(function () { deferred.reject(); alert(“删除失败!”) }); return deferred.promise; }; return result; }])//欢迎加入前端全栈开发交流圈一起学习交流:864305860controller里面注意事项要特别注意执行顺序:确保己经删除完成之后再去reload数据,不然会出来视图不更新/** * 删除博客 */ $scope.deleteBlog = function (blogId) { var deletePromise = deleteBlogService.operate(blogId); deletePromise.then(function (data) { if (data.status == 200) { var promise = getBlogListService.operate($scope.currentPage); promise.then(function (data) { $scope.blogs = data.blogs; $scope.pageCount = $scope.blogs.totalPages; });//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 }//面向1-3年前端人员 });//帮助突破技术瓶颈,提升思维能力 };结语感谢您的观看,如有不足之处,欢迎批评指正。 ...

December 17, 2018 · 1 min · jiezi