组件允许我们将代码拆分为独立可复用的代码片段,这是一个十分重要的概念
1、函数组件
我们可以通过编写 JavaScript 函数定义组件
<!DOCTYPE html> <html> <head> <title>Demo</title> <script src="https://unpkg.com/react@16/umd/react.development.js"></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <script src="https://unpkg.com/babel-standalone"></script> </head> <body> <div id="app"></div> <script type="text/babel"> // 通过 function 定义组件,函数返回一个 React 元素 // 需要注意的是,组件名称必须以大写字母开头 // 因为 React 会将以小写字母开头的组件视为原生 DOM 标签 function SayHello() { return <h1>Hello World</h1>; }; // React 元素不仅仅是 DOM 标签,也可以是自定义组件 const element = <SayHello />; ReactDOM.render( element, document.getElementById('app') ); </script> </body> </html>
2、class 组件
我们也可以通过使用 class 定义组件,并且推荐使用这种方式
因为使用 class 定义组件允许我们为组件添加一些额外的方法和属性,使得组件具有更好的拓展性
<!DOCTYPE html> <html> <head> <title>Demo</title> <script src="https://unpkg.com/react@16/umd/react.development.js"></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <script src="https://unpkg.com/babel-standalone"></script> </head> <body> <div id="app"></div> <script type="text/babel"> // class 组件必须继承 React.Component class SayHello extends React.Component { // 使用 render() 函数返回一个 React 元素 // 注意,该元素必须只能具有一个顶层标签 render() { return <h1>Hello World</h1>; } }; // 使用组件的方法是一样的 const element = <SayHello />; ReactDOM.render( element, document.getElementById('app') ); </script> </body> </html>
3、props
当 React元素为自定义组件时,JSX 接收的属性会转换为单个对象传递给组件,我们将这个对象称之为 props
通过这种方式,我们可以给组件传递数据
class SayHello extends React.Component { constructor(props) { // 为什么要调用 super? // 如果不调用 super,那就无法在构造函数中访问 this // 为什么要传入 props? // 如果不传入 props,那就无法在构造函数中访问 this.props super(props); console.log(this); console.log(this.props); } render() { // 在组件中通过 this.props 访问 props // 注意,props 应该是只读的,也就是说我们不应该修改它的取值 return <h1>Hello { this.props.name }</h1>; } }; // 这里将属性转换为 props 对象并传入组件 const element = <SayHello name='Alice' />; ReactDOM.render( element, document.getElementById('app') );
4、自定义函数
通过自定义函数,我们可以在 class 组件中添加额外的方法
class SayHello extends React.Component { // 构造函数 constructor(props) { // 将 props 传递到父类构造函数 super(props); } // 自定义函数 format(name) { return name.substring(0,1).toUpperCase() + name.substring(1); } render() { // 通过 this.functionName() 调用自定义函数 let formatedName = this.format(this.props.name); return <h1>Hello { formatedName }</h1>; } }; const element = <SayHello name='alice'/>; ReactDOM.render( element, document.getElementById('app') );
5、state
通过 state,我们可以在 class 组件中添加额外的数据
state 与 props 都是组件的数据,不同之处在于 props 是从父组件传入的数据,state 是本组件私有的数据
class Timer extends React.Component { constructor(props) { super(props); // 初始化 state this.state = { date: new Date() }; } render() { // 通过 this.state 访问 state return ( <div> <h1>Hello</h1> <p>现在是{ this.state.date.toLocaleTimeString() }</p> </div> ) } }; const element = <Timer />; ReactDOM.render( element, document.getElementById('app') );
使用 state 有几个需要特别注意的地方:
- 不要直接修改 state
我们可以直接访问 state,但是不能直接修改 state,例如
this.state.date = new Date() // 这样的语句并不会重新渲染组件
要想修改 state 并重新渲染组件,我们可以使用 this.setState()
,例如
this.setState({date: new Date()})
但是构造函数是唯一特别的地方,我们可以直接在构造函数里面给 this.state
赋值,例如
this.state = { date: new Date() }
- state 的更新是异步的
出于性能的考虑,React 可能会把多个 setState()
调用合并成一个调用
因此,this.props
和 this.state
可能会异步更新,所以不要依赖它们的值来更新 state
this.setState({ counter: this.state.counter + this.props.increment }) // 这样的代码可能不会正常更新 counter
要解决这个问题,可以让 setState()
接收一个函数作为参数,而不是一个对象
这个函数的第一个参数为上一个 state,第二个参数为此次更新被应用时的 props,返回一个对象
this.setState((state, props) => ({ counter: state.counter + props.increment }));
- state 的更新会被合并
setState()
调用会把提供的对象合并到当前 state,而非替换
也就是说,假如我们在构造函数中初始化 state 如下:
constructor(props) { super(props); this.state = { name: 'Alice', phone: '12345679810' }; }
然后调用 setState()
方法更新 state 如下:
this.setState({ phone: '10987654321' })
此时,state 中的数据应该是 { name: 'Alice', phone: '10987654321'}
,而不是 { phone: '10987654321' }
6、生命周期
每个组件都有生命周期,都会包含生命周期方法,我们可以重写这些方法,使组件在特定阶段完成特定操作
组件的生命周期大体可以分为三个阶段,每个阶段调用生命周期方法的顺序如下:
挂载:当组件插入到 DOM 中时触发
constructor()
:在组件挂载前调用,主要用于初始化 state 或为事件处理函数绑定实例
这里不要使用setState()
方法,如果需要初始化 state,可以直接给this.state
赋值render()
:class 组件中唯一必须实现的方法,该函数应该为纯函数
也就是说在不修改 state 的情况下,每次调用返回相同的结果,并且它不与浏览器进行交互componentDidMount()
:在组件挂载后调用,主要进行依赖于 DOM 结点的初始化操作
例如实例化请求或添加订阅,这里可以使用setState()
方法
更新:当组件的 props 或 state 发生改变时触发
render()
componentDidUpdate()
:在更新之后调用,但是首次渲染不会执行
这里可以进行 DOM 操作,并且可以使用setState()
方法,但必须包含在一个条件语句里面
卸载:当组件从 DOM 中移除时触发
componentWillUnmount()
:在组件卸载之前调用
这里不应使用setState()
方法,因为组件卸载之后,将永远不会重新渲染,也不会重新挂载
class Timer extends React.Component { // 生命周期方法,在挂载前调用 constructor(props) { super(props); // 初始化 state this.state = { date: new Date() }; } // 自定义方法 tick() { // 通过 this.setState() 修改 state this.setState({ date: new Date() }) } // 生命周期方法,在挂载后调用 componentDidMount() { this.timerID = setInterval(() => this.tick(), 1000) } // 生命周期方法,在卸载前调用 componentWillUnmount() { clearInterval(this.timerID) } // 生命周期方法,在挂载或更新时调用 render() { // 通过 this.state 访问 state return ( <div> <h1>Hello</h1> <p>现在是{ this.state.date.toLocaleTimeString() }</p> </div> ) } }; const element = <Timer />; ReactDOM.render( element, document.getElementById('app') );