怎样实现React diff算法

这次给大家带来怎样实现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,所以我们只需要对比同一层级的变化。

怎样实现React diff算法

只需要对比同一颜色框内的节点

总而言之,我们的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: []}// 文本节点的vnode"hello,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  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 (      

count: { this.state.num }

); }}

登录后复制

不使用diff

使用上一篇文章的实现,从chrome的调试工具中可以看到,闪烁的部分是每次更新的部分,每次点击按钮,都会重新渲染整个组件。

怎样实现React diff算法

使用diff

而实现了diff方法后,每次点击按钮,都只会重新渲染变化的部分。

怎样实现React 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次组件,对性能肯定有很大的影响。

相信看了本文案例你已经掌握了方法,更多精彩请关注【创想鸟】其它相关文章!

推荐阅读:

如何操作JS获取用户所在城市及地理位置

如何使用vue源码解析事件机制

以上就是怎样实现React diff算法的详细内容,更多请关注【创想鸟】其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至253000106@qq.com举报,一经查实,本站将立刻删除。

发布者:PHP中文网,转转请注明出处:https://www.chuangxiangniao.com/p/2750563.html

(0)
上一篇 2025年3月8日 05:38:07
下一篇 2025年3月7日 02:03:37

AD推荐 黄金广告位招租... 更多推荐

相关推荐

  • 如何使用react redux

    这次给大家带来如何使用react redux,使用react redux的注意事项有哪些,下面就是实战案例,一起来看一下。 环境准备 为了方便,这里使用create-react-app搭建react环境 create-react-app m…

    编程技术 2025年3月8日
    200
  • React高阶组件使用详解

    这次给大家带来React高阶组件使用详解,React高阶组件使用的注意事项有哪些,下面就是实战案例,一起来看一下。 是什么 高阶组件是一个函数,能够接受一个组件并返回一个新的组件。没有任何副作用。 为什么用 封装并抽离组件的通用逻辑,让此部…

    编程技术 2025年3月8日
    200
  • 在React组件中详细讲解this的使用方法。

    这篇文章主要介绍了react组件中的this的具体使用,现在分享给大家,也给大家做个参考。 React组件的this是什么 通过编写一个简单组件,并渲染出来,分别打印出自定义函数和render中的this: import React fro…

    2025年3月8日
    200
  • 在使用React组件中转 Vue 组件的命令写法有哪些?

    本文先介绍两个框架的组件共性和不兼容的地方,再介绍react-to-vue的使用和原理,需要的朋友可以参考下 基于目前React和Vue比较火,开发react-to-vue 工具的目的是为了进一步提高组件的可复用用性,让组件复用不仅仅局限在…

    2025年3月8日
    200
  • 详细解读React中的元素、组件、实例和节点

    这篇文章主要介绍了浅谈react中的元素、组件、实例和节点,现在分享给大家,也给大家做个参考。 React 深入系列,深入讲解了React中的重点概念、特性和模式等,旨在帮助大家加深对React的理解,以及在项目中更加灵活地使用React。…

    编程技术 2025年3月8日
    200
  • js项目中常用算法

    这次给大家带来js项目中常用算法,js项目中使用算法的注意事项有哪些,下面就是实战案例,一起来看一下。 数组去重 var arr = [1,2,3,4,4,2,2,6,9,1,0];var newArr = [];var onOff = t…

    编程技术 2025年3月8日
    200
  • 在react中有关组件通信有哪些方法?

    这篇文章主要给大家介绍了关于react中组件通信的几种方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。 前言 刚入门React可能会因为React的单向数据流的特性…

    2025年3月8日
    200
  • 在react中state和setState的使用方法(详细教程)

    这篇文章主要介绍了react学习笔记之state以及setstate的使用,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧 在react中通过 state 以及 setState() 来控制组件的状态。 stat…

    编程技术 2025年3月8日
    200
  • 如何使用React Native 截屏组件(详细教程)

    本篇文章主要介绍了react native 截屏组件的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧 React Native 截屏组件:react-native-view-shot,可以截取当前屏幕或…

    编程技术 2025年3月8日
    200
  • 在React-Router中如何进行页面权限管理

    本篇文章主要介绍了react-router如何进行页面权限管理的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧 前言 在一个复杂的SAP应用中,我们可能需要根据用户的角色控制用户进行页面的权限,甚至在用户进…

    编程技术 2025年3月8日
    200

发表回复

登录后才能评论