目录
V8引擎是如何执行JS代码的
- 编译型语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。比如 C/C++、GO 等都是编译型语言。
- 而由解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。比如 Python、JavaScript 等都属于解释型语言。
(1)在编译型语言的编译过程中,编译器首先会依次对源代码进行词法分析、语法分析,生成抽象语法树(AST),然后是优化代码,最后再生成处理器能够理解的机器码。如果编译成功,将会生成一个可执行的文件。但如果编译过程发生了语法或者其他的错误,那么编译器就会抛出异常,最后的二进制文件也不会生成成功。
(2)在解释型语言的解释过程中,同样解释器也会对源代码进行词法分析、语法分析,并生成抽象语法树(AST),不过它会再基于抽象语法树生成字节码,最后再根据字节码来执行程序、输出结果。
(1)V8引擎在执行过程中既有解释器 Ignition,又有编译器 TurboFan
(2)首先对源代码进行词法分析、语法分析 转换成抽象语法树AST并生成执行上下文。高级语言是开发者可以理解的语言,但是让编译器或者解释器不认识,他们只认识AST
如何生成AST树:
(1)第一阶段是分词(tokenize),又称为词法分析,其作用是将一行行的源码拆解成一个个token。所谓token,指的是语法上不可能再分的、最小的单个字符或字符串。
关键字“var”、标识符“myName” 、赋值运算符“=”、字符串“极客时间”四个都是token,而且它们代表的属性还不一样。
(2)第二阶段是解析(parse),又称为语法分析,其作用是将上一步生成的 token 数据,根据语法规则转为 AST。如果源码符合语法规则,这一步就会顺利完成。但如果源码存在语法错误,这一步就会终止,并抛出一个“语法错误”。
(3)解释器根据 AST生成字节码,并解释执行字节码。字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。
(4)通常,如果有一段第一次执行的字节码,解释器会逐条解释执行。在执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。
即时编译(JIT):在执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了
在浏览器中 JS代码是如何被执行的?
- 我们在浏览器地址栏中输入一个url 该url经DNS(域名解析器)解析成服务器ip地址 浏览器向该ip地址发送请求 经TCP三次连接简历请求 服务器处理浏览器的请求 返回静态资源index.html 浏览器解析index.html 在解析的过程中可能遇到css文件 浏览器再去服务器下载css文件 也可能遇到scipt标签 浏览器又向服务器请求该script标签的资源 然后开始执行代码 如何执行?
- 认识浏览器内核:浏览器下载下来的东西非常多 浏览器内核将他们进行解析 变成个网页呈现出来
- 浏览器渲染过程:HTML是第一个被下载下来的 因为你在浏览器地址栏中输入一个url 服务器首先返回的就是index.html 就是个HTML index.html中有很多标签 浏览器内核中有个HTMLparser HTMLparser将HTML转成DOM树 可以使用JS操作DMO树 在这里是JS引擎来执行JS以操作DOM树 浏览器内核中有个CSSparser CSS中有很多规则 CSSparser解析CSS(得到元素的样式) 将这些规则附加到DOM树上 生成渲染树 通过布局引擎对渲染树进行一些具体的操作生成最终的渲染树 然后将最终的渲染树绘制出来 浏览器展示出来
- CSS和HTML已经结合了 为啥还需要布局引擎对渲染树进行一些具体操作:元素再不同宽度浏览器上放的位置不一样 假如我们给一个元素设置了绝对定位 定位到浏览器右下角 假如当前浏览器窗口比较宽 元素放在浏览器右下角 距离左边700px的位置 假如当前浏览器窗口比较窄 元素放在浏览器右下角 距离左边100px的位置 所以最终每个元素放在浏览器窗口哪个位置 展示什么样的效果 是需要当前浏览器所处的状态再进行layout(布局)再做一个布局后生成最终的渲染树
在上面紫色的倒三角框里有些JS代码对DOM树进行操作 我们需要执行这些js代码 因为这些js代码可能对操作DOM树 是JS引擎来执行这些JS代码
- JS是高级语言 不能直接在电脑CPU中执行 所以我们需要借助V8引擎将JS转为机器语言 V8引擎中有个parse 用来解析JS代码 解析主要分为词法分析和语法分析 假如你现在有一段代码:
const name = 'why';
词法分析会将代码中每一个词进行解析 生成很多tokens tokens是一个数组 数组中的放的是对象 对const进行词法分析( const是关键字 用keyword表示)后 tokens中会多出来一个对象{type:'keyword',value:'const'}
接下来对name进行词法分析(name是标识符 用identifier标识)后 tokens中会多出来一个对象{type:'identifier',value:'name'}
……等号 分号都会进行词法分析得到一个个对象 然后根据每个对象的type对对象进行语法分析生成AST抽象语法树
- 为什么要转成抽象语法树:他是树结构 关键字固定 操作更方便 有了抽象语法树 我可以将抽象语法树转成ES5代码 或转成字节码(V8引擎的ignation将抽象语法树转成字节码)等等 更方便 为什么要转成字节码不直接转成机器码?因为这个JS代码运行在哪里是不确定的 他可能运行在windows电脑/mac电脑/Linux电脑的谷歌浏览器上 不同的系统有不同的CPU 不同CPU有不同架构 不同的CPU架构能执行的机器指令不一样 所以ignation并不知道应该把JS代码转成什么类型的机器码 所以先将抽象语法树转成字节码 字节码是跨平台的 不管在什么系统中都可以执行 等真正运行的时候 V8引擎会将字节码转成不同平台上面对应的CPU指令(字节码先转化成对应的汇编指令 再由机器指令去执行相应的机器指令) 如果我要执行JS代码 每次都需要将字节码转化成对应的汇编指令 再由机器指令去执行相应的机器指令 很麻烦 直接将字节码转换成对应平台的机器代码 之后直接执行这个机器代码就可以了
- V8引擎中有一个TurboFan库 ignation会收集函数的执行信息 如果某个函数被执行了很多次 ignation会将其标记为hot TurboFan会将有hot标记的函数转换为优化的机器指令 以后你再需要执行有hot标记的函数时 直接执行机器指令就可以了 就不用再根据字节码转化成对应的汇编指令 再由机器指令去执行相应的机器指令 这样可以提高性能
假如现在有个sum函数 我们会多次调用sum进行两数相加 TurboFan会将其转换为优化的机器指令 假如有次你调用sum函数 传入两个字符串 需要做字符串拼接 再机器语言中 字符串拼接和两数相加用的指令不一样 所以现在原来经TurboFan转换的机器指令不能用了 机器指令会经Deoptimization反向优化变回字节码 按照字节码的方式转化为对应的汇编指令 再由机器指令去执行相应的机器指令 再执行
- Blink就是内核 内核会解析HTML 再解析的过程中遇到了JS 把JS下载下来 blink内核拿到Js之后 将JS代码以流的方式传递给V8引擎 scanner用来做词法分析 将JS代码转换成很多tokens parser把tokens转成AST树 再由ignation转换成字节码 字节码再转成CPU能认识的指令 最后执行
没有必要解析一开始不会运行的JS代码
像inner函数就没必要解析 压根没有被调用 没必要解析成AST结构 没必要解析成bytecode 更没必要运行 但是他会进行预解析 做一个简单的解析 知道里面有inner这个函数 但是inner里面有什么他不管 inner里面的代码统统不解析
- 从JS代码到AST树这一过程 V8引擎内部会帮我们创建一个GlobalObject对象 当前浏览器/node环境中所用到的全局对象(String Date Number setTimeout console window等)都会放到GlobalObject对象中 window指向当前对象(GlobalObject对象)我自己写的JS代码也会在这里进行解析 用var声明的变量会放到GlobalObject对象中 但此时由于代码没有执行 只有在代码执行时才会把值赋给变量 在解析时 只是知道你有这么个属性 时没有给属性赋值的 所以此时 用var声明的变量是undefined
解析完后变成AST AST经ignation转换为字节码 然后执行代码
代码要想运行 需要先从磁盘加载到内存 再把内存里面的东西转成机器指令 然后再在CPU中执行 我们会对内存划分为栈结构 堆结构
HTML CSS JS解析顺序
- 从上到下运行,先解析head标签中的代码,head标签中会包含一些引用外部文件的代码,从开始运行就会下载这些被引用的外部文件 当遇到
script
标签的时候 浏览器暂停解析(不是暂停下载),将控制权交给JavaScript引擎(解释器) 如果<script>
标签引用了外部脚本,就下载该脚本,否则就直接执行,执行完毕后将控制权交给浏览器渲染引擎 - 当head中代码解析完毕,会开始解析body中的代码 如果此时head中引用的外部文件没有下载完,将会继续下载 浏览器解析body代码中的元素,会按照head中声明一部分样式去解析 如果此时遇到body标签中的
<script>
,同样会将控制权交给JavaScript引擎来解析JavaScript 解析完毕后将控制权交还给浏览器渲染引擎。当body中的代码全部执行完毕、并且整个页面的css样式加载完毕后,css会重新渲染整个页面的html元素。 - 所以
<script>
写到body标签内靠后比较好,因为JavaScript 会操作html元素, 如果在body加载完之前写JavaScript 会造成JavaScript 找不到页面元素 但是我们经常将<script>
写到head中,body中不会有大量的js代码,body中的html代码结构会比较清晰
window.onload: 等待页面中的所有内容加载完毕之后才会执行
$(document).ready(): 页面中所有DOM结构绘制完毕之后就能够执行
可以这样理解:window.onload和$(document).ready()/$(function(){}); 相当于写在body内最靠后的`<script>` 代码段
消息队列和事件循环系统(?)
- 为了在线程运行过程中,能接收并执行新的任务,所以采用了事件循环机制
- 循环:for循环
for(; ;)
- 为了处理其他线程发送过来的任务 引入消息队列。消息队列就是一个队列 从队列头部取出任务执行 从队列尾部添加新任务
- 渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息
- 如果 DOM 发生变化,采用同步通知的方式,会影响当前任务的执行效率(当前任务的执行时间会被拉长);如果采用异步方式,又会影响到监控的实时性(因为在添加到消息队列的过程中,可能前面就有很多任务在排队了)。所以提出了宏任务和微任务。
- 通常我们把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列,在执行宏任务的过程中,如果 DOM 有变化,那么就会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行,因此也就解决了执行效率的问题。等宏任务中的主要功能都直接完成之后,这时候,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,因为 DOM 变化的事件都保存在这些微任务队列中,这样也就解决了实时性问题。
(1)同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
(2)当指定的事情完成时,Event Table会将这个函数移入Event Queue。
(3)主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
(4)上述过程会不断重复,也就是常说的Event Loop(事件循环)。
setTimeout是怎么实现的(?)
- 渲染进程中所有运行在主线程上的任务都需要先添加到消息队列,然后事件循环系统再按照顺序执行消息队列中的任务。
- 通过定时器设置回调函数需要在指定的时间间隔内被调用,但消息队列中的任务是按照顺序执行的,所以为了保证回调函数能在指定时间内执行,不能将定时器的回调函数直接添加到消息队列中。
- 当通过 JavaScript 调用 setTimeout 设置回调函数的时候,渲染进程将会创建一个回调任务,包含了回调函数、当前发起时间、延迟执行时间。创建好回调任务之后,再将该任务添加到延迟执行队列中。ProcessDelayTask函数是专门用来处理延迟执行任务的。处理完消息队列中的一个任务之后,就开始执行 ProcessDelayTask 函数。ProcessDelayTask 函数会根据发起时间和延迟时间计算出到期的任务,然后依次执行这些到期的任务。等到期的任务执行完成之后,再继续下一个循环过程。
- 取消定时器:直接从延迟队列中,通过 ID 查找到对应的任务,然后再将其从队列中删除掉就可以了。
- 如果当前任务执行时间过久,会影响(延迟)到期定时器任务的执行
- 在 Chrome 中,定时器被嵌套调用 5 次以上,系统会判断该函数方法被阻塞了,如果定时器的调用时间间隔小于 4 毫秒,那么浏览器会将每次调用的时间间隔设置为 4 毫秒。
- 未被激活的页面中定时器最小值大于 1000 毫秒,也就是说,如果标签不是当前的激活标签,那么定时器最小的时间间隔是 1000 毫秒,目的是为了优化后台页面的加载损耗以及降低耗电量。
- Chrome、Safari、Firefox 都是以 32 个 bit 来存储延时值的,32bit 最大只能存放的数字是 2147483647 毫秒,这就意味着,如果 setTimeout 设置的延迟值大于 2147483647 毫秒(大约 24.8 天)时就会溢出,这导致定时器会被立即执行
- 如果被 setTimeout 推迟执行的回调函数是某个对象的方法,那么该方法中的 this 关键字将指向全局环境,而不是定义时所在的那个对象。
var name= 1;
var MyObj = {
name: 2,
showName: function(){
console.log(this.name);
}
}
setTimeout(MyObj.showName,1000)
解决:
(1)将MyObj.showName放在匿名函数中执行
// 箭头函数
setTimeout(() => {
MyObj.showName()
}, 1000);
// 或者 function 函数
setTimeout(function() {
MyObj.showName();
}, 1000)
(2)使用 bind 方法,将 showName 绑定在 MyObj 上面
setTimeout(MyObj.showName.bind(MyObj), 1000)
setTimeout(() => {
task()
},3000)
sleep(10000000)
(1)task()进入Event Table并注册,计时开始。
(2)执行sleep函数,很慢,非常慢,计时仍在继续。
(3)3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep还没执行完,只好等着。
(4)sleep终于执行完了,task()终于从Event Queue进入了主线程执行。
- setTimeout会经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中
setTimeout(fn,0)
的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。- setInterval会每隔指定的时间将注册的函数置入Event Queue。对
setInterval(fn,ms)
来说每过ms秒,会有fn进入Event Queue。
XMLHttpRequest是怎么实现的(?)
- XMLHttpRequest 提供了从 Web 服务器获取数据的能力,如果你想要更新某条数据,只需要通过 XMLHttpRequest 请求服务器提供的接口,就可以获取到服务器的数据,然后再操作 DOM 来更新页面内容,整个过程只需要更新网页的一部分就可以了,而不用像之前那样还得刷新整个页面,这样既有效率又不会打扰到用户。
- 将一个函数作为参数传递给另外一个函数,那作为参数的这个函数就是回调函数
- 同步回调:回调函数 callback 是在主函数 doWork 返回之前执行的
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)
- 异步回调:回调函数在主函数外部执行的过程。
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)
使用 setTimeout 函数让 callback 在 doWork 函数执行结束后,又延时了 1 秒再执行,这次 callback 并没有在主函数 doWork 内部被调用
- XMLHttpRequest 工作流程图
(1)创建 XMLHttpRequest 对象:let xhr = new XMLHttpRequest()
(2)为 xhr 对象注册回调函数:因为网络请求比较耗时,所以要注册回调函数,这样后台任务执行完成之后就会通过调用回调函数来告诉其执行结果。XMLHttpRequest 的回调函数主要有下面几种:
ontimeout
:用来监控超时请求,如果后台请求超时了,该函数会被调用;
onerror
:用来监控出错信息,如果后台请求出错了,该函数会被调用;
onreadystatechange
:用来监控后台请求过程中的状态,比如可以监控到 HTTP 头加载完成的消息、HTTP 响应体消息以及数据加载完成的消息等。
(3)配置基础的请求信息:通过 open 接口配置一些基础的请求信息,包括请求的地址、请求方法(是 get 还是 post)和请求方式(同步还是异步请求)。通过 xhr 内部属性类配置一些其他可选的请求信息 如超时时间 服务器返回资源的格式(设置了之后 系统会将服务器返回的数据自动转换为你想要的格式)
(4)发起请求:渲染进程会将请求发送给网络进程,然后网络进程负责资源的下载,等网络进程接收到数据之后,就会利用 IPC 来通知渲染进程;渲染进程接收到消息之后,会将 xhr 的回调函数封装成任务并添加到消息队列中,等主线程循环系统执行到该任务的时候,就会根据相关的状态来调用对应的回调函数。如果网络请求出错了,就会执行 xhr.onerror;如果超时了,就会执行 xhr.ontimeout;如果是正常的数据接收,就会执行 onreadystatechange 来反馈相应的状态。
宏任务和微任务(?)
- 渲染进程内部会维护多个消息队列,比如延迟执行队列和普通的消息队列。然后主线程采用一个 for 循环,不断地从这些任务队列中取出任务并执行任务。我们把这些消息队列中的任务称为宏任务。
- 事件循环机制:
(1)先从多个消息队列中选出一个最老的任务,这个任务称为 oldestTask;
(2)然后循环系统记录任务开始执行的时间,并把这个 oldestTask 设置为当前正在执行的任务;
(3)当任务执行完成之后,删除当前正在执行的任务,并从对应的消息队列中删除掉这个 oldestTask;
(4)最后统计执行完成的时长等信息。 - 宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合了,比如后面要介绍的监听 DOM 变化的需求
- 微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。
- 当 JavaScript 执行一段脚本的时候,V8 会为其创建一个全局执行上下文,在创建全局执行上下文的同时,V8 引擎也会在内部创建一个微任务队列
- 产生微任务有两种方式:
(1)使用 MutationObserver 监控某个 DOM 节点,然后再通过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。
(2)使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务。
通过 DOM 节点变化产生的微任务或者使用 Promise 产生的微任务都会被 JavaScript 引擎按照顺序保存到微任务队列中。 - 执行微任务:在当前宏任务中的 JavaScript 快执行完成时,也就在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。WHATWG 把执行微任务的时间点称为检查点。
- 如果在执行微任务的过程中,产生了新的微任务,同样会将该微任务添加到微任务队列中,V8 引擎一直循环执行微任务队列中的任务,直到队列为空才算执行结束。也就是说在执行微任务过程中产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行。
- 微任务添加和执行示意图
该示意图是在执行一个 ParseHTML 的宏任务,在执行过程中,遇到了 JavaScript 脚本,那么就暂停解析流程,进入到 JavaScript 的执行环境。从图中可以看到,全局上下文中包含了微任务列表。在 JavaScript 脚本的后续执行过程中,分别通过 Promise 和 removeChild 创建了两个微任务,并被添加到微任务列表中。接着 JavaScript 执行结束,准备退出全局执行上下文,这时候就到了检查点了,JavaScript 引擎会检查微任务列表,发现微任务列表中有微任务,那么接下来,依次执行这两个微任务。等微任务队列清空之后,就退出全局执行上下文。 - 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
- 微任务的执行时长会影响到当前宏任务的时长。比如一个宏任务在执行过程中,产生了 100 个微任务,执行每个微任务的时间是 10 毫秒,那么执行这 100 个微任务的时间就是 1000 毫秒,也可以说这 100 个微任务让宏任务的执行时间延长了 1000 毫秒。所以你在写代码的时候一定要注意控制微任务的执行时长。
- 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。
- MutationObserver 是用来监听 DOM 变化的一套方法
- MutationObserver采用了“异步 + 微任务”的策略 通过异步操作解决了同步操作的性能问题;通过微任务解决了实时性的问题:等多次 DOM 变化后,一次触发异步调用,并且还会使用一个数据结构来记录这期间所有的 DOM 变化。这样即使频繁地操纵 DOM,也不会对性能造成太大的影响。在每次 DOM 节点发生变化的时候,渲染引擎将变化记录封装成微任务,并将微任务添加进当前的微任务队列中。这样当执行到检查点的时候,V8 引擎就会按照顺序执行微任务了。
Promise(?)
- 产生嵌套函数的一个主要原因是在发起任务请求时会带上回调函数,这样当任务处理结束之后,下个任务就只能在回调函数中来处理了。
- Promise 如何解决嵌套回调问题:回调函数延迟绑定和回调函数返回值穿透的技术
(1) Promise 实现了回调函数的延时绑定。回调函数的延时绑定在代码上体现就是先创建 Promise 对象 x1,通过 Promise 的构造函数 executor 来执行业务逻辑;创建好 Promise 对象 x1 之后,再使用 x1.then 来设置回调函数。
// 创建 Promise 对象 x1,并在 executor 函数中执行业务逻辑
function executor(resolve, reject){
resolve(100)
}
let x1 = new Promise(executor)
//x1 延迟绑定回调函数 onResolve
function onResolve(value){
console.log(value)
}
x1.then(onResolve)
(2)需要将回调函数 onResolve 的返回值穿透到最外层
- Promise如何处理异常:Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被 onReject 函数处理或 catch 语句捕获为止。
async/await(?)
- 生成器函数是一个带星号函数,而且是可以暂停执行和恢复执行的
- 在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
- 外部函数可以通过 next 方法恢复函数的执行。
function* genDemo() {
console.log(" 开始执行第一段 ")// 2
yield 'generator 2'// 3
console.log(" 开始执行第二段 ")// 5
yield 'generator 2'// 6
console.log(" 开始执行第三段 ")// 8
yield 'generator 2'// 9
console.log(" 执行结束 ")// 11
return 'generator 2'// 12
}
console.log('main 0')// 1
let gen = genDemo()
console.log(gen.next().value)
console.log('main 1')// 4
console.log(gen.next().value)
console.log('main 2')// 7
console.log(gen.next().value)
console.log('main 3')// 10
console.log(gen.next().value)
console.log('main 4')// 13
- 协程是一种比线程更加轻量级的存在。你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。
- 正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
(1)通过调用生成器函数 genDemo 来创建一个协程 gen,创建之后,gen 协程并没有立即执行。
(2)要让 gen 协程执行,需要通过调用 gen.next。
(3)当协程正在执行的时候,可以通过 yield 关键字来暂停 gen 协程的执行,并返回主要信息给父协程。
(4)如果协程在执行期间,遇到了 return 关键字,那么 JavaScript 引擎会结束当前协程,并将 return 后面的内容返回给父协程。
- gen 协程和父协程是在主线程上交互执行的,并不是并发执行的,它们之前的切换是通过 yield 和 gen.next 来配合完成的。
- 当在 gen 协程中调用了 yield 方法时,JavaScript 引擎会保存 gen 协程当前的调用栈信息,并恢复父协程的调用栈信息。同样,当在父协程中执行 gen.next 时,JavaScript 引擎会保存父协程的调用栈信息,并恢复 gen 协程的调用栈信息。
- 生成器就是协程的一种实现方式
- 我们把执行生成器的代码封装成一个函数,并把这个执行生成器代码的函数称为执行器
- async/await 技术背后的秘密就是 Promise 和生成器应用,往低层说就是微任务和协程应用
- async 是一个通过异步执行并隐式返回 Promise作为结果的函数。
虚拟DOM
- 虚拟 DOM 到底要解决哪些事情:
(1)将页面改变的内容应用到虚拟 DOM 上,而不是直接应用到 DOM 上。
(2)变化被应用到虚拟 DOM 上时,虚拟 DOM 并不急着去渲染页面,而仅仅是调整虚拟 DOM 的内部状态,这样操作虚拟 DOM 的代价就变得非常轻了。
(3)在虚拟 DOM 收集到足够的改变时,再把这些变化一次性应用到真实的 DOM 上。 - 虚拟 DOM 到底怎么运行的
(1)创建阶段。首先依据 JSX 和基础数据创建出来虚拟 DOM,它反映了真实的 DOM 树的结构。然后由虚拟 DOM 树创建出真实 DOM 树,真实的 DOM 树生成完后,再触发渲染流水线往屏幕输出页面。
(2)更新阶段。如果数据发生了改变,那么就需要根据新的数据创建一个新的虚拟 DOM 树;然后 React 比较两个树,找出变化的地方,并把变化的地方一次性更新到真实的 DOM 树上;最后渲染引擎更新渲染流水线,并生成新的页面。