前言

hi~
本文写在阅读《React设计模式与最佳实践》这本书后,偏向于该书的一种内容梳理~
阅读本文可以快速对自己掌握的React技能进行一次梳理,寻找知识漏洞;可以看到一些常见的错误模式,错误的原因以及正确的写法;可以了解到React的一些高级用法以及底层实现原理;

本文主要包括四个部分:

  1. 概述:这一部分主要阐释了什么是React组件设计模式;

  2. 知识梳理:这一部分主要是《React设计模式与最佳实践》的脉络梳理,相当于React的知识图谱;

  3. 这个模式不太妥:这一部分主要介绍了一些新手或是老司机都会忽视的一些问题、问题的原因以及如何正确的书写;

  4. ReactJS你不知道:这一部分主要通过一些简单的例子来介绍React的一些高级用法以及底层实现原理;

📖概述

设计模式:解决问题的方案。 React组件设计模式:React组件开发过程中,遇见的各种问题的常用解决方案。

React组件开发规范(常用模式) - 图1

🌲知识梳理

推荐阅读 —-《React设计模式与最佳实践》作者:Michele Bertoli
React组件开发规范(常用模式) - 图2

👀这个模式不太妥

1. 这个组件书写格式有何不妥?

  1. class Parent extends React.Component{
  2. render(){
  3. return
  4. <div />
  5. }
  6. }

🤓️很明显,很不妥滴!
原因是JSX本质上会替换为函数,由于自动分号插入机制的存在,另起一行会导致意外的结果。转换成:

  1. return;
  2. React.createElement('div',null)

所以在多行书写JSX时,记得要用括号扩起来封装它们。
正确写法如下:

  1. class Parent extends React.Component{
  2. render(){
  3. return <div />
  4. }
  5. }
  6. // 或者
  7. class Parent extends React.Component{
  8. render(){
  9. return(
  10. <div />
  11. )
  12. }
  13. }

2. 在渲染函数中绑定函数有何不妥?

  1. class Parent extends React.Component{
  2. handleClick(){
  3. console.log(this);
  4. }
  5. render(){
  6. return <Children onClick={()=>this.handleClick()}/>
  7. }
  8. }

🤓️仔细看,也是有丝丝不妥滴~
每次渲染Children组件时都会触发箭头函数 ,在渲染方法内多次触发某个函数不太理想,但本身没有什么问题。
问题在于,如果这个函数传递给子组件,那么子组件在每次更新时,都会接收新的props,进而导致低效的渲染。

即使函数实现不变,但每次调用箭头函数都会返回一个全新创建的函数。
所有对象在创建时都会返回新的实例,即使包含相同的值,两个新数组也永远不会相等,所以组件属性值是字面量数组也会存在此问题。

正确做法是,在构造器内进行绑定操作,这样即使多次渲染组件,也不会造成额外的低效渲染性能损耗。
正确写法如下:

  1. class Parent extends React.Component {
  2. constructor(props) {
  3. super(props)
  4. this.handleClick = this.handleClick.bind(this)
  5. }
  6. handleClick() {
  7. console.log(this);
  8. }
  9. render() {
  10. return <Children onClick={this.handleClick} />
  11. }
  12. }

3. 将数组索引作为key有什么不妥?

  1. class List extends React.Component {
  2. constructor(props) {
  3. super(props)
  4. this.state = {
  5. items: ['foo', 'bar'],
  6. }
  7. this.handleClick = this.handleClick.bind(this)
  8. }
  9. handleClick() {
  10. const items = this.state.items.slice()
  11. items.unshift('baz')
  12. this.setState({
  13. items,
  14. })
  15. }
  16. render() {
  17. return (
  18. <div>
  19. <ul>
  20. {
  21. this.state.items.map((item, index) => (
  22. <li key={index}>
  23. {item}
  24. <input type="text"/>
  25. </li>
  26. ))
  27. }
  28. </ul>
  29. <button onClick = {this.handleclick}>+</button>
  30. </div>
  31. )
  32. }
  33. }

🤓️em~~不妥不妥~
在浏览器中运行组件,将每项的值复制到输入框中,然后点击 + 按钮,此时就会出现意外行为。所有的列表项向下移了一位,但输入框元素的位置保持不变。

原因是我们将map函数的数组索引用作了key属性。数组索引从0开始,即便在列表顶部推入了新元素,react也会认为我们只是修改已有内容,并在最后的位置添加新元素,而不是移动全部的列表元素,在顶部创新新元素。

正确的做法是:
提供唯一且确定的key,只能标识唯一一项数据的key,才是优化性能的最佳做法。

4. 在DOM元素上展开props对象有什么不妥?

  1. //在组件上
  2. <Component {...props}/>
  3. //在DOM元素上
  4. <div {...props}>

🤓️真のマジックショーが始まるぜ!
展开props对象,可以避免手动编写每个属性。但在DOM元素上展开props对象时,就会有添加未知html属性的风险,这样的做法很糟糕~非标准属性会导致相同的问题和警告。
正确做法:

  1. // 创建一个名为domProps的属性,可以在组件上安全的展开它,因为我们显示申明了它包含有效的DOM属性
  2. const Spread = props => <div {...props.domProps}/>
  3. <Spread foo="bar" domProps={{className:'baz'}}>

🤔ReactJS你不知道

1. 何时使用mixin,何时使用HOC?

mixin:用来定义一些组件之间公用的一些功能、方法,使用这个mixin的组件能够自由的使用这些方法,就像是在组件中定义的一样。简单示例如下:

  1. //一个监听window的resize事件来获取窗口大小的mixin
  2. const WindowResizeMixin = {
  3. getIntialState(){
  4. return {
  5. innerWidth = window.innerWidth
  6. }
  7. },
  8. componentDidMount() {
  9. window.addEventListener('resize',this.handleResize)
  10. },
  11. handleResize() {
  12. this.setState({
  13. innerWidth = window.innerWidth;
  14. })
  15. },
  16. componentWillUnmount() {
  17. window.removeEventListener('resize',this.handleResize)
  18. }
  19. };
  20. //使用mixin
  21. const MyComponent = React.createClass({
  22. mixins:[WindowResizeMixin],
  23. componentDidMount: function() {
  24. this.handleResize() // Call a method on the mixin
  25. },
  26. render(){
  27. console.log('window.innerWidth',this.state.innerWidth);
  28. ...
  29. }
  30. })

使用场景:

  1. mixin只能和createClass工厂方法搭配使用,使用 ES6 class 定义的组件已经不支持 mixin 了;

  2. 在不同的组件之间共享代码;

优点:

  1. 允许合并生命周期函数和初始状态,如果某个组件使用了一个mixin,组件与mixin都使用了componentDidMount钩子,则两者会顺序执行,使用相同生命周期钩子的多个mixin同理。mixin的先执行,组件内的后执行。

缺点:

  1. 外来属性污染组件状态:虽然react可以聪明的合并生命周期回调,但是如果两个mixin定义或者调用了同样的函数名,抑或在状态中使用了相同的属性,发生冲突,那么react对此无能为力;

  2. 可维护性差:mixin往往需要使用状态与组件进行通信,因此如果mixin在组件中更新了一个特殊属性,那么组件就会因为这个新的属性重新渲染,导致组件包含不必要的状态;

  3. mixin可能会相互依赖,这种耦合导致组件重构和应用扩展变得困难。

HOC:高阶组件,对传入的组件进行增强,并返回一个添加了额外行为的新组件。组件复用的高级技术~
简单示例如下:

  1. //接收组件作为参数的函数
  2. const withInnerWidth = Component => (
  3. class extends React.Component {
  4. constructor(props) {
  5. super(props)
  6. this.state = {
  7. innerWidth: window.innerWidth,
  8. }
  9. this.handleResize = this.handleResize.bind(this)
  10. }
  11. componentDidMount() {
  12. window.addEventListener('resize', this.handleResize)
  13. }
  14. handleResize() {
  15. this.setState({
  16. innerWidth = window.innerWidth
  17. })
  18. }
  19. componentWillUnmount() {
  20. window.removeEventListener('resize', this.handleResize)
  21. }
  22. render(){
  23. return <Component {...this.props} {...this.state}/>
  24. }
  25. }
  26. )
  27. //创建一个无状态组件,并期望传入一个innerWidth属性
  28. const MyComponent = ({ innerWidth }) => {
  29. console.log('window.innerWidth', innerWidth)
  30. ...
  31. }
  32. MyComponent.protoTypes = {
  33. innerWidth: React.PropTypes.number,
  34. }
  35. //使用高阶组件增强该组件
  36. const MyComponentWidthInnerWidth = withInnerWidth(MyComponent)

使用场景:

  1. 增强原有组件的功能,对组件能力的补充;

  2. 在提取不同类别组件相似的行为时,例如把组件的显示隐藏的逻辑抽象出来,或者对所有组件的某个字段进行统一的校验等等;

  3. 不只是工厂函数的组件,任何形式创建的组件都可以使用。

优点:

  1. 使用高阶组件从高阶组件中获取数据,转换成props后传递给组件,不污染组件状态;

  2. 不需要组件实现任何方法,组件与高阶组件没有耦合,高阶组件可以组合使用;

  3. 使用高阶组件从context中获取数据,转换成props后传递给组件,使得组件不需要感知context的存在,与context解耦。

2. React是如何往DOM上添加事件处理器的?

简单示例

  1. class Button extends React.Component {
  2. constructor(props) {
  3. super(props)
  4. this.handleClick = this.handleClick.bind(this)
  5. }
  6. handleClick() {
  7. console.log(this);
  8. }
  9. render() {
  10. return <button
  11. onClick={this.handleClick}
  12. onDoubleClick={this.handleClick} />
  13. }
  14. }

原理:

  1. “onClick”、”onDoubleClick” 只要是以 on 开头的属性,就是在向react描述期望达成的行为。

  2. 但react本身不会在底层DOM节点上添加真正的事件处理器。

  3. React实际做的是在根元素上添加单个事件处理器,由于事件冒泡机制,这个处理器会监听所有事件。当浏览器触发我们想要的事件时,react会代表相应组件调用处理器,这个技巧称作事件代理,可以优化内存和速度。

3. ref的原理

  1. render(){
  2. return <input ref={element => {this.element = element}/>
  3. }

当我们需要访问底层DOM节点来执行一些命令式操作,ref就很重要了。但一般情况下都应该尽量避免使用ref,因为它让我们的代码更偏向与命令式,可读性与可维护性都变差了。

ref的用法与原理:
在返回元素的ref属性上定义一个回掉函数。这个回掉函数会在组件挂在时被调用,元素参数表示输入的DOM实例,值得注意的是,卸载时也会调用这个回掉,并传入null参数来释放内存。
设置非原生组件(以大写字母开头的自定义组件)的ref回掉时,接收到的回掉参数引用不是DOM节点实例,而是组件本身的实例。允许我们访问子组件的内部实例。

🍓更多

想要了解更多,看这里 —>《React设计模式与最佳实践》
这本书将带你了解react中最有价值的设计模式,并展示了如何在全新或者已有的真实项目中应用设计模式与最佳实践~你值得拥有哇~