react.js 从零开始(六)Reconciliation
Reconciliation
React 的关键设计目标是使 API 看起来就像每一次有数据更新的时候,整个应用重新渲染了一样。这就极大地简化了应用的编写,但是同时使 React 易于驾驭,也是一个很大的挑战。这篇文章解释了我们如何使用强大的试探法来将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题。
动机(Motivation)
生成最少的将一颗树形结构转换成另一颗树形结构的操作,是一个复杂的,并且值得研究的问题。最优算法的复杂度是 O(n3),n 是树中节点的总数。
这意味着要展示1000个节点,就要依次执行上十亿次的比较。这对我们的使用场景来说太昂贵了。准确地感受下这个数字:现今的 CPU 每秒钟能执行大约三十亿条指令。因此即便是最高效的实现,也不可能在一秒内计算出差异情况。
既然最优的算法都不好处理这个问题,我们实现一个非最优的 O(n) 算法,使用试探法,基于如下两个假设:
1、拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
2、可以为元素提供一个唯一的标志,该元素在不同的渲染过程中保持不变。
实际上,这些假设会使在几乎所有的应用场景下,应用变得出奇地快。
两个节点的差异检查(Pair-wise diff)
为了进行一次树结构的差异检查,首先需要能够检查两个节点的差异。此处有三种不同的情况需要处理:
不同的节点类型
如果节点的类型不同,React 将会把它们当做两个不同的子树,移除之前的那棵子树,然后创建并插入第二棵子树。
renderA: <div />
renderB: <span />
=> [removeNode <div />], [insertNode <span />]
该方法也同样应用于传统的组件。如果它们不是相同的类型,React 甚至将不会尝试计算出该渲染什么,仅会从 DOM 中移除之前的节点,然后插入新的节点。
renderA: <Header />
renderB: <Content />
=> [removeNode <Header />], [insertNode <Content />]
具备这种高级的知识点对于理解为什么 React 的差异检测逻辑既快又精确是很重要的。它对于避开树形结构大部分的检测,然后聚焦于似乎相同的部分,提供了启发。
一个 <Header> 元素与一个 <Content> 元素生成的 DOM 结构不太可能一样。React 将重新创建树形结构,而不是耗费时间去尝试匹配这两个树形结构。
如果在两个连续的渲染过程中的相同位置都有一个 <Header> 元素,将会希望生成一个非常相似的 DOM 结构,因此值得去做一做匹配。
DOM 节点
当比较两个 DOM 节点的时候,我们查看两者的属性,然后能够找出哪一个属性随着时间产生了变化。
renderA: <div id="before" />
renderB: <div id="after" />
=> [replaceAttribute id "after"]
React 不会把 style 当做难以操作的字符串,而是使用键值对对象。这就很容易地仅更新改变了的样式属性。
renderA: <div style={{color: 'red'}} />
renderB: <div style={{fontWeight: 'bold'}} />
=> [removeStyle color], [addStyle font-weight 'bold']
在属性更新完毕之后,递归检测所有的子级的属性。
自定义组件
我们决定两个自定义组件是相同的。因为组件是状态化的,不可能每次状态改变都要创建一个新的组件实例。React 利用新组件上的所有属性,然后在之前的组件实例上调用component[Will/Did]ReceiveProps()。
现在,之前的组件就是可操作了的。它的 render() 方法被调用,然后差异算法重新比较新的状态和上一次的状态。
子级优化差异算法(List-wise diff)
问题点(Problematic Case)
为了完成子级更新,React 选用了一种很原始的方法。React 同时遍历两个子级列表,当发现差异的时候,就产生一次 DOM 修改。
例如在末尾添加一个元素:
renderA: <div><span>first</span></div>
renderB: <div><span>first</span><span>second</span></div>
=> [insertNode <span>second</span>]
在开始处插入元素比较麻烦。React 发现两个节点都是 span,因此直接修改已有 span 的文本内容,然后在后面插入一个新的 span 节点。
renderA: <div><span>first</span></div>
renderB: <div><span>second</span><span>first</span></div>
=> [replaceAttribute textContent 'second'], [insertNode <span>first</span>]
有很多的算法尝试找出变换一组元素的最小操作集合。Levenshtein distance算法能够找出这个最小的操作集合,使用单一元素插入、删除和替换,复杂度为 O(n2) 。即使使用 Levenshtein 算法,不会检测出一个节点已经移到了另外一个位置去了,要实现这个检测算法,会引入更加糟糕的复杂度。
键(Keys)
为了解决这个看起来很棘手的问题,引入了一个可选的属性。可以给每个子级一个键值,用于将来的匹配比较。如果指定了一个键值,React 就能够检测出节点插入、移除和替换,并且借助哈希表使节点移动复杂度为 O(n)。
renderA: <div><span key="first">first</span></div>
renderB: <div><span key="second">second</span><span key="first">first</span></div>
=> [insertNode <span>second</span>]
在实际开发中,生成一个键值不是很困难。大多数时候,要展示的元素已经有一个唯一的标识了。当没有唯一标识的时候,可以给组件模型添加一个新的 ID 属性,或者计算部分内容的哈希值来生成一个键值。记住,键值仅需要在兄弟节点中唯一,而不是全局唯一。
权衡(Trade-offs)
同步更新算法只是一种实现细节,记住这点很重要。React 能在每次操作中重新渲染整个应用,最终的结果将会是一样的。我们定期优化这个启发式算法来使常规的应用场景更加快速。
在当前的实现中,能够检测到某个子级树已经从它的兄弟节点中移除,但是不能指出它是否已经移到了其它某个地方。当前算法将会重新渲染整个子树。
由于依赖于两个预判条件,如果这两个条件都没有满足,性能将会大打折扣。
1、算法将不会尝试匹配不同组件类的子树。如果发现正在使用的两个组件类输出的 DOM 结构非常相似,你或许想把这两个组件类改成一个组件类。实际上, 这不是个问题。
2、如果没有提供稳定的键值(例如通过 Math.random() 生成),所有子树将会在每次数据更新中重新渲染。通过给开发者设置键值的机会,能够给特定场景写出更优化的代码。
react.js 从零开始(六)Reconciliation的更多相关文章
- react.js 从零开始(一)
React 是什么? 网络上的解释很多...我这里把他定义为 通过javascript 的形式组件化 html的框架... React 仅仅是 VIEW 层. React 提供了模板语法以及一些函数钩 ...
- react.js 从零开始(四)React 属性和状态详解
属性的含义和用法: 1.属性的含义. props=properties 属性:一个事物的性质和关系. 属性往往与生俱来,不可以修改. 2. 属性的用法. <Helloworld name=??? ...
- react.js 从零开始(三)JSX 语法及特点介绍
什么是jsx? jsx = JavaScript + xml jsx 是一种 Ecmascript 的一种新标准. jsx 是一种 带有结构性的语法. jsx 的特点: 1.类xml语法易于理解. 2 ...
- react.js 从零开始(七)React (虚拟)DOM
React 元素 React 中最主要的类型就是 ReactElement.它有四个属性:type,props,key 和ref.它没有方法,并且原型上什么都没有. 可以通过 React.create ...
- react.js 从零开始(五)React 中事件的用法
事件系统 虚拟事件对象 事件处理器将会传入虚拟事件对象的实例,一个对浏览器本地事件的跨浏览器封装.它有和浏览器本地事件相同的属性和方法,包括 stopPropagation() 和 prevent ...
- react.js 从零开始(二)组件的生命周期
什么是生命周期? 组件本质上是一个状态机,输入确定,输出一定确定. 当状态改变的时候 会触发不同的钩子函数,可以让开发者做出响应.. 一个组件的生命周期可以概括为 初始化:状态下 可以自定义的函数 g ...
- React.js 小书 Lesson27 - 实战分析:评论功能(六)
作者:胡子大哈 原文链接:http://huziketang.com/books/react/lesson27 转载请注明出处,保留原文链接和作者信息. (本文未审核) 删除评论 现在发布评论,评论不 ...
- React.js 小书介绍
React.js 小书 Github 关于作者 这是一本关于 React.js 的小书. 因为工作中一直在使用 React.js,也一直以来想总结一下自己关于 React.js 的一些知识.经验.于是 ...
- 前端迷思与React.js
前端迷思与React.js 前端技术这几年蓬勃发展, 这是当时某几个项目需要做前端技术选型时, 相关资料整理, 部分评论引用自社区. 开始吧: 目前, Web 开发技术框架选型为两种的占 80% .这 ...
随机推荐
- java JNI开发
Jni程序开发的一般操作步骤如下: l 编写java中的调用类 l 用javah生成c/c++原生函数的头文件 l c/c++中调用需要的其他函数功能, ...
- 玩转Web之servlet(五)---- 怎样解决servlet的线程安全问题
servlet默认是存在线程安全问题的,但是说白了,servlet的线程安全问题实际上就是多线程的线程安全问题,因为servlet恰巧是一个多线程才会出现安全性问题. 浏览器每次通过http协议去提交 ...
- 乐在其中设计模式(C#) - 抽象工厂模式(Abstract Factory Pattern)
原文:乐在其中设计模式(C#) - 抽象工厂模式(Abstract Factory Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 抽象工厂模式(Abstract Factor ...
- MongoDB学习笔记-维护
主从复制 MongoDB有主从复制技术,解决高可用和容灾问题,也就是备份. 配置主从的特点: N 个节点的集群 任何节点可作为主节点 所有写入操作都在主节点上 自动故障转移 自动恢复 数据分布式存储 ...
- CCLayer在Touch事件(Standard Touch Delegate和Targeted Touch Delegate)
在做练习,触摸故障,看到源代码,以了解下触摸事件. 练习操作:直CClayer子类init在 this->setTouchEnabled(true); 事件处理方法覆盖 virtual bool ...
- ReactJS学习 相关网站
React 入门实例教程-阮一峰 http://www.ruanyifeng.com/blog/2015/03/react.html汇智网-React 互动学习http://hubwiz.com/co ...
- java提高篇(五)-----使用序列化实现对象的拷贝
我们知道在Java中存在这个接口Cloneable,实现该接口的类都会具备被拷贝的能力,同时拷贝是在内存中进行,在性能方面比我们直接通过new生成对象来的快,特别是在大对象的生成上,使得性 ...
- System.Threading.ThreadStateException
异常:"System.Threading.ThreadStateException"在未处理的异常类型 System.Windows.Forms.dll 发生 其它信息: 在能够调 ...
- —教训深刻—SQL Server大约TempDB使用
场景现象 中午查询了流水,因未与业务人员沟通好.忘了删选条件,导致TempDB不能分配空间,SQL Server高负载执行. 错误分析 我们来看看错误日志: 再来看看TempDB自增长记录: 事件 逻 ...
- Direct3D 使用质地
关于使用质地 1 创建纹理 2 纹理寻址模式 3 纹理过滤 1 创建纹理 <1> D3DXCreateTexture功能 创建一个空的纹理. HRESULT D3DXCreateText ...