事件循环

221 阅读8分钟

什么是进程?

一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该 程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。

早期浏览器是单进程浏览器,后来慢慢发展演化为多进程的浏览器。 一个进程上可以有多个线程,叫做多线程。一个进程上只有一个线程是单线程。

单线程VS多线程

线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率。

进程中的任意一线程执行出错,都会导致整个进程的崩溃。

线程之间共享进程中的数据。

现在的浏览器是多线程的

浏览器渲染进程(Renderer进程,内部是多线程的):每一个标签页的打开都会创建一个浏览器渲染进程(浏览器内核)。默认每个Tab页面一个进程,互不影响。主要作用为页面渲染,脚本执行,事件处理等。

在线程上接收并执行新的任务,采用事件循环机制

要想在线程运行过程中,能接收并执行新的任务,就需要采用事件循环机制。

在线程运行

    1. 添加一个消息队列;
    1. IO 线程中产生的新任务添加进消息队列尾部;
    1. 渲染主线程会循环地从消息队列头部中读取任务,执行任务。

,我们添加了一个消息队列的对象,然后在主线程的 for 循环代码块中,从 消息队列中读取一个任务,然后执行该任务,主线程就这样一直循环往下执行,因此只要消 息队列中有任务,主线程就会去执行。

主线程执行的任务都全部从消息队列中获 取。所以如果有其他线程想要发送任务让主线程去执行,只需要将任务添加到该消息队列中 就可以了。

在线程中如何处理其他线程传过来的任务

渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,主线程从消息队列中获取 消息队列中的任务。

任务 类型有哪些包括 输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JavaScript 定时 器等等。

通常我们把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列

  • 如果有一些确定好的任务,可以使用一个单线程来按照顺序处理这些任务,这是第一版线 程模型。
  • 要在线程执行过程中接收并处理新的任务,就需要引入循环语句和事件系统,这是第二版 线程模型。
  • 如果要接收其他线程发送过来的任务,就需要引入消息队列,这是第三版线程模型。
  • 如果其他进程想要发送任务给页面主线程,那么先通过 IPC 把任务发送给渲染进程的 IO 线程,IO 线程再把任务发送给页面主线程。
  • 消息队列机制并不是太灵活,为了适应效率和实时性,引入了微任务。
  • 基于消息队列的设计是目前使用最广的消息架构,无论是安卓还是 Chrome 都采用了类似 的任务机制。

浏览器怎么实现 setTimeout

过了多少毫秒之后执行一个事件。

渲染进程中所有运行在主线程上的任务都需要先添加到消息队列,然后事件循环系统再按照顺序执行消息队列中的任务。

但是消息队列中的任务是不能等待的,所以我们需要把setTimeout添加到一个延迟队列中。

事件循环系统,首先执行消息队列中的任务,处理完消息队列中 的一个任务之后,就开始执行 ProcessDelayTask 函数。ProcessDelayTask 函数会根据发 起时间和延迟时间计算出到期的任务,然后依次执行这些到期的任务。等到期的任务执行完 成之后,再继续下一个循环过程。通过这样的方式,一个完整的定时器就实现了。

当一个定时器 的任务还没有被执行的时候,也是可以取消的,具体方法是调用clearTimeout 函数.即通过 ID 查找到对应的任务,然后再将其从队列中删除即可。

setTimeOut的一些注意点

  1. 如果当前任务执行时间过久,会影延迟到期定时器任务的执行。 即当消息队列中的任务执行了很长事件,延时队列需要等待消息队列中的任务执行完。
    function bar() {
     console.log('bar')
    }
    function foo() {
     setTimeout(bar, 0);
     for (let i = 0; i < 5000; i++) {
     let i = 5+8+8+8
     console.log(i)
     }
    }
    foo()
    循环执行结束之后才会立即执行setTimeOut。
    
  2. 如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒。

是因为在 Chrome 中,定时器被嵌套调用5次以上,系统会判断该函数方法被阻塞了,如果定时器的调用时间间隔小于4毫秒,那么浏览器会将每次调用的时间间隔设置为4 毫秒。

```
function cb() { 
    setTimeout(cb, 0);
}
setTimeout(cb, 0);

```
  1. 延时执行时间有最大值

    function showName(){
        console.log(" 11 ")
    }
    var timerID = setTimeout(showName,2147483648);
    // 会被理解调用立即执行
    
    
  2. setTimeOut this默认非严格模式指向window

    var name= 1;
    var MyObj = {
     name: 2,
     showName: function(){
     console.log(this.name);//打印结果是1,this获取的是window对象。
     }
    }
    setTimeout(MyObj.showName,1000)
    
    

    不改变this可以放在匿名函数中调用

    // 箭头函数
    setTimeout(() => {
     MyObj.showName()
    }, 1000);
    // 或者 function 函数
    setTimeout(function() {
     MyObj.showName();
    }, 1000)
    //或者通过bind显式声明改变this指向
    setTimeout(MyObj.showName.bind(MyObj), 1000)
    

setTimeOut的问题

setTimeout 在时效性上面有很多先天的不足,所以对于一 些时间精度要求比较高的需求


同步回调VS异步回调

同步回调

同步回调就是在当前主函数的上下文中执行回调函数。

let callback = function(){
 console.log('i am do homework')
}
function doWork(cb) {
 console.log('start do work')
 cb()
 console.log('end do work')
}
doWork(callback)

异步回调(最后执行callback函数)

  • 第一种是把异步函数做成一个任务,添加到信息队列尾部;

  • 第二种是把异步函数添加到微任务队列中,这样就可以在当前任务的末尾处执行微任务 了。

let callback = function(){
console.log('i am do homework')
}
function doWork(cb) {
 console.log('start do work')
 setTimeout(cb,1000)
 console.log('end do work')
}
doWork(callback)

XMLHttpRequest

XMLHttpRequest 发起请求,是由浏览器的其他进程或者线程去执行,然后再将执行结果,利用 IPC 的方式通知渲染进程,之后渲染进程再将对应的消息添加到消息队列中。

宏任务VS微任务

所有添加到消息队列中的任务都是宏任务,宏任务主要包括,

  • 渲染事件(如解析 DOM、计算布局、绘制);
  • 用户交互事件(如鼠标点击、滚动页面、放大缩小等);
  • JavaScript 脚本执行事件;
  • 网络请求完成、文件读

setTimeOut和XMLHttpRequest也是宏任务,setTimeOut通过把任务的回调函数添加到延迟队列中,XMLHttpRequest通过把任务添加到消息队列的末尾。

主进程在读取消息队列任务的时候,是按照顺序读取的。JavaScript 代码不能准确掌控任务要添加到队列中的位置,控制不了任务在消息队列中的 位置,所以很难控制开始执行任务的时间。

每个宏任务中都有一个微任务队列。 微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。

Promise

Promise解决的问题,

  • 回调地狱,嵌套太多层,代码凌乱不容易维护。
  • 是合并多个任务的错误处理,每个任务都有两种可能的结果(成功或者失败),体现在代码中就需要对每个任务的执行结果做两次判断,代码冗余。

Promise的实现

  • 1返回值穿透
  • 2回调函数延迟调用
  • 3错误冒泡

Promise 通过回调函数延迟绑定、回调函数返回值穿透和错误“冒泡”技术解决了上面的 两个问题。

构建 Promise 对象时,需要传入一个executor 函数,主要业务流程都在 executor 函数中执行。

如果运行在 excutor 函数中的业务执行成功了,会调用 resolve 函数;如果执行失败了,则调用 reject 函数。

在 excutor 函数中调用 resolve 函数时,会触发 promise.then 设置的回调函数;而调 用 reject 函数时,会触发 promise.catch 设置的回调函数。

产生嵌套函数的一个主要原因是在发起任务请求时会带上回调函数,这样当任务处理结束之后,下个任务就只能在回调函数中来处理 了。

Promise 实现了回调函数的延时绑定。

回调函数的延时绑定在代码上体现就是先创建 Promise 对象 x1,通过 Promise 的构造函数 executor 来执行业务逻辑;创建好Promise 对象 x1 之后,再使用 x1.then 来设置回调函数。

将回调函数 onResolve 的返回值穿透到最外层

因为我们会根据 onResolve函数的传入值来决定创建什么类型的 Promise 任务,创建好的 Promise 对象需要返回到最 外层,这样就可以摆脱嵌套循环了。