前言
JS 的异步由来已久, 各种异步概念也早早堆在开发者面前. 可现实代码中, 仍然充斥了各种因异步顺序处理不当的 bug, 或因不好好思考, 或因不了解真相. 今天, 就特来再次好好探索一番 JS 的异步世界.
01 异步的由来 -- 单线程
上世纪末, 互联网仍处于极慢速时代, 穿梭于客户端与服务端的请求, 对于时间的耗费是如此的奢侈. 而即将面世的 LiveScript, 便被网景公司考虑同时在浏览器和服务端使用, 在浏览器端对表单进行校验, 从而提高表单提交效率. 为了将这一脚本语言推向市场, 网景与 sun 联合开发, 最终以 Java 冠名为 JavaScript.
刚面世的 JavaScript, 是为网页设计人员准备的, 不需要太复杂的语言设计, 能简单上手, 自然就是最好的.
于是, 单线程, 弱类型, 一开始就成为了 JavaScript 的基因. 而其中的单线程, 便是最戏剧性的存在, Ryan Dahl 因为 JavaScript 是单线程语言, 从而选择了 JS 开发了轻量级服务器(Node.JS), 使得 JS 从浏览器端延伸到服务器. 随着 JS 开发队伍和程序复杂度的同步发展, 异步处理成为了 JS 程序的重中之重.
02 JS 是一个充满异步的世界
先来导入几个异步的常见场景
dom 用户输入响应
- ducument.addEventListener('click', function(){
- })
- Ajax
- $.Ajax(<url>, function() {
- })
定时 / 延时
- setTimeout(function() {
- }, 1000)
- setInterval(function() {
- }, 1000)
文件读取
- var reader = new FileReader();
- reader.readAsDataURL(file);
- reader.onload = function() {
- }
以上的场景基本有个共同特性, 耗时!
举个栗子, 我们去银行取钱, 当人很多时, 如果还是排队模式, 会耗费很多时间(同步模式). 于是设立了取号机, 取了号, 不用排队, 在一旁坐着, 安心打开电脑写个文档, 等叫号后再去办业务(异步模式).
同理, 由于单线程的特性, 当 JS 应用越来越复杂, 耗时的程序如果以同步来进行, 就会阻塞 JS 的单线程, 如大水冲过狭窄的河道, 势必决堤. 那 JS 是怎么开拓导流渠道的呢? 其实在 JS 的单线程 (主线程) 背后, 规律的运行了很多线程:
dom 事件处理线程
http 请求线程
定时器线程
...
这些线程就充当了 JS 大江的小河道, 当短时有大流量时, 接纳吸收, 将过滤处理后的正常水流, 再汇入 JS 主干道.
与其说 JS 是单线程, 不如说 JS 是有着自动化多线程处理的主线程. 无需手动编码介入新开线程, 切换线程, 消息同步等冗繁的处理. 专用线程会接管相关任务, 并将处理结果送回主线程进行顺序处理.
说到这里稍微提一下 web worker, 虽然是自定义的多线程, 最终还是子线程地位, 仍旧将处理完成的结果以回调函数方式汇入到主线程进行异步处理.
03 异步处理一般流程
先看以下代码, 异步模式开始了
- var img = new Image()
- var imgLoadCallback = function() {
- }
- img.src = 'http://????'
- img.onload = callback
"http 君, 麻烦帮取一个图片数据, 好了后交给 imgLoadCallback 君." - JS 主线程老大
"任务收到, 您先忙, 图片请求交给我了, 好了之后我叫 imgLoadCallback 君到休息室排队, 您空了通知下 Event Loop 巡检官." - http 请求线程
- img.src = 'http://????'
- img.onload = imgLoadCallback
"图片已取到, imgLoadCallback 君去休息室排队等候吧!" - http 请求线程
imgLoadCallback 入栈 JS 任务队列
"刚好忙完手上的事情了, Event Loop 君, 帮看下休息室有没有人排队" - JS 主线程老大
"老大, 已把等候者 imgLoadCallback 叫过来处理任务" - Event Loop 巡检官
执行 imgLoadCallback
"事情都交给合适的人去办了, 突然就清闲下来了, 老大就是要这样当啊, 嘿嘿嘿... Event Loop 君, 定时看下休息室有没有人排队吧..." - JS 主线程老大
JS 主线程通过 Event Loop 读取任务队列
讲完故事, 再来看这张异步示意图, 是否能理解了?
image
04 回调处理工具的进化
从前面的篇章已经能看出来了, 异步处理的结果是通过回调放置到任务队列转接到主线程中的.
北京猿人刀跟火种, 这么写异步回调, 看上去也能令人接受.
- $.Ajax(
- url: '自家香蕉树林',
- data: {
- picker: '猴子 A'
- },
- success: function(data) {
- $.Ajax(
- url: '隔壁老孙家桃林',
- data: {
exchanges: data. 香蕉,
- buyer: '猴子 A'
- },
- success: function(data) {
- console.log('向本猴王进贡', data. 桃子)
- }
- )
- }
- )
进化成人类后交易过程变的复杂了, 于是就变成回调地狱, 传说中的 callback hell
- $.Ajax(
- url: '自家香蕉树林',
- data: {
- picker: '老王'
- },
- success: function(data) {
- $.Ajax(
- url: '集市贩卖',
- data: {
goods: data. 香蕉,
- seller: '老王'
- },
- success: function(data) {
- $.Ajax(
- url: '隔壁老李桃子铺',
- data: {
exchanges: data. 钱,
- buyer: '老王'
- },
- success: function(data) {
- console.log('向本王进贡', data. 桃子)
- }
- )
- }
- )
- }
- )
于是发明了铁器 promise, 解决回调地狱之痛
- $.Ajax(
- url: '自家香蕉树林',
- data: {
- picker: '老王'
- }
- )
- .then(function(data) {
- return $.Ajax(
- url: '集市贩卖',
- data: {
goods: data. 香蕉,
- seller: '老王'
- }
- )
- })
- .then(function(data) {
- return $.Ajax(
- url: '隔壁老李桃子铺',
- data: {
exchanges: data. 钱,
- buyer: '老王'
- }
- )
- })
- .then(function(data) {
- console.log('向本王进贡', data. 桃子)
- })
关于 promise 的升级版 async,await, 本篇不多说了, 理念上基本一致.
继续...
这下一次命令, 只会来供给本王一次桃子, 每次都要发令, 好麻烦, 得下个令让老王每天去卖香蕉买桃子, 给我月供 100 个, 于是就发生了以下的故事
- var contributeTime;
- setInterval(function(){
- $.Ajax(
- url: '自家香蕉树林',
- data: {
- picker: '老王'
- }
- )
- .then(function(data) {
- return $.Ajax(
- url: '集市贩卖',
- data: {
goods: data. 香蕉,
- seller: '老王'
- }
- )
- })
- .then(function(data) {
- return $.Ajax(
- url: '隔壁老李桃子铺',
- data: {
exchanges: data. 钱,
- buyer: '老王'
- }
- )
- })
- .then(function(data) {
- var currentTime = new Date().getTime();
- if (!contributeTime || (currentTime - contributeTime> '月')) {
- console.log('向本王进贡', [data. 桃子,...]); //length=100
- currentTime = contributeTime;
- }
- })
- }, '天')
这过程, 好像也太不优雅了点.
ReactX 的 JS 版, RxJs 来了, 将异步看作为单点, 将其扩展了时间线, 作为流来处理. 所以对于一次又一次的进贡, 都可进行时序管理, 于是整个过程变成这样:
- import { Ajax } from 'rxjs/ajax'; // 此处特别写引入, 目的为不与 jQuery.Ajax 混淆
- import { interval } from 'rxjs';
- const ob = interval('天');
- const peachPay = ob
- .pipe(switchMap(x => Ajax.post('自家香蕉树林', {picker: '老王'})))
- .pipe(switchMap(data => Ajax.post('集市贩卖', {seller: '老王', goods: data. 香蕉})))
- .pipe(switchMap(data => Ajax.post('隔壁老李桃子铺', {buyer: '老王', exchanges: data. 钱})))
- .pipe(throttle(data => interval('月')))
- .subscribe(data => console.log(` 每月收到月供:${data. 桃子. length}个 ${data. 桃子}`));
整个过程顺着管道不断变换处理, 就是一条全自动流水线! 然鹅, 然鹅, 并一定每月就能供出 100 个桃子啊, 万一遇到农灾, 或者经济萧条...
以上例子仅提供思路, 且读且珍重!
05 比工具更重要的, 是理解
前端开发中, 诸多剪不断理还乱的偶现 bug 来源于异步处理的顺序混乱. 即便是异步处理工具越来越先进, 由于代码层面的顺序和真实执行顺序的不一致, 也还是容易一不小心犯错误.
异步处理工具不是万能的, 还是需不断将异步原理内化入思维模式中, 种码的时候, 就需清晰的知道该段代码会什么时候结出果实.
再注: 以上代码未经运行验证, 仅示意流程与思路. 望各路大神多多包涵! 如有思路上都提供错误的, 求板砖~
来源: https://www.cnblogs.com/asie-huang/p/10562912.html