前言
异步,就是非同步 ….
这节内容可能会有点干燥,然而却是 JavaScript 中十分重要的概念,十分有必要去学习。
目标
- 晋升开发效率,编写易保护的代码
引子问题
- 申请时候为什么页面卡死??
$.ajax({
url: "www.xx.com/api",
async: false, // true
success: function(result) {console.log(result);
},
});
- 为什么数据更新了,DOM 却没有更新??
// 异步批量更新 DOM(vue-nextTick)// <div id="app">{{num}}</div>
new Vue({
el: "#app",
data: {num: 0,},
mounted() {let dom = document.getElementById("app");
while (this.num !== 100) {this.num++;}
console.log("Vue num=" + this.num, "DOM num=" + dom.innerHTML);
// Vue num=100,DOM num=0
// nextTick or setTimeout
},
});
产生异步的起因
起因:单线程(一个工夫点,只做一件事),浏览器的 JS 引擎是单线程导致的。
单线程 是指在 JS 引擎中负责解释和执行 IavaScript 代码的线程只有一个,无妨叫它主线程。
所谓单线程,就是指一次只能实现一件工作。如果有多个工作,就必须排队,后面一个工作实现再执行前面一个工作。
先看看一下浏览器内核的线程图:
其中,渲染线程和 JS 线程互斥。
假如有两个函数,一个批改一个删除,同时操作一个 DOM 节点,如果有多个线程的话,两个线程一起执行,必定就死锁了,就会有问题。
为什么 JS 要设计为单线程,因为浏览器的非凡环境。
单线程的优缺点:
这种模式的益处是实现起来比较简单,执行环境绝对单纯;害处是只有有一个工作耗时很长,前面的工作都必须排队等着,会迁延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段 Javascript 代码长时间运行(比方死循环),导致整个页面卡在这个中央,其余工作无奈执行。
常见的梗塞(死循环):
while (true) {}
JS 在设计之初就以运行在浏览器中的脚本语言,所以也不想搞得这么简单,就设计成了单线程,也就是,一个工夫点,只能做一件事。
为了 解决单线程梗塞 这个毛病:产生了异步。
拿吃泡面举例:
- 同步:买泡面 =》烧水(盯着)=》煮面 =》吃泡面
- 异步:买泡面 =》烧水(水开了热水壶响 - 回调)=》看电视 =》煮面(面好了热水壶响 - 回调)=》看电视 =》熟了叫我 =》吃泡面
看电视就是异步操作,热水壶响,就是回调函数。
异步编程
JS 中大多的代码都是同步执行的,只有极个别的函数是异步执行的,异步执行的代码,则须要异步编程。
异步代码
setTimeout(() => {console.log("log2");
}, 0);
console.log("log1");
// ?? log1 log2
异步代码的特点:不是立刻执行,而是须要期待,在将来的某一个工夫点执行。
同步代码 | 异步代码 |
---|---|
<script> 代码 |
网络申请(Ajax) |
I/O 操作 | 定时器(setTimeout、setInterval) |
渲染操作 | Promise(then) |
async/await |
回调函数
异步代码最常见的写法就是应用回调函数。
- HTTP 网络申请(申请胜利、辨认后执行 xx 操作)
- DOM 事件绑定机制(用户触发事件后执行 xx 操作)
- 定时器(setTimeout、setInterval)(在达到设定工夫后执行 xx 操作)
// 留神到 click 办法中是一个函数而不是一个变量
// 它就是回调函数
$("#btn_1").click(function() {alert("Btn 1 Clicked");
});
// 或者
function click() {
// 它就是回调函数
alert("Btn 1 Clicked");
}
$("#btn_1").click(click);
回调函数的毛病也很显著,容易产生回调天堂:
异步编程的三种形式
- callback
function getOneNews() {
$.ajax({
url: topicsUrl,
success: function(res) {let id = res.data[0].id;
$.ajax({
url: topicOneUrl + id,
success: function(ress) {console.log(ress);
render(ress.data);
},
});
},
});
}
- promise
function getOneNews() {
axios
.get(topicsUrl)
.then(function(response) {let id = response.data.data[0].id;
return axios.get(topicOneUrl + id);
})
.then((res) => {render(res.data.data);
})
.catch(function(error) {console.log(error);
});
}
- async/await
async function getOneNews() {let listData = await axios.get(topicsUrl);
let id = listData.data.data[0].id;
let data = await axios.get(topicOneUrl + id);
render(data.data.data);
}
在线预览
预览地址:http://jsrun.net/s43Kp/embedded/all/light
问题??
如果多个异步代码同时存在,那么执行程序应该是怎么的?那个先执行、那个后执行了?
宏工作和微工作
异步代码的划分,异步代码分宏工作和微工作。
宏工作(不焦急) | 微工作(焦急) |
---|---|
<script> 整体代码 |
Promise |
setTimeout/setInterval |
事件循环(Event loop)
执行程序:
- 执行整体代码
<script>
(宏工作) - 执行所有微工作
- 执行一个宏工作
- 执行渲染线程
- 2->3->2->3… 顺次循环(在 2、3 步中又创立了新的宏、微工作)
反复从宏工作和微工作队列里拿出工作去执行。
总结
因为浏览器设计的起因,JS 线程和渲染线程互斥,所以 JS 线程被设计成了单线程。
因为单线程执行一些操作(如网络申请)时有梗塞的问题,所有产生了异步。
因为有了异步,所以产生了异步编程,从而有了回调函数。
因为回调函数写多了会产生回调天堂,所有又有了解决回调天堂的 Promise 写法
自 ES7 规范后有了比 Promise 更加优雅的写法 ———— async/await 写法,也是异步编程的最终解决办法。
因为 JS 的代码分为同步和异步代码,同步代码的执行程序不用多说,自上而下的执行。
然而如果有多个异步的代码,他的执行程序又是怎么的呢??
为了解决多个异步代码的执行程序问了,有了事件循环(EventLoop),将异步工作辨别为宏工作、微工作,根据规定顺次执行。
至此 完!
练习
console.log("script start");
setTimeout(function() {console.log("timeout1");
}, 10);
new Promise((resolve) => {console.log("promise1");
resolve();
setTimeout(() => console.log("timeout2"), 10);
}).then(function() {console.log("then1");
});
console.log("script end");
写出 log 的输入后果,并说出理由。
参考
- 浅谈浏览器多过程与 JS 线程
- 一次搞懂 -JS 事件循环之宏工作和微工作
- 【JS】深刻了解事件循环, 这一篇就够了!(必看)
- Tasks, microtasks, queues and schedules
- 浏览器的线程有哪些
来自九旬的原创文章