JS重学— 事件循环

307 阅读7分钟

这是我参与更文挑战的第16天,活动详情查看: 更文挑战

JavaScript基础知识包括:JS变量,类型,类型转换,运算符,表达式,直接量,语句,函数,函数表达式,函数参数,构造函数,对象创建,对象字面量,引用类型,数组,数组排序,栈,队列,包装类型,字符串操作, Math对象,全局对象,上面基本上都是JS的基础知识,在这篇自学里面没有,这篇重学主要讲的是JS的高级内容。

1, 线程,进程

(1)一个进程就是一个程序,比如:用浏览器打开一个网页,就是开启了一个进程,打开五个网页,就是五个进程。打开QQ也是一个进程

(2)一个进程的运行,可以有很多个线程互相配合,比如打开微信这个进程,里面有:接收消息线 程,文件传输线程,等等

打开一个能够正常运行交互的网页,也需要很多线程:

JS 引擎线程, 
GUI渲染线程
事件轮询处理线程
定时器触发线程
异步请求线程
浏览器事件线程

注意1:其中JS引擎线程,我们称之为:主线程, 这个线程就是运行JS代码的线程。

注意2:主线程运行JS代码的时候,会生成执行栈

2, 栈内存,堆内存

(1)栈内存:存放简单类型的变量名称,变量值,还有引入类型的堆地址

(2)堆内存: 存放引入类型,复杂类型,可以理解为存放:JS的对象和数组,函数

(3)值类型和引用类型复制和传递

引用类型的赋值操作:是将引用类型的堆上的内存地址进行复制

参数:如果是简单类型会做一个值类型的数组副本传到函数内部

​ 如果是引入类型,会将引入类型的地址值复制给传入函数的参数

(4)函数的参数

参数匹配从左向右进行匹配,如果实惨个数少于形参,后面的参数对应复制undefineds, 
实参个数如果多于形参的个数,可以通过arguments访问, arguments是一个类数组,

注意: 函数的length的属性是指形参的个数
      arguments也有一个length属性,属性值就是传递实参个数
      
技巧:函数参数如果过多,在传递的时候,可以封装成对象进行传递

3, 事件循环机制

知识预热:

1,JS是单线程的,同一时间只能做一件事情

思考一个问题:JavaScript为什么是单线程?

JavaScript是单线程,和它的用途有关。JS用于实现用户交互,操作DOM为主,这就决定了它只能是单线程,否则会
带来很多复杂的同步问题,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除
了这个节点,这时浏览器应该以哪个线程为准,所以JS必须是单线程,为了利用多核CPU的计算能力,HTML5提出Web 
Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标
准并没有改变JavaScript单线程的本质

2,什么是任务队列?

队列的数据结构:先进先出

Array.prototype.push()  从数组的尾部插入元素
Array.prototype.shift() 从数组头部弹出一个元素
 单线程就意味着,所以任务需要排队完成,只有前一个任务结束,才会执行后一个任务。除了主线程之外,还存在一
 个任务队列,这个任务队列存放的是什么东西,异步任务有了运行结果,就在任务队列之中放置一个事件。

4, 什么是同步任务?

在主线程上排队执行的任务,只有前一个任务执行完了,才能执行后一个任务,所有的同步任务都在主线程上执行,形
成一个执行栈。一旦执行栈中所有的同步任务都执行完毕了,系统就会去读取任务队列,并把在任务队列中的事件放入
执行栈中,开始执行

5,什么是异步任务?

   不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某一个异步任务可以执行了,这个任务才会进
   入主线程执行。

注意:只要主线程空了,就会去读取任务队列中的事件。主线程丛任务队列中读取事件,这个过程是不断循环的,所以又叫做:事件循环

6,什么是宏任务?

可以理解为:每次执行栈执行的代码就是一个宏任务。 每次从任务队列中获取的回调事件放到执行栈中执行,这也是一个宏任务
  • 宏任务包含: script整体代码, setTimeout, setInterval, postMessage, setImmediate, UI 渲染

I/O

7, 什么是微任务?

微任务,就是指在当前宏任务执行后需要立即执行的任务
  • 微任务包含: promisse. then() , process.nextTick()

注意:微任务是跟屁虫,一直跟着当前宏任务后面,

8, 宏任务和微任务执行顺序?

执行顺序: 首先执行主线程 —> 主线程上创建的微任务 —> 主线程上创建的宏任务 —> 主线程上创建的宏任务后面跟的微任务

<script>
    setTimeout(function() {
        console.log('setTimeout');
    })
    new Promise(function(resolve) {
        console.log('promise');
    }).then(function() {
        console.log('then');
    })
    console.log('console');
</script>

总结:执行顺序

1,<script>中的整段代码作为第一个宏任务,进入主线程。即开启第一次事件循环

2,遇到setTimeout,将其回调函数放入Event table中注册,然后分发到宏任务Event Queue中

3,接下来遇到new Promise、Promise,立即执行;

4,promise后面的.then是微任务,将then函数分发到微任务Event Queue中。

5,遇到console.log,立即执行。输出: console

6,整体代码作为第一个宏任务执行结束,此时去微任务队列中查看有哪些微任务,结果发现了then函数,然后将它推入主线程并执行。 输出: then

7,第一轮事件循环结束

8,开启第二轮事件循环。

9,先从宏任务开始,去宏任务事件队列中查看有哪些宏任务,在宏任务事件队列中找到了setTimeout对应的回调函数,立即执行之

9,此时宏任务事件队列中已经没有事件了,然后去微任务事件队列中查看是否有事件,结果没有。此时第二轮事件循环结束;输出:setTimeout

4, 事件和回调函数

任务队列就是一个先进先出的数据结构,它是一个事件的队列,也可以理解为消息队列,排在前面的事件,优先被主线程读取,只要执行栈一清空,任务队列上的第一个事件就会主动进入主线程,这个过程是自动的

1,简单理解就是你定义的函数,你没有调用,但是它最终执行了, 常见的回调函数有:dom事件回调,定时器回调,ajax请求回调函数,生命周期函数回调

​ 所谓的回调,就是那些会被主线程挂起来的事件,异步任务必须指定回调函数,当主线程开始执行异步任务的时候,就是执行对应的回调函数。

2, IIFE(立即执行函数)

隐藏实现,不会污染外部命名空间,用它来编码JS模块

;(function() { // 匿名函数
  console.log(11)
})()

5, 总结

JS执行从上往下,最先进入任务队列的宏任务开始,通常是整体代码,整体代码就是第一个宏任务,宏任务队列事件全部执行完毕后,检查微任务队列是否有事件,有则执行,直到没有事件为止,一次事件循环结束,然后渲染更新DOM, 第二次事件循环事件开始。