这是我参与「掘金日新计划 · 2 月更文挑战」的第 2天,点击查看活动详情”
keep-alive的实现原理
keepalive是一个组件,该组件不会渲染成真实的DOM,因为设置了abstract为true,该组件的作用是缓存不活动的组件实例。该组件有三个属性,一个是include:指定缓存的组件,一个是exclude指定不缓存的组件。一个是max:缓存组件的上限,当超过上限时使用LRU算法,该算法将缓存的组件按数组的格式依次往后缓存,当超过上限的时候,会删除最久缓存的那个组件,也就是组件下标为0的那个组件。
keepalive的实现原理,在created的时候创建缓存列表和key值,在destroyed中循环清空缓存的列表的key值,在mounted中监听include和exclude,当发生改变时动态的进行添加和删除。在render中使用默认插槽,并找到一个缓存的组件提取该组件的名字,进行判断是否在缓存中,如果在的话进行缓存,如果不在的话return掉。如果没有key的话利用组件的标签名、key和cid拼接一个key。
nextTick的实现原理
nextTick就是一个异步任务。nextTick方法主要使用了宏任务和微任务(时间循环机制)定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过异步方法清空当前队列。这个nextTick方法就是异步方法。
作用:将回调函数延迟在下一次DOM更新数据后调用。
nextTick中定义的三个重要变量:
- callbacks:用来存储所有需要执行的回调函数
- pending:用来标志是否正在执行回调函数
- timerFunc:用来触发执行回调函数
原理
:当调用nextTick方法时会传入两个参数,回调函数和执行回调函数的上下文环境,如果没有提供回调函数,那么将返回promise对象。首先将拿到的回调函数存放到数组中,判断是否正在执行回调函数,如果当前没有在pending的时候,就会执行timerFunc,多次执行nextTick只会执行一次timerFunc,timerFunc其实就是执行异步的方法,在timerFunc方法中选择一个异步方法(首先判断是否支持promise,如果支持就将flushCallbacks放在promise中异步执行,并且标记使用微任务。如果不支持promise就看是否支持MutationObserver方法,如果支持就new一个MutationObserver类,创建一个文本节点进行监听,当数据发生变化了就会异步执行flushCallbacks方法。如果以上两个都不支持就看是否支持setImmediate方法,如果支持setImmediate就去异步执行flushCallbacks方法。如果以上三种方法都不支持就是用setTimeout),然后异步去执行flushCallbacks方法,flushCallbacks中就是将传递的函数依次执行。nextTick多次调用会维持一个数组,之后会异步的把数组中的方法依次执行,这样的话用户就会在视图更新之后在获取到真实的dom元素。
类式组件和函数式组件的区别
早期的时候函数式组件大部分情况下只用来做数据展示,没有状态,被称为无状态组件,后期引入了hooks使用频繁起来。
- 函数式组件使用useState相当于类式组件的state,但是类式组件在操作中得到的是最新的状态,因为props不变但this是随之改变的。而函数式组件是按顺序得到状态值的
- 函数式组件使用useEffect进行异步操作,没有生命周期。而类式组件在不同的生命周期中进行相应的操作。
- 函数式组件通过useCallback和useMemo进行性能优化。类式组件使用生命周期中的shouldComponentUpdate进行性能优化
- 函数式组件引入hooks时解决了this难以理解的一些问题,无this。类式组件可以获取实例化的this,并且基于this做各种操作。
- 类式组件需要class继承,函数式组件不需要
- 函数式组件捕获了渲染时所使用的值
浏览器的一些任务队列
- 延时队列:用于存放计时器到达后的回调任务,优先级[中]
- 交互队列:用于存放用户操作后产生的事件处理任务,优先级[高]
- 微队列:用于存放需要最快执行的任务,优先级[最高]
宏任务和微任务
- 宏任务
- 消息队列(回调队列)中的任务被称为宏任务,是由宿主环境(浏览器或者node)提供的,不断地从消息队列中取出并被事件循环执行,宏任务在执行时,他不能获取到任务外的上下文。
- 宏任务包含:script(可以理解为外层同步代码)setTimeout/setInterval,setImmediate,requestAnimationFrame,I/O,UI rendering,注册事件
- 微任务
- 在当主线程任务结束之后就立即执行,会在当前宏任务之前运行,而不是整个任务的后面。它是由js引擎自身提供的,微任务在执行时,他能获取到任务外的上下文。
- 微任务包含:promise.then catch finally,MutationObserver,process.nextTick(node),questMicrotask
- questMicrotask开启一个微任务
- MutationObserver:观察器,观察一个dom对象是否发生变化,发生变化执行
- 执行顺序
- 先执行同步代码,遇到异步宏任务则将异步宏任务放入宏任务队列中,遇到异步微任务则将异步微任务放入微任务中,当所有同步代码执行完毕后,如果有微任务,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将异步宏任务从队列中调入主线程执行,一直循环直至所有任务执行完毕。
浏览器是如何渲染页面的
当浏览器的网络线程收到HTML文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。整个渲染流程分为多个阶段。分别是:HTML解析、样式计算、布局、分层、绘制、分块、光栅化、画。
每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入。这样,整个渲染流程就形成了一套组织严密的生产流水线。
- 渲染第一步:
解析HTML
:- 解析过程中遇到CSS解析CSS,遇到JS解析JS。为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载HTML中的外部CSS文件和外部的JS文件。
- 如果主线程解析到
link
位置,此时外部的CSS文件还没有下载解析好主线程不会等待,继续解析后续的HTML。这是因为下载和解析CSS 的工作是在预解析线程中进行的、这就是CSS不会阻塞HTML解析的根本原因。 - 如果主线程解析到
script
位置,会停止解析HTML,转而等待JS文件下载好,并将全局代码解析执行完成后,才能继续解析HTML。这是因为JS代码的执行过程可能会修改当前的DOM树,所以DOM树的生成必须暂停。这就是JS会阻塞HTML解析的根本原因。 - 第一步完成后,会得到DOM树和CSSOM树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在CSSOM树中
- 渲染的第二步:
样式计算
- 主线程会遍历得到DOM树,依次为树中的每个节点计算出它的最终样式,称之为Computed Style.
- 在这一过程中,很多预设值会变成绝对值,比如
red
会变成rgb(255,0,0)
,相对单位会变成绝对单位,比如em
会变成px
- 这一步完成后,会得到一棵带有样式的DOM树。
- 渲染的第三步:
布局
- 布局完成后会得到布局树。
- 布局阶段会依次遍历DOM树的每一个节点,计算每个节点的几何信息。例如节点的宽高、相对包含块的位置。大部分时候,DOM树和布局树并非一一对应。
- 比如
display:none
的节点没有几何信息,因此不会生成到布局树;又比如使用了伪元素选择器,虽然DOM树不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。还有匿名行盒、匿名块盒等等都会导致DOM树和布局树无法一一对应。
- 渲染的第四步:
分层
- 主线程会使用一套复杂的策略对整个布局树中进行分层
- 分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。
- 滚动条、堆叠上下文、transform、opacity等样式都会或多或少的影响分层结果,也可以通过
will-change
属性更大程度的影响分层结果。
- 渲染的第五步:
绘制
- 主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。
- 完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。
- 合成线程首先对每个图层进行分块,将其划分为更多的小区域。它会从线程池中拿取多个线程来完成分块工作
- 渲染的第六步:
光栅化
- 合成线程会将块信息交给GPU进程,以极高的速度完成光栅化。
- GPU进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。
- 光栅化的结果,就是一块一块的位图。
- 渲染的最后一个阶段:
画
- 合成线程拿到每个层、每个块的位图后,生成一个个[指引(quad)]信息。
- 指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。
- 变形发生在合成线程,与渲染主线程无关,这就是
transform
效率高的本质原因。 - 合成线程会把quad提交给GPU进程,由GPU进程产生系统调用,提交给GPU硬件,完成最终的屏幕成像。
什么是reflow?
- reflow的本质就是重新计算layout树。
- 当进行了会影响布局树的操作后,需要重新计算布局树,会引发layout。
- 为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当JS代码全部完成后在进行统一计算。所以,改动属性造成的reflow是异步完成的。
- 也同样如此,当JS获取到布局属性时,就可能造成无法获取到最新的布局信息。
- 浏览器在反复权衡下,最终决定获取属性立即reflow。
什么是repaint
repaint的本质就是重新根据分层计算了绘制指令。当改动了可见样式后,就需要重新计算,会引发repaint。由于元素的布局信息也属于可见样式,所以reflow一定会引起repaint。由于元素的布局信息也属于可见样式,所以reflow一定会引起repaint。
为什么transform的效率高?
因为transform既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个[draw]阶段。
由于draw阶段在合成线程中,所以transform的变化几乎不会影响渲染主线程。反之,渲染主线程无论如何忙碌,也不会影响transform的变化。