什么是异步
JS 为何会有异步
首先记住一句话 —— JS 是单线程的语言,所谓“单线程”就是一根筋,对于拿到的程序,一行一行的执行,上面的执行为完成,就傻傻的等着。例如
var i, t = Date.now()
for (i = 0; i < 100000000; i++) {
}
console.log(Date.now() - t) // 308 (chrome浏览器)
上面的程序花费 308ms 的时间执行完成,执行过程中就会有卡顿,其他的事儿就先撂一边不管了。
执行程序这样没有问题,但是对于 JS 最初使用的环境 ———— 浏览器客户端 ———— 就不一样了。因此在浏览器端运行的 js ,可能会有大量的网络请求,而一个网络资源啥时候返回,这个时间是不可预估的。这种情况也要傻傻的等着、卡顿着、啥都不做吗?———— 那肯定不行。
因此,JS 对于这种场景就设计了异步 ———— 即,发起一个网络请求,就先不管这边了,先干其他事儿,网络请求啥时候返回结果,到时候再说。这样就能保证一个网页的流程运行。
异步的实现原理
先看一段比较常见的代码
var ajax = $.ajax({
url: '/data/data1.json',
success: function () {
console.log('success')
}
})
上面代码中$.ajax()
需要传入两个参数进去,url和success,其中url是请求的路由,success是一个函数。这个函数传递过去不会立即执行,而是等着请求成功之后才能执行。对于这种传递过去不执行,等出来结果之后再执行的函数,叫做callback,即回调函数
再看一段更加能说明回调函数的 nodejs 代码。和上面代码基本一样,唯一区别就是:上面代码时网络请求,而下面代码时 IO 操作。
var fs = require(‘fs’)
fs.readFile(‘data1.json’, (err, data) => {
console.log(data.toString())
})
从上面两个 demo 看来,实现异步的最核心原理,就是将callback作为参数传递给异步执行函数,当有结果返回之后再触发 callback执行,就是如此简单!
常用的异步操作
开发中比较常用的异步操作有:
- 网络请求,如ajax http.get
- IO 操作,如readFile readdir
- 定时函数,如setTimeout setInterval
jQuery-1.5 之后的 ajax
$.ajax
这个函数各位应该都比较熟悉了,要完整的讲解 js 的异步操作,就必须先从$.ajax这个方法说起。
传统的$.ajax
先来一段最常见的$.ajax的代码,当然是使用万恶的callback方式
var ajax = $.ajax({
url: 'data.json',
success: function () {
console.log('success')
},
error: function () {
console.log('error')
}
})
console.log(ajax) // 返回一个 XHR 对象
至于这么做会产生什么样子的诟病,我想大家应该都很明白了。不明白的自己私下去查,但是你也可以继续往下看,你只需要记住这样做很不好就是了,要不然 jquery 也不会再后面进行改进
1.5 版本之后的$.ajax
但是从v1.5开始,以上代码就可以这样写了:可以链式的执行done或者fail方法
var ajax = $.ajax('data.json')
ajax.done(function () {
console.log('success 1')
})
.fail(function () {
console.log('error')
})
.done(function () {
console.log('success 2')
})
console.log(ajax) // 返回一个 deferred 对象
大家注意看以上两段代码中都有一个console.log(ajax),但是返回值是完全不一样的。
v1.5之前,返回的是一个XHR对象,这个对象不可能有done或者fail的方法的
v1.5开始,返回一个deferred对象,这个对象就带有done和fail的方法,并且是等着请求返回之后再去调用
改进之后的好处
这是一个标志性的改造,不管这个概念是谁最先提出的,它在 jquery 中首先大量使用并让全球开发者都知道原来 ajax 请求还可以这样写。这为以后的Promise标准制定提供了很大意义的参考,你可以以为这就是后面Promise的原型。
记住一句话————虽然 JS 是异步执行的语言,但是人的思维是同步的————因此,开发者总是在寻求如何使用逻辑上看似同步的代码来完成 JS 的异步请求。而 jquery 的这一次更新,让开发者在一定程度上得到了这样的好处。
之前无论是什么操作,我都需要一股脑写到callback中,现在不用了。现在成功了就写到done中,失败了就写到fail中,如果成功了有多个步骤的操作,那我就写很多个done,然后链式连接起来就 OK 了。
和后来的Promise的关系
以上的这段代码,我们还可以这样写。即不用done和fail函数,而是用then函数。then函数的第一个参数是成功之后执行的函数(即之前的done),第二个参数是失败之后执行的函数(即之前的fail)。而且then函数还可以链式连接。
var ajax = $.ajax('data.json')
ajax.then(function () {
console.log('success 1')
}, function () {
console.log('error 1')
})
.then(function () {
console.log('success 2')
}, function () {
console.log('error 2')
})
jQuery deferred
写一个传统的异步操作
给出一段非常简单的异步操作代码,使用setTimeout函数。
var wait = function () {
var task = function () {
console.log('执行完成')
}
setTimeout(task, 2000)
}
wait()
以上这些代码执行的结果大家应该都比较明确了,即 2s 之后打印出执行完成。但是我如果再加一个需求 ———— 要在执行完成之后进行某些特别复杂的操作,代码可能会很多,而且分好几个步骤 ———— 那该怎么办? 大家思考一下!
如果你不看下面的内容,而且目前还没有Promise的这个思维,那估计你会说:直接在task函数中写就是了!不过相信你看完下面的内容之后,会放弃你现在的想法。
使用$.Deferred封装
好,接下来我们让刚才简单的几行代码变得更加复杂。为何要变得更加复杂?是因为让以后更加复杂的地方变得简单。这里我们使用了 jquery 的$.Deferred
,至于这个是个什么鬼,大家先不用关心,只需要知道$.Deferred()
会返回一个deferred对象,先看代码,deferred对象的作用我们会面会说。
function waitHandle() {
var dtd = $.Deferred() // 创建一个 deferred 对象
var wait = function (dtd) { // 要求传入一个 deferred 对象
var task = function () {
console.log('执行完成')
dtd.resolve() // 表示异步任务已经完成
}
setTimeout(task, 2000)
return dtd // 要求返回 deferred 对象
}
// 注意,这里一定要有返回值
return wait(dtd)
}
以上代码中,又使用一个waitHandle方法对wait方法进行再次的封装。waitHandle内部代码,我们分步骤来分析。跟着我的节奏慢慢来,保证你不会乱。
- 使用var dtd = $.Deferred()创建deferred对象。通过上一节我们知道,一个deferred对象会有done fail和then方法(不明白的去看上一节)
- 重新定义wait函数,但是:第一,要传入一个deferred对象(dtd参数);第二,当task函数(即callback)执行完成之后,要执行dtd.resolve()告诉传入的deferred对象,革命已经成功。第三;将这个deferred对象返回。
- 返回wait(dtd)的执行结果。因为wait函数中返回的是一个deferred对象(dtd参数),因此wait(dtd)返回的就是dtd————如果你感觉这里很乱,没关系,慢慢捋,一行一行看,相信两三分钟就能捋顺!
最后总结一下,waitHandle函数最终return wait(dtd)即最终返回dtd(一个deferred)对象。针对一个deferred对象,它有done fail和then方法(上一节说过),它还有resolve()方法(其实和resolve相对的还有一个reject方法,后面会提到)
应用then方法
接着上面的代码继续写
var w = waitHandle()
w.then(function () {
console.log('ok 1')
}, function () {
console.log('err 1')
}).then(function () {
console.log('ok 2')
}, function () {
console.log('err 2')
})
上面已经说过,waitHandle函数最终返回一个deferred对象,而deferred对象具有done fail then方法,现在我们正在使用的是then方法。至于then方法的作用,我们上一节已经讲过了,不明白的同学抓紧回去补课。
执行这段代码,我们打印出来以下结果。可以将结果对标以下代码时哪一行。
执行完成
ok 1
ok 2
此时,你再回头想想我刚才说提出的需求(要在执行完成之后进行某些特别复杂的操作,代码可能会很多,而且分好几个步骤),是不是有更好的解决方案了?
有同学肯定发现了,代码中console.log('err 1')和console.log('err 2')
什么时候会执行呢 ———— 你自己把waitHandle函数中的dtd.resolve()
改成dtd.reject()
试一下就知道了。
dtd.resolve()
表示革命已经成功,会触发then中第一个参数(函数)的执行,dtd.reject()
表示革命失败了,会触发then中第二个参数(函数)执行
总结一下一个deferred对象具有的函数属性,并分为两组:
- dtd.resolve dtd.reject
- dtd.then dtd.done dtd.fail
我为何要分成两组 ———— 这两组函数,从设计到执行之后的效果是完全不一样的。第一组是主动触发用来改变状态(成功或者失败),第二组是状态变化之后才会触发的监听函数。
jQuery promise
返回promise
我们对上一节的的代码做一点小小的改动,只改动了一行,下面注释。
function waitHandle() {
var dtd = $.Deferred()
var wait = function (dtd) {
var task = function () {
console.log('执行完成')
dtd.resolve()
}
setTimeout(task, 2000)
return dtd.promise() // 注意,这里返回的是 primise 而不是直接返回 deferred 对象
}
return wait(dtd)
}
var w = waitHandle() // 经过上面的改动,w 接收的就是一个 promise 对象
$.when(w)
.then(function () {
console.log('ok 1')
})
.then(function () {
console.log('ok 2')
})
改动的一行在这里return dtd.promise(),之前是return dtd。dtd是一个deferred对象,而dtd.promise就是一个promise对象。
promise对象和deferred对象最重要的区别,记住了————promise对象相比于deferred对象,缺少了.resolve和.reject这俩函数属性。这么一来,可就完全不一样了。
上一节我们提到一个问题,就是在程序的最后一行加一句w.reject()
会导致乱套,你现在再在最后一行加w.reject()
试试 ———— 保证乱套不了 ———— 而是你的程序不能执行,直接报错。因为,w是promise对象,不具备.reject
属性。
返回promise的好处
上一节提到deferred对象有两组属性函数,而且提到应该把这两组彻底分开。现在通过上面一行代码的改动,就分开了。
- waitHandle函数内部,使用
dtd.resolve()
来该表状态,做主动的修改操作 - waitHandle最终返回promise对象,只能去被动监听变化(then函数),而不能去主动修改操作
一个“主动”一个“被动”,完全分开了。
promise 的概念
jquery v1.5 版本发布时间距离现在已经老早之前了,那会儿大家网页标配都是 jquery 。无论里面的deferred和promise这个概念和想法最早是哪位提出来的,但是最早展示给全世界开发者的是 jquery ,这算是Promise这一概念最先的提出者。
其实本次课程主要是给大家分析 ES6 的Promise Generator和async-await,但是为何要从 jquery 开始(大家现在用 jquery 越来越少)?就是要给大家展示一下这段历史的一些起点和发展的知识。有了这些基础,你再去接受最新的概念会非常容易,因为所有的东西都是从最初顺其自然发展进化而来的,我们要去用一个发展进化的眼光学习知识,而不是死记硬背。
Promise 加入 ES6 标准
写一段传统的异步操作
还是拿之前讲 jquery deferred对象时的那段setTimeout程序
var wait = function () {
var task = function () {
console.log('执行完成')
}
setTimeout(task, 2000)
}
wait()
之前我们使用 jquery 封装的,接下来将使用 ES6 的Promise进行封装,大家注意看有何不同。
用Promise进行封装
const wait = function () {
// 定义一个 promise 对象
const promise = new Promise((resolve, reject) => {
// 将之前的异步操作,包括到这个 new Promise 函数之内
const task = function () {
console.log('执行完成')
resolve() // callback 中去执行 resolve 或者 reject
}
setTimeout(task, 2000)
})
// 返回 promise 对象
return promise
}
注意看看程序中的注释,那都是重点部分。从整体看来,感觉这次比用 jquery 那次简单一些,逻辑上也更加清晰一些。
将之前的异步操作那几行程序,用new Promise((resolve,reject) => {.....})
包装起来,最后return即可
异步操作的内部,在callback中执行resolve()(表明成功了,失败的话执行reject)
接着上面的程序继续往下写。wait()返回的肯定是一个promise对象,而promise对象有then属性。
const w = wait()
w.then(() => {
console.log('ok 1')
}, () => {
console.log('err 1')
}).then(() => {
console.log('ok 2')
}, () => {
console.log('err 2')
})
then还是和之前一样,接收两个参数(函数),第一个在成功时(触发resolve)执行,第二个在失败时(触发reject)时执行。而且,then还可以进行链式操作。
以上就是 ES6 的Promise的基本使用演示。看完你可能会觉得,这跟之前讲述 jquery 的不差不多吗 ———— 对了,这就是我要在之前先讲 jquery 的原因,让你感觉一篇一篇看起来如丝般顺滑!
Promise 在 ES6 中的具体应用
Promise封装
因为以下所有的代码都会用到Promise,因此干脆在所有介绍之前,先封装一个Promise,封装一次,为下面多次应用。
const fs = require('fs')
const path = require('path') // 后面获取文件路径时候会用到
const readFilePromise = function (fileName) {
return new Promise((resolve, reject) => {
fs.readFile(fileName, (err, data) => {
if (err) {
reject(err) // 注意,这里执行 reject 是传递了参数,后面会有地方接收到这个参数
} else {
resolve(data.toString()) // 注意,这里执行 resolve 时传递了参数,后面会有地方接收到这个参数
}
})
})
}
以上代码一个一段 nodejs 代码,将读取文件的函数fs.readFile封装为一个Promise。经过上一节的学习,我想大家肯定都能看明白代码的含义,要是看不明白,你就需要回炉重造了!