KeepAlive原理,nextTick原理、类式组件函数式组件的区别,宏任务微任务、浏览器是如何渲染页面的、reflow、repaint、transform

79 阅读10分钟

这是我参与「掘金日新计划 · 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中定义的三个重要变量:

  1. callbacks:用来存储所有需要执行的回调函数
  2. pending:用来标志是否正在执行回调函数
  3. 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元素。

image.png

image.png

image.png

类式组件和函数式组件的区别

早期的时候函数式组件大部分情况下只用来做数据展示,没有状态,被称为无状态组件,后期引入了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的变化。