JavaScript特性
JavaScript属于单线程语言,即在同一时间,只能执行一个任务。在执行任务时,所有任务需要排队,前一个任务结束,才会执行后一个任务。
当我们向后台发送一个请求时,主线程读取 “向后台发送请求” 这个事件并执行之后,到获取后台返回的数据这一过程会有段时间间隔,这时CPU处于空闲阶段,直到获取数据后再继续执行后面的任务,这就降低了用户体验度,使得页面加载变慢。于是,所有任务可以分成两种:同步任务和异步任务。
- 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
- 异步任务:不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制,这个过程会不断重复。"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
JavaScript异步实现的5种方式
1. callback(回调函数)
回调函数,也被称为高阶函数,是一个被作为参数传递给另一个函数并在该函数中被调用的函数。看一个在JQuery中简单普遍的例子:
// 注意: click方法是一个函数而不是变量
$("#button").click(function() {
alert("Button Clicked");
});
可以看到,上述例子将一个函数作为参数传递给了click
方法,click
方法会调用该函数,这是JavaScript中回调函数的典型用法,它在jQuery
中广泛被使用。它不会立即执行,因为我们没有在后面加( ),而是在点击事件发生时才会执行。
比如,我们要下载一个gif,但是不希望在下载的时候阻断其他程序,可以实现如下:
downloadPhoto('http://coolcats.com/cat.gif', handlePhoto)
function handlePhoto (error, photo) {
if (error) {
console.error('Download error!', error);
} else {
console.log('Download finished', photo);
}
}
console.log('Download started')
首先声明handlePhoto
函数,然后调用downloadPhoto
函数并传递handlePhoto
作为其回调函数,最后打印出“Download started”。
请注意,handlePhoto
尚未被调用,它只是被创建并作为回调传入downloadPhoto
。但直到downloadPhoto
完成其任务后才能运行,这可能需要很长时间,具体取决于Internet
连接的速度,所以运行代码后,会先打印出Download started
。
这个例子是为了说明两个重要的概念:
-
handlePhoto
回调只是稍后存储一些事情的一种方式; - 事情发生的顺序不是从顶部到底部读取,而是基于事情完成时跳转;
1. callback hell(回调地狱)
var fs = require('fs');
/**
* 如果三个异步api操作的话 无法保证他们的执行顺序
* 我们在每个操作后用回调函数就可以保证执行顺序
*/
fs.readFile('./data1.json', 'utf8', function(err, data){
if (err) {
throw err;
} else {
console.log(data);
fs.readFile('./data2.json', 'utf8', function(err, data){
if (err) {
throw err;
} else {
console.log(data)
fs.readFile('./data3.json', 'utf8', function(err, data){
if (err) {
throw err;
} else {
console.log(data);
}
})
}
})
}
})
有没有看到这些以"})"结尾的金字塔结构?由于回调函数是异步的,在上面的代码中每一层的回调函数都需要依赖上一层的回调执行完,所以形成了层层嵌套的关系最终形成类似上面的回调地狱。
2. 代码层面解决回调地狱
1. 保持代码简短
var form = document.querySelector('form')
form.onsubmit = function formSubmit (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
})
}
可以看到,上面的代码给两个函数加了描述性功能名称,使代码更容易阅读,当发生异常时,你将获得引用实际函数名称而不是“匿名”的堆栈跟踪。
现在我们可以将这些功能移到我们程序的顶层:
document.querySelector('form').onsubmit = formSubmit;
function formSubmit (submitEvent) {
var name = document.querySelector('input').value;
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, postResponse);
}
function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status');
if (err) return statusMessage.value = err;
statusMessage.value = body;
}
重新整改代码结构之后,可以清晰的看到这段函数的功能。
2. 模块化
从上面取出样板代码,并将其分成几个文件,将其转换为模块。
这是一个名为formuploader.js
的新文件,它包含了之前的两个函数:
module.exports.submit = formSubmit;
function formSubmit (submitEvent) {
var name = document.querySelector('input').value;
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, postResponse)
}
function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status');
if (err) return statusMessage.value = err;
statusMessage.value = body;
}
把它们exports
后,在应用程序中引入并使用,这就使得代码更加简洁易懂了:
var formUploader = require('formuploader');
document.querySelector('form').onsubmit = formUploader.submit;
3. error first
处理每一处错误,并且回调的第一个参数始终保留用于错误:
var fs = require('fs')
fs.readFile('/Does/not/exist', handleFile);
function handleFile (error, file) {
if (error) return console.error('Uhoh, there was an error', error);
// otherwise, continue on and use `file` in your code;
}
有第一个参数是错误是一个简单的惯例,鼓励你记住处理你的错误。如果它是第二个参数,会更容易忽略错误。
除了上述代码层面的解决方法,还可以使用以下更高级的方法,也是另外4种实现异步的方法。但是请记住,回调是JavaScript的基本组成部分(因为它们只是函数),在学习更先进的语言特性之前学习如何读写它们,因为它们都依赖于对回调。
2. 发布订阅模式
订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心(顺带上下文),由调度中心统一调度订阅者注册到调度中心的处理代码。
比如有个界面是实时显示天气,它就订阅天气事件(注册到调度中心,包括处理程序),当天气变化时(定时获取数据),就作为发布者发布天气信息到调度中心,调度中心就调度订阅者的天气处理程序。简单来说,发布订阅模式,有一个事件池,用来给你订阅(注册)事件,当你订阅的事件发生时就会通知你,然后你就可以去处理此事件。
使用发布订阅模式,来修改Ajax
:
xhr.onreadystatechange = function () {//监听事件
if (this.readyState === 4) {
if (this.status === 200) {
switch (dataType) {
case 'json': {
Event.emit('data '+method,JSON.parse(this.responseText)); //触发事件
break;
}
case 'text': {
Event.emit('data '+method,this.responseText);
break;
}
case 'xml': {
Event.emit('data '+method,this.responseXML);
break;
}
default: {
break;
}
}
}
}
}
3. Promise
ES6
将Promise
写进了语言标准,统一了用法,原生提供了Promise
对象。Promise
,简单说就是一个容器,里面保存着一个异步操作的结果。从语法上说,Promise
是一个对象,从它可以获取异步操作的消息。
Promise
有3种状态:pending
(进行中)、fulfilled
(成功)、rejected
(失败)。
Promise
很重要的两个特点:
- 状态不受外界影响;只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果;
Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为resolved
(已定型)。
1. 基本用法
const p = new Promise((resolve,reject) => {
// resolve在异步操作成功时调用
resolve('success');
// reject在异步操作失败时调用
reject('error');
});
p.then(result => {
console.log(result);
});
p.catch(result => {
console.log(result);
})
ES6
规定,Promise
对象是一个构造函数,用来生成Promise
实例。new
一个Promise
实例时,这个对象的起始状态就是Pending
状态,再根据resolve
或reject
返回Fulfilled
状态 / Rejected
状态。
2. Promise.prototype.then( )
前面可以看到,Promise
实例具有then
方法,所以then
方法是定义在原型对象Promise.prototype
上的,它的作用是为Promise
实例添加状态改变时的回调函数。
then
方法返回的是一个新的Promise
实例,因此then
可以采用链式写法:
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
3. Promise.prototype.catch( )
Promise.prototype.catch
方法是.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
4. Promise.all( )
Promise.all
方法用于将多个Promise
实例,包装成一个新的Promise
实例。
const p = Promise.all([p1, p2, p3]);
上面代码中,p
的状态由p1
、p2
、p3
决定,分成两种情况:
- 只有
p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p的回调函数。 - 只要
p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
5. Promise.race( )
Promise.race
方法同样是将多个Promise
实例,包装成一个新的Promise
实例。不同的是,race()
接受的对象中,哪个对象返回快就返回哪个对象,如果指定时间内没有获得结果,就将Promise
的状态变为reject
。
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p
.then(console.log)
.catch(console.error);
上面代码中,如果 5 秒之内fetch
方法无法返回结果,变量p
的状态就会变为rejected
,从而触发catch
方法指定的回调函数。
6. Promise.resolve( )
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
7. Promise.reject( )
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
});
// 出错了
下面是一个用Promise
对象实现的Ajax
操作的例子:
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
8. callbackify & promisify
Node 8
提供了两个工具函数util.promisify
、util.callbackify
用于在回调函数和Promise
之间做方便的切换,我们也可以用JavaScript
代码来实现一下。
1. promisify:把callback转化为promise
function promisify(fn_callback) { //接收一个有回调函数的函数,回调函数一般在最后一个参数
if(typeof fn_callback !== 'function') throw new Error('The argument must be of type Function.');
//返回一个函数
return function (...args) {
//返回Promise对象
return new Promise((resolve, reject) => {
try {
if(args.length > fn_callback.length) reject(new Error('arguments too much.'));
fn_callback.call(this,...args,function (...args) {
//nodejs的回调,第一个参数为err, Error对象
args[0] && args[0] instanceof Error && reject(args[0]);
//除去undefined,null参数
args = args.filter(v => v !== undefined && v !== null);
resolve(args);
}.bind(this)); //保证this还是原来的this
} catch (e) {
reject(e)
}
})
}
}
2. callbackify:promise转换为callback
function callbackify(fn_promise) {
if(typeof fn_promise !== 'function') throw new Error('The argument must be of type Function.');
return function (...args) {
//返回一个函数 最后一个参数是回调
let callback = args.pop();
if(typeof callback !== 'function') throw new Error('The last argument must be of type Function.');
if(fn_promise() instanceof Promise){
fn_promise(args).then(data => {
//回调执行
callback(null,data)
}).catch(err => {
//回调执行
callback(err,null)
})
}else{
throw new Error('function must be return a Promise object');
}
}
}
个人而言,最好直接把代码改成promise
形式的,而不是对已有的callback
加上这个中间层,因为其实改动的成本差不多。但总有各种各样的情况,比如,你的回调函数已经有很多地方使用了,牵一发而动全身,这时这个中间层还是比较有用的。
4. generator(生成器)函数
Generator
函数是ES6
提供的一种异步编程解决方案,通过yield
标识位和next()
方法调用,实现函数的分段执行。
1. next( )方法
先从下面的例子看一下Generator
函数是怎么定义和运行的。
function *gen() {
yield "hello";
yield "generator";
return;
}
gen(); // 没有输出结果
var g = gen();
console.log(g.next()); // { value: 'hello', done: false }
console.log(g.next()); // { value: 'generator', done: false }
console.log(g.next()); // { value: 'undefined', done: true }
从上面可以看到,Generator
函数定义时要带*
,在直接执行gen()
时,没有像普通的函数一样,输出结果,而是通过调用next()
方法得到了结果。
这个例子中我们引入了yield
关键字,分析下这个执行过程:
- 创建了
g
对象,指向gen
的句柄 - 第一次调用
next()
,执行到yield hello
,暂缓执行,并返回了hello
- 第二次调用
next()
,继续上一次的执行,执行到yield generator
,暂缓执行,并返回了generator
- 第三次调用
next()
,直接执行return
,并返回done:true
,表明结束。
经过上面的分析,yield
实际就是暂缓执行的标示,每执行一次next()
,相当于指针移动到下一个yield
位置。next()
方法返回的结果是个对象,对象里面的value
是运行结果,done
表示是否运行完成。
2. throw( )方法
throw()
方法在函数体外抛出一个错误,然后在函数体内捕获。
function *gen1() {
try{
yield;
} catch(e) {
console.log('内部捕获')
}
}
let g1 = gen1();
g1.next();
g1.throw(new Error());
3. return( )方法
return()
方法返回给定值,并终结生成器,在return
后面的yield
不会再被执行。
function *gen2(){
yield 1;
yield 2;
yield 3;
}
let g2 = gen2();
g2.next(); // { value:1, done:false }
g2.return(); // { value:undefined, done:true }
g2.next(); // { value:undefined, done:true }
5. Promise + async & await
在ES2017
中,提供了async / await
两个关键字来实现异步,是异步编程的最高境界,就是根本不用关心它是否是异步,很多人认为它是异步编程的终极解决方案。async / await
寄生于Promise
,本质上还是基于Generator
函数,可以说是Generator
函数的语法糖,async
用于申明一个function
是异步的,而await
可以认为是async wait
的简写,等待一个异步方法执行完成。
async function demo() {
let result = await Promise.resolve(123);
console.log(result);
}
demo();
async
函数返回的是一个Promise
对象,在上述例子中,表示demo
是一个async
函数,await
只能用在async
函数里面,表示等待Promise
返回结果后,再继续执行,await
后面应该跟着Promise
对象(当然,跟着其他返回值也没关系,只是会立即执行,这样就没有意义了)。
Promise
虽然一方面解决了callback
的回调地狱,但是相对的把回调 “纵向发展” 了,形成了一个回调链:
function sleep(wait) {
return new Promise((res,rej) => {
setTimeout(() => {
res(wait);
},wait);
});
}
/*
let p1 = sleep(100);
let p2 = sleep(200);
let p =*/
sleep(100).then(result => {
return sleep(result + 100);
}).then(result02 => {
return sleep(result02 + 100);
}).then(result03 => {
console.log(result03);
})
将上述代码改成async/await
写法:
async function demo() {
let result01 = await sleep(100);
//上一个await执行之后才会执行下一句
let result02 = await sleep(result01 + 100);
let result03 = await sleep(result02 + 100);
// console.log(result03);
return result03;
}
demo().then(result => {
console.log(result);
});
因为async
返回的也是promise
对象,所以用then
接收就行了。
如果是reject
状态,可以用try-catch
捕捉:
let p = new Promise((resolve,reject) => {
setTimeout(() => {
reject('error');
},1000);
});
async function demo(params) {
try {
let result = await p;
} catch(e) {
console.log(e);
}
}
demo();
这是基本的错误处理,但是当内部出现一些错误时,和Promise
有点类似,demo()
函数不会报错,还是需要catch
回调捕捉,这就是内部的错误被 “静默” 处理了。
let p = new Promise((resolve,reject) => {
setTimeout(() => {
reject('error');
},1000);
});
async function demo(params) {
// try {
let result = name;
// } catch(e) {
// console.log(e);
// }
}
demo().catch((err) => {
console.log(err);
})
最后,总结一下JavaScript
实现异步的5种方式的优缺点:
- 回调函数:写起来方便,但是过多的回调会产生回调地狱,代码横向扩展,不易于维护和理解。
- 发布订阅模式:方便管理和修改事件,不同的事件对应不同的回调,但是容易产生一些命名冲突的问题,事件到处触发,可能代码可读性不好。
-
Promise
对象:通过then
方法来替代掉回调,解决了回调产生的参数不容易确定的问题,但是相对的把回调 “纵向发展” 了,形成了一个回调链。 -
Generator
函数:确实很好的解决了JavaScript
中异步的问题,但是得依赖执行器函数。 -
async/await
:这可能是javascript
中,解决异步的最好的方式了,让异步代码写起来跟同步代码一样,可读性和维护性都上来了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。