<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 后怎么处理请求的逻辑
参考文章:
什么时候使用shouldComponentUpdate方法?
转载请注明文章来源:重谈react优势--react技术栈回顾 - ECMAScript,js,javascript - 周陆军的个人网站:https://www.zhoulujun.cn/html/webfront/ECMAScript/jsBase/2018_0424_8101.html,如有不妥之处,望告知!