平时开发常常会用到js异步编程,因为前端展现页面都是基于网络机顶盒(IPTV的通常性能不太好,OTT较好),目前公司主要采起的异步编程的方式有setTimeout、setInterval、requestAnimationFrame、ajax,为何会用到异步呢,就拿业务来讲,若前端所有采起同步的方式,那加载图片、生成dom、网络数据请求都会大大增长页面渲染时长。前端
JS 是单线程运行的,这意味着两段代码不能同时运行,而是必须逐步地运行,因此在同步代码执行过程当中,异步代码是不执行的。只有等同步代码执行结束后,异步代码才会被添加到事件队列中。node
这里就涉及到执行栈和任务队列:web
同步代码是依次存放在执行栈中,遵循LIFO原则;ajax
异步代码存放在任务队列中,任务队列又分宏任务和微任务(微任务执行优先级高于宏任务),遵循FIFO原则;chrome
请看下面代码执行的顺序(能够先思考一下看看与正确输出顺序是否一致)编程
function foo(){
console.log('start...');
return bar();
}
function bar(){
console.log('bar...');
}
//这里采用ES6的箭头函数、Promise函数
var promise = new Promise(function(resolve,reject){
console.log('promise...');
resolve();
});
promise.then(()=>console.log('promise resolve...'));
setTimeout(()=>console.log('timeout...'),0);
foo()
console.log('end...');
复制代码
请看答案
json
promise...
start...
bar...
end...
promise resolve...
timeout...数组
这里分析一下(你们不要纠结任务队列的叫法,本人说明的异步微任务、异步宏任务暂无根据,理解便可,请勿深究):promise
程序正式开始执行是从9行初始化promise对象开始,首先打印promise...浏览器
而后往下执行发现是promise.then回调函数,此为异步微任务,放入任务队列中,等待同步任务执行完才能执行
再往下执行是timeout定时器,此为异步宏任务,也放入任务队列中,等待同步任务执行完、异步微任务才能执行
再往下是foo方法,此为同步任务,借用网络流行的一句话 “JavaScript中的函数是一等公民”,打印日志start...后回调执行bar方法,到这里就有两个执行栈了(依次将foo、bar放入栈中,bar执行完就弹出栈,foo依次弹出)
关于并发模型和Event Loop 请看MDN
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop
复制代码
关于异步编程的方式,经常使用的定时器、ajax、Promise、Generator、async/await,详细介绍以下:
3.1.1.setTimeout与setInterval
这里拿setTimeout来举例
简单的时钟
(function(){
var div = document.createElement('div'),timer;
document.body.appendChild(div);
//同步代码,5s后执行异步代码块显示时钟
//doSomething()
setTimeout(function(){
execFn();
},5000);
function timeChange(callback){
div.innerHTML = '当前时间:'+getCurrentTime();
if(new Date().getSeconds() %5 === 0){
//当前秒数是5的倍数关闭定时器
clearTimeout(timer);
//doSomething...
console.log(timer);
timer = setTimeout(execFn,100);
}else{
clearTimeout(timer);
execFn();
}
}
function execFn(){
timer1 = window.setTimeout(timeChange,1000);
}
function getCurrentTime(){
var d = new Date();
return d.getFullYear()+'-'+(d.getMonth()+1)+'-'+d.getDate()+' '+d.getHours()+':'+d.getMinutes()+':'+d.getSeconds()+' 星期'+getWeek();
}
function getWeek(){
var d = new Date();
var week;
switch(d.getDay()){
case(4):week='四';break;
//省略
default:week='*';break;
}
return week;
}
})();
复制代码
正常的逻辑代码确定要复杂的多,可是利用setTimeou编写异步代码的逻辑大体上是这么处理的。
看下面的例子
你们是否有疑问,为啥不是先输出2再输出1
setTimeout与setInterval执行的间隔时间为4~5ms
下面看setInterval代码
计数count输出为252,因此执行的间隔时间约为4ms
看看caniuser支持的状况
看这趋势除了opera外其余浏览器之后都支持requestAnimationFrame方法
平时业务中也看到公司同事封装了requestAnimationFrame方法。若是碰到某些版本的浏览器不支持此方法,则须要重写,requestAnimationFrame其实与防抖节流实现的原理有些类似,请看代码
var vendors = ['webkit', 'moz'];
for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
var vp = vendors[i];
window.requestAnimationFrame = window[vp+'RequestAnimationFrame'];
}
if(!window.requestAnimationFrame){
var lastTime = 0;
window.requestAnimationFrame = function(callback){
var now = new Date().getTime();
var nextTime = Math.max(lastTime + 16, now);//浏览器渲染的间隔时间大约16ms
return window.setTimeout(function(){
lastTime = nextTime;
callback();
},nextTime - now);
};
}
复制代码
有兴趣的同窗能够看看这位大神的杰做
https://codepen.io/caryforchristine/pen/oMQMQz
直接看一个简单的ajax异步处理代码
(function(){
var xmlhttp = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
var url = "authorInfo.json";
xmlhttp.onreadystatechange = function(){
if(xmlhttp.readyState==4){
if(xmlhttp.status==200){
console.log(xmlhttp.response);
//异步获取数据后再doSomething
}
}
}
xmlhttp.open('GET',url,true);
xmlhttp.send(null);
})();
复制代码
chrome打印日志
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最先提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象
简单的读取文件实例
var fs = require('fs')
var read = function (filename){
var promise = new Promise(function(resolve, reject){
fs.readFile(filename, 'utf8', function(err, data){
if (err){
reject(err);
}
resolve(data);
})
});
return promise;
}
read('authorInfo.json')
.then(function(data){
console.log(data);
return read('not_exist_file');
})
.then(function(data){
console.log(data);
})
.catch(function(err){
console.log("error caught: " + err);
})
.then(function(data){
console.log("completed");
})
复制代码
用node运行结果以下:
Promise构造函数接受一个函数做为参数,该函数的两个参数分别是resolve和reject(函数)
当状态由pending变成resolved执行resolve(),变成rejected则执行reject(),当promise实例生成时能够用then指定回调
then(function success(){},function fail(){}),此方法仍是会返回一个新的promise对象,因此能够进行链式调用
有关Promise包括下文要提到的Generator请看阮老师博客
本人在第一次接触Generator的时候以为特神奇,毕竟以前历来没有想过函数会断点执行(在下描述不许确,勿喷),也就是说函数执行一部分能够停下来处理另外的代码块,而后再回到暂停处继续执行。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,仍是一个遍历器对象生成函数。返回的遍历器对象,能够依次遍历 Generator 函数内部的每个状态。
因而可知Generator返回的是一个遍历器对象,能够用for of(ES6新特性,主要是针对具备Symbol.iterator属性的对象,包括数组,set,map,类数组等等)进行遍历,
Generator语法 function* name(){},通常*和函数名中间有个空格,函数体内可经过yield关键字修饰,须要注意的是,yield后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行。你们是否会以为Generator要手动执行next方法过于麻烦呢,接下来介绍当前js对异步的终极解决方案
async和await是ES 7中的新语法,新到连ES 6都不支持。
能够利用babel转换
在线转换地址:https://babeljs.io/ ,也能够本身安装babel-cli进行转换
const fs = require('fs');
const utils = require('util');
const readFile = utils.promisify(fs.readFile);
async function readJsonFile() {
try {
const file1 = await readFile('zh_cn.json');
const file2 = await readFile('authorInfo.json');
console.log(file1.toString(),file2.toString());
} catch (e) {
console.log('出错啦');
}
}
readJsonFile();
复制代码
能够看到异步依次读取两个文件,若是利用Generator的话须要手动执行next,async/await实现了自动化
写的不周到或者有错误的地方欢迎各位大神及时指出,转载请注明出处,谢谢!
欢迎纠错~