[TOC]

react使用手册

1. react样式的写法

var HelloWorld = React.createClass({  
  render:function(){  
    var styles = {   color: 'blue',  fontSize: '30'  }  
    return (  
      <div className="box">  
        <h3 className="title" 
          style={{color:'red',backgroundColor:'lime'}}>默认标题</h3>  
        <p className="subtitle" style={styles}>说明</p>  
        <p className="details">这个是用来教学的案例</p>  
      </div>  
    )  
  }  
})  
ReactDOM.render(<HelloWorld/>,document.getElementById("app"))  
  • JSX中使用样式

    1、行内样式:写行内样式的时候需要使用两个{}  ==>{{}}

    2、对象样式:在return前面定义一个样式对象,注意样式的写法,与HTML的不同点

    3、CSS样式

  • 在HTML5中与在React中的样式的书写区别

    1、HTML5中以;结束,在React中以,结束

    2、在HTML5中属性与值都不需要加上引号。在React中,属于javascript对象,key中不能存在 -,需要使用驼峰命名,如果是value值,需要加上引号

    3、在HTML中,设置带数字的值,宽度,高度==,需要带上单位。在React中可以不用带单位,直接写数字 这里是指那些规定了默认单位的值。比如说像素px,如果要使用em或者是rem则需要加上单位

  • 其他注意事项

    在使用插值符号的时候,里面需要时一个对象或者是一个表达式

参考资料:https://blog.csdn.net/chuipaopao163/article/details/73432229

1.1 JSX中根据条件显示特殊样式

<div className={(this.state.menuIndex === i ? 'active' : '')} />
<div className={(this.state.menuIndex === i ? 'active button' : 'button')} />

1.2 多个类样式根据不同条件来显示

可使用npm包工具来实现:https://www.npmjs.com/package/classnames

或者自己根据条件来拼字符串:

// 参数是对象,对象的属性是类名,属性值判断是否加这个类名
setClassNames = (obj) => {
  if ( typeof obj !== 'object') {return '';} // 参数不是对象直接返回空不处理
  let key;
  let str = '';
  for (key in obj) {
    if(obj[key]) { str += ' ' + key; } // 如果对象属性值是true,则加上这个类
  }
  return str;
}
return (<span
   className={this.setClassNames({
       'active': this.state[e.key] === item.value,
       'forbidden': this.state.dimensional === e.value,
   })}
>{item.name}</span>)

2. 生命周期(Lifecycle)

https://segmentfault.com/a/1190000004168886

https://segmentfault.com/a/1190000018490987

React 的生命周期包括三个阶段:mount(挂载)、update(更新)和 unmount(移除)

2.1 mount

mount 就是第一次让组件出现在页面中的过程。这个过程的关键就是 render 方法。React 会将 render 的返回值(一般是虚拟 DOM,也可以是 DOM 或者 null)插入到页面中。

这个过程会暴露几个钩子(hook)方便你往里面加代码:

  1. constructor()
  2. componentWillMount()
  3. render()
  4. componentDidMount()

我用一幅图解释一下:

2.2 update

mount 之后,如果数据有任何变动,就会来到 update 过程,这个过程有 5 个钩子:

  1. componentWillReceiveProps(nextProps) - 我要读取 props 啦!

  2. shouldComponentUpdate(nextProps, nextState) - 请问要不要更新组件?true / false

  3. componentWillUpdate() - 我要更新组件啦!

  4. render() - 更新!

  5. componentDidUpdate() - 更新完毕啦!

2.3 unmount

当一个组件将要从页面中移除时,会进入 unmount 过程,这个过程就一个钩子:

  1. componentWillUnmount() - 我要死啦!

你可以在这个组件死之前做一些清理工作。

3. ref的使用

ref是使用回调函数的方式去使用:

class Input extends Component {
  focus = () => { this.textInput.focus(); }
  render(){
    return (<input ref={(input) => { this.textInput = input }} />)
	}
}

input参数来源:

当我们在DOM Element中使用ref时,回调函数将接收当前的DOM元素作为参数,然后存储一个指向这个DOM元素的引用。那么在示例代码中,我们已经把input元素存储在了this.textInput中,在focus函数中直接使用原生DOM API实现focus聚焦。

父组件使用ref调用子组件方法:

class Son extends React.Component {
  getShowData = (params) => { console.log('params ', params) }
  render() { return ( <div>12312</div> ) }
}
class father extends React.Component {
  // 获取子组件引用
  getRef = (ele) => {
    this.incomeTable = ele;
    if (ele) {
      const params = {a: 1, b: 2}
      ele.getShowData(params);
    }
  }
  render() {
    return (  <Son  ref={this.getRef} /> )
  }
}
  • ref的回调函数执行时间

    当组件挂载后和卸载后,以及ref属性本身发生变化时,回调函数就会被调用。

可以在组件实例中使用ref

// <Input>来源于上面的示例代码👆
class AutoFocusTextInput extends Component {
  componentDidMount(){
    this.textInput.focus();
  }
  render(){
    return (
      <Input ref={(input) => { this.textInput = input }}>
    )
	}
}

当我们在<Input>中添加ref属性时,其回调函数接收已经挂载的组件实例<Input>作为参数,并通过this.textInput访问到其内部的focus方法。也就是说,上面的示例代码实现了当AutoFocusTextInput组件挂载后<Input>组件的自动聚焦。

接下来文档指出,<Input>组件必须是使用class声明的组件,不然无法使用。这意味着React逐渐与ES6全面接轨了。

父组件的ref回调函数可以使用子组件的DOM:

这是Facebook非常不推荐的做法,因为这样会打破组件的封装性,这种方法只是某些特殊场景下的权宜之计。我们看看如何实现,上代码:

function CustomTextInput(props) {
  return (<input ref={props.inputRef} />);
}
class Parent extends React.Component {
  render() {
    return (
      <CustomTextInput inputRef={el => this.inputElement = el} />
		);
	}
}

原理就是父组件把ref的回调函数当做inputRefprops传递给子组件,然后子组件<CustomTextInput>把这个函数和当前的DOM绑定,最终的结果是父组件<Parent>this.inputElement存储的DOM是子组件<CustomTextInput>中的input

同样的道理,如果A组件是B组件的父组件,B组件是C组件的父组件,那么可用上面的方法,让A组件拿到C组件的DOM。但是官方态度是discouraged,这种多级调用确实不雅,我们确实需要考虑其他更好的方案了。

参考资料:https://juejin.im/post/5927f51244d904006414925a

4.Context:组件间共享变量

Context是react 16.0以上版本才支持的。

在react中数据是通过props属性自上而下传递,当组件层级数量增多时,在每一层传递数据就变得很繁琐。Context提供了一种新的组件之间共享数据的方式,允许数据隔代传递,而不必显示的通过组件树逐层传递。

使用方式:
1、创建一个文件,所有用Context的组件都要引用这个文件,这个文件里面声明Context。
2、共同的父组件里面使用Provider发布数据,子组件使用Consumer来使用数据
3、函数组件还可以使用useContext hooks方法接收数据。类组件可以使用静态属性ContextType接收数据。
4、子组件如果需要修改数据,在Provider里面传修改数据的方法。

4.1 API说明

import React from 'react';
// 1. 创建一个Context文件,所有地方都只能引入这一个文件{Provider, Consumer}
export const UserContext = React.createContext(defaultValue);
// 也可以用这个:export default React.createContext({});

// 2. 设置变量,Provider接收一个 value 属性,这个变量就是Context用到的变量,所有用到的变量的组件都要包裹在Provider下面。
// 要引入UserContext文件
<UserContext.Provider value={{ name: 'Alice', age: age, setAge: setAge }}>
  <Content />
</UserContext.Provider>

// 3. 使用变量:Consumer接收一个函数作为子节点函数接收当前 context 的值并返回一个 React 节点。传递给函数的 value 将等于组件树中上层 context 的最近的 Provider 的 value 属性。
// 如果 context 没有 Provider ,那么 value 参数将等于被传递给 createContext() 的 defaultValue 。
// 要引入UserContext文件
<UserContext.Consumer>
  { value => (<div>名字:{value.name},年龄:{value.age}</div>)}
</UserContext.Consumer>

// 4. 其他方式使用变量
// 4.1 函数组件:使用React.useContext(UserContext),具体看下面例子
// 4.1 类组件:使用 contextType,具体看下面例子

每当Provider的值发送改变时, 作为Provider后代的所有Consumers都会重新渲染。 从Provider到其后代的Consumers传播不受shouldComponentUpdate方法的约束,因此即使祖先组件退出更新时,后代Consumer也会被更新。

通过使用与Object.is相同的算法比较新值和旧值来确定变化。

4.2 使用的例子(发布、使用、修改变量):

// contextTest.js:定义一个context 文件,所有用到这个context的组件都要引用这个文件
import React from 'react';
export const UserContext = React.createContext();

// 共同的父组件发布变量
import React from 'react';
import { UserContext } from '@views/contextTest.js'; // 引入共同的 Context
const Content = () => (<div> <UserInfo /> </div>);
const App = () => (
  const [age, setAge] = React.useState(256); // 让子组件可以修改 Context
  <UserContext.Provider value={{ name: 'Alice', age: age, setAge: setAge }}>
    <Content />
  </UserContext.Provider>
);

// 函数组件使用变量:1、通过useContext使用。2、通过Consumer使用
// 修改变量可以通过给useContext 传方法,然后调用方法修改变量
import { useContext } from 'react';
import { UserContext } from '@views/contextTest.js'; // 引入共同的 Context
export default function UserInfo() {
  const user = useContext(UserContext); // 1、通过useContext获取变量
  return (<div>
    <p>用户名: {user.name}</p> <p>年龄: {user.age}</p>
    <button onClick={() => { user.setAge(user.age + 1); }}>增加年龄</button>
    <UserContext.Consumer> {/* 2、通过Consumer获取变量 */}
      { value => (<div>名字:{value.name},年龄:{value.age}</div>)}
    </UserContext.Consumer>
  </div>);
}

// 类组件使用变量:1、通过contextType。2、通过Consumer使用(跟上面的函数组件一样,这里不写了)
// class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。
// 这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。
// 如果你正在使用实验性的 public class fields 语法,你可以使用 static 这个类属性来初始化你的 contextType。
class UserInfo extends React.PureComponent {
  static contextType = UserContext;
  componentDidMount() {
    // 使用contexType可以在任意生命周期访问数据,使用 this.context 来消费最近 Context 上的那个值
    const value = this.context;
    console.log(value);
  }
  render() {
    return (<div>name:{this.context.name}age:{this.context.age}</div>);
  }
}

4.3 多个Context使用例子

// 多个context 文件
const SchoolContext = React.createContext({
  name: '南师附中',
  location: '南京',
});
const StudentContext = React.createContext({
  name: 'chengzhu',
  age: 17,
});
export { SchoolContext, StudentContext };

// 共同父组件发布变量, 直接写调用的地方
<SchoolContext.Provider value={school}>
  <StudentContext.Provider value={student}>
    <MiddlePage />
  </StudentContext.Provider>
</SchoolContext.Provider>

// 使用变量,直接写调用的地方
<SchoolContext.Consumer>
  {school => (
    <StudentContext.Consumer>
      {student => {
        return (
          <div>
            学校: {school.name},位置: {school.location}
            学生姓名: {student.name},学生年龄: {student.age}
          </div>
        );
      }}
    </StudentContext.Consumer>
  )}
</SchoolContext.Consumer>

参考资料:https://blog.csdn.net/qq_34307801/article/details/109774612

5. setState()说明

setState()将对组件状态的改变排队,并告诉该组件及其子组件需要用已经更新的状态来重新渲染。这个方法主要是用来更新用户界面以响应事件处理和服务器响应。

1、setState()不总是立刻更新组件。其可能是成批处理或推迟更新。这使得在调用setState()后立刻读取this.state变成一个潜在陷阱。

this.setState({ tt: 20}, () => { console.log('tt: ', this.state.tt) });
this.setState({ tt: 50}, () => { console.log('tt: ', this.state.tt) });

上面代码执行后,输出50 50, 说明方法合并到一起执行了

2、setState()永远都会导致重新渲染,除非shouldComponentUpdate() 返回false。所以和渲染无关的状态不要放在state中。

3、setState()的第二个参数是一个可选的回调函数,其执行将是在一旦setState完成,并且组件被重新渲染之后。通常,对于这类逻辑,我们推荐使用componentDidUpdate

由于使用setState()更新了值后,不会立刻就能使用this.state看到最新的值,如:

constructor(props, context) {
  super(props, context)
  this.state = { value: 1 };
}
// 第一次render后的生命周期
componentDidMount () {
  let value = 10;
  this.setState({tt: 10,})
  console.log('tt: ', this.state.tt);	// 此时输出的是1
}

5.1 执行setState()后this.state立即获取到更新方法

setState()的第二个参数是一个可选的回调函数,其执行将是在一旦setState完成,并且组件被重新渲染之后。可用如下方法:

componentDidMount () {
  this.setState({ tt: 20}, () => {
    this.pp();
  });
  // this.setState({tt: 30}, this.pp ) 此方法也可以
}
pp = () => { console.log('tt: ', this.state.tt) }

可以使用setTimeout异步函数来替代。

componentDidMount () {
 this.setState({tt: 40})
 setTimeout(this.pp, 0);	// 40
}
pp = () => { console.log('tt: ', this.state.tt) }

或者把需要的实时的变量放到类变量里面:

constructor(props, context) {
  super(props, context)
  this.state = { value: 1 };
  this.value = 1; // 直接把变量放在这里
}

参考资料:

https://www.cnblogs.com/feiyu6/p/9202873.html

https://react.docschina.org/docs/react-component.html#setstate

5.2 在setState里面使用state的变量

this.setState(prevState => ({
  collapsed: !prevState.collapsed
}));

6. 无状态组件SFC

无状态组件顾名思义就是没有状态的组件,如果一个组件不需要管理 state 只是纯的展示,那么就可以定义成无状态组件。无状态组件是在 React 在 v0.14 之后推出的

无状态组件是没有 refs 属性的。

无状态的函数创建的组件是无状态组件,它是一种只负责展示的纯组件:

function HelloComponent(props) {
    return <div>Hello {props.name}</div>
}
ReactDOM.render(<HelloComponent name="marlon" />, mountNode)

对于这种无状态的组件,使用函数式的方式声明,会使得代码的可读性更好,并能大大减少代码量,箭头函数则是函数式写法的最佳搭档:

const Todo = (props) => (
  <li
    onClick={props.onClick}
    style={{textDecoration: props.complete ? "line-through" : "none"}}
  >
    {props.text}
  </li>
)

上面定义的 Todo 组件,输入输出数据完全由props决定,而且不会产生任何副作用。对于propsObject 类型时,我们还可以使用 ES6 的解构赋值:

const Todo = ({ onClick, complete, text, ...props }) => (
  <li
    onClick={onClick}
    style={{textDecoration: complete ? "line-through" : "none"}}
    {...props}
  >
    {props.text}
  </li>
)

优点

  • 适当减少代码量,可读性增强;

  • 无状态,统一移交给高阶组件(HOC)或者 Redux 进行管理;

    这种模式在大型项目或者组件中经常被使用,未来 React 也会对 SFC 做一些专门的优化;

这种模式被鼓励在大型项目中尽可能以简单的写法 来分割原本庞大的组件,而未来 React 也会面向这种无状态的组件进行一些专门的优化,比如避免无意义的检查或内存分配。所以建议大家尽可能在项目中使用无状态组件。

无状态组件内部其实是可以使用ref功能的,虽然不能通过this.refs访问到,但是可以通过将ref内容保存到无状态组件内部的一个本地变量中获取到。

例如下面这段代码可以使用ref来获取组件挂载到DOM中后所指向的DOM元素:

function TestComp(props){
  let ref;
  return ( <div ref={(node) => ref = node}></div> )
}

参考资料:

https://www.w3cplus.com/react/stateful-vs-stateless-components.html

7. PureComponent 纯组件

PureComponent 的作用:用来提升性能,因为它减少了应用中的渲染次数。

React15.3 中新加了一个 PureComponent 类,它是优化 React 应用程序最重要的方法之一。

在简单组件(纯展示组件)上的性能可以高出 React.Component 几十倍,所以性能还是很可观的~

7.1 原理

当组件更新时,如果组件的 propsstate 都没发生改变,render 方法就不会触发,省去 Virtual DOM 的「生成」和「比对」过程,达到提升性能的目的。

React 做了如下判断:

if (this._compositeType === CompositeTypes.PureClass) {
  shouldUpdate = !shallowEqual(prevProps, nextProps)
  || !shallowEqual(inst.state, nextState);
}

这里的 shallowEqual 会比较 Object.keys(state | props)长度是否一致,每一个 key 是否两者都有,并且是否是一个引用,也就是只比较了第一层的值,确实很浅,所以深层的嵌套数据是对比不出来的。

7.2 注意点

  1. 如果 PureComponent 里有 shouldComponentUpdate 函数的话,React 会直接使用 shouldComponentUpdate 的结果作为是否更新的依据;

    只有不存在 shouldComponentUpdate 函数,React 才会去判断是不是 PureComponent,是的话再去做 shallowEqual 浅比较。

    也因为可以少写 shouldComponentUpdate 函数,倒也节省了点代码。

  2. 因为只做了浅比较,所以需要注意 state 或 props 中修改前后的对象引用是否一致;

  3. 由于是 React15.3 之后才有的,所以可能需要进行兼容操作;

    import React { PureComponent, Component } from 'react';
    class Foo extends (PureComponent || Component) {
      //...
    }
    

参考资料:https://blog.lbinin.com/frontEnd/React/React-SFC.html

react官网关于PureComponent组件介绍:https://zh-hans.reactjs.org/docs/react-api.html#reactpurecomponent

8. Fragments:解决必须有一个根节点问题

<React.Fragment>是为了解决render函数必须有一个跟节点问题。

react版本15以前,render函数的返回必须有一个根节点,否则报错,为满足这一原则我会使用一个没有任何样式的 div 包裹一下。

react版本16开始,render支持返回数据,这一特性已经可以减少不必要节点嵌套:

import React from 'react';
export default function () {
    return [ <div>一步 01</div>, <div>一步 02</div>, <div>一步 03</div> ];
}

如果你不喜欢用数组,React 16为我们提供了Fragments:

import React from 'react';
export default function () {
  return (
    <React.Fragment>
      <div>一步 01</div>
      <div>一步 02</div>
    </React.Fragment>
  );
}

8.1 Fragments简写形式<></>

<></>形式,前有些前端工具*支持的还不太好*,用 create-react-app 创建的项目就不能通过编译

import React from 'react';
export default function () {
  return (
    <>
    <div>一步 01</div>
    <div>一步 02</div>
    </>
  );
}

9 react中DOM元素

https://zh-hans.reactjs.org/docs/dom-elements.html

React 实现了一套独立于浏览器的 DOM 系统,兼顾了性能和跨浏览器的兼容性。我们借此机会完善了浏览器 DOM 实现的一些特殊情况。

在 React 中,所有的 DOM 特性和属性(包括事件处理)都应该是小驼峰命名的方式。例如,与 HTML 中的 tabindex 属性对应的 React 的属性是 tabIndex。例外的情况是 aria-* 以及 data-* 属性,一律使用小写字母命名。比如, 你依然可以用 aria-label 作为 aria-label

9.1 dangerouslySetInnerHTML

使用代码直接设置 HTML 存在风险,因为很容易无意中使用户暴露于跨站脚本(XSS)的攻击。

在react中html字符串不会渲染成真正的html,也不会执行脚本文件。

dangerouslySetInnerHTML 是 React 为浏览器 DOM 提供 innerHTML 的替换方案。当你想设置 dangerouslySetInnerHTML 时,需要向其传递包含 key 为 __html 的对象,以此来警示:

function MyComponent(item) {
  const replaceHtml = (str) => { return str.replace('/', '')}
  // return <div dangerouslySetInnerHTML={createMarkup()} />;
  // <div dangerouslySetInnerHTML={{ __html: '<div>123</div>' }} />
  return <div dangerouslySetInnerHTML={{__html:replaceHtml(item.process)}} />
}

1、dangerouslySetInnerHTMl 是React标签的一个属性。2、有2个{{}},第一{}代表jsx语法开始,第二个是代表dangerouslySetInnerHTML接收的是一个对象键值对。3、.既可以插入DOM,又可以插入字符串。

10. props.children(容器类组件、插槽)

在编写html页面的时候,标签嵌套是很常见的。有时候你会希望以同样的方式嵌套自己开发的组件。当组件标签有子节点时,组件将在名为 children 的 props 中接收到该内容。

children 属性:表示组件标签的子节点。children 属性与普通的props一样,可以是任意值(文本、JSX、组件,甚至是函数)

function ListItem ({ children }) {
  children() // 当函数执行,需要加判断
  return (<div>{ children } </div>);
}
{/* 当写了子组件, 那么props.children的值就是子组件,手动传的children属性无效*/}
<ListItem children="手写的">
  普通文本
  <div>标签</div>
  {() => console.log('函数')}
</ListItem>

props.children 就可以获得组件的子节点,有以下几种情况:
1、组件没有子节点,props.children 类型为 undefined;
2、组件有一个子节点,props.children 类型为 子节点的类型(原始类型、对象、函数等);
3、组件有多个子节点,props.children 类型为 array。

注意:JSX将会自动删除每行开头和结尾空格,以及空行。它还会把字符串中间空白行压缩为一个空格。以下的这些例子都会渲染出一样的情况:

<Grid>Hello world!</Grid>
<Grid>
  
  Hello
  
  world!
</Grid>

为什么 children 属性并不总是一个数组?

在 React 中,children 属性是被视为 不透明的 数据结构。这意味着你不应该依赖它的结构。如果要转换,过滤,或者统计子节点,你应该使用 React.Children 方法。

实际操作过程中,children 在底层常常被表示为数组。但是如果这里只有一个子节点,那么 React 将不会创建数组,因为这将导致不必要的内存开销。只要你使用 React.Children 方法而不是直接操作 children 底层结构,即使 React 改变了 children 数据结构的实际实现方式,你的代码也不会被中断。

children 是一个数组时,Children.map 会有许多有用的特性。比如,Children.map 将被返回元素上的 key 和 你传递给它的 children 上的 key 绑定。这保证了原本的 JSX 子元素不会“丢失” key,即使它们上面的例子中那样被包裹。

11. React.Children:处理和转化props.children

由于 props.children 可以是任何类型,比如原始类型、数组、函数、对象等等。因此React提供了一系列的函数助手来使得操作children更加方便。

官方文档地址:https://zh-hans.react.dev/reference/react/Children

注意:使用 Children 的场景并不常见,使用它可能会削弱代码的健壮性。查看常见的替代方案

11.1 Children.count 统计子节点数量

因为this.props.children 可以是任何类型的,检查一个组件有多少个children是非常困难的。而片面的使用this.props.children.length时, 当传入的是字符串或者函数时,不会得到想要的结果,比如有个child:"Hello World!" ,但是使用 .length 的方法将会显示为12。

这就是为什么我们有 React.Children.count 方法的原因。

class ChildrenCounter extends React.Component {
  render() {
    return <p>React.Children.count(this.props.children)</p>
  }
}
<ChildrenCounter> {/* 1个 */}
  Second!
</ChildrenCounter>
<ChildrenCounter> {/* 2个 */}
  <p>First</p>
  <ChildComponent />
</ChildrenCounter>
<ChildrenCounter> {/* 2个, 函数不统计,实测结果 */}
  {() => <h1>First!</h1>}
  Second!
  <p>Third!</p>
</ChildrenCounter>

注意:空节点(nullundefined 以及布尔值),字符串,数字和 React 元素 都会被统计为一个节点。在遍历统计的过程中,React 元素不会被渲染,所以其子节点不会被统计Fragment 也不会被统计。函数不统计。对于数组,它本身也不会被统计,但其中的元素遵循上述规则。

11.2 遍历子节点

遍历处理子节点方法 React.Children.mapReact.Children.forEach 。它们在对应数组的情况下能起作用,除此之外,当函数、对象或者任何东西作为children传递时,它们也会起作用。

function IgnoreFirstChild({ children }) {
  return (<div>
    {React.Children.map(children, (child, i) => {
      if (i < 1) return null; // 忽略第一个元素
      return child;
    })}
  </div>);
}
{/* 使用 */}
<IgnoreFirstChild>
  <ListItem /> {/* 忽略这个元素 */}
  <p>Third!</p>
</IgnoreFirstChild>

上面的例子,我们也可以使用children.map方法。但是如果将一个函数作为child传递过来。children就会变成一个函数而不是数组,由于函数没有map方法,所以会导致报错。而使用 React.Children.map 函数,无论什么都不会报错。

<IgnoreFirstChild>
  {() => <h1>First!</h1>} {/* React.Children 会忽略这个函数 */}
</IgnoreFirstChild>

map、forEach参数说明(children, fn, thisArg?):

  • children:组件接收到的 children 属性
  • fn:和 数组的 forEach 方法 中的回调类似,是你希望为每个子节点执行的函数。当这个函数执行时,对应的子节点和其下标将分别作为函数的第一、第二个参数,下标从 0 开始自增。
  • 可选 thisArg:为 fn 函数绑定 this。默认值为 undefined

返回值:

  • forEach 返回值: undefined
  • map返回值:如果 childrennull 或者 undefined,那么就返回这个值。否则就返回一个由 fn 函数返回节点组成的一维数组。这个数组将包含除 nullundefined 以外的所有节点。

注意事项

  • 空节点(nullundefined 以及布尔值),字符串,数字和 React 元素 都会被统计为单个节点。在遍历统计的过程中,React 元素不会被渲染,所以其子节点不会被统计Fragment 也不会被统计。对于数组,它本身也不会被统计,但其中的元素遵循上述规则。
  • 如果你在 fn 中返回了一个具有 key 的元素或者元素数组,各个元素的 key 将自动与其在 children 中对应的原始项的 key 绑定。当你在 fn 中返回了一个包含了多个元素的数组时,其中的每个元素的 key 都需要保证在这个数组中是独一无二的。

11.3 Children.toArray(children):将 children 转换成数组

如果以上的方法你都不适合,你能将children转换为数组通过 React.Children.toArray 方法。如果你需要对它们进行排序,这个方法是非常有用的。

class Sort extends React.Component {
  render() {
    const children = React.Children.toArray(this.props.children)
    return <p>{children.sort().join(' ')}</p> // 对子元素排序渲染
  }
}
<Sort>
  {/* 我们使用表达式容器来确保我们的字符串,作为三个子字符串传递,而不是作为一个字符串传递  */}
  {'bananas'}{'oranges'}{'apples'}
</Sort>

上例会渲染为三个排好序的字符串。

11.4 Children.only(children):断言children是React元素

const element = Children.only(children);

参数

返回值

如果 children 是一个合法的元素,那么就会返回这个元素。否则会抛出一个异常。

注意事项

  • 如果传入一个数组(比如 Children.map 的返回值)作为 children,那么这个方法会抛出异常。也就是说,这个方法强制要求 children 是一个 React 元素,而不是一个元素数组。

12 React.Children 替代方案

使用 Children 方法操作子节点通常会削弱代码的健壮性。在 JSX 中将子节点传递给组件时,通常不希望操作或转换子节点。如果能够的话,尽量避免使用 Children 方法。

12.1 暴露多个组件

如果你希望 RowList 的每一个子节点都被 <div className="Row"> 包裹,那么可以导出一个 Row 组件,然后像下面这样手动把包裹每一行:

export function RowList({ children }) {
  return (<div className="RowList"> 1{children} </div>);
}

export function Row({ children }) {
  return (<div className="Row">{children}</div>);
}
export default function App() {
  return (
    <RowList>
      <Row><p>这是第一项。</p></Row>
      <Row><p>这是第二项。</p></Row>
      <Row><p>这是第三项。</p></Row>
    </RowList>
  );
}

和使用 Children.map 不同,这种方式不会自动包裹每个子节点。但是,和 上文中关于 Children.map 例子 相比,这种方式具有明显的优势,因为即使你继续抽离更多的组件,它也仍然有效。

21.2 接收对象数组作为参数

你也可以显示地传递一个数组作为组件的参数。例如,下面的 RowList 接收了一个 rows 数组作为组件的参数,因为 rows 是一个常规的 JavaScript 数组,RowList 组件可以对其使用 map 等数组内置方法。:

export function RowList({ rows }) {
  return (
    <div className="RowList">
      {rows.map(row => (
        <div className="Row" key={row.id}>
          {row.content}
        </div>
      ))}
    </div>
  );
}
export default function App() {
  return (
    <RowList rows={[
      { id: 'first', content: <p>这是第一项。</p> },
      { id: 'second', content: <p>这是第二项。</p> },
      { id: 'third', content: <p>这是第三项。</p> }
    ]} />
  );
}

和将子节点作为 JSX 传递不同,这个方法允许你将一些额外的数据,比如 header,与每个子项关联。因为你直接使用 tabs,并且它是一个数组,所以你并不需要 Children 方法。

12.3 调用渲染属性以自定义渲染

除了为每一个子项生成 JSX,你还可以传递一个返回值类型是 JSX 的函数,并且在必要的时候调用这个函数。在这个示例中,App 组件向 TabSwitcher 组件传递了一个 renderContent 函数。TabSwitcher 组件仅对被选中的 tab 调用 renderContent

export function TabSwitcher({ tabIds, getHeader, renderContent }) {
  const [selectedId, setSelectedId] = React.useState(tabIds[0]);
  return (
    <div>
      {tabIds.map(tabId => (
        <button key={tabId} onClick={() => setSelectedId(tabId)}>
          {getHeader(tabId)}
        </button>
      ))}
      <hr />
      <div key={selectedId}>
        <h3>{getHeader(selectedId)}</h3>
        {renderContent(selectedId)}
      </div>
    </div>
  );
}
export function App() {
  return (
    <TabSwitcher
      tabIds={['first', 'second', 'third']}
      getHeader={tabId => tabId[0].toUpperCase() + tabId.slice(1)}
      renderContent={tabId => <p>This is the {tabId} item.</p>}
    />
  );
}

这是如何在不操纵子组件的情况下,父组件和子组件进行协作的另一个示例。

13. cloneElement克隆元素

cloneElement 官方文档:https://zh-hans.react.dev/reference/react/cloneElement

使用方式:cloneElement(element, props, ...children)

说明:调用 cloneElement 方法会基于 element 创建一个新的 React 元素,但新元素具有不同的 propschildren

参数

  • elementelement 参数必须是一个有效的 React 元素。例如,它可以是一个类似 <Something /> 这样的 JSX 节点,也可以是 createElement 调用的结果,或者也可以是另一个 cloneElement 调用的结果。
  • propsprops 参数必须是一个对象或 null。如果传 null,克隆后的元素将保留所有原始的 element.props。否则,对于 props 对象中的每个 prop 属性,返回的元素将“优先”使用 props 中的值而不是 element.props 中的值。其余的 props 将从原始的 element.props 中填充。如果你传递 props.key 或者 props.ref,它们将替换原来的。
  • 可选 ...children:零个或多个子节点。它们可以是任何 React 节点,包括 React 元素、字符串、数字、portals、空节点(nullundefinedtruefalse),和 React 元素数组。如果你不传递任何 ...children 参数,则原始的 element.props.children 将被保留。

返回值

cloneElement 返回一个具有一些属性的 React element 对象:

  • type:与 element.type 相同。
  • props:将 element.props 与你传递的 props 浅合并的结果。
  • ref:原始的 element.ref,除非它被 props.ref 覆盖。
  • key:原始的 element.key,除非它被 props.key 覆盖。

通常,你将从组件返回该元素或使其成为另一个元素的子元素。尽管你可以读取元素的属性,但最好在创建每个元素后将其视为不透明的,并且仅渲染它。

使用例子:

export function Father({ children }) {
  const name = 'jack';
  return (<div>
    父组件,引入的子组件{React.cloneElement(children, { name: name })}
  </div>);
}
export function Show({ name, age }) {
  return (<p>我是子组件 名字:{name}, 年龄:{age}</p>);
}
export function App() {
  return (<Father><Show age="18" /></Father>);
}

用法:覆盖元素的 props

想象一个 List 组件将其 children 渲染为可选择行的列表,并带有可更改的“下一步”按钮选择了哪一行。List 组件需要以不同的方式渲染所选的 Row,因此它克隆它收到的每个 <Row> 子级,并添加额外的 isHighlighted: trueisHighlighted: false 属性。则可以使用克隆元素。具体例子参考官方文档。

14. 函数和函数组件在JSX中区别

由于函数组件在JSX中既可以当组件用,也可以当函数调用。但是会有一些细微的差别。

首先关于函数和函数组件的区别:
1、函数组件:用函数描述的组件,本质上还是一个组件。因此它可以:使用hooks、拥有自己的状态、使用memo缓存上次渲染;在devtools里显示为组件、设置displayName;渲染可中断、可恢复;有自己的生命周期。
2、函数:返回值为JSX的函数仅仅是代码片段的复用而已,相当于直接在写JSX。

在实际使用中会发现:
1、函数当组件用:在类组件和函数组件中都没问题。
2、无hooks函数当函数执行:在类组件和函数组件中都没问题。
3、有hooks函数当函数执行:在类组件中会报错,在函数组件中没问题

有hooks的函数在 函数组件中当函数执行:
1、函数中的hooks依旧有效,但是也会触发父组件的hooks,首先执行父组件中的hooks,然后在执行函数中的hooks。
2、如果调用了多次函数,每个函数里面触发的hooks,其他的函数组件里面的hooks也会触发。首先是父组件hooks执行,然后在按照函数代码顺序依次执行每个函数的hooks。

注意:由于函数和函数组件的区别,在代码拆分的时候,一定要注意,有的地方只能拆分成JSX形式,也就是只能是使用执行函数。比如form表单子项,需要自定绑定输入框之类的JSX元素,如果在代码拆分的时候变成了组件,那么就会失去对输入框的控制,导致form表单不能自动更新至和自动校验了。

下面是测试代码:

function ShowName({ id }) { // 有hooks状态的函数
  const [name, setName] = React.useState('hello');
  React.useEffect(() => {
    console.log(`${id} name:`, name);
  });
  return (<div>
    <div>{name}</div>
    <button onClick={() => setName(`${name} o`)}>{id} name</button>
  </div>);
}
function ShowAge({ age }) { // 无状态的纯函数
  return (<div>{age}</div>);
}
// 在类组件中测试
class Home extends React.Component {
  render() {
    return (<div>
      <ShowName />
      {/* {ShowName()} */} {/* 执行有hooks的函数会报错:Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. */}
      <ShowAge age={18} />
      {ShowAge({ age: 19 })}
    </div>);
  }
}
// 在函数组件中测试
function People(params) {
  React.useEffect(() => {
    console.log('父 useEffect');
  });
  return (<div>
    <ShowName />
    {ShowName({ id: 'one' })}
    {ShowName({ id: 'two' })}
    <ShowAge age={18} />
    {ShowAge({ age: 19 })}
  </div>);
}

15. React.memo

组件中状态(State)发生改变会导致该组件重新渲染,其中的子组件也会被重新渲染。如果子组件中并未使用该状态(State),重复渲染会导致无效的性能损耗。

在阻止重新渲染这个需求的基础上,诞生了memo函数,memo是react的一种缓存技术,这个函数可以检测从父组件接收的props,并且在父组件改变state的时候比对这个state是否是本组件在使用,如果不是,则不会重新渲染子组件。

注意: memo React 16.6.0版本出现的,只有版本高于这个才能使用

基本使用:

const SomeComponent = memo(Component, arePropsEqual?)
  • Component:要进行记忆化的组件。memo 不会修改该组件,而是返回一个新的、记忆化的组件。它接受任何有效的 React 组件,包括函数组件和 forwardRef 组件。
  • arePropsEqual:可选参数,一个函数,接受两个参数:组件的前一个 props 和新的 props。如果旧的和新的 props 相等,即组件使用新的 props 渲染的输出和表现与旧的 props 完全相同,则它应该返回 true。否则返回 false。通常情况下,你不需要指定此函数。默认情况下,React 将使用 Object.is 比较每个 prop。
  • 返回值:memo 返回一个新的 React 组件。它的行为与提供给 memo 的组件相同,只是当它的父组件重新渲染时 React 不会总是重新渲染它,除非它的 props 发生了变化。

测试例子:

function Head() {
  return <div>Head,{Math.random()}</div>;
}
const HeadOne = React.memo(() => <div>HeadOne,{Math.random()}</div>);
function App() {
  const [count, setCount] = React.useState(1);
  const list = [10, 20];
  const show = () => {};
  return (<div>
    <div>App count={count}</div>
    <button onClick={() => { setCount(count + 1); }}>添加</button>
    <Head />
    <HeadOne name="jack" />
    <HeadOne list={list} />
    <HeadOne onClick={show} />
  </div>);
}
// 类组件中,由于this在类存在期间不会变,所以绑定到this上的变量和方法不会变,当props属性传给子组件,值不变不会触发子组件渲染
class App extends React.PureComponent {
  constructor(props) { super(); this.state = { count: 0 }; this.list = [10, 20]; }
  show = () => {};
  render() {
    const { count } = this.state;
    return (<div className="">
      <div>App11 count={count}</div>
      <button onClick={() => { this.setState({ count: count + 1 }); }}>添加</button>
      <HeadOne list={this.list} />
      <HeadOne onClick={this.show} />
    </div>);
  }
}

经过上面例子可以发现:
1、正常的组件,每次父组件渲染,即使没有传props,子组件都会渲染。
2、使用memo后,如果props没有修改,父组件渲染,子组件不会渲染。
3、父组件是函数组件:每次渲染,使用const声明的变量和函数都是新创建的,即使值没有变。传到子组件中依然是不同的props,还是会导致子组件重新渲染。可以使用可以使用 useMemo 方法包裹缓存变量。
4、父组件是类组件:使用this变量和组件内的方法每次都是不变的。没有函数组件的问题。

memo 官方文档:https://zh-hans.react.dev/reference/react/memo

Last Updated: 12/12/2024, 11:12:33 AM