什么是进程?
一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该 程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。
早期浏览器是单进程浏览器,后来慢慢发展演化为多进程的浏览器。 一个进程上可以有多个线程,叫做多线程。一个进程上只有一个线程是单线程。
单线程VS多线程
线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率。
进程中的任意一线程执行出错,都会导致整个进程的崩溃。
线程之间共享进程中的数据。
现在的浏览器是多线程的
浏览器渲染进程(Renderer进程,内部是多线程的):每一个标签页的打开都会创建一个浏览器渲染进程(浏览器内核)。默认每个Tab页面一个进程,互不影响。主要作用为页面渲染,脚本执行,事件处理等。
在线程上接收并执行新的任务,采用事件循环机制
要想在线程运行过程中,能接收并执行新的任务,就需要采用事件循环机制。
在线程运行
-
- 添加一个消息队列;
-
- IO 线程中产生的新任务添加进消息队列尾部;
-
- 渲染主线程会循环地从消息队列头部中读取任务,执行任务。
,我们添加了一个消息队列的对象,然后在主线程的 for 循环代码块中,从 消息队列中读取一个任务,然后执行该任务,主线程就这样一直循环往下执行,因此只要消 息队列中有任务,主线程就会去执行。
主线程执行的任务都全部从消息队列中获 取。所以如果有其他线程想要发送任务让主线程去执行,只需要将任务添加到该消息队列中 就可以了。
在线程中如何处理其他线程传过来的任务
渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,主线程从消息队列中获取 消息队列中的任务。
任务 类型有哪些包括 输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JavaScript 定时 器等等。
通常我们把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列
- 如果有一些确定好的任务,可以使用一个单线程来按照顺序处理这些任务,这是第一版线 程模型。
- 要在线程执行过程中接收并处理新的任务,就需要引入循环语句和事件系统,这是第二版 线程模型。
- 如果要接收其他线程发送过来的任务,就需要引入消息队列,这是第三版线程模型。
- 如果其他进程想要发送任务给页面主线程,那么先通过 IPC 把任务发送给渲染进程的 IO 线程,IO 线程再把任务发送给页面主线程。
- 消息队列机制并不是太灵活,为了适应效率和实时性,引入了微任务。
- 基于消息队列的设计是目前使用最广的消息架构,无论是安卓还是 Chrome 都采用了类似 的任务机制。
浏览器怎么实现 setTimeout
过了多少毫秒之后执行一个事件。
渲染进程中所有运行在主线程上的任务都需要先添加到消息队列,然后事件循环系统再按照顺序执行消息队列中的任务。
但是消息队列中的任务是不能等待的,所以我们需要把setTimeout添加到一个延迟队列中。
事件循环系统,首先执行消息队列中的任务,处理完消息队列中 的一个任务之后,就开始执行 ProcessDelayTask 函数。ProcessDelayTask 函数会根据发 起时间和延迟时间计算出到期的任务,然后依次执行这些到期的任务。等到期的任务执行完 成之后,再继续下一个循环过程。通过这样的方式,一个完整的定时器就实现了。
当一个定时器 的任务还没有被执行的时候,也是可以取消的,具体方法是调用clearTimeout 函数.即通过 ID 查找到对应的任务,然后再将其从队列中删除即可。
setTimeOut的一些注意点
- 如果当前任务执行时间过久,会影延迟到期定时器任务的执行。
即当消息队列中的任务执行了很长事件,延时队列需要等待消息队列中的任务执行完。
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。 - 如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒。
是因为在 Chrome 中,定时器被嵌套调用5次以上,系统会判断该函数方法被阻塞了,如果定时器的调用时间间隔小于4毫秒,那么浏览器会将每次调用的时间间隔设置为4 毫秒。
```
function cb() {
setTimeout(cb, 0);
}
setTimeout(cb, 0);
```
-
延时执行时间有最大值
function showName(){ console.log(" 11 ") } var timerID = setTimeout(showName,2147483648); // 会被理解调用立即执行 -
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 对象需要返回到最 外层,这样就可以摆脱嵌套循环了。