07-异步操作
异步操作¶
统计信息:字数 8138 阅读17分钟
2021-11-18 2023-04-08
概念¶
1、单线程模式¶
JS 语言只在一个线程上运行,JS在某时刻只能执行一个任务,其他任务必须在后面排队等待。实际上 JS V8 引擎宏观上具有多线程,单个 JS 脚本文件只能在一个线程(主线程)上运行,其他线程在后台配合。
单线程的原因:浏览器线程主要和 DOM 相关,如果多个线程同时更改 DOM,就造成了不确定性,所以早期使用单线程完成。
单线程好处:设计简单,执行环境单纯,线程不会冲突。
单线程缺点:读写外部数据,或者发送请求,可能脚本长时间停滞(浏览器假死),此时 CPU 还在闲置,界面是空白。
H5规定:一个JS脚本可以创建多个线程,子线程完全被主线程控制,子线程不能控制DOM(本质单线程没变)。
2、同步任务和异步任务¶
程序中的任务分成**同步任务和异步任务**;同步任务是主线程中的任务,必须等上一个任务完成后再执行下一个任务。异步任务不在主线程中,进入**任务队列**。当主线程的一个状态变化后(例如ajax接到数据),任务队列中的任务会以回调函数的形式进入主线程执行,不会产生堵塞。Ajax可以实现同步任务和异步任务。如果是同步任务,需要等请求返回后执行后面的代码;如果是异步任务,在请求的同时,会执行后面的代码;请求响应后,主线程再调用回调函数。
3、任务队列和事件循环¶
JS 程序中具有一个主线程,还有若干任务队列。主线程首先执行同步任务,执行完同步任务后,就会查看任务队列中的异步任务,如果满足条件,异步任务进入主线程变成同步任务执行。
事件循环:JS 引擎不断检查异步事件;当同步事件进行完,JS引擎会检查任务队列中的异步任务并执行,直到全部任务执行结束,对应程序执行结束。
4、异步操作的模式¶
回调函数:简单,但是不同程序高度耦合,多个回调函数嵌套很复杂,每个任务对应一个回调函数。
function f1() {
//
}
function f2(callback) {
//
callback();
}
f2(f1);
// 把回调函数以参数的形式传入同步任务函数中
事件驱动:可以绑定多个事件,每个事件可以执行多个回调函数,去耦合,模块化。整个程序如果是事件驱动,那么主线程不明确。
function f1(){
setTimeout(function() {
f1.trigger('onclick');
}, 1000);
}
f1.on('click', f2);
发布订阅(观察者模式,publish-subscribe pattern, observer pattern)
$.subscribe('done', f2); // f2 函数向信号中心$订阅 done 信号
function f1 (){
setTimeout(function() {
$.publish('done');
// f1 执行完毕后,向信号中心 $ 发布done信号(从而引发f2函数执行)
});
}
function f2() {
// do something
$.unsubscribe('done');
// 函数执行完毕后,取消订阅
}
发布订阅模式比事件驱动模式好,可以查看消息中心获取信号数量,信号订阅者,从整体上监控程序的运行
5、多个异步任务的流程控制¶
如果多个回调函数嵌套,结构复杂难以维护。可以采用串行和并行执行。
串行执行:使用一个串行函数,多个任务依次执行。缺点耗时。
并行执行:同时发布异步任务,优点省时间,缺点耗性能。
串行和并行结合:设置一个变量存放异步任务的结果和当前的数量,设置一个当前运行的最大值,通过性能控制任务运行的数量。
定时器¶
主要是 setTimeout setInterval clearTimeout, clearInterval,可以向任务队列添加定时任务。
setTimeout 函数返回一个变量(递增的整数),用于清除定时器。函数默认传入两个参数,分别是异步执行的函数和间隔时间,可选参数是回调函数的变量(ab);函数内部的作用于是 window,this 的指向发生变化,注意。
let that = this;
let a = setTimeout(function(a, b) {
// that.fucntion...
}, 1000, a, b);
setInterval 函数的参数、返回值相同,可以定时执行某个函数,不会主动停止。缺点:间隔不包括函数运行的时间,如果函数执行时间较长,那么后一次函数也会准时执行。
在滚动函数或者渐变函数中,最好的办法是设置 setTimeout 并在函数内部设置下一次时间间隔函数。
清除所有定时器
(function() {
// 每轮事件循环检查一次
var gid = setInterval(clearAllTimeouts, 0);
function clearAllTimeouts() {
var id = setTimeout(function() {}, 0);
while (id > 0) {
if (id !== gid) {
clearTimeout(id);
}
id--;
}
}
})();
(function(){
var gid = setInterval(clearAllTimeouts, 0); // 事件循环时检查一次
function clearAllTimeouts() {
var id = setTimeout(function() {}, 0); // 首先创建一个定时器,获取定时器最大值
while (id > 0) {
// 清除全部的定时器
if (id !== gid) {
clearTimeout(id);
}
id--;
}
}
})();
防抖
$('textarea').on('keydown', debounce(ajaxAction));
function debounce(fn) {
var timer;
var that = this;
var args = arguments; // 传入的参数
clearTimeout(timer); // 首先清除一下上次的定时器
timer = setTimeout(function() {
fn.apply(this, args);
}, 2000);
}
定时器原理
把当前事件移出事件队列,等固定的时间后执行这个事件。
不足:如果当前主线程执行的时间比较长,超过了定期器的时间间隔,那么主线程任务执行完后会立即执行定时器内容(可能多个interval同时触发),即时间间隔 + JS 执行的时间 = 设置的时间。
setTimeout(fn , 0) 的特殊用法:表示当前主线程任务执行完毕后,执行现在的程序(优化性能),不同浏览器执行的时间不同。例如,在事件冒泡中,首先触发子元素的回调函数,然后触发父元素的回调函数。在子元素上使用setTimeout(0)即可改变执行的顺序。例如2,DOM渲染的速度比JS执行的慢,可以在JS中使用这个函数,让DOM渲染结束后再执行JS代码。
在 react 中,setTimeout 的特殊用法:生命周期函数中,componentDidMount componentDidUpdate 是组件已经加载或者组件已经更新(但是此时界面 DOM 可能没有更新)在这两个生命周期中获取 dom 的属性,可能是不正确的。所以可以设置定时器,然后时间设置0,表示当前JS执行后(即DOM渲染完成后),再执行对应的代码,这样可以正确获取 DOM 的属性。
Promise对象¶
Promise 是异步操作和回调函数的很好的解决方案,这里简单介绍(主要在ES6)
异步任务返回一个 Promise 实例,这个实例具有 then 的方法,可以处理下一步的回调函数。Promise 简化了回调函数的写法。
function f1 (resolve, reject) {
// deal with async event
}
let a = new Promise(f1);
a.then((res) => {
console.log(res);
// deal with response
});
传统写法和 Promise 写法,回调函数变成链式函数
f1(function(value1) {
f2(value1, function(value2) {
//
});
});
(new Promise(f1)).then(f2).then(f3)...
Promise 实例对象的状态(对应ajax请求对象的状态),结果可能是两个,成功或者失败(成功返回 result, 失败返回 error,如果发出异步那么结果一定会发生,所以称为承诺)
Promise.prototype.then 可以为异步事件添加回调函数,例如下面的图片加载
var preloadImage = function(path) {
return new Promise(function(resolve, reject) {
var image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
preloadImage('https://example.com/my.jpg').then((e) => {
document.body.append(e.target)
}).then(() => {
console.log('加载成功')
});