0

0

如何使用React diff算法

php中世界最好的语言

php中世界最好的语言

发布时间:2018-06-07 11:00:19

|

2335人浏览过

|

来源于php中文网

原创

这次给大家带来如何使用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算法有两个原则:

  1. 对比当前真实的DOM和虚拟DOM,在对比过程中直接更新真实DOM

  2. 只对比同一层级的变化实现

我们需要实现一个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 node
if ( 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次组件,对性能肯定有很大的影响。

相信看了本文案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!

推荐阅读:

使用ES6中class模仿Vue做出双向绑定

网趣网上购物系统HTML静态版
网趣网上购物系统HTML静态版

网趣购物系统静态版支持网站一键静态生成,采用动态进度条模式生成静态,生成过程更加清晰明确,商品管理上增加淘宝数据包导入功能,与淘宝数据同步更新!采用领先的AJAX+XML相融技术,速度更快更高效!系统进行了大量的实用性更新,如优化核心算法、增加商品图片批量上传、谷歌地图浏览插入等,静态版独特的生成算法技术使静态生成过程可随意掌控,从而可以大大减轻服务器的负担,结合多种强大的SEO优化方式于一体,使

下载

怎样操作echarts节点显示动态数据

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
Golang 测试体系与代码质量保障:工程级可靠性建设
Golang 测试体系与代码质量保障:工程级可靠性建设

Go语言测试体系与代码质量保障聚焦于构建工程级可靠性系统。本专题深入解析Go的测试工具链(如go test)、单元测试、集成测试及端到端测试实践,结合代码覆盖率分析、静态代码扫描(如go vet)和动态分析工具,建立全链路质量监控机制。通过自动化测试框架、持续集成(CI)流水线配置及代码审查规范,实现测试用例管理、缺陷追踪与质量门禁控制,确保代码健壮性与可维护性,为高可靠性工程系统提供质量保障。

48

2026.02.28

Golang 工程化架构设计:可维护与可演进系统构建
Golang 工程化架构设计:可维护与可演进系统构建

Go语言工程化架构设计专注于构建高可维护性、可演进的企业级系统。本专题深入探讨Go项目的目录结构设计、模块划分、依赖管理等核心架构原则,涵盖微服务架构、领域驱动设计(DDD)在Go中的实践应用。通过实战案例解析接口抽象、错误处理、配置管理、日志监控等关键工程化技术,帮助开发者掌握构建稳定、可扩展Go应用的最佳实践方法。

44

2026.02.28

Golang 性能分析与运行时机制:构建高性能程序
Golang 性能分析与运行时机制:构建高性能程序

Go语言以其高效的并发模型和优异的性能表现广泛应用于高并发、高性能场景。其运行时机制包括 Goroutine 调度、内存管理、垃圾回收等方面,深入理解这些机制有助于编写更高效稳定的程序。本专题将系统讲解 Golang 的性能分析工具使用、常见性能瓶颈定位及优化策略,并结合实际案例剖析 Go 程序的运行时行为,帮助开发者掌握构建高性能应用的关键技能。

37

2026.02.28

Golang 并发编程模型与工程实践:从语言特性到系统性能
Golang 并发编程模型与工程实践:从语言特性到系统性能

本专题系统讲解 Golang 并发编程模型,从语言级特性出发,深入理解 goroutine、channel 与调度机制。结合工程实践,分析并发设计模式、性能瓶颈与资源控制策略,帮助将并发能力有效转化为稳定、可扩展的系统性能优势。

22

2026.02.27

Golang 高级特性与最佳实践:提升代码艺术
Golang 高级特性与最佳实践:提升代码艺术

本专题深入剖析 Golang 的高级特性与工程级最佳实践,涵盖并发模型、内存管理、接口设计与错误处理策略。通过真实场景与代码对比,引导从“可运行”走向“高质量”,帮助构建高性能、可扩展、易维护的优雅 Go 代码体系。

19

2026.02.27

Golang 测试与调试专题:确保代码可靠性
Golang 测试与调试专题:确保代码可靠性

本专题聚焦 Golang 的测试与调试体系,系统讲解单元测试、表驱动测试、基准测试与覆盖率分析方法,并深入剖析调试工具与常见问题定位思路。通过实践示例,引导建立可验证、可回归的工程习惯,从而持续提升代码可靠性与可维护性。

3

2026.02.27

漫蛙app官网链接入口
漫蛙app官网链接入口

漫蛙App官网提供多条稳定入口,包括 https://manwa.me、https

268

2026.02.27

deepseek在线提问
deepseek在线提问

本合集汇总了DeepSeek在线提问技巧与免登录使用入口,助你快速上手AI对话、写作、分析等功能。阅读专题下面的文章了解更多详细内容。

51

2026.02.27

AO3官网直接进入
AO3官网直接进入

AO3官网最新入口合集,汇总2026年可用官方及镜像链接,助你快速稳定访问Archive of Our Own平台。阅读专题下面的文章了解更多详细内容。

430

2026.02.27

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 5.7万人学习

国外Web开发全栈课程全集
国外Web开发全栈课程全集

共12课时 | 1万人学习

React核心原理新老生命周期精讲
React核心原理新老生命周期精讲

共12课时 | 1.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号