JS中的同步与异步,解释的最准确的一段话:
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。于是就有一个概念,任务队列。
如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行.
外加一幅图理解程序的执行顺序:
同步与异步的区别总结:
//1. 执行时机不同: 同步代码会一步一步的执行,而异步代码不会等后面的后面的代码执行,会自己先执行
//2. 获取结果的方式不一样,同步是函数的返回值,异步是回调函数
一 异步
在JS中那些是异步操作呢?
//js中的异步操作(语句)
1. 定时器
2. ajax
3. 事件函数
4. 回调函数
5. promise
6. Generator 函数
1.1 定时器
js中的定时器,是异步代码,也就是说程序会先执行同步代码,当定时器时间到了之后,通知主线程来执行同步代码.(相当于开启了一个线程)
function fun() {
var name = '李四';
setTimeout(function () {
name = '张三';
},2000)
return name;
}
var name = fun();
console.log(name);
我们希望代码是先打印出张三,但是最终只会打印李四.因为定时器是异步的,当定时器时间到来时,函数都已经调用完了.所以打印的一直是默认值李四.
1.2 ajax
ajax同样是异步代码,本例打印出来的结果同样是李四,道理与定时器一样.
function fun() {
var name = '李四';
$.ajax({
url:'url',
success:function (data) {
name = data;
}
})
return name;
}
var name = fun();
console.log(name);
1.3 事件函数
事件同样具有异步性,因为事件只有等待触发之后,才会执行,事件触发形式通常会用于表示某些状态变化(加载、出错、进度变化、收到消息等等)
如果一开始主线程上就有同步代码,同样会先执行同步代码,直到事件触发后才会执行事件函数
这个例子里面: 函数调用时先于事件触发的,函数调用完后,便不会再去调用了,所以打印的还是李四.
function fun() {
var name = '李四';
div.addEventListener('click', function () {
name = '张三';
});
return name;
}
var name = fun();
console.log(name);
1.4 怎样访问异步代码里面的变量(回调函数)
回调函数: 把一个函数作为参数传入到另一个函数中,那么这个作为参数的函数就叫做回调函数,形式是为了获取某一个函数的运行结果
如果我就要拿上面代码里的name变量,我们可以利用回调函数来实现.因为异步函数里面是不能使用return的.
//callback是一个回调函数
function fun(callback) {
var name;
setTimeout(function () {
name = '张三';
callback(name);
},1000)
}
//fun函数的参数就是一个callback的实际参数
fun(function (name) {
console.log(name);
});
上述代码相当于:
//callback是一个回调函数
function fun() {
var name;
setTimeout(function () {
name = '张三';
//调用callback函数
callback(name);
function callback (name) {
console.log(name);
}
},1000)
}
//调用函数
fun();
例如: W3C的File System API中,在请求虚拟文件系统实例、读写文件等接口中,都采用了回调函数的形式:
requestFileSystem(TEMPORARY, 1024 * 1024, function(fs) {
// 异步获取虚拟文件系统实例fs
fs.root.getFile("already_there.txt", null, function (f) {
// 获取文件already_there.txt
getAsText(f.file());
}, function(err) {
// 获取文件出错
});
}, function(err) {
// 获取虚拟文件系统失败
});
1.5 promise对象
Promises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。
简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。比如,f1的回调函数f2,可以写成:
f1().then(f2)
function f1() {
return new Promise((resolve,reject) => {
console.log("f1")
setTimeout(() => {
resolve("f2")
},1000)
})
}
function f2(res) {
console.log(res)
}
先打印f1,间隔一秒后打印f2
这样写的优点在于,回调函数变成了链式写法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以实现许多强大的功能。
比如,指定多个回调函数:
f1().then(f2).then(f3);
再比如,指定发生错误时的回调函数:
f1().then(f2).fail(f3);
1.6 Generator 函数
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
总结一下在同步与异步中关于变量的操作:
// 1. 什么时候使用全局变量 : 就是这个变量在后面很多地方都要用到就会使用全局变量
// 2. 什么时候使用return返回值 : 这个函数是同步执行能够拿到返回值
// 3. 什么时候使用回调函数 : 这个函数在异步执行的时候就要使用回调函数来获取私
有变量
案例:回调函数加ajax的事件响应函数
function sendAjax (pageId,callback){
var baseURL = 'http://localhost:9090/api/';
$.ajax({
url : baseURL + 'getmoneyctrl',
dataType : 'json',
data : {
pageid : pageId
},
success : function(obj) {
//obj是ajax响应成功返回的数据
callback(obj);
}
})
}
}
//回调函数
function callback (obj) {
doSomething.....
}
上面的代码用在,发送ajax的路径一定,但是对于返回数据操作却不同的地方.
注意: 数组遍历的API(forEach,map)虽然利用回调来取值,但是却不是异步的,程序是同步执行的
[1,2,3,4].forEach(_ => {
setTimeout(() => {
console.log(_)
},5000)
})
上面的代码5秒后一次性打印数组里的每一项
改造成递归的方式就可以,每隔随机的时间打印对应的元素
function recurTest(j, length){
setTimeout(function(){
console.log("第"+(j+1)+"次循环");
if(++j < length){
recurTest(j, length);
}
}, Math.random() * 3000);
}
recurTest(0, 5)
例题1 :
function fn() {
console.log('frist')
setTimeout(function(){
console.log('second')
},0)
}
for(let i = 0; i < 999; i++) {
fn()
}
这段程序里面for循环每次执行都会遇到同步程序console,以及异步程序settTimeout那么实际的代码执行是:
- 先打印999次first
- 再打印999次second
程序会先执行完同步程序再执行异步程序