首页 > webfront > ECMAS > react > > 正文

重谈react优势——react技术栈回顾

发布人:zhoulujun    点击:

react刚刚推出的时候,讲react优势搜索结果是几十页。现在,react已经慢慢退火,该用用react技术栈的已经使用上,填过多少坑,加过多少班,

react刚刚推出的时候,讲react优势搜索结果是几十页。

现在,react已经慢慢退火,该用用react技术栈的已经使用上,填过多少坑,加过多少班,血泪控诉也不下千文。

今天,再谈一遍react优势,WTF?

React的收益有哪些?React的优势是什么?react和vue、angularJS等其它框架对比优势?


而作为总结回顾。react在工程实践中,带来哪些思想上的质变?


virtual dom虚拟DOM概念

它并不直接对DOM进行操作,引入了一个叫做virtual dom的概念,安插在javascript逻辑和实际的DOM之间,好处是减少DOM操作,减少DOM操作的目的是提高浏览器的渲染性能。

虚拟dom就中小型项目而言,的确从表象上看不出太多的优势,因为它解决的是底层的dom渲染,IO开销问题。但是想想facebook的体量,不难猜出react的诞生是为了解决更复杂更大型的项目开发和管理的。

实际上React和Vue其实也在操作DOM,只是比较高效地在操作DOM而已,虚拟DOM其实最终也会映射到真实DOM,虽然虚拟DOM只会将变化的部分更新到真实DOM,但实际上直接操作DOM也可以通过某些方式去优化,那么:

    1、操作data,不直接操作DOM有什么好处?

         更少的代码做更多的事。

    2、操作data会给DOM操作带来什么不好的地方吗?

          不会,但是不是所有功能“使用操作data”都可以代替的。

    3、会不会比直接操作DOM存在什么难度?

         不会有难度,但是思维需要有一些转变。


JSX虽然做了抽象视图,但她是声明式API,能够保证你看一眼就知道组件树的结构,譬如:


这结构还算清楚吧,基本一眼就知道这个一个面板由输入框、列表、摘要组成,而且布局也清楚了,自上而下。而且,通过查看一个源文件就可以知道你的组件将会如何渲染。这是最大的好处,尽管这和 Angular 模板没什么不同。具体参看:ReactJS For Stupid People


之前写UI的时候往往为了性能,要设计很多DOM的操作逻辑,用了react之后,这些都不给你做了,由他的state跟props来传递给VDOM,很省事,更专注于UI层面。


学会了react以及这个JSX语法,你不光可以通过react写web;也可以通过react-native写ios或者android的应用;甚至可以通过react-blessed写terminal可视化应用;当然也可以通过react-native-desktop写桌面应用。因为JSX这种声明式语法实际是在构建一个抽象的视图层,这种抽象可以通过不同适配器适配到各种显示终端,这总够屌吧?


unidirectional data flow-单向数据流

React倡导使用flux模式来进行组件间数据传输,这种做法叫unidirectional data flow(单向数据流),单向数据流的好处是与之前angularJS提出的two-way data binding相比较而言,因为单向,所以各种变化都是可预计、可控制的。不像two-way data binding那样,变化一但复杂起来,大家都互相触发变化,到最后一个地方变了,你根本猜不出来她还会导致其他什么地方跟着一起变。这个需要大量实践才能有所感受,如果你初学,那听听就算了,不必死磕。


react项目结构更加清晰:

virtual dom、redux、action,分部分别存放,就象java写后台查数据本来用jdbc一条sql就搞定,但分成action service dao分门别类地存放,这样维护性好,大公司的代码需要规范,这样出了问题好找原因。


组件化

一切都是component:代码更加模块化,重用代码更容易,可维护性高。

这里就涉及到react的 架构,比如:

smart, dumb component  

把组件分成两大类 Smart Components (容器) & Dumb Components(颗粒化组件)

这样做的好处:

  • 有助理你分离关注点,这样的话更有助于理解你的app的业务逻辑 和 它的ui

  • 更有助于复用你的dumb组件,你可以将你的dumb组件复用于别的state下,而且这两个state还完全不同

  • 本质上dumb 组件 其实 就是你的app的调色版。。你可以将它们放到一个页面上。。然后让设计师除了app的业务逻辑,样式随便怎么改,

参看文章:Smart and Dumb Components 

高阶组件(HOC-higher order component) 

高阶组件(HOC)是react中对组件逻辑进行重用的高级技术。但高阶组件本身并不是React API。它只是一种模式,这种模式是由react自身的组合性质必然产生的。

具体而言,高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件

const EnhancedComponent = higherOrderComponent(WrappedComponent);

对比组件将props属性转变成UI,高阶组件则是将一个组件转换成另一个新组件。

好处:使用高阶组件(HOC)解决交叉问题

参看文章:高阶组件

总结下,看看一个人的组件化水准,

  • pure component

  • functional component

  • smart, dumb component 

  • higher order component

  • hoc render hijacking

  • 会用 props.children React.children cloneElement

  • 提供 instance method

  • context

并理解react 内部实现原理

  • 懂 setState  是异步的

  • 懂 synthetic event

  • 懂 react-dom 分层和 react 没有关系

  • 懂 reconciler

  • 懂 fiber  

具体问题如下:

  • 1. 怎么抽象一个带搜索,单多选复合,有请求的 Selector,区分 smart 和 dumped。如果我再往上加功能,比如 autocomplete  等

  • 2. 怎么实现对表单的抽象,数据验证怎么统一处理

  • 3. 用 react 来实现一个可视化编辑器的引擎,怎么设计,怎么抽象与 model 的交互,再引入 redux 呢,怎么支持第三方组件热插拔

  • 4. 用 react 和 redux 模拟多人协作的 Todo,node 作为后端,怎么设计


同构、纯粹的javascrip

因为搜索引擎的爬虫程序依赖的是服务端响应而不是JavaScript的执行,预渲染你的应用有助于搜索引擎优化。


react一些常见问题:

setState()函数在任何情况下都会导致组件重渲染吗?如果setState()中参数还是原来没有发生任何变化的state呢?

对setState用得深了,就容易犯错,所以我们开门见山先把理解setState的关键点列出来。

  • setState不会立刻改变React组件中state的值;

  • setState通过引发一次组件的更新过程来引发重新绘制;

  • 多次setState函数调用产生的效果会合并

  • setState后,知道reader时,才真正改变state的值

    shouldComponentUpdate函数返回false,因为更新被中断,所以不调用render,但是React不会放弃掉对this.state的更新的,依然会更新this.state


传入 setState 函数的第二个参数的作用是什么?

该函数会在setState函数调用完成并且组件开始重渲染的时候被调用,我们可以用该函数来监听渲染是否完成(一般没有什么卵用)

 调用 setState 之后发生了什么?

 在代码中调用setState函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发所谓的调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个UI界面。在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。


用shouldComponentUpdate做优化的意义大吗?shouldComponentUpdate将带来可测量和可感知的提升?

如果不能,那就别用:你可能应该避免用它。据React团队的说,shouldComponentUpdate是一个保证性能的紧急出口,意思就是你不到万不得已就别用它。具体参考:什么时候使用shouldComponentUpdate方法?

一般情况下setState() 确立后总是触发一次重绘,除非在 shouldComponentUpdate() 中实现了条件渲染逻辑。如果使用可变的对象,但是又不能在 shouldComponentUpdate() 中实现这种逻辑,仅在新 state 和之前的 state 存在差异的时候调用 setState() 可以避免不必要的重新渲染。


react异步数据如ajax请求应该放在哪个生命周期?

对于同步的状态改变,是可以放在componentWillMount,对于异步的,最好好放在componentDidMount。但如果此时有若干细节需要处理,比如你的组件需要渲染子组件,而且子组件取决于父组件的某个属性,那么在子组件的componentDidMount中进行处理会有问题:因为此时父组件中对应的属性可能还没有完整获取,因此就让其在子组件的componentDidUpdate中处理。

具体参考:《react异步数据如ajax请求应该放在哪个生命周期?


React 中的 keys 是什么,为什么它们很重要?

在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。在 React Diff 算法中 React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重渲染。此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系,因此我们绝不可忽视转换函数中 Key 的重要性。

keys 是帮助 React 跟踪哪些项目已更改、添加或从列表中删除的属性。

每个keys 在兄弟元素之间是独一无二的。我们已经谈过几次关于一致化处理(reconciliation)的过程,而且这个一致化处理过程(reconciliation)中的一部分正在执行一个新的元素树与最前一个的差异。keys 使处理列表时更加高效,因为 React 可以使用子元素上的 keys 快速知道元素是新的还是在比较树时才被移动的。

而且 keys 不仅使这个过程更有效率,而且没有keys,React 不知道哪个本地状态对应于移动中的哪个项目。所以当你 map 的时候,不要忽略了 keys 。


受控组件( controlled component )与不受控制的组件( uncontrolled component )有什么区别?

React 的很大一部分是这样的想法,即组件负责控制和管理自己的状态(任何改变代用setSate处理)

那么不受控组件呢?组件数据不全部是setState来处理,还有DOM交互,比如refs这玩意来操控真实DOM

虽然不受控制的组件通常更容易实现,因为您只需使用引用从DOM获取值,但是通常建议您通过不受控制的组件来支持受控组件。


主要原因是受控组件支持即时字段验证,允许您有条件地禁用/启用按钮,强制输入格式,并且更多的是 『the React way』。


描述事件在React中的处理方式


为了解决跨浏览器兼容性问题,您的 React 中的事件处理程序将传递SyntheticEvent 的实例,它是 React 的浏览器本机事件的跨浏览器包装器。


这些 SyntheticEvent 与您习惯的原生事件具有相同的接口,除了它们在所有浏览器中都兼容。有趣的是,React 实际上并没有将事件附加到子节点本身。React 将使用单个事件监听器监听顶层的所有事件。这对于性能是有好处的,这也意味着在更新DOM时,React 不需要担心跟踪事件监听器。




在什么情况下你会优先选择使用 Class Component 而不是 Functional Component?

在组件需要包含内部状态或者使用到生命周期函数的时候使用 Class Component ,否则使用函数式组件。


简单介绍下react的diff

计算一棵树形结构转换成另一棵树形结构的最少操作,是一个复杂且值得研究的问题。传统 diff 算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),其中 n 是树中节点的总数。O(n^3) 到底有多可怕,这意味着如果要展示1000个节点,就要依次执行上十亿次的比较。这种指数型的性能消耗对于前端渲染场景来说代价太高了!现今的 CPU 每秒钟能执行大约30亿条指令,即便是最高效的实现,也不可能在一秒内计算出差异情况。。React 通过制定大胆的策略,将 O(n^3) 复杂度的问题转换成 O(n) 复杂度的问题。

 react的diff 策略:

  •  Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。

  •  拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。 

  • 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。 

基于以上三个前提策略,React 分别对 tree diffcomponent diff 以及 element diff 进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。 


  • tree diff:

    基于策略一,React 对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较。

既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。

0c08dbb6b1e0745780de4d208ad51d34_hd.jpg

updateChildren: function(nextNestedChildrenElements, transaction, context) {
  updateDepth++;
  var errorThrown = true;
  try {
    this._updateChildren(nextNestedChildrenElements, transaction, context);
    errorThrown = false;
  } finally {
    updateDepth--;
    if (!updateDepth) {
      if (errorThrown) {
        clearQueue();
      } else {
        processQueue();
      }
    }
  }
}

分析至此,大部分人可能都存在这样的疑问:如果出现了 DOM 节点跨层级的移动操作,React diff 会有怎样的表现呢?是的,对此我也好奇不已,不如试验一番。


如下图,A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。此时,React diff 的执行情况:create A -> create B -> create C -> delete A。

d712a73769688afe1ef1a055391d99ed_hd (1).jpg

由此可发现,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的树被整个重新创建,这是一种影响 React 性能的操作,因此 React 官方建议不要进行 DOM 节点跨层级的操作。


提示:在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。

component diff:

  • 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。

  • 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户shouldComponentUpdate() 来判断该组件是否需要进行 diff。

如下图,当 component D 改变为 component G 时,即使这两个 component 结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除 component D,重新创建 component G 以及其子节点。虽然当两个 component 是不同类型但结构相似时,React diff 会影响性能,但正如 React 官方博客所言:不同类型的 component 是很少存在相似 DOM tree 的机会,因此这种极端因素很难在实现开发过程中造成重大影响的。

52654992aba15fc90e2dac8b2387d0c4_hd.jpg

element diff:

当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)

  • INSERT_MARKUP,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。

  • MOVE_EXISTING,在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。

  • REMOVE_NODE,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。

如下图,老集合中包含节点:A、B、C、D,更新后的新集合中包含节点:B、A、D、C,此时新老集合进行 diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。

7541670c089b84c59b84e9438e92a8e9_hd.jpg

React 提出优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分,虽然只是小小的改动,性能上却发生了翻天覆地的变化!

7b9beae0cf0a5bc8c2e82d00c43d1c90_hd.jpg

总结

  • React 通过制定大胆的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题;

  • React 通过分层求异的策略,对 tree diff 进行算法优化;

  • React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化;

  • React 通过设置唯一 key的策略,对 element diff 进行算法优化;

  • 建议,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升;

  • 建议,在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。


diff算法作为react的核心,非三言两语能够说起道明,建议参看:React 源码剖析系列 - 不可思议的 react diff 


怎么看待不可变数据?

这个暂待完善


ssr (server side render)会有什么性能问题,哪些会引起内存泄露,引入 redux 后怎么处理请求的逻辑


参考:从零开始搭建React同构应用(三):配置SSR


参考文章:

什么时候使用shouldComponentUpdate方法?

setState为什么不会同步更新组件状态

setState:这个API设计到底怎么样

高阶组件