js执行机制

一. JS代码虽然是单线程的,但是浏览器是多线程的
所以人们就想到可以调用浏览器的API,把需要异步运行的代码扔给浏览器。
等到浏览器运行完,再塞回来js线程中,然后继续运行 (异步的原理)。
二.事件循环JS 会创建一个类似于 while (true) 的循环,每执行一次循环体的过程称之为 Tick。每次 Tick 的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行。待处理的事件会存储在一个任务队列中,也就是每次 Tick 会查看任务队列中是否有需要执行的任务。
任务队列:异步操作会将相关回调添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同,如 onclick, setTimeout, ajax 处理的方式都不同,这些异步操作是由浏览器内核的 webcore 来执行的,webcore 包含上图中的3种 webAPI,分别是 DOM Binding、network、timer模块。
  onclick 由浏览器内核的 DOM Binding 模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。
  setTimeout 会由浏览器内核的 timer 模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。
  ajax 则会由浏览器内核的 network 模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中。
主线程:JS 只有一个线程,称之为主线程。而事件循环是主线程中执行栈里的代码执行完毕之后,才开始执行的。所以,主线程中要执行的代码时间过长,会阻塞事件循环的执行,也就会阻塞异步操作的执行。只有当主线程中执行栈为空的时候(即同步代码执行完后),才会进行事件循环来观察要执行的事件回调,当事件循环检测到任务队列中有事件就取出相关回调放入执行栈中由主线程执行。
下图就是主线程和任务队列的示意图。

只要主线程空了,就会去读取”任务队列”,这就是JavaScript的运行机制。这个过程会不断重复。

三、事件和回调函数

“任务队列”是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在”任务队列”中添加一个事件,表示相关的异步任务可以进入”执行栈”了。主线程读取”任务队列”,就是读取里面有哪些事件。

“任务队列”中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入”任务队列”,等待主线程读取。

所谓”回调函数”(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

“任务队列”是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,”任务队列”上第一位的事件就自动进入主线程。但是,由于存在后文提到的”定时器”功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

四、Event Loop

主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

为了更好地理解Event Loop,请看下图。



五.seTimeout
setTimeout(function(){},0);
// 如果有大量的操作,可能会阻塞 UI 等,则可以使用 setTimeout 让这些操作在主线程把更重要的代码执行完毕之后,再来执行这里的操作。从而提高浏览器的性能。
// 设置为 0,也会有个最小间隔值,也会在主线程中的代码运行完成后,由事件循环从任务队列将回调添加到执行栈中才执行
应该值得注意的是: setTimeout 也有个小细节,第二个参数设置为 0 也许会有人认为这样就不是异步了,其实还是异步。这是因为 HTML5 标准规定这个
函数第二个参数不得小于 4 毫秒,不足会自动增加。

setTimeout(function () { func1() }, 0)
setTimeout(function () { func1() })
有什么差别?
0秒延迟,此回调将会放到一个能立即执行的时段进行触发。javascript代码大体上是自顶向下的,但中间穿插着有关DOM渲染,事件回应等异步代码,他们将组成一个队列,零秒延迟将会实现插队操作。
不写第二个参数,浏览器自动配置时间,在IE,FireFox中,第一次配可能给个

很大的数字,100ms上下,往后会缩小到最小时间间隔,Safari,chrome,opera则多为10ms上下。
setTimeout的使用:
在业务中,基本上是禁止在业务逻辑中使用setTimeout的,因为我所看到的很多使用方式都是一些问题不好解决,setTimeout作为一个hack的方式。
例如,当一个实例还没有初始化的前,我们就使用这个实例,错误的解决办法是使用实例时加个setTimeout,确保实例先初始化。
为什么错误?这里其实就是使用hack的手段
第一是埋下了坑,打乱模块的生命周期
第二是出现问题时,setTimeout其实是很难调试的。
我认为正确的使用方式是,看看生命周期(可参考《 关于软件的生命周期 》),把实例化提到使用前执行。
综上,setTimeout其实想用好还是很困难的, 他更多的出现是在框架和类库中,例如一些实现Promis的框架,就用上了setTimeout去实现异步。

setTimeout中的this
setTimeout(code, ms)的code函数或者代码块最终是在全局上下文执行的,所以如果代码中有用到this就需要注意了。
var foo = {name: '噪点,lmqy.net',sayName: function(){setTimeout(function(){console.log(this.name)}, 200);}}foo.sayName(); //undefined
上面foo.sayName()输出undefined还是比较好理解的,但还有一种情况
var name = 'global';var foo = {name: '噪点,lmqy.net',sayName: function(){console.log(this.name)}}setTimeout(foo.sayName, 200);
最后输出的是'global',跟上面的情况不同,上面那种是匿名函数在全局下面执行的,this肯定指向全局(window),而这里是foo.sayName()按理this应该是foo才对,自己感觉应该问题还是在setTimeout(foo.sayName, 200);这步的时候应该是把foo.sayName的函数返回了,所以最终执行的是foo.sayName的函数体而不是foo.sayName,
setINterval:
setInterval间歇调用,是在前一个方法执行前,就开始计时,比如间歇时间是500ms,那么不管那时候前一个方法是否已经执行完毕,都会把后一个方法放入执行的序列中。这时候就会发生一个问题,假如前一个方法的执行时间超过500ms,加入是1000ms,那么就意味着,前一个方法执行结束后,后一个方法马上就会执行,因为此时间歇时间已经超过500ms了。

①回调函数
这是异步编程最基本的方法。
假定有两个函数f1和f2,后者等待前者的执行结果。
  f1();
  f2();
如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数。
  function f1(callback){
    setTimeout(function () {
      // f1的任务代码
      callback();
    }, 1000);
  }
执行代码就变成下面这样:
  f1(f2);
采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。
回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度 耦合 (Coupling),流程会很混乱,而且每个任务只能指定一个回调函数。


回调函数还有一个问题就是我们在回调函数之外无法捕获到回调函数中的异常
Tips: 为什么异步代码回调函数中的异常无法被最外层的try/catch语句捕获?
注:Trycatch只能捕获当前事件循环
异步调用一般分为两个阶段,提交请求和处理结果,这两个阶段之间有事件循环的调用,它们属于两个不同的事件循环(tick),彼此没有关联。
异步调用一般以传入callback的方式来指定异步操作完成后要执行的动作。而异步调用本体和callback属于不同的事件循环。
try/catch语句只能捕获当次事件循环的异常,对callback无能为力。
也就是说,一旦我们在异步调用函数中扔出一个异步I/O请求,异步调用函数立即返回,此时,这个异步调用函数和这个异步I/O请求没有任何关系。

②事件监听
另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
还是以f1和f2为例。首先,为f1绑定一个事件(这里采用的jQuery的 写法 )。
  f1.on('done', f2);
上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:
  function f1(){
    setTimeout(function () {
      // f1的任务代码
       f1.trigger('done');
    }, 1000);
  }
f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2。
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以 "去耦合" (Decoupling),有利于实现 模块化 。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。
③发布/订阅
上一节的"事件",完全可以理解成"信号"。
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做 "发布/订阅模式" (publish-subscribe pattern),又称 "观察者模式" (observer pattern)。
这个模式有多种 实现 ,下面采用的是Ben Alman的 Tiny Pub/Sub ,这是jQuery的一个插件。
首先,f2向"信号中心"jQuery订阅"done"信号。
  jQuery.subscribe("done", f2);
然后,f1进行如下改写:
  function f1(){
    setTimeout(function () {
      // f1的任务代码
       jQuery.publish("done");
    }, 1000);
  }
jQuery.publish("done")的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。
此外,f2完成执行后,也可以取消订阅(unsubscribe)。
  jQuery.unsubscribe("done", f2);
这种方法的性质与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
④Promises对象
Promises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供 统一接口
简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。比如,f1的回调函数f2,可以写成:
  f1().then(f2);
f1要进行如下改写(这里使用的是jQuery的 实现 ):
  function f1(){
    var dfd = $.Deferred();
    setTimeout(function () {
      // f1的任务代码
      dfd.resolve();
    }, 500);
     return dfd.promise;
  }
这样写的优点在于,回调函数变成了链式写法,程序的流程可以看得很清楚, 可以实现许多强大的功能。
比如,指定多个回调函数:
  f1().then(f2).then(f3);
再比如,指定发生错误时的回调函数:
  f1().then(f2).fail(f3);
而且,它还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,你不用担心是否错过了某个事件或信号。这种方法的缺点就是编写和理解,都相对比较难。

(前端小白,如有错误,欢迎指正~~)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值