您好,欢迎来到三六零分类信息网!老站,搜索引擎当天收录,欢迎发信息

如何使用React diff算法

2024/2/22 11:56:09发布40次查看
这次给大家带来如何使用react diff算法,使用react diff算法的注意事项有哪些,下面就是实战案例,一起来看一下。
前言
在上一篇文章,我们已经实现了react的组件功能,从功能的角度来说已经实现了react的核心功能了。
但是我们的实现方式有很大的问题:每次更新都重新渲染整个应用或者整个组件,dom操作十分昂贵,这样性能损耗非常大。
为了减少dom更新,我们需要找渲染前后真正变化的部分,只更新这一部分dom。而对比变化,找出需要更新部分的算法我们称之为diff算法。
对比策略
在前面两篇文章后,我们实现了一个render方法,它能将虚拟dom渲染成真正的dom,我们现在就需要改进它,让它不要再傻乎乎地重新渲染整个dom树,而是找出真正变化的部分。
这部分很多类react框架实现方式都不太一样,有的框架会选择保存上次渲染的虚拟dom,然后对比虚拟dom前后的变化,得到一系列更新的数据,然后再将这些更新应用到真正的dom上。
但也有一些框架会选择直接对比虚拟dom和真实dom,这样就不需要额外保存上一次渲染的虚拟dom,并且能够一边对比一边更新,这也是我们选择的方式。
不管是dom还是虚拟dom,它们的结构都是一棵树,完全对比两棵树变化的算法时间复杂度是o(n^3),但是考虑到我们很少会跨层级移动dom,所以我们只需要对比同一层级的变化。
只需要对比同一颜色框内的节点
总而言之,我们的diff算法有两个原则:
对比当前真实的dom和虚拟dom,在对比过程中直接更新真实dom
只对比同一层级的变化实现
我们需要实现一个diff方法,它的作用是对比真实dom和虚拟dom,最后返回更新后的dom
/** * @param {htmlelement} dom 真实dom * @param {vnode} vnode 虚拟dom * @returns {htmlelement} 更新后的dom */function diff( dom, vnode ) {  // ...}
接下来就要实现这个方法。
在这之前先来回忆一下我们虚拟dom的结构:
虚拟dom的结构可以分为三种,分别表示文本、原生dom节点以及组件。
// 原生dom节点的vnode{  tag: 'p',  attrs: {    classname: 'container'  },  children: []}// 文本节点的vnodehello,world// 组件的vnode{  tag: componentconstrucotr,  attrs: {    classname: 'container'  },  children: []}
对比文本节点
首先考虑最简单的文本节点,如果当前的dom就是文本节点,则直接更新内容,否则就新建一个文本节点,并移除掉原来的dom。
// diff text nodeif ( typeof vnode === 'string' ) {  // 如果当前的dom就是文本节点,则直接更新内容  if ( dom && dom.nodetype === 3 ) {  // nodetype: https://developer.mozilla.org/zh-cn/docs/web/api/node/nodetype    if ( dom.textcontent !== vnode ) {      dom.textcontent = vnode;    }  // 如果dom不是文本节点,则新建一个文本节点dom,并移除掉原来的  } else {    out = document.createtextnode( vnode );    if ( dom && dom.parentnode ) {      dom.parentnode.replacechild( out, dom );    }  }  return out;}
文本节点十分简单,它没有属性,也没有子元素,所以这一步结束后就可以直接返回结果了。
对比非文本dom节点
如果vnode表示的是一个非文本的dom节点,那就要分几种情况了:
如果真实dom和虚拟dom的类型不同,例如当前真实dom是一个p,而vnode的tag的值是'button',那么原来的p就没有利用价值了,直接新建一个button元素,并将p的所有子节点移到button下,然后用replacechild方法将p替换成button。
if ( !dom || dom.nodename.tolowercase() !== vnode.tag.tolowercase() ) {  out = document.createelement( vnode.tag );  if ( dom ) {    [ ...dom.childnodes ].map( out.appendchild );  // 将原来的子节点移到新节点下    if ( dom.parentnode ) {      dom.parentnode.replacechild( out, dom );  // 移除掉原来的dom对象    }  }}
如果真实dom和虚拟dom是同一类型的,那我们暂时不需要做别的,只需要等待后面对比属性和对比子节点。
对比属性
实际上diff算法不仅仅是找出节点类型的变化,它还要找出来节点的属性以及事件监听的变化。我们将对比属性单独拿出来作为一个方法:
function diffattributes( dom, vnode ) {  const old = dom.attributes;  // 当前dom的属性  const attrs = vnode.attrs;   // 虚拟dom的属性  // 如果原来的属性不在新的属性当中,则将其移除掉(属性值设为undefined)  for ( let name in old ) {    if ( !( name in attrs ) ) {      setattribute( dom, name, undefined );    }  }  // 更新新的属性值  for ( let name in attrs ) {    if ( old[ name ] !== attrs[ name ] ) {      setattribute( dom, name, attrs[ name ] );    }  }}
setattribute方法的实现参见第一篇文章
对比子节点
节点本身对比完成了,接下来就是对比它的子节点。
这里会面临一个问题,前面我们实现的不同diff方法,都是明确知道哪一个真实dom和虚拟dom对比,但是子节点是一个数组,它们可能改变了顺序,或者数量有所变化,我们很难确定要和虚拟dom对比的是哪一个。
为了简化逻辑,我们可以让用户提供一些线索:给节点设一个key值,重新渲染时对比key值相同的节点。
// diff方法if ( vnode.children && vnode.children.length > 0 || ( out.childnodes && out.childnodes.length > 0 ) ) {  diffchildren( out, vnode.children );}
function diffchildren( dom, vchildren ) {  const domchildren = dom.childnodes;  const children = [];  const keyed = {};  // 将有key的节点和没有key的节点分开  if ( domchildren.length > 0 ) {    for ( let i = 0; i < domchildren.length; i++ ) { const child = domchildren[ i ]; const key = child.key; if ( key ) { keyedlen++; keyed[ key ] = child; } else { children.push( child ); } } } if ( vchildren && vchildren.length > 0 ) {    let min = 0;    let childrenlen = children.length;    for ( let i = 0; i < vchildren.length; i++ ) { const vchild = vchildren[ i ]; const key = vchild.key; let child; // 如果有key,找到对应key值的节点 if ( key ) { if ( keyed[ key ] ) { child = keyed[ key ]; keyed[ key ] = undefined; } // 如果没有key,则优先找类型相同的节点 } else if ( min < childrenlen ) { for ( let j = min; j < childrenlen; j++ ) { let c = children[ j ]; if ( c && issamenodetype( c, vchild ) ) { child = c; children[ j ] = undefined; if ( j === childrenlen - 1 ) childrenlen--; if ( j === min ) min++; break; } } } // 对比 child = diff( child, vchild ); // 更新dom const f = domchildren[ i ]; if ( child && child !== dom && child !== f ) { if ( !f ) { dom.appendchild(child); } else if ( child === f.nextsibling ) { removenode( f ); } else { dom.insertbefore( child, f ); } } } }}
对比组件
如果vnode是一个组件,我们也单独拿出来作为一个方法:
function diffcomponent( dom, vnode ) { let c = dom && dom._component; let olddom = dom; // 如果组件类型没有变化,则重新set props if ( c && c.constructor === vnode.tag ) { setcomponentprops( c, vnode.attrs ); dom = c.base; // 如果组件类型变化,则移除掉原来组件,并渲染新的组件 } else { if ( c ) { unmountcomponent( c ); olddom = null; } c = createcomponent( vnode.tag, vnode.attrs ); setcomponentprops( c, vnode.attrs ); dom = c.base; if ( olddom && dom !== olddom ) { olddom._component = null; removenode( olddom ); } } return dom;}
下面是相关的工具方法的实现,和上一篇文章的实现相比,只需要修改rendercomponent方法其中的一行。
function rendercomponent( component ) { // ... // base = base = _render( renderer ); // 将_render改成diff base = diff( component.base, renderer ); // ...}
完整diff实现看这个文件
渲染
现在我们实现了diff方法,我们尝试渲染上一篇文章中定义的counter组件,来感受一下有无diff方法的不同。
class counter extends react.component { constructor( props ) { super( props ); this.state = { num: 1 } } onclick() { this.setstate( { num: this.state.num + 1 } ); } render() { return ( <p>        <h1>count: { this.state.num }</h1>        <button onclick={ () => this.onclick()}>add</button>      </p>    );  }}
不使用diff
使用上一篇文章的实现,从chrome的调试工具中可以看到,闪烁的部分是每次更新的部分,每次点击按钮,都会重新渲染整个组件。
使用diff
而实现了diff方法后,每次点击按钮,都只会重新渲染变化的部分。
后话
在这篇文章中我们实现了diff算法,通过它做到了每次只更新需要更新的部分,极大地减少了dom操作。react实现远比这个要复杂,特别是在react 16之后还引入了fiber架构,但是主要的思想是一致的。
实现diff算法可以说性能有了很大的提升,但是在别的地方仍然后很多改进的空间:每次调用setstate后会立即调用rendercomponent重新渲染组件,但现实情况是,我们可能会在极短的时间内多次调用setstate。
假设我们在上文的counter组件中写出了这种代码
onclick() {  for ( let i = 0; i < 100; i++ ) {    this.setstate( { num: this.state.num + 1 } );  }}
那以目前的实现,每次点击都会渲染100次组件,对性能肯定有很大的影响。
相信看了本文案例你已经掌握了方法,更多精彩请关注其它相关文章!
推荐阅读:
使用es6中class模仿vue做出双向绑定
怎样操作echarts节点显示动态数据
以上就是如何使用react diff算法的详细内容。
该用户其它信息

VIP推荐

免费发布信息,免费发布B2B信息网站平台 - 三六零分类信息网 沪ICP备09012988号-2
企业名录 Product