当前位置: 首页 > Web前端 > HTML

React访谈:谈谈虚拟DOM、Diff算法和Key机制

时间:2023-03-29 10:55:18 HTML

1.virtualdom的原生JSDOM操作非常耗性能,React将真正的原生JSDOM转换为JavaScript对象。这就是虚拟Dom(VirtualDom)在每次数据更新后重新计算虚拟Dom,与上次生成的虚拟dom进行比较,批量更新变化的部分。其中,React提供了componentShouldUpdate生命周期,让开发者可以手动控制,减少数据变化后不必要的虚拟dom比较,提高性能和渲染效率。原生html元素代码:HelloConardLi

  • Apple
  • Orange
中React可以像这样存储为JS代码:constVitrualDom={type:'div',props:{class:'title'},children:[{type:'span',children:'HelloConardLi'},{type:'ul',children:[{type:'li',children:'apple'},{type:'li',children:'orange'}]}]}当我们需要创建或更新一个元素时,React会先让这个VitrualDom对象被创建和改变,然后将VitrualDom对象渲染成真实的DOM;当我们需要监听DOM事件时,首先监听VitrualDom事件,VitrualDom会代理原生DOM事件进行响应。虚拟DOM的组成:通过JSX或者React.createElement、React.createClass等创建虚拟元素和组件。即ReactElementelement对象,我们的组件最终会被渲染成如下结构:type:元素的类型,可以是原生html类型(字符串),也可以是自定义组件(函数或类)key:唯一组件的标识符,用于Diff算法,下面会详细介绍ref:用于访问原生dom节点props:传入组件的props,chidren是props中的一个属性,存放当前组件的子节点,可以beanarray(multiplechildnodes)orObject(onlyonechildnode)owner:当前正在构建的Component属于Componentself:(非生产环境)specifiedcurrentlylocatedwhichcomponentinstance_source:(非生产环境)指定文件(fileName)和调试代码来自的代码行数(lineNumber)HelloConardLi
  • Apple
  • Orange
把这个JSX的el打印出ement,确认虚拟DOM的本质就是js对象:其中,jsx中使用的原生元素标签的类型就是标签名。而如果是函数组件或者类组件,它的类型就是对应的类或者函数对象2.diff算法React需要同时维护两棵虚拟DOM树:一棵代表当前的DOM结构,另一棵将当React状态发生变化时被重新渲染。React会比较这两种树的不同点来决定是否修改DOM结构以及如何修改。这种算法称为Diff算法。对于生成将一棵树转换为另一棵树的最少操作数的算法问题,有一些通用的解决方案。然而,即使在最前沿的算法中,该算法的复杂度也是O(n3),其中n是树中元素的数量。如果在React中使用此算法,显示1000个元素所执行的计算量将达到数十亿级。这个成本实在是太高了。所以React基于以下两个假设提出了一套O(n)的启发式算法:1:两种不同类型的元素会产生不同的树;2:开发者可以使用keyprops来提示哪些子元素在不同渲染下可以保持稳定;Reactdiff算法的大致执行过程详见前端进阶面试题详细解答:Diff算法会对新老树进行深度优先遍历,避免将两棵树进行完整比较,所以该算法的复杂度可以达到O(n)。然后为每个节点生成一个唯一的标志:遍历过程中,每遍历一个节点,就比较新旧树,只比较同一层次的元素:即只比较中的虚线diagram对于连接的部分,记录前后的不同。Reactdiff算法具体策略:(1)treedifftreediff主要针对Reactdom节点的跨级操作。由于跨级DOM移动操作较少,Reactdiff算法的treediff并未对此类操作进行深入对比,只??是简单的删除和创建操作。如图,将整个A节点(包括其子节点)移到了D节点下,因为React会简单的考虑同级节点的位置变换,而对于不同级别的节点,只做创建和删除操作可用。当根节点发现子节点中的A消失时,会直接销毁A;当D发现多了一个子节点A时,就会创建一个新的A(包括子节点)作为自己的子节点。此时diff的执行状态:createA→createB→createC→deleteA由此可以发现,节点跨层移动时,并没有发生想象中的移动操作,而是以A为根节点重新创建了整棵树。这是一个影响React性能的操作,所以官方建议不要进行跨层的DOM节点操作。基于以上原因,在开发组件时保持稳定的DOM结构将有助于提高性能。例如,你可以通过CSS隐藏或显示节点,而不是实际移除或添加DOM节点(2)组件差异:组件差异是一种差异算法,专门用于比较更新前后同一级别的React组件:如果它们是同类型的Components,继续按照原来的策略比较VirtualDOM树(比如继续比较Component中的组件props和子节点及其属性)。如果不是,则判断该组件为脏组件,替换整个组件下的所有子节点,即销毁原组件,创建新组件。对于同一类型的组件,其VirtualDOM可能没有变化。如果你能知道这一点,你可以节省很多diff时间。因此,React允许用户通过shouldComponentUpdate()来判断组件是否需要通过diff算法进行分析。如图,当组件D变成组件G时,即使两个组件结构相似,一旦React判断D和G是不同类型的组件,就不会比较两者的结构,而是比较组件D直接删除,重新创建组件G及其子节点。虽然当两个组件类型不同但结构相似时,diff会影响性能,但正如React官方博客所说:不同类型的组件很少有相似的DOM树,所以这种极端因素在实际开发过程中很难做到(3)elementdiffelementdiff是对同一层级的所有节点(包括元素节点和组件节点)的diff算法。当节点处于同一层级时,diff提供3种节点操作,分别是INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和REMOVE_NODE(删除)。我们将虚拟dom树中所有同级待比较节点的集合称为新集合和旧集合,使用如下策略:INSERT_MARKUP:新集合的某类组件或元素节点旧集合中不存在,即一个全新的节点,需要在新节点上进行插入操作。MOVE_EXISTING:新集合中某类组件或元素节点存在于旧集合中,且该元素为可更新类型。generateComponent-Children调用了receiveComponent。在这种情况下,需要移动prevChild=nextChild并且可以在DOM节点之前重复使用。REMOVE_NODE:旧集合的某个组件或节点类型在新集合中也存在,但如果对应的元素不同,则不能直接重用和更新。需要删除,或者旧的组件或节点不在新的集合中,也需要执行删除操作。如图,旧集合包含节点A、B、C、D,更新后的新集合包含节点B、A、D、C(只是位置变了,各自的节点和内部数据没有变).当新旧集合按顺序一一比较时,如果B!=A,则创建B插入新集合,删除旧集合A;依此类推,创建并插入A、D和C,删除B和C和D。React发现这样的操作很繁琐和冗余,因为这些是相同的节点,但是由于位置顺序的变化,需要复杂的以及低效的删除和创建操作。其实只需要移动这些节点的位置即可。针对这种现象,React提出了一种优化策略:允许开发者添加一个唯一的键来区分同一级别的同一组子节点。参见下面的密钥机制3.密钥机制(1)密钥的作用当同级节点添加了同级其他节点独有的密钥属性时,当其在当前级的位置发生变化时。reactdiff算法对新旧节点进行比较后,如果发现key值相同的新旧节点,则进行move操作(然后还是按照原来的策略深入节点比较更新的区别),而不是按照原来的策略删除老节点。创建新节点的操作。这无疑会大大提高React性能和渲染效率(2)key的具体执行过程首先循环遍历新集合中的节点for(nameinnextChildren),使用唯一key判断是否存在相同的节点新老集合if(prevChild===nextChild),如果有相同的节点,则进行移动操作,但移动前,需要将当前节点在旧集合中的位置与lastIndex进行比较if(child._mountIndexlastIndex被认为是B为集合中其他元素的位置,不会受到影响,不会移动。之后,lastIndex=max(index,lastIndex)=1A,oldcollection中index=0,此时lastIndex=1,如果满足index)}//在开头:['a','b','c']=>
    abc
//数组重排->['c','b','a']=>
    cba
上面的例子中,数组重新排序后,对应的实例密钥None被销毁,但重新更新。具体的更新过程以key=0的元素来说明。数组重新排序后:重新渲染组件,得到一个新的虚拟dom;新旧虚拟dom是不同的。新旧版本都有key=0的组件。React认为对于同一个组件,只能更新组件;然后比较它的children,发现content的文本内容不同(由a--->c),但是input组件没有变化,然后触发组件的componentWillReceiveProps方法更新它的childComponent文本内容;因为组件子组件中的input组件没有发生变化,并且没有关联到父组件传入的任何props,所以input组件不会被更新(即它的componentWillReceiveProps方法不会被执行),导致用户输入的值。会改变。(4)密钥机制的缺点如图所示。如果将新集合中的节点更新为D、A、B、C,与旧集合相比,只有D节点会移动,而A、B、C仍保持原来的顺序。理论上diff应该只需要对D进行move操作,但是由于D在旧集合中的位置最大,其他节点的_mountIndex