前端学习笔记(十七)--React学习-3

341 阅读8分钟

今天(指2天)把 React 文档高级指引部分看完了。感觉 React 文档写的很分散,顺序太乱了,看完感觉没学到什么。
再把 hook 部分看完,就去看慕课网的教程了。

1. 生命周期

挂载期:

  1. constructor(),而且此时已经可以用原型方法了
  2. render()
  3. 渲染 DOM,组件挂载,添加 ref
  4. componentDidMount() 钩子函数触发 更新期:
  5. 通过setState(),更新 props 等方式触发更新
  6. shouldComponentUpdate(),默认返回 true,可以自定义判断返回 false 来阻止后面的步骤
  7. 如果上面返回 true,则执行 render()
  8. React 更新 DOM 和 ref
  9. componentDidUpdate() 卸载期:
  10. componentWillUnmount()

2. context

context 用于存放大量的组件都需要的数据,如用户信息,语言,等。

const MyContext = React.createContext(); // 只能在外面定义
class App extends React.Component {
	render(
		<MyContext.provider value="想传的context的值">
			<OtherComponent1 /> // 被 provider 包裹住的组件都能使用 context
			<OtherComponent2 />
			<OtherComponent3 />
		</MyContext>
	);
}

。。。 // 假设这中间有一大堆中间组件,都无需传递这个 context 了

// 然后↓这是需要使用 context 的组件
class ContextNeededComponent extends React.Component {
    static contextType = MyContext; // 实验性语法
    render(
    	{this.context} // 如果设置了 contextType 就可以通过 this.context 访问了
    );
}
ContextNeededComponent.contextType = MyContext; // 设置 contextType 普通语法

context 会让代码复用性变差,如果只是想避免每个中间件都传一大堆参数的话,可以用组合先把这一大堆参数组合成一个变量,再把这个变量传下去,这样每个中间件也只需要传一个参数了。如果要修改变量的值,也无需修改中间件。
如果要在函数式组件中使用 context,则需要用 Consumer。
如果 value 的值发生变化,则所有使用这个 value 的 Comsumer 都会变化。
如果 value 传的是一个对象,则会有性能问题,因为每次 provider 重新渲染的时候,value 都会被赋值为一个新对象,如:

<MyContext.provider value = { { name: "Alice" } }>

每次渲染的时候,都会新生成一个 { name: "Alice" } 对象,然后被赋值给 value,而 JS 会认为这是两个不同的值(不同的地址),然后重新渲染所有的 Comsumer 组件。
为了解决这个问题,可以把 {name: "Alice"} 放在 state 里,然后 value 传入 state:

constructor(props) {
  super(props);
  this.state = {
    myObject: { name: "Alice" }
  }
}
/* 省略各种代码 */
<MyContext.provider value = { this.state.myObject }>

3. 错误边界

暂时跳过,这是文档

4. Refs

Refs 用于超出自上而下的数据流之外,访问 DOM 节点或者 React 元素。
Refs 比较适合用于①管理焦点,文本选择,媒体播放②强制播放动画③集成第三方DOM库。
如果可以不用 refs,最好就不用 refs。

4.1 refs 使用

4.1.1 html 元素的 ref

class MyComponent extends React.Component {
  construtor(props) {
    super(props);
    this.myRef = React.createRef(); // 创建 ref
  }
  focusOnInput() {
    this.myRef.current.focus(); // 通过 this.myRef 访问 render() 中的这个 input 元素
  }
  render() {
    return (<input ref={this.myRef}></input>); // 通过特殊命名属性绑定
  }
}

4.1.2 class 组件的 ref

用法一样

4.1.3 函数式组件无法使用 ref

函数式组件无法使用 ref,ref 是和组件实例绑定的,而函数式组件没有实例。
不过可以在函数式组件里使用绑定在别的元素上的 ref,需要一些额外操作

4.1.4 回调 ref

可以更精细的调控 ref,参考这篇文章

4.2 ref 转发

之前的 ref 都只能在本组件内使用,如果想把 ref 传递给子组件,则需要用到 ref 转发。

// 使用 React.forwardRef 创建组件,ref 作为第二个参数传入
const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton" onClick={props.onClick}>
    {props.children}
  </button>
));
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    // 普通的创建 ref
    this.myRef = React.createRef();
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    console.log(this.myRef);
  }
  render() {
    return (
      <div>
        // 把 myRef 传给子组件
        <FancyButton ref={this.myRef} onClick={this.handleClick}>Click me!</FancyButton>
      </div>
    )
  }
}

下图可以看到 ref 绑定的确实是 button,而不是 FancyButton。绑定到 FancyButton 的 ref 被转发到了 button 上。

5. 非受控组件

受控组件需要对输入框写一个 onChange 函数,每次值更改都会改变 state。
但有时候我们并不需要对输入值做特别复杂的处理,这时候可以使用非受控组件减少很多代码量。
非受控组件的意思是不受 state 控制,相当于 DOM 操作。

5.1 使用

非受控组件使用 ref 指向对应的输入框

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.input = React.createRef(); // 使用 ref
  }

  handleSubmit(event) {
    /* DO Something*/
    this.input.current.value; // 提交的时候对输入框做一些处理 
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" defaultValue="12" ref={this.input} /> // 还可以添加默认值
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

由于添加 value 值会变成只读,因此需要一个方法添加表单的默认值。在输入框里是 defaultValue,选择框则是 defaultChecked

6. fragment

由于组件 render 只能返回一个元素,有时候想返回多个元素只能用<div>包裹起来,但这显然会引起很多问题。因此 React 引入了 fragment 解决这个问题。

<React.fragment>
  <li></li>
  <li></li>
</React.fragment>

可以用短语法来代替,效果和上面一致

<>
  <li></li>
  <li></li>
</>

7. JSX 深入

7.1 背后是 createElement 语法

JSX 引用的是 React.createElement(Component, props, ...children) 语法。(props 为 object)
对应的是: <Component {...props}>...children</Component> JSX 语法。(props 为展开)。自闭合标签则没有 children 参数。

7.2 点语法

标签名可以是通过点语法引用的,比如下面这样:

const MyComponents = {
  MyFirstComponent: function MyFirstComponent (props) {/* DO SOMETHING */}
}
<MyComponents.MyFirstComponent /> // 是可以这么用的,只要这个点语法确实是指向一个组件

7.3 标签名不能是通用表达式

虽然可以用点语法,但是动态表达式是不可以的:

<MyComponent[0] /> // 报错

需要先赋值给一个大写字母开头的变量才行。

const Myxxx = MyComponent[0];
<Myxxx /> // 可以

7.4 不赋值默认为 true

比如 <MyInput autocomplete> 等价于 <MyInput autocomplete={true}>
通常建议后者写法,因为会和 es6 对象简写混淆。

7.5 props 展开

就这样→<MyComponent {...props}>
可以通过展开把一些不要的属性移除。

const {removedProp, ...otherProps} = this.props;
<MyOtherComponent {...otherProps} /> // 把 removedProp 移除后的 props 们。

7.6 JSX 中的子元素

  • 包含在组件中的子元素会作为{props.children}传给组件。
  • 组件虽然不能返回多个元素,但是可以返回一个多个元素组成的数组,无需被 fragment 包裹
  • 子元素可以通过大括号表示的表达式返回任意数据类型:

    如图所示,函数可以被传入 props.children,不过不能渲染出来,可以传给组件做别的处理。
  • null,undefined,false 等值会被无视,如果要传,只能先转为字符串。

8. shouldComponentUpdate

参见这里,注意不可变部分。尤其是在使用 setState 的时候,千万不要通过直接更改原state的值来更改,这会导致 shouldComponentUpdate 出问题,因为此时下一个 state 和上一个 state 是同一个对象,即使数据改变了也会被当做没有改变。多用 concat 这种方式创建新对象。
或者使用第三方库。

9. Portals

参见原文

10. diffing 算法

diffing 是一个启发式算法,建立在以下两个假设之上:

  1. 不同类型的元素生成的树不一样。
  2. 通过 key 可以保持子元素复用。 diffing 算法:
  3. 对比不同类型的元素:如果两个元素类型不一样,则直接销毁该节点(以及子节点)并建立一个新的元素。即使子节点是一样的,这些子节点也会被销毁并被重新建立。(这使用了假设1)
  4. 如果类型是一样的,则保留节点,只修改属性。修改完属性后,继续递归到子节点。
  5. 组件修改属性的时候,组件实例保持不变,props 被修改,重新 render()。
  6. 在递归子节点的时候,React 会同时递归更新前后的节点,当发现差异的时候,生成一个 mutation。
  7. 如果子元素列表前后有很多元素是一样的,但是顺序发生了很大改变(尤其是把其中一个元素放到了第一个),那么元素会被重新创建,这样会有性能损耗。因此需要引入 key。(使用假设2)

  8. 当子元素有 key 的时候,React 就知道哪些元素是移动过的了。
  9. 一般来说,如果元素本身就有 id,那就可以用。如果没有,则可以新增一个 id 字段,或者根据元素一部分内容的 hash 值生成。key 的唯一性只需要在这个列表中唯一即可。
  10. 如果设置 key,则会默认用数组下标。这会引起很多问题,不只是性能,有时候还会导致 state 值的篡改。

11. render props/HOC

都是为了增加代码复用性的技巧,见文档吧。

12. 严格模式

不是 JS 的严格模式,React 自己也有专门的严格模式。
被 <React.StrictMode> 包围的元素都会以严格模式进行。<React.StrictMode> 本身不作渲染,和 fragment 一样。严格模式只会在开发版本有效。

<React.StrictMode>
  /*balabala*/
</ React.StrictMode>

效果如下,主要还是检测过时 API:

  1. 不安全的生命周期函数
  2. 过时的字符串 ref API
  3. 废弃的 findDOMNode
  4. 检测意外的副作用:故意重复调用一些生命周期函数,帮助检测潜在问题。
  5. 过时的 context API

13. PropTypes

从 V15.5 开始,使用 PropTypes 都要导入 prop-types 库(毕竟官方推荐用 flow 或者 typescript 进行类型检查)。
propTypes 是 react 自带的一个类型检查功能,用法如下:

import PropTypes from "prop-types" // 导入

class MyComponent extends React.Component {
  render() {return ( <div>{this.props.name}</div> );}
}
MyComponent.PropTypes = {
  name: PropTypes.string // 限制
}

可以很直观看出对 name 限定了字符串类型,如果不是的话控制台会警告。

13.1 可以限定的类型

有哪些类型可以限制可以参考这里

13.2 限定 props.children 唯一元素

可以限定传过来的 props.children 只能有一个元素

MyComponent.propTypes ={ // 这里第一个字母小写
  children: PropTypes.element.isRequired; // 这里第一个字母大写
}

isRequired 限制了如果没有传入该 prop,也会显示警告。

13.3 通过 defaultProps 设定 props 的默认值

用法如下:

MyComponent.defaultProps = {
  name: "alice",
  age: 25
}

如上可以设置 props 的默认值,这会消除 isRequired 的限制,因为首先通过 defaultProps 赋值,然后才会被 propTypes 检查。
函数组件的使用有一点点小区别