<span>重谈react优势——react技术栈回顾</span>

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 diff、component diff 以及 element diff 进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。 

 

  • tree diff:

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

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

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。

由此可发现,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 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 的机会,因此这种极端因素很难在实现开发过程中造成重大影响的。

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。

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

总结

  • 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设计到底怎么样

高阶组件

转载请注明文章来源:重谈react优势--react技术栈回顾 - ECMAScript,js,javascript - 周陆军的个人网站https://www.zhoulujun.cn/html/webfront/ECMAScript/jsBase/2018_0424_8101.html,如有不妥之处,望告知!

全部评论

相关推荐

点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务