异步操作

1、概述

1.1、JS是单线程的,JS的核心特征,单线程指主线程只有一个

防止过度消耗浏览器资源以及防止多线程共享资源导致的同时修改一个DOM问题,主要还是避免复杂性。

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。

1.2、同步任务和异步任务

同步任务在主线程上排队等待执行
异步任务的会被挂起进入任务队列,等待执行结果之后进入主线程。所以异步不阻塞代码执行。

1.3、任务队列和事件循环

异步任务都存放在任务队列中(实际上根据任务的类型会有多个任务队列)
主线程执行完所有同步任务之后就会去任务队列里面看是否可以执行异步任务,满足条件就会变成同步任务进入主线程,执行完成之后就会再次到任务队列中寻找可以执行的异步任务。
Js引擎会在同步任务执行完之后一遍遍的检查异步任务队列是否可以进入主线程。这种循环检查的机制,就叫“事件循环(Event Loop)”。
维基百科:事件循环是一个程序结构,用于等待和发送消息和事件。

1.4、异步任务的模式

1.回调函数---最普通的
回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(coupling),使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。
2.事件监听
比如window.addEventListener事件监听,以及各种浏览器事件(click等)的监听。
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以“去耦合”(decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
3.“发布/订阅模式”(publish-subscribe pattern)---观察者模式
通过一个消息中心监听事件的发生,而不针对某一个方法或者对象以及DOM了。
这种方法的性质与“事件监听”类似,但是明显优于后者。因为可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。

1.5、异步操作的流程控制

让多个异步操作按照自己规定的先后顺序去执行
1.串行执行--一个任务完成以后,再执行另一个,最后执行final函数。

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];

function async(arg, callback) {
    console.log('参数为 ' + arg +' , 1秒后返回结果');
    setTimeout(function () { callback(arg * 2); }, 1000);
}
function final(value) {
    console.log('完成: ', value);
}
function series(item) {
    if(item) {
        async( item, function(result) {
	    results.push(result);
	    return series(items.shift());
	});
    } else {
        return final(results[results.length - 1]);
    }
}
series(items.shift());

上面的写法需要六秒,才能完成整个脚本。
2.并行执行--所有异步任务同时执行,之后执行final函数。

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];

function async(arg, callback) {
    console.log('参数为 ' + arg +' , 1秒后返回结果');
    setTimeout(function () { callback(arg * 2); }, 1000);
}
function final(value) {
    console.log('完成: ', value);
}
items.forEach(function(item) {
    async(item, function(result){
        results.push(result);
	if(results.length === items.length) {
	    final(results[results.length - 1]);
	}
    })
});

上面的写法只要一秒,就能完成整个脚本。并行执行比起串行执行一次只能执行一个任务,较为节约时间,效率较高。但是问题在于如果并行的任务较多,很容易耗尽系统资源,拖慢运行速度。
3.串行和并行结合--设置一个门槛,每次最多只能并行执行n个异步任务,这样就避免了过分占用系统资源。

// 所谓并行与串行的结合,就是设置一个门槛,每次最多只能并行执行n个异步任务,这样就避免了过分占用系统资源。
// 最多只能同时运行两个异步任务。变量running记录当前正在运行的任务数,只要低于门槛值,就再启动一个新的任务,如果等于0,就表示所有任务都执行完了,这时就执行final函数。
// 这段代码需要三秒完成整个脚本,处在串行执行和并行执行之间。通过调节limit变量,达到效率和资源的最佳平衡。
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
var running = 0;
var limit = 2;

function async(arg, callback) {
    console.log('参数为 ' + arg +' , 1秒后返回结果');
    setTimeout(function () { callback(arg * 2); }, 1000);
}
function final(value) {
    console.log('完成: ', value);
}
function launcher() {
    while(running < limit && items.length > 0) {
        var item = items.shift();
	async(item, function(result) {
	    results.push(result);
	    running--;
	    if(items.length > 0) {
	        launcher();
	    } else if(running == 0) {
	        final(results);
	    }
	});
	running++;
    }
}
launcher();

这段代码需要三秒完成整个脚本,处在串行执行和并行执行之间。通过调节limit变量,达到效率和资源的最佳平衡。

2、定时器

2.1、setTimeout(),setInterval()

setTimeout()是指定某个函数或某段代码在多少毫秒之后执行,返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。
setInterval()的区别在于每隔一段时间执行一次,其他使用方式相同。间隔时间是开始执行的间隔时间,如果代码片段或者函数的执行时间大于了间隔时间,将在函数执行完之后立即开始下一次执行,无需等待。

这两个函数都有两个参数,第一个参数是待执行的代码片段或者函数,第二个参数是指定间隔时间,如果不设置,默认0;另外这两个函数都可以不止这两个参数,多余的后面的参数将会按顺序作为内部待执行函数的参数。
如果执行的函数是一个对象的函数,那么内部的this指向的将是window全局环境,解决这个问题的办法就是使用bind(这个对象)就可以了。

2.2、clearTimeout(),clearInterval()

setTimeout和setInterval函数,都返回一个整数值,表示计数器编号。将该整数传入clearTimeout和clearInterval函数,就可以取消对应的定时器。
setTimeout和setInterval返回的整数值是连续的,也就是说,第二个setTimeout方法返回的整数值,将比第一个的整数值大1。
利用这一点,可以写一个函数,取消当前所有的setTimeout定时器。

(function() {
    var gid = setInterval(clearAllTimeouts, 0);	
    function clearAllTimeouts() {
        var id = setTimeout(function() {}, 0);
	while(id > 0){
	    if(id !== gid){
	        clearTimeout(id);
	    }
	    id--;
	}
    }
})();

2.3、debounce 函数(防抖动)

防抖原理:设置一个门槛值,表示两次 Ajax 通信的最小间隔时间。如果在间隔时间内,发生新的keydown事件,则不触发 Ajax 通信,并且重新开始计时。如果过了指定时间,没有发生新的keydown事件,再将数据发送出去。

document.getElementById('timer').addEventListener('click', debounce(shijian, 2500));
function debounce(fn, delay){
    var timer = null; // 声明计时器
    return function() {
        var context = this;
	var args = arguments;
	clearTimeout(timer);
	timer = setTimeout(function () {
	    fn.apply(context, args);
	}, delay);
    };
}
function shijian(){
    console.log(123);
}

上面代码中,只要在2500毫秒之内,用户再次击键,就会取消上一次的定时器,然后再新建一个定时器。这样就保证了回调函数之间的调用间隔,至少是2500毫秒。
备注:
假设ajaxAction函数是一个需要时间的异步请求函数,按照以上执行机制,防抖取消的是定时器,如果定时器的2.5秒时间已过,代码运行已进入异步请求,此时取消定时器后异步函数还是会继续执行。也就是说取消只能取消定时器,在进入异步函数之前取消的话是可以防止重复执行的,但是一旦进入异步函数即使取消了定时器,异步函数已经不能阻止了。但是防抖还是生效的,代码依旧是最短2.5秒执行一次。为什么说最短2.5秒执行一次,因为定时器计时的方式是每次点击之后重新计时的。

2.4、运行机制

setTimeout和setInterval的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。
这意味着,setTimeout和setInterval指定的回调函数,必须等到本轮事件循环的所有同步任务都执行完,才会开始执行。由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证,setTimeout和setInterval指定的任务,一定会按照预定时间执行。

setInterval(funcrion() {
    console.log(2);
},1000);
sleep(3000);
function sleep(ms){
    var start = Date.now();
    while((Date.now()-start) < ms){
    }
}

休眠3秒之后,setInterval不会产生累积效应,不会一下子输出三个2,而是只有一个2。

2.5、setTimeout(f, 0)含义以及应用

setTimeout的作用是将代码推迟到指定时间执行,如果指定时间为0,即setTimeout(f, 0),他也不会立即执行。setTimeout就是将代码块移除本轮同步事件循环,到下一个事件循环才会执行。总之,setTimeout(f, 0)这种写法的目的是,尽可能早地执行f,但是并不能保证立刻就执行f。
实际上,setTimeout(f, 0)不会真的在0毫秒之后运行,不同的浏览器有不同的实现。以 Edge 浏览器为例,会等到4毫秒之后运行。如果电脑正在使用电池供电,会等到16毫秒之后运行;如果网页不在当前 Tab 页,会推迟到1000毫秒(1秒)之后运行。这样是为了节省系统资源。
用途一:调整事件的发生顺序
比如,网页开发中,某个事件先发生在子元素,然后冒泡到父元素,即子元素的事件回调函数,会早于父元素的事件回调函数触发。如果,想让父元素的事件回调函数先发生,就要用到setTimeout(f, 0)。以下代码的执行顺序就是:A->C->B。

var input =  document.getElementById('myButton');
input.onclick  = function A() {
    setTimeout(function B() {
        input.value += ' input';
    }, 0)
};
document.body.onclick = function C() {
    input.value += ' body'
};

用途二:将自定义回调函数移到浏览器默认函数之后执行。
用户自定义的回调函数,通常在浏览器的默认动作之前触发。如果想通过回调函数将输入框内的字母实时转化为大写字母,需要如下写法:

document.getElementById('input-box').onkeypress = function() {
    var self = this;
    setTimeout(function() {
        self.value = self.value.toUpperCase();
    }, 0);
}

否则每次转换都是在浏览器接收到文本之前,所以只能将前一次的输入结果转换成大写(不包括最后输入的字符),上面代码将代码放入setTimeout之中,就能使得它在浏览器接收到文本之后触发。
由于setTimeout(f, 0)实际上意味着,将任务放到浏览器最早可得的空闲时段执行,所以那些计算量大、耗时长的任务,常常会被分为几个小部分,分别放到setTimeout(f, 0)里面执行。

var div = document.getElementById('id');
//写法一
for(var i=0xA00000;i<0xFFFFFF;i++){
    div.style.backgroundColor = '#' + i.toString(16);
}
//写法二
var timer;
var i='0x100000';
function func() {
    timer = setTimeout(func, 0);
    div.style.backgroundColor = '#' + i.toString(16);
    if(i++ = 0xFFFFFF) clearTimeout(timer);
}
timer = setTimeout(func, 0);

上面代码有两种写法,都是改变一个网页元素的背景色。写法一会造成浏览器“堵塞”,因为 JavaScript 执行速度远高于 DOM,会造成大量 DOM 操作“堆积”,而写法二就不会,这就是setTimeout(f, 0)的好处。
另一个使用这种技巧的例子是代码高亮的处理。如果代码块很大,一次性处理,可能会对性能造成很大的压力,那么将其分成一个个小块,一次处理一块,比如写成setTimeout(highlightNext, 50)的样子,性能压力就会减轻。

3、Promise 对象

3.1、概述

Promise 对象是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。它起到代理作用(proxy),充当异步操作与回调函数之间的中介,使得异步操作具备同步操作的接口。Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。

未完待续!

posted @ 2021-03-27 21:15  木-鱼  阅读(473)  评论(0编辑  收藏  举报