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. PromiseES6将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 & promisifyNode 8提供了两个工具函数util.promisify、util.callbackify用于在回调函数和Promise之间做方便的切换,我们也可以用JavaScript代码来实现一下。1. promisify:把callback转化为promisefunction 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转换为callbackfunction 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中,解决异步的最好的方式了,让异步代码写起来跟同步代码一样,可读性和维护性都上来了。
...