好的app设计网站有哪些,深圳公司注册地址要求,免费做苗木的网站,营销策划方案怎么做看完渡一的课后#xff0c;感觉这块内容确实非常重要#xff0c;写 JS 的连 JS 的执行原理都不知道可不行。 事件循环
在写 JS 的时候#xff0c;你有没有想过 JS 是按照什么顺序执行的#xff1f;浏览器是怎么执行 JS 代码的#xff1f;为什么有时候代码没有按照我们认为… 看完渡一的课后感觉这块内容确实非常重要写 JS 的连 JS 的执行原理都不知道可不行。 事件循环
在写 JS 的时候你有没有想过 JS 是按照什么顺序执行的浏览器是怎么执行 JS 代码的为什么有时候代码没有按照我们认为的顺序执行JS 作为解释型脚本语言怎么能用上定时器、回调函数之类的操作
其实浏览器背后隐藏着一个精密而复杂的机制那就是事件循环。这个机制使得网页能够响应用户的操作同时保持了界面的流畅性和高效性。事件循环是现代前端开发中至关重要的概念之一它负责管理各种异步操作例如用户输入、网络请求和定时器等。这是浏览器层面的做前端必须知道的东西。
深入了解浏览器中的事件循环将使我们能够更好地理解JavaScript在前端开发中的工作原理。本文会详细解析事件循环的内部机制同时提供实用的示例来帮助你更好地利用这个机制来构建出色的交互体验。
一、进程模型
为了理解事件循环的作用域不得不提到一些操作系统的底层概念——进程、线程理解不好这些概念肯定理解不了事件循环。我尽量简略地解释这里理解得多事件循环理解得也越快越好。
当然进程、线程的概念也是极为重要的底层知识如果你完全不了解这些最好可以先看看详细解释。
何为进程
程序运行需要有自己专属的内存空间可以把这块内存空间简单理解成进程每个应用至少有一个进程进程之间相互独立即使要通信也要双方同意
在操作系统中进程指的是正在运行的程序的实例它包括了程序的代码、数据以及程序执行时所需要的资源。每个进程都有它自己独立的内存空间可以同时执行不同的任务。并且一个程序可以占用多个进程其中有一个主进程是在程序运行之初就被操作系统启动的而另外的进程都是这个进程启动来为他分担别的任务的。它负责管理系统资源、调度任务的执行顺序、以及为程序提供必要的环境。
每个进程都拥有独立的内存空间它们之间不会直接共享内存因此彼此之间互相隔离。这也是为什么在一个进程中的变量不能直接被另一个进程所访问的原因但是他们如果达成了一定约定双发都同意消息的传递那么是可以互相通信传送数据的。
何为线程
有了进程之后就可以运行代码了运行的代码可以成为线程他在进程环境中运行一个进程至少有一个线程进程在开启之后会自动创建一个线程来运行代码该线程被成为主线程如果需要同时执行多个代码即并行去执行多个操作主线程就会启动更多的线程去执行另外的代码
线程是程序执行的最小单位它是进程中的一个独立执行流程。一个进程可以包含多个线程可以同时执行不同的任务这些线程共享相同的内存空间和其他资源所以可以很容易互相通信、相互协调。
浏览器的进程和线程
浏览器是一个多进程、多线程的应用程序为了避免相互影响启动浏览器之后会运行多个进程同时也因为浏览器是一个异常复杂的软件只用几个进程很难协调好工作浏览器有三个重要进程 浏览器进程界面显示、用户交互、子进程管理等等其中会启动多个线程处理不同的任务网络进程负责加载网络资源同样在该进程中会启动多个线程来处理不同的网络任务渲染进程1个标签页一个渲染进程 渲染进程 启动后会开启一个渲染主线程主线程执行HTML、CSS、JS代码默认情况下浏览器会每个标签页开启一个全新的渲染进程保证不同的标签页之间不会相互影响后面可能会改变这种模式有太多进程的时候占用大量资源 为什么要一个页面一个进程 因为用户是很容易多开很多页面的如果这么多个页面公用一块内存空间也就是公用同一个进程很容易出现一个页面出 Bug 把内存卡崩了一整个进程都卡死整个浏览器都得重启因为所有页面都用不了了。 但是一个页面一个进程可以让一个页面的异常不会影响到其他页面。比如你平时使用浏览器不会因为知乎的网站崩了把旁边的CSDN也卡死你照样可以用CSDN并且只用重新打开知乎。 关于进程和线程就说这么多因为这个并不是本文的重点。
二、渲染主线程
如何工作的
渲染主线程是工作量最大的线程需要处理的程序包括但不限于
解析HTML解析CSS计算样式布局处理图层每秒刷新页面60次渲染帧执行全局 JS 代码执行事件处理函数执行计时器的回调函数… 思考为什么浏览器不用多个线程来处理上述任务 这么多的任务如何调度
例如
正在执行一个JS函数执行到一半的时候用户点击了按钮我该立即去执行点击事件的处理函数吗我正在执行一个JS函数执行到一半的时候某个计时器到达了时间节点我该立即去执行这个计时器的回调吗浏览器进程监听到用户点击了某个按钮但同时某个计时器也到达了时间节点我应该处理哪儿一个呢…
为了解决上面的一些调度问题渲染主线程采用了“排队”的方式。
我们把所有要做的事一件一件统称为任务渲染主线程的动作可以看做是对一个接一个的任务的响应和执行。当渲染主线程正在执行一个任务的时候到来的所有需要执行的任务都会进入一个消息队列事件队列每到来一个任务该任务在前一个任务没有执行完的时间中都会在这个队列中排队等候。
下面是渲染主线程的主要工作
在渲染主线程启动的时候都会进入一个无限的循环在每一循环中都会去查询上面提到的消息队列中是否有未执行的任务在等待如果有就取出第一个任务也就是最早到来的任务开始执行执行完后进入下一个循环如果没有则进入休眠状态其他所有的线程都能随时往消息队列中添加任务。新任务会添加到消息队列的末尾如果这个时候主线程是休眠状态则主线程会被唤醒开始循环并在循环中获取任务去执行
事件循环消息循环的主要步骤就是上面三点保证了页面能够正常执行事件完成功能。
更深理解
上面讲述了事件循环的大致概念和步骤下面解释一些更细节的东西可以让我们理解得更深入。
什么是异步
前端写 JS 总是绕不开同步和异步同步很好理解就是一步一步从上到下一行一行执行 js 代码那什么是异步
代码在执行过程中可能遇到一些没有办法立即执行的任务例如
计时器结束后触发回调任务setTimeout、setInterval…网络通信完成后需要执行的任务向后端发送请求后的操作…用户操作后需要执行的任务addEventListener…
如果让渲染主线程等待每个任务执行完再执行下一个任务那么可能会浪费大量时间影响页面正常运行。例如假如设置了一个一分钟的定时器在消息队列中取到这个任务的时候不可能一直等待直到一分钟后执行完该定时器任务后才执行下一个任务这样等待的这一分钟内什么事儿都不干完全浪费掉了甚至导致页面卡死。 简单提一下计时器的工作原理 在计时器开始被调用的时候计时器会通知计时线程让计时线程开始计时。主线程和计时线程是并行执行的同属与页面进程。 如果采用一个接一个上一个任务全部执行完再执行下一个任务这种思路就是同步虽然这样可以保证时间线单一不混乱但是如上面的例子所说问题十分严重。所以渲染主线程并不是这么工作的。
setTimeout(() {console.log(计时器结束)
}, 3000)
console.log(1)如果浏览器是同步执行的那么 JS 代码是从上到下上一个代码块执行完才会执行下一行代码块那么上面的代码会先输出“计时器结束“再输出1。如果是异步的那么打印顺序应该是反过来的先打印1再打印“计时器结束”。可以自己试一试上面的测试代码。
实际上上面的测试代码的运行原理是这样的主线程触发计时器之后会立刻获取下一个任务当计时线程计时结束后计时线程是不会通知主线程的而是直接将回调函数加入到消息队列。所以主线程虽然不能直接知道计时器已经结束但是任然可以从消息队列中知道该何时执行计时器的回调函数。 对上面的知识来个总结概述 JS 是一门单线程的语言这是因为他运行在浏览器的渲染主线程中而渲染主线程只有一个。 主线程承担着许多工作例如渲染页面、执行 JS 等等 如果采用同步的方式极可能会导致主线程产生阻塞从而导致消息队列中很多任务无法执行浪费大量时间甚至导致页面卡顿无法刷新、崩溃。 所以浏览器采用异步的方式避免阻塞问题。当某些需要等待的任务发生时比如计时器、网络、事件监听主线程将任务交给其他线程去处理自身立即结束当前任务进入下一个循环从消息队列中获取并执行下一个任务。当其他线程完成任务后将事先传递的回调函数包装成任务任务是一个对象不能直接把回调函数加入消息队列添加到消息队列的队尾等待主线程执行。 这样最大限度的保证了单线程的流畅运行。 JS为何会阻碍渲染
假如你有一个页面其中的主要内容如下
/*** html:* h1hello/h1* buttonclick/button*/const h1 document.querySelector(h1)
const btn document.querySelector(button)const delay (duration) {const start Date.now()while(Date.now() - start duration) {}
}btn.onclick () {h1.textContent hello worlddelay(3000)
}在打开这个页面后我们通过“事件循环”的角度来分析一下
首先通过docuemnt.querySelector获取了两个元素实例设置了一个延时函数在按钮上绑定了一个事件渲染主线程将监听的工作交给交互线程去执行交互线程等待按钮被点击
在页面加载完毕后我们点击了按钮会发现一个神奇的现象标题中的文字内容并没有直接从 hello 变成 helloworld而是在等待了三秒后才变化。这是为什么
点击按钮交互线程监听到了马上将点击事件的回调包装成任务加入消息队列主线程执行到了交互线程添加的点击回调任务开始执行并执行到h1.textContent hello world上面异步执行的代码成功将元素的文本内容修改了但是修改了不是马上就能被同步到页面的而是在执行完这一步后马上生成一个“绘制”任务添加到消息队列只有等“绘制”任务执行页面重绘后才能看见。但是这个时候html和页面是统一的并不是html内容改了但是页面显示有问题点击的回调继续执行调用delay函数延迟三秒。这个delay并没有生成新任务而是在主线程当前执行的点击回调任务中执行的所以得等他执行完之后才能获取下一个任务也就是第三步生成的“重绘”任务 虽然这个问题浏览器还不能很好解决但是前端的一些框架已经做出了一定优化例如 React 会监听一段 JS 的运行时间不会让某些无用的 JS 持续太长时间。 任务的优先级
任务有没有优先级有没有加急的任务
很可惜任务是不区分优先级的所有任务都是一视同仁该排队就得排队。
但是消息队列是有优先级的队列不是只有一个。最新W3C标准优化了之前宏任务微任务的架构 每个任务都有一个任务类型同一个类型的任务必须都在同一个队列不同类型的任务可以分属于不同的队列例如网络任务和交互任务可以都放在A队列但是有新的网络任务或者新的交互任务那么必须放在A队列而不能放在B队列注意区分“一个队列只能放同一种任务”这种说法这种是错误的理解在一次时间循环中可以根据实际情况从不同的队列中取出任务这个就看不同的浏览的不同实现和策略了 浏览器必须准备好一个微队列微队列中的任务具有最高的优先级 不再只使用宏队列和微队列两个队列无法应对当前浏览器的复杂度了 目前 chrome 的实现中至少包含了下面的队列 延时队列用于存放计时器到达后的回调任务优先级中交互队列用于存放用户操作后产生的事件处理任务优先级高微队列用户存放需要最快执行的任务优先级最高 添加任务到微队列的主要方式 Promise、MutationObserver 例如 // 将一个函数立即添加到微队列
Promise.resolve().then(() {console.log(1)
})一个小题目输出顺序是什么 const a () {console.log(1)Promise.resolve().then(() {console.log(2)})
}setTimeout (() {console.log(3)Promise.resolve().then(a)
}, 0)Promise.resolve().then(() {console.log(4)
})console.log(5)浏览器还有很多队列但是和开发关系弱一些就不说了
三、总结
主要总结两部分
JS 的事件循环
事件循环又叫消息循环是浏览器渲染主线程的工作方式。
在Chrome的源码中主线程开启一个死循环for(;;)每次循环都会从消息队列中取出第一个任务并执行而且他线程不需要和主线程通信只需要将任务添加到消息队列即可让主线程执行对应 JS。
过去把消息队列简单分为宏队列和微队列但现在已经无法满足复杂的浏览器环境现在的消息队列有更多的分类。
JS 的计时器能精确计时吗
不能
计算器硬件限制操作系统本身有时间上的偏差而 JS 计时器本质上是调用操作系统的时间系统按照 W3C 的标准浏览器实现的计时器如果嵌套层级超过5层则会带有 4 毫秒的最少时间导致在计时时间少于 4 毫秒时有一定偏差事件循环决定了计时器的回调只能在主线程空闲的时候运行而不能直接打断主线程当前运行的任务