异步,有一个从丑小鸭变成白天鹅的故事
JS中异步存在的必要性
丑小鸭在孤独和凌乱的时候常常在想:“我是从哪里来?为什么我和别鸭(同步任务)不一样?”
为了解决上述的问题,JS的设计者改变了下方案。我们可以先让这些执行时间漫长,需要等待的任务在主线程中挂起,由任务对应的线程(事件触发线程、定时触发器线程、异步http请求线程等)先处理着,主线程先去执行后面的任务,等到这些被挂起的任务,有了结果后,所属的线程再回过头来通知主线程这些任务可以执行了。
这也正是异步的起初与概念的根本。那么处理异步任务的线程,怎么与主线程之间建立连接,告诉主线程可以开始执行这个异步任务了呢?
这件事情只有一等公民函数可以完成,处理异步任务的线程将异步任务的回调函数放入任务队列中去排队,等待主线程去获取并执行。
回调函数就是实现异步编程起初与根本。它解决了同步的阻塞问题,但同时也带来了很多其他的问题。
下面我们详尽的讲述一下由回调函数衍生而来的异步发展史。
异步发展历程概述
异步发展的历程好比丑小鸭变白天鹅的故事。它的几个阶段想必你已经滚瓜烂熟:回调函数、promise、生成器函数(generator)& co、async/await。
为了避免俗套,我们不去赘述上述四个历程的使用。而是去分别比对一下每一个阶段究竟比上一个阶段美在哪里,为什么说是一种进步,又有哪些要注意的坑。
-
回调函数
首先什么是回调函数,他又为什么会被嫌弃?
回调本质上是一种设计模式,一个函数A当做参数传递给函数B,在B的内部去执行函数A。这时候函数A就被称作回调函数。
从概念我们可以看出,回调函数实现的方式实际就是函数嵌套函数,那么当代码逻辑相对复杂的时候,难免会出现多层嵌套的问题。
下面我们用一个小球碰撞的例子来说明一下回调函数的问题,为了方便各个历程的对比,这个例子也将贯穿全文。
假设我们的页面上现在有三个小球,均处于页面的最左端,我们依次将他们移动到页面的指定位置。html代码片段:
<div id="box"> <div id="ball-1"></div> <div id="ball-2"></div> <div id="ball-3"></div> </div> 复制代码
css 代码片段:
#box div{ width: 100px; height: 100px; background: red; border-radius: 50%; position: relative; margin-top: 10px; left: 0; } 复制代码
callback方式实现交互的代码:
// 小球运动的基本方法 function move(ele, position) { return new Promise((resolve, reject) => { let left = 0; let timer = setInterval(() => { left += 5; if (left >= position) { clearInterval(timer); ele.style.transform = `translateX(${position}px)`; resolve(); } else { ele.style.transform = `translateX(${left}px)`; } }, 15); }) } // 回调函数方式实现 move(ball1,500,function () { move(ball2, 500, function () { move(ball3, 500, function () { console.log('终于挪动完了'); }) }) }) 复制代码
仅仅是挪动3个小球,我们就要嵌套3层,那要是100个、1000个呢?
层层的嵌套让代码的可读性和可维护性差。每一步的异常捕获也只能在对应的回调函数中进行,很容易被漏掉。
额!人家真的有那么丑嘛~被你们这么嫌弃~~
-
promise
哇塞!终于可以变美一点了。在promise的世界里,可以通过then的链式调用来解决回调地狱的问题,我们可以把层层的嵌套展平来写,使代码的逻辑更为清晰。
promise的catch方法轻松的解决了回调函数异常处理难得问题。
来面镜子吧!我们看看到底有没有变美那么一点点。
// promise方式实现 move(ball1,500).then(data=>{ return move(ball2,500) }).then(data=>{ return move(ball3,200); }).then(data=>{ console.log('终于挪动完了'); }) 复制代码
确实是变美了那么一点,但是promise为什么可以实现链式调用呢?它靠的是什么?其实很简单,就是then中返回另一个 promise 即可。这与jquery中链式调用返回this是一个道理。
又由于promise具有值穿透的特性,所以我们可以在所有then的最后,加上一个catch方法,来捕获前面then执行发生的异常及其错误的情况。其实promise中catch方法本质上就是省略了onFulfilled函数的then方法。
这里我们不去详细论述promise的原理,后面我会写一篇关于promise实现的文章来重点分析这块。
我们说promise有哪些缺点呢?我觉得唯一的缺点就是长得太像异步,和同步调用的习惯还是相差有点远。
这个时候generator悄悄的登场了。
-
generator & co
generator从定义形式相比于promise更像是同步。执行generator函数它不会立即执行内部的代码,而是返回一个遍历器(指向内部状态的指针对象)。
我们要想真正的执行函数里面的代码,需要调用遍历器对象的next方法,使得指针移向下一个状态。让函数逐步的执行。换言之,我们可以使用next方法控制函数的执行了。
通常情况下generator函数需要配合co库来使用。所谓co,就是借助于Promise,让你可以使用更加优雅的方式编写非阻塞代码。
我们先看下通过generator函数实现小球运动的例子吧!// 这里的co是我们自己实现的一个简单的co逻辑 function co(it) { return new Promise((resolve, reject) => { function next(data) { let { done, value } = it.next(data); if (done) return resolve(value); value.then(data => { next(data); }, reject); } next(); }) } // 小球动起来 function* m() { yield move(ball1, 500); yield move(ball2, 500); yield move(ball3, 500); } co(m()).then(data => { console.log('终于挪动完了'); }) 复制代码
我们发现generator是很接近同步的写法了,但是它需要别人他辅助它来执行,这事就变得不完美了。是不是总感觉差点什么?
这个时候generator & co 结合的语法糖出现了--async/await。 -
async/await
async函数是对 Generator函数的改进,实际上就是把Generator自动执行给封装起来,同时返回的是 Promise 对象更便于操作。
这个封装让js在异步发展的慢慢长路中一下子变得完美了。既在写法上接近了同步的外表,又在功能上实现了异步。终于不用再层层嵌套,不用在需要其他库的辅助。async function m() { await move(ball1, 500); await move(ball2, 500); await move(ball3, 500); } m().then(data=>{ console.log('终于挪动完了'); }); 复制代码