单线程
javascript
只再一个线程上,同时只能执行一个任务,一行一行执行。- javascript 运行在单线程上,并不代表 JavaScript引擎就是单线程的,其实它有多个线程,单个脚本只能在一个线程上运行(主线程),其他线程在后台配合运行。
- 为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。
这种模式好处在于, 实现起来简单,环境单纯。 但是 耗时较长,后面的任务必须等待前面的任务执行完毕。
同步和异步任务
- 同步
未被引擎挂起、在主线程上排队执行的任务。需要前面的任务执行完后才能执行。 - 异步
- 被引擎挂起,暂不处理的任务,不进入主线程、而进入任务队列;
异步操作模式
回调函数 Callback
将函数传递进一个方法中,函数不会立即执行,等待出来结果之后在执行。
回调函数是异步操作最基本的方法
容易出现回调地狱(Callback hell
)
比如多个 ajax 嵌套请求1
2
3
4
5
6
7
8
9ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})
})
这种方式容易理解和简单,但是不利于;
耦合度高,结构混乱,错误较难追踪,而且每个任务只能指定一个回调。
事件监听
采用事件驱动。
W3C规范中定义3个事件阶段:捕获(Netscape),目标,冒泡(IE)。
事件冒泡:在目标元素上发生click事件的顺序 目标元素 -> 父级元素 -> body -> html -> document
事件捕获: 与冒泡相反,document -> html -> body -> 父级元素 -> 目标元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 // 原生事件委托
var parent = document.getElementById('parent');
parent.addEventListener('click',showColor,false);
function showColor(e){
var son = e.target;
if(son.nodeName.toLowerCase() === 'li'){
console.log('The color is ' + son.innerHTML);
}
}
//类似 jQuery写法
fn.on('click',function(){});
//等同于
function fn (){
setTimeout(function(){
//do someing
fn.trigger('done')
},1000)
}
这种方法比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,可以去“耦合”(decoupling
),便于实现模块化。 但是整个程序都会变成事件驱动,流程不清晰。
发布/订阅
消息的发送者(称为发布者) 不会将消息直接发送给特定的接收者(称为订阅者),而是将不消息分为不同的类别,不需要了解哪些订阅者;
订阅者,只接收感兴趣的消息,不需要了解哪些发布者
可以把事件理解成“信号”,如果存在一个“信号中心”;
某个任务执行完成,就向信号中心“发布” (publish
) 一个信号,其他任务可以向信号中心“订阅”(subscribe
)这个信号,从而知道什么时候自己开始执行
1 |
|
当 foo
执行完毕后,想消息纵向发布 done
信号,引发执行fn
消息过滤
订阅者通常接收信号中心中(消息代理)的一个子集,选择接受和处理的消息过程叫过滤
过滤形式
- 基于主题
消息被发布到主题或命名通道上;订阅者将受到所有信息,并且所有订阅同一主题的订阅者都将收到同样的信息;发布者赋值定义订阅者所订阅的消息类别
- 基于内容
订阅者定义感兴趣的条件,只有当消息的属性或内容满足订阅者的条件,消息才投递到该订阅者。订阅者负责堆消息分类。
拓扑
发布者 发布消息到一个消息代理,订阅者向其注册订阅,由消息代理来过滤
优缺点:
- 松耦合,发布者和订阅者只需要关注主题内容,相互独立地运行。
- 扩展性强,通过并行操作,消息缓存,基于树或网路路由等技术,比传统客户端具有更好的扩展性。
- 缺点: 发布者解耦订阅者,问题难以跟踪,无法知道消息传送是成功的还是失败的
异步操作的流程控制(多个异步操作如何确定异步操作的执行顺序,如何保证这种顺序执行)
串行执行
一个任务执行完毕后,再执行另一个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 let arr = [1,2,3,4,5];
let results = [];
const async = (arg,callback)=>{
console.log(`参数:${arg},1秒后返回`);
setTimeout(()=>callback(arr*2)},1000);
}
const final = res => console.log(`完成:`,res);
const series = item =>{
if(item){
async(item,result=>{
results.push(result);
return serise(items.shift();)
})
}else{
return final(results[results.length-1]);
}
}
series(items.shift());
上面代码 series
就是一个串行函数; 类似与同步任务
并行执行
所有异步任务同时执行,全部执行完毕,再执行 最终 (
final
) 函数
1 | let arr = [1,2,3,4,5]; |
上面代码,for 循环会同时执行5异步任务,等他们执行完毕再执行 final
函数。 过个并行任务较多,容易耗尽系统资源,拖慢运行
并串结合
限制并行执行任务的数量,避免占用过多系统资源
1 | let items = [1,2,3,4,5]; |
上面代码,最多只能运行两个异步任务,当前 running
记录运行的任务数量,低于门槛 limit 就会新增一个任务,直到任务执行完毕。
轮询
1 | let hash = window.location.hash; |
setInterval
时间 间隔是” 开始执行 “ 之间的间隔,不会考虑每次执行的任务时间,所以两次执行间隔会小于指定时间。比如 指定 100ms 任务本身消耗 105 ms,那么第一次执行完毕后,第二次会立即执行。
如果要固定间隔,可以使用 setTimeout
1
2
3
4
5
6
7
8let hash = window.location.hash;
const hashWatcher = () =>{
if(window.location.hash != hash){
updatePage()
}
setTimeout(hashWatcher,1000)
}
setTimeout(hashWatcher,1000)
注意:
HTML标准规定
setTimeout
最小间隔 4ms
由于定时器每执行一次,会返回一个整数,连续执行,返回值比上一次大1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 (function() {
// 每轮事件循环检查一次
var gid = setInterval(clearAllTimeouts, 1000);
function clearAllTimeouts() {
var id = setTimeout(function() {
console.log("g:"+gid)
},0);
while (id > 0) {
if (id !== gid) {
clearTimeout(id);
}
id--;
}
}
})();
防抖(debounce
)
在某些场景下不希望事件执行太频繁,我们可以设置一个阀门临界值,再一段时间内只执行一次,或者一段时间过后执完所有任务
1 | //一段时间只执行一次 |