01.React 使用

React 使用

1. JSX 基本使用

  • 变量、表达式
  • class style
  • 子元素和组件
在 render 函数中举例子
  1. 获取变量,插值语法

    const pElem = <p>{this.state.name}</p>
    return pElem
    
  2. 表达式

    大括号里都是 js 表达式

    const exprElem = <p>{this.state.flag ? 'yes' : 'no'}</p>
    return exprElem
    
  3. 子元素

    const imgElem = <div>
                        <p>我的头像</p>
                        <img src="xxxx.png"/>
                        <img src={this.state.imgUrl}/>
                    </div>
    return imgElem
    
  4. class

    // class
    const classElem = <p className="title">设置 css class</p>
    return classElem
    
  5. style

    // style
    const styleData = { fontSize: '30px',  color: 'blue' }
    const styleElem = <p style={styleData}>设置 style</p>
    
    // 内联写法,注意 {{ 和 }}
    const styleElem = <p style={{ fontSize: '30px',  color: 'blue' }}>设置 style</p>
    return styleElem
    
  6. 原生 HTML

    // 原生 html
    const rawHtml = '<span>富文本内容<i>斜体</i><b>加粗</b></span>'
    const rawHtmlData = {
        __html: rawHtml // 注意,必须是这种格式
    }
    const rawHtmlElem = <div>
    	<p dangerouslySetInnerHTML={rawHtmlData}></p>
    		<p>{rawHtml}</p>
    	</div>
    return rawHtmlElem
    
  7. 加载组件

    // 加载组件
    const componentElem = <div>
        <p>JSX 中加载一个组件</p>
            <hr/>
            <List/>
        </div>
    return componentElem
    

2. 条件判断

  1. if … else …

    render() {
        const blackBtn = <button className="btn-black">black btn</button>
        const whiteBtn = <button className="btn-white">white btn</button>
    
        // if else
        if (this.state.theme === 'black') {
            return blackBtn
        } else {
            return whiteBtn
        }
    
  2. 三元表达式

    // 三元运算符
    return <div>
        { this.state.theme === 'black' ? blackBtn : whiteBtn }
    </div>
    
  3. || 和 &&

    // &&
    return <div>
        { this.state.theme === 'black' && blackBtn }
    </div>
    

3. 渲染列表

  1. map
  2. key
class ListDemo extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      list: [
        {
          id: "id-1",
          title: "标题1",
        },
        {
          id: "id-2",
          title: "标题2",
        },
        {
          id: "id-3",
          title: "标题3",
        },
      ],
    };
  }
  render() {
    return (
      <ul>
        {
          /* vue v-for */
          this.state.list.map((item, index) => {
            // 这里的 key 和 Vue 的 key 类似,必填,不能是 index 或 random
            return (
              <li key={item.id}>
                index {index}; id {item.id}; title {item.title}
              </li>
            );
          })
        }
      </ul>
    );
  }
}

4. this 指向

bind this 用来修改方法中的 this 指向。

举例子:

标签触发方法

render() {
    return <p onClick={this.clickHandler1}>
       {this.state.name}
    </p>
}

clickHandler1() {
    // console.log('this....', this) // this 默认是 undefined
    this.setState({
        name: 'lisi'
    })
}

这时候点击 p 标签,会报错。

原因在于 this 指向的丢失,具体细节是,clickHandler1 函数赋值给 onClick,此时触发事件就是直接调用,而不是通过实例调用的了,按理来说,此时 this 应该是全局的 window,但结果是 undefined。

babel 下为严格模式,禁止自定义函数的 this 指向 window,所以此时的 this 为 undefined。但这里并不是 babel 严格模式的原因。

重点来了:class 中所有定义的方法,方法里开启了局部严格模式,所以禁止自定义函数的 this 指向 window,因此 this 为 undefined。

绑定 this 的方法
  1. bind 函数

    通过 bind 来绑定 this 指向,在 render 方法中使用 .bind(this) 是可行的,但是在构造器里进行 bind,只要在构造的时候 bind 一次,性能最优。

    constructor(props) {
      ......
      this.clickHandler1 = this.clickHandler1.bind(this)
    }
    
    render() {
         // this - 使用 bind
         return <p onClick={this.clickHandler1}>
             {this.state.name}
         </p>
    

    bind 函数传参

    // 传递参数 - 用 bind(this, a, b)
    	render() {
            return <ul>{this.state.list.map((item, index) => {
                return <li key={item.id} onClick={this.clickHandler4.bind(this, item.id, item.title)}>
                    index {index}; title {item.title}
                </li>
            })}</ul>
    	......
    	}
        
        // 传递参数
        clickHandler4(id, title, event) {
            console.log(id, title)
            console.log('event', event) // 最后追加一个参数,即可接收 event
        }
    
  2. 使用箭头函数

    箭头函数体内的 this 对象,就是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。所以箭头函数里的 this 恒定指向对象实例。简单来说,箭头函数不绑定 this,会不会其所在上下文的 this 作为自己的 this。

    这种方法最省事,推荐使用。

    拓展:箭头函数不绑定 arguments,取而代之用 rest 参数解决。

    clickHandler2 = () => {
        this.setState({
            name: 'lisi'
        })
    }
    
    render() {
         // this - 使用箭头函数
         return <p onClick={this.clickHandler2}>
             {this.state.name}
         </p>
    

5. React 事件和 DOM 事件的区别

// 获取 event
clickHandler3 = (event) => {
    event.preventDefault() // 阻止默认行为
    event.stopPropagation() // 阻止冒泡
    console.log('target', event.target) // 指向当前元素,即当前元素触发
    console.log('current target', event.currentTarget) // 指向当前元素,假象!!!

    // 注意,event 其实是 React 封装的。可以看 __proto__.constructor 是 SyntheticEvent 组合事件
    console.log('event', event) // 不是原生的 Event ,原生的是 MouseEvent
    console.log('event.__proto__.constructor', event.__proto__.constructor)

    // 原生 event 如下。其 __proto__.constructor 是 MouseEvent
    console.log('nativeEvent', event.nativeEvent)
    console.log('nativeEvent target', event.nativeEvent.target)  // 指向当前元素,即当前元素触发
    console.log('nativeEvent current target', event.nativeEvent.currentTarget) // 指向 document !!!即,绑定事件的元素是 document。但是从 React17 版本开始,事件就不再绑定打 document 上了。

    // 1. event 是 SyntheticEvent ,模拟出来 DOM 事件所有能力
    // 2. event.nativeEvent 是原生事件对象
    // 3. 所有的事件,都被挂载到 document 上
    // 4. 和 DOM 事件不一样,和 Vue 事件也不一样
}

render() {
     // event
    return <a onClick={this.clickHandler3}>
        click me
    </a>
  • React 16 绑定到 document
  • React 17 事件绑定到 root 组件
  • 有利于多个 React 版本并存,例如微前端。document 只能有一个,但是 root 可以有多个

6. React 表单

  • 受控组件

      render() {
        // 受控组件(非受控组件,后面再讲)
        return (
          <div>
            <p>{this.state.name}</p>
            <label htmlFor="inputName">姓名:</label> {/* 用 htmlFor 代替 for */}
            <input
              id="inputName"
              value={this.state.name}
              onChange={this.onInputChange}
            />
          </div>
        );
    
        // textarea - 使用 value
        return (
          <div>
            <textarea value={this.state.info} onChange={this.onTextareaChange} />
            <p>{this.state.info}</p>
          </div>
        );
    
        // select - 使用 value
        return (
          <div>
            <select value={this.state.city} onChange={this.onSelectChange}>
              <option value="beijing">北京</option>
              <option value="shanghai">上海</option>
              <option value="shenzhen">深圳</option>
            </select>
            <p>{this.state.city}</p>
          </div>
        );
    
        // checkbox
        return (
          <div>
            <input
              type="checkbox"
              checked={this.state.flag}
              onChange={this.onCheckboxChange}
            />
            <p>{this.state.flag.toString()}</p>
          </div>
        );
    
        // radio
        return (
          <div>
            male{" "}
            <input
              type="radio"
              name="gender"
              value="male"
              checked={this.state.gender === "male"}
              onChange={this.onRadioChange}
            />
            female{" "}
            <input
              type="radio"
              name="gender"
              value="female"
              checked={this.state.gender === "female"}
              onChange={this.onRadioChange}
            />
            <p>{this.state.gender}</p>
          </div>
        );
    
        // 非受控组件 - 后面再讲
      }
      onInputChange = (e) => {
        this.setState({
          name: e.target.value,
        });
      };
    
      onTextareaChange = (e) => {
        this.setState({
          info: e.target.value,
        });
      };
    
      onSelectChange = (e) => {
        this.setState({
          city: e.target.value,
        });
      };
    
      onCheckboxChange = () => {
        this.setState({
          flag: !this.state.flag,
        });
      };
      
      onRadioChange = (e) => {
        this.setState({
          gender: e.target.value,
        });
      };
    }
    

    受控组件指的是,表单内容和 state 关联在一起,因此通过 state 就可以控制组件内容。

    非受控组件就是不受 state 控制的组件

  • input textarea select 用 value

  • checkbox radio 用 checked

7. React 组件使用

  • props 传递数据
  • props 传递函数
  • props 类型检查

以 TodoList 为例,里边有详细的流程:

父组件 TodoListDemo:

class TodoListDemo extends React.Component {
  constructor(props) {
    super(props);
    // 状态(数据)提升,父组件管理数据
    this.state = {
      list: [
        {
          id: "id-1",
          title: "标题1",
        },
        {
          id: "id-2",
          title: "标题2",
        },
        {
          id: "id-3",
          title: "标题3",
        },
      ],
      footerInfo: "底部文字",
    };
  }
  render() {
    return (
      <div>
        <Input submitTitle={this.onSubmitTitle} />
        <List list={this.state.list} />
        <Footer text={this.state.footerInfo} length={this.state.list.length} />
      </div>
    );
  }
  onSubmitTitle = (title) => {
    this.setState({
      list: this.state.list.concat({
        id: `id-${Date.now()}`,
        title,
      }),
    });
  };
}

输入框组件 Input:

class Input extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      title: "",
    };
  }
  render() {
    return (
      <div>
        <input value={this.state.title} onChange={this.onTitleChange} />
        <button onClick={this.onSubmit}>提交</button>
      </div>
    );
  }
  onTitleChange = (e) => {
    this.setState({
      title: e.target.value,
    });
  };
  onSubmit = () => {
    const { submitTitle } = this.props;
    submitTitle(this.state.title); // 'abc'

    this.setState({
      title: "",
    });
  };
}
// props 类型检查
Input.propTypes = {
  submitTitle: PropTypes.func.isRequired,
};

列表组件 List:

class List extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    const { list } = this.props;

    return (
      <ul>
        {list.map((item, index) => {
          return (
            <li key={item.id}>
              <span>{item.title}</span>
            </li>
          );
        })}
      </ul>
    );
  }
}
// props 类型检查
List.propTypes = {
  list: PropTypes.arrayOf(PropTypes.object).isRequired,
};

Footer 组件,底部文字和列表内项目数量展示:

class Footer extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <p>
        {this.props.text}
        {this.props.length}
      </p>
    );
  }
  componentDidUpdate() {
    console.log("footer did update");
  }
  shouldComponentUpdate(nextProps, nextState) {
    if (
      nextProps.text !== this.props.text ||
      nextProps.length !== this.props.length
    ) {
      return true; // 可以渲染
    }
    return false; // 不重复渲染
  }

  // React 默认:父组件有更新,子组件则无条件也更新!!!
  // 性能优化对于 React 更加重要!
  // SCU 一定要每次都用吗?—— 需要的时候才优化
}

8. setState

  • 不可变值
  • 可能是异步更新
  • 可能会被合并

8.1 setState 不可变值

state 要在构造函数中定义
class StateDemo extends React.Component {
  constructor(props) {
    super(props);

    // 第一,state 要在构造函数中定义
    this.state = {
      count: 0,
    };
  }
不能直接修改 state

不能直接修改的原因在于,eact的底层实现是diff,所以必须依赖数据的 immutability,破坏了immutability 会发生各种难以注意到的问题。在性能提升上,为了 SCU,比较新 state 和旧 state 是否相同时,能够分清楚哪些属性是真正引起视图更新的。

  increase = () => {
    // 第二,不要直接修改 state ,使用不可变值 ----------------------------
    // this.state.count++ // 错误
    this.setState({
        count: this.state.count + 1 // SCU
    })
    // 操作数组、对象的的常用形式
对 state 里的数组进行修改
// 不可变值(函数式编程,纯函数) - 数组
const list5Copy = this.state.list5.slice()
list5Copy.splice(2, 0, 'a') // 中间插入/删除
this.setState({
  list1: this.state.list1.concat(100), // 追加
  list2: [...this.state.list2, 100], // 追加
  list3: this.state.list3.slice(0, 3), // 截取
  list4: this.state.list4.filter(item => item > 100), // 筛选
  list5: list5Copy // 其他操作
})
// 注意,不能直接对 this.state.list 进行 push pop splice 等,这样违反不可变值
对 state 里的对象进行修改
// 不可变值 - 对象
this.setState({
  // 旧的对象传进去再拼接上新内容
  obj1: Object.assign({}, this.state.obj1, {a: 100}),
  obj2: {...this.state.obj2, a: 100}
})
// 注意,不能直接对 this.state.obj 进行属性设置,这样违反不可变值

8.2 setState 是同步还是异步

一般情况下是异步的

    this.setState(
      {
        count: this.state.count + 1,
      },
      () => {
        // 联想 Vue $nextTick - DOM
        console.log("count by callback", this.state.count); // 回调函数中可以拿到最新的 state
      }
    );
    console.log("count", this.state.count); // 因为是异步的,所以拿不到最新值

setTimeout 中 setState 是同步的

    // setTimeout 中 setState 是同步的
    setTimeout(() => {
      this.setState({
        count: this.state.count + 1,
      });
      console.log("count in setTimeout", this.state.count);
    }, 0);

自己定义的 DOM 事件,setState 是同步的

  bodyClickHandler = () => {
    this.setState({
      count: this.state.count + 1,
    });
    console.log("count in body event", this.state.count);
  };
  componentDidMount() {
    // 自己定义的 DOM 事件,setState 是同步的
    document.body.addEventListener("click", this.bodyClickHandler);
  }
  componentWillUnmount() {
    // 及时销毁自定义 DOM 事件
    document.body.removeEventListener("click", this.bodyClickHandler);
    // clearTimeout
  }

8.3 setState 何时会合并 state

传入对象,会被合并。原因在于异步更新,setState 之前的初始值都是一样的。

    // 传入对象,会被合并(类似 Object.assign )。执行结果只一次 +1
    this.setState({
      count: this.state.count + 1,
    });
    this.setState({
      count: this.state.count + 1,
    });
    this.setState({
      count: this.state.count + 1,
    });

传入函数就不会被合并,一个一个去执行

    // 传入函数,不会被合并。执行结果是 +3
    this.setState((prevState, props) => {
      return {
        count: prevState.count + 1,
      };
    });
    this.setState((prevState, props) => {
      return {
        count: prevState.count + 1,
      };
    });
    this.setState((prevState, props) => {
      return {
        count: prevState.count + 1,
      };
    });

9. 组件生命周期

最常用的生命周期

基本的声明周期

父子组件生命周期函数执行顺序

  • 进入页面:parent-constructor -> parent-getDerivedStateFromProps -> parent-render -> child-constructor -> child-getDerivedStateFromProps -> child-render -> child-componentDidMount -> parent-componentDidMount

  • 更新页面:parent-getDerivedStateFromProps -> parent-shouldComponentUpdate -> parent-render -> child-getDerivedStateFromProps -> child-shouldComponentUpdate -> child-render -> child-componentDidUpdate -> parent-componentDidUpdate

  • 销毁页面:parent-componentWillUnmount -> child-componentWillUnmount

10. React 高级特性

  • 函数组件
  • 非受控组件
  • Portals
  • context
  • 异步组件
  • 性能优化
  • 高阶组件 HOC
  • Render Props

10. React 函数组件和 class 组件有何区别

函数组件
  • 纯函数,输入 props,输出 JSX
  • 没有实例,没有生命周期,没有 state
  • 不能扩展其他方法
函数组件
  • 有组件实例
  • 有生命周期
  • 有 state 和 setState

11. 非受控组件

  • ref
  • defaultValue defaultChecked
  • 手动操作 DOM 元素

非受控组件的特点,该组件不受 state 操控。

输入框组件作为非受控组件

通过 React.createRef() 创建 ref 创建一个属性,如下面代码的 nameInputRef,然后在非受控组件里使用新建的 ref。

.current 获取当前的节点,便可以获得里边的值

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: "双越",
      flag: true,
    };
    this.nameInputRef = React.createRef(); // 创建 ref
  }
  render() {
    // input defaultValue
    return (
      <div>
        {/* 使用 defaultValue 而不是 value ,使用 ref */}
        <input defaultValue={this.state.name} ref={this.nameInputRef} />
        {/* state 并不会随着改变 */}
        <span>state.name: {this.state.name}</span>
        <br />
        <button onClick={this.alertName}>alert name</button>
      </div>
    );
  alertName = () => {
    const elem = this.nameInputRef.current; // 通过 ref 获取 DOM 节点
    alert(elem.value); // 不是 this.state.name
  };
}
checkbox 作为非受控组件

默认初始值使用 defaultChecked,但是并不会同步到 state 中。

    // checkbox defaultChecked
    return (
      <div>
        <input type="checkbox" defaultChecked={this.state.flag} />
      </div>
    );
例子:通过 ref 获取文件名
class App extends React.Component {
  constructor(props) {
    super(props);
    this.fileInputRef = React.createRef();
  }
  render() {
	// file
    return (
      <div>
        <input type="file" ref={this.fileInputRef} />
        <button onClick={this.alertFile}>alert file</button>
      </div>
    );
  }
  alertFile = () => {
    const elem = this.fileInputRef.current; // 通过 ref 获取 DOM 节点
    alert(elem.files[0].name);
  };
}
总结:非受控组件 - 使用场景
  • 必须手动操作 DOM 元素,setState 实现不了
  • 文件上传 <input type="file" />
  • 某些富文本编辑器,需要传入 DOM 元素
受控组件 vs 非受控组件
  • 优先使用受控组件,符合 React 设计原则
  • 必须操作 DOM 时,再使用非受控组件

12. React Portals

  • 组件默认会按照既定层次嵌套渲染
  • 如何让组件渲染到父组件以外?
例子,将样式渲染到 body 上

为了浏览器的兼容,想要把 model 内容挂在 body 上,因为有更好的浏览器兼容性。

.modal {
    position: fixed;
    width: 300px;
    height: 100px;
    top: 100px;
    left: 50%;
    margin-left: -150px;
    background-color: #000;
    /* opacity: .2; */
    color: #fff;
    text-align: center;
}
正常渲染

父组件传东西进来,可以通过 this.props.children 获得,就很像 vue 里的 slot。

父组件:

class AdvancedUse extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <div>
        <PortalsDemo>Modal 内容</PortalsDemo>
      </div>
    );
  }
}

子组件:

class PortalsDemo extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
        }
    }
    render() {
      // 正常渲染
      return (
        <div className="modal">
          {this.props.children} {/* vue slot */}
        </div>
      );
......

渲染结果:

使用 React Portals

子组件:

portal 有传送门的意思,使用 createPortal 建立一个传送门,重点是第二个参数,第二个参数指传送到哪个 DOM 节点下。

class PortalsDemo extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
        }
    }
    render() {
      // 使用 Portals 渲染到 body 上。
      // fixed 元素要放在 body 上,有更好的浏览器兼容性。
      return ReactDOM.createPortal(
        <div className="modal">{this.props.children}</div>,
        document.body // DOM 节点
      );
......

渲染结果:

可以看到,父组件里没有 class 为 model 的组件了,而是挂载到了 body 的第一层

总结:Portals 使用场景
  • 父组件 overflow: hidden,是 BFC,限制子组件展示,可以让子组件逃离父组件
  • 父组件 z-index 值太小,导致子组件在里边展示的话会被覆盖
  • fixed 需要放在 body 第一层级

13. React Context

  • 公共信息(语言、主题)如何传递给每个组件?
  • 用 props 太繁琐
  • 用 redux 有时小题大做
举例:切换主题

例如,theme 属性要通过两层传递到子组件。

import React from "react";

// 创建 Context 填入默认值(任何一个 js 变量)
const ThemeContext = React.createContext("light");

// 底层组件 - 函数是组件
function ThemeLink(props) {
  // const theme = this.context // 会报错。函数式组件没有实例,即没有 this

  // 函数式组件可以使用 Consumer
  return (
    <ThemeContext.Consumer>
      {(value) => <p>link's theme is {value}</p>}
    </ThemeContext.Consumer>
  );
}

// 底层组件 - class 组件
class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // static contextType = ThemeContext // 也可以用 ThemedButton.contextType = ThemeContext
  render() {
    const theme = this.context; // React 会往上找到最近的 theme Provider,然后使用它的值。
    return (
      <div>
        <p>button's theme is {theme}</p>
      </div>
    );
  }
}
ThemedButton.contextType = ThemeContext; // 指定 contextType 读取当前的 theme context。

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
      <ThemeLink />
    </div>
  );
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      theme: "light",
    };
  }
  render() {
    return (
      <ThemeContext.Provider value={this.state.theme}>
        <Toolbar />
        <hr />
        <button onClick={this.changeTheme}>change theme</button>
      </ThemeContext.Provider>
    );
  }
  changeTheme = () => {
    this.setState({
      theme: this.state.theme === "light" ? "dark" : "light",
    });
  };
}

export default App;
  1. 先使用 createContext 创建一个 context 对象并赋予一个默认值

    const ThemeContext = React.createContext("light");
    
  2. 用生成的对象,作为一个生产方(Provider),值为 value 属性,里边的值可以自己控制

      render() {
        return (
          <ThemeContext.Provider value={this.state.theme}>
            <Toolbar />
            <hr />
            <button onClick={this.changeTheme}>change theme</button>
          </ThemeContext.Provider>
        );
      }
    
  3. 消费者消费数据,class 组件和函数组件的写法不同

    • class 组件,先定义一个静态的属性,contextType,然后赋值为 context 对象,然后用 this.context 获取值即可

      // 底层组件 - class 组件
      class ThemedButton extends React.Component {
        // 指定 contextType 读取当前的 theme context。
        // static contextType = ThemeContext // 也可以用 ThemedButton.contextType = ThemeContext
        render() {
          const theme = this.context; // React 会往上找到最近的 theme Provider,然后使用它的值。
          return (
            <div>
              <p>button's theme is {theme}</p>
            </div>
          );
        }
      }
      ThemedButton.contextType = ThemeContext; 
      
    • 函数组件,通过 Consumer 包裹一个函数,函数的参数就是目标值

      function ThemeLink(props) {
        // const theme = this.context // 会报错。函数式组件没有实例,即没有 this
      
        // 函数式组件可以使用 Consumer
        return (
          <ThemeContext.Consumer>
            {(value) => <p>link's theme is {value}</p>}
          </ThemeContext.Consumer>
        );
      }
      

关键知识点:使用场景和使用的相关语法

14. 异步组件

  • webpack 支持 import() 异步加载 js 模块

    if (isRequire) {
        const result = await import('./b') // 返回一个 promise
    }
    

    异步组件在打包的时候都会把当前的组件单独打成一个 js 包去异步加载。

    但是这个也会产生问题:因为是动态加载模块,所以 Tree-Shaking 不起作用,所以可以创建一个新模块来按需引入需要异步加载的模块,然后动态加载新模块即可。

  • React .lazy 和 React.Suspense

    • React.lazy 动态加载异步组件
    • React.Suspense 组件用来,当异步组件未加载出来的时候提供加载信息
    const ContextDemo = React.lazy(() => import('./ContextDemo'))
    
    class App extends React.Component {
      constructor(props) {
        super(props);
      }
      render() {
        return (
          <div>
            <p>引入一个动态组件</p>
            <hr />
            <React.Suspense fallback={<div>Loading...</div>}>
              <ContextDemo />
            </React.Suspense>
          </div>
        );
        // 1. 强制刷新,可看到 loading (看不到就限制一下 chrome 网速)
        // 2. 看 network 的 js 加载
    

15. React 性能优化

  • 性能优化,永远都是面试的重点
  • 性能优化对于 React 更加重要
  • 回顾讲 setState 时重点强调的不可变值

15.1 shouldComponentUpdate (SCU)

SCU 基本用法
  shouldComponentUpdate(nextProps, nextState) {
    if (nextState.count !== this.state.count) {
      return true; // 可以渲染
    }
    return false; // 不重复渲染
  }

重点在于:为什么 React 的 SCU 默认返回值为 true,且提供了定制是否更新的权力,而不是将 SCU 的比较写在框架里?

SCU 默认返回什么

如果 React 没有做优化,只要父组件更新,子组件就会无条件更新。

SCU 默认返回值为 true,即重新触发了组件渲染,就默认需要更新。

因此性能优化对于 React 更加重要。

TodoList 里 Footer 组件正确写法:todo items 数量或者 footer 展示文字不变,就不进行更新。

class Footer extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <p>
        {this.props.text}
        {this.props.length}
      </p>
    );
  }
  componentDidUpdate() {
    console.log("footer did update");
  }
  shouldComponentUpdate(nextProps, nextState) {
    if (
      nextProps.text !== this.props.text ||
      nextProps.length !== this.props.length
    ) {
      return true; // 可以渲染
    }
    return false; // 不重复渲染
  }

  // React 默认:父组件有更新,子组件则无条件也更新!!!
  // 性能优化对于 React 更加重要!
  // SCU 一定要每次都用吗?—— 需要的时候才优化
}
SCU 一定要配合不可变值

在 List 组件里,进行深度比较,如果相等,则不重复渲染,不相等则渲染。

如果在提交输入框内容的时候,用下面的写法,将会产生一个很严重的 bug,即 List 组件不更新。

  onSubmitTitle = (title) => {
    // 正确的用法
    this.setState({
      list: this.state.list.concat({
        id: `id-${Date.now()}`,
        title,
      }),
    });

    // 为了演示 SCU ,故意写的错误用法
    this.state.list.push({
        id: `id-${Date.now()}`,
        title
    })
    this.setState({
        list: this.state.list
    })
  };

原因在于,直接修改了 state 内容,违反了不可变规则。这样搞的话,nextProps 便是修改后的内容,比较的时候,this.props.list 和 nextProps.list 两者内容相同,因此理所当然不能更新了。

但是 React 有很多的开发者,其中不乏不规范的开发者或者新手,并不了解这种规则,会瞎写。React 为了避免这种行为造成的 bug,索性 SCU 返回值为 true。

引申:SCU 使用了深度比较,非常耗费性能,可能还不如直接返回 true 效果好。使用浅层比较效果会比较好。

因此,SCU 一定要配合不可变值!

import React from "react";
import PropTypes from "prop-types";
import _ from "lodash";

class Input extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      title: "",
    };
  }
  render() {
    return (
      <div>
        <input value={this.state.title} onChange={this.onTitleChange} />
        <button onClick={this.onSubmit}>提交</button>
      </div>
    );
  }
  onTitleChange = (e) => {
    this.setState({
      title: e.target.value,
    });
  };
  onSubmit = () => {
    const { submitTitle } = this.props;
    submitTitle(this.state.title);

    this.setState({
      title: "",
    });
  };
}
// props 类型检查
Input.propTypes = {
  submitTitle: PropTypes.func.isRequired,
};

class List extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    const { list } = this.props;

    return (
      <ul>
        {list.map((item, index) => {
          return (
            <li key={item.id}>
              <span>{item.title}</span>
            </li>
          );
        })}
      </ul>
    );
  }

  // 增加 shouldComponentUpdate
  shouldComponentUpdate(nextProps, nextState) {
    // _.isEqual 做对象或者数组的深度比较(一次性递归到底)
    if (_.isEqual(nextProps.list, this.props.list)) {
      // 相等,则不重复渲染
      return false;
    }
    return true; // 不相等,则渲染
  }
}
// props 类型检查
List.propTypes = {
  list: PropTypes.arrayOf(PropTypes.object).isRequired,
};

class TodoListDemo extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      list: [
        {
          id: "id-1",
          title: "标题1",
        },
        {
          id: "id-2",
          title: "标题2",
        },
        {
          id: "id-3",
          title: "标题3",
        },
      ],
    };
  }
  render() {
    return (
      <div>
        <Input submitTitle={this.onSubmitTitle} />
        <List list={this.state.list} />
      </div>
    );
  }
  onSubmitTitle = (title) => {
    // 正确的用法
    this.setState({
      list: this.state.list.concat({
        id: `id-${Date.now()}`,
        title,
      }),
    });

    // // 为了演示 SCU ,故意写的错误用法
    // this.state.list.push({
    //     id: `id-${Date.now()}`,
    //     title
    // })
    // this.setState({
    //     list: this.state.list
    // })
  };
}

export default TodoListDemo;
SCU 使用总结
  • SCU 默认返回 true,即 React 默认重新渲染所有子组件
  • 必须配合”不可变值“一起使用
  • 可先不用 SCU,有性能问题时再考虑使用

15.2 PureComponent 和 memo

  • PureComponent,SCU 中实现了浅比较
  • memo,函数组件中的 PureComponent
  • 浅比较已使用大部分情况(尽量不要做深度比较)
PureComponent
class List extends React.PureComponent {
    constructor(props) {
        super(props)
    }
    render() {
        const { list } = this.props

        return <ul>{list.map((item, index) => {
            return <li key={item.id}>
                <span>{item.title}</span>
            </li>
        })}</ul>
    }
    shouldComponentUpdate() {/*浅比较*/}
}
// props 类型检查
List.propTypes = {
    list: PropTypes.arrayOf(PropTypes.object).isRequired
}
memo

15.3 immutable.js

  • 彻底拥抱”不可变值“
  • 基于共享数据(不是深拷贝),速度好
  • 有一定学习和迁移成本,按需使用

性能优化 - 小结
  • 面试重点,且涉及 React 设计理念
  • SCU PureComponent memo immutable.js
  • 按需使用 & state 层级

16. React 高阶组件

关于组件公共逻辑的抽离
  • mixin,已被 React 弃用
  • 高阶组件 HOC
  • Render Props
高阶组件基本用法

Higher-Order Components就是一个函数,传给它一个组件,它返回一个新的组件。就相当于手机壳,通过包装组件,增强组件功能,抽离公共逻辑。

示例:抽离公共功能 - 鼠标划过显示 position 的值
import React from "react";

// 高阶组件
const withMouse = (Component) => {
  class withMouseComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = { x: 0, y: 0 };
    }

    handleMouseMove = (event) => {
      this.setState({
        x: event.clientX,
        y: event.clientY,
      });
    };

    render() {
      return (
        <div style={{ height: "500px" }} onMouseMove={this.handleMouseMove}>
          {/* 1. 透传所有 props 2. 增加 mouse 属性 */}
          <Component {...this.props} mouse={this.state} />
        </div>
      );
    }
  }
  return withMouseComponent;
};

const App = (props) => {
  const a = props.a;
  const { x, y } = props.mouse; // 接收 mouse 属性
  return (
    <div style={{ height: "500px" }}>
      <h1>
        The mouse position is ({x}, {y})
      </h1>
      <p>{a}</p>
    </div>
  );
};

export default withMouse(App); // 返回高阶函数
redux connect 是高阶组件

有两层括号,connect 先返回一个工厂函数,第二个括号放组件,返回一个高阶组件。

17. React Render Props

通过一个函数将 class 组件的 state 作为 props 传递给纯函数组件。

在父组件中向子组件添加了一个函数,子组件通过函数的调用传入一个值或者模板,来调用父元素方法,或者将模板插入组件。

举例:通过 Render Props 实现鼠标 position 的实现
import React from "react";
import PropTypes from "prop-types";

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY,
    });
  };

  render() {
    return (
      <div style={{ height: "500px" }} onMouseMove={this.handleMouseMove}>
        {/* 将当前 state 作为 props ,传递给 render (render 是一个函数组件) */}
        {this.props.render(this.state)}
      </div>
    );
  }
}
Mouse.propTypes = {
  render: PropTypes.func.isRequired, // 必须接收一个 render 属性,而且是函数
};

const App = (props) => (
  <div style={{ height: "500px" }}>
    <p>{props.a}</p>
    <Mouse
      render={
        /* render 是一个函数组件 */
        ({ x, y }) => (
          <h1>
            The mouse position is ({x}, {y})
          </h1>
        )
      }
    />
  </div>
);

/**
 * 即,定义了 Mouse 组件,只有获取 x y 的能力。
 * 至于 Mouse 组件如何渲染,App 说了算,通过 render prop 的方式告诉 Mouse 。
 */

export default App;
HOC vs Render Props
  • HOC:模式简单,但会增加组件层级
  • Render Props:代码简洁,学习成本较高
  • 按需使用

18. React 高级特性 - 知识点总结

  • 函数组件
  • 非受控组件
  • Portals
  • context
  • 异步组件
  • 性能优化(重要)
  • 高阶组件 HOC
  • Render Props

19. Redux

  • 和 Vuex 作用相同,但比 Vuex 学习成本高
  • 不可变值,纯函数
  • 面试常考

19.1 Redux 使用

  • 基本概念
  • 单向数据流
  • react-redux
  • 异步 action
  • 中间件
基本概念
  • store state
  • action
  • reducer
单向数据流概述

https://www.redux.org.cn/docs/basics/DataFlow.html

  • dispatch(action)
  • reducer -> newState
  • subscribe 触发通知
react-redux
  • <Provider> connect
  • connect
  • mapStateToProps mapDispatchToProp

使用流程:

  1. 引入 provider
import React from "react";
import { Provider } from "react-redux";
import { createStore } from "redux";
import todoApp from "./reducers";
import App from "./components/App";

let store = createStore(todoApp);

export default function () {
  return (
    <Provider store={store}>
      <App />
    </Provider>
  );
}
  1. 想要消费数据的话,需要使用 connect
  • 函数组件中的普通写法,把 dispatch 函数作为参数传进去。

    // 函数组件,接收 props 参数
    let AddTodo = ({ dispatch }) => {
      // dispatch 即 props.dispatch
    
      let input
    
      return (
        <div>
          <form
            onSubmit={e => {
              e.preventDefault()
              if (!input.value.trim()) {
                return
              }
              // 创建一个 todo
              dispatch(addTodo(input.value))
              input.value = ''
            }}
          >
            <input
              ref={node => {
                input = node
              }}
            />
            <button type="submit">
              Add Todo
            </button>
          </form>
        </div>
      )
    }
    
    // connect 高阶组件 ,将 dispatch 作为 props 注入到 AddTodo 组件中
    AddTodo = connect()(AddTodo)
    
    export default AddTodo
    
  • 如果需要携带更多信息,如 mapStateToProps,mapDispatchToProps 等,就需要自己自定义。定义后,就可以通过 props 的形式获取 store 里面的数据,或者 dispatch action。

    // 函数组件 { todos, onTodoClick } = props
    const TodoList = ({ todos, onTodoClick }) => (
      <ul>
        {todos.map(todo => (
          <Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)} /> // 点击切换完成状态
        ))}
      </ul>
    )
    
    // 不同类型的 todo 列表
    const getVisibleTodos = (todos, filter) => {
      switch (filter) {
        case 'SHOW_ALL':
          return todos
        case 'SHOW_COMPLETED':
          return todos.filter(t => t.completed)
        case 'SHOW_ACTIVE':
          return todos.filter(t => !t.completed)
      }
    }
    
    const mapStateToProps = state => {
      // state 即 vuex 的总状态,在 reducer/index.js 中定义
      return {
        // 根据完成状态,筛选数据
        todos: getVisibleTodos(state.todos, state.visibilityFilter)
      }
    }
    
    const mapDispatchToProps = dispatch => {
      return {
        // 切换完成状态
        onTodoClick: id => {
          dispatch(toggleTodo(id))
        }
      }
    }
    
    // connect 高阶组件,将 state 和 dispatch 注入到组件 props 中
    const VisibleTodoList = connect(
      mapStateToProps,
      mapDispatchToProps
    )(TodoList)
    
    export default VisibleTodoList
    
异步 action

同步直接返回 action 对象,异步返回一个函数,里边再做一层 dispatch

但是前提是,需要引入中间件 redux-thunk

其他的异步 action 中间件:

  • redux-thunk
  • redux-promise
  • redux-saga

19.2 Redux 中间件

在 dispatch 部分添加一些逻辑。

使用中间件

redux 中间件 - logger 实现

在 dispatch 的时候会输出相应的信息。

redux 数据流图

Redux 知识点总结
  • 基本概念
  • 单向数据流
  • react-redux
  • 异步 action
  • 中间件

20. React-router

  • 面试考点并不多(前提是熟悉 React)
  • 路由模式(hash,H5 history),同 vue-router
  • 路由配置(动态路由、懒加载),同 vue-router
路由模式

动态路由和跳转路由

Link 跳转路由,还可以用 JS history 跳转:

懒加载

总结
  • 路由模式(hash、H5 history)
  • 路由配置(动态路由、懒加载)
  • 掌握基本使用

21. React 使用总结

  • 基本使用 —— 常用,必须会
  • 高级特性 —— 不常用,但体现深度
  • Redux 和 React-router 使用
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值