最近项目中在用到react,这里就整理一下react相关的基础知识。
安装
如果使用npm的话,需要安装两个基础的npm包,react
和react-dom
。
第一个组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class ShoppingList extends React.Component { render () { return ( <div className="shopping-list"> <h1>{this.porps.name}</h1> <ul> <li>显示器</li> <li>主机</li> <li>机械键盘</li> </ul> </div> ) } }
|
上面创建了一个最基本的react组件,其中render函数中用到了JSX
的语法。
JSX仅仅是js的语法糖,其最终还是要被转换成js的,可以使用webpack
+babel-loader
。
一个react组件接收一个参数props
,然后通过render函数返回一个reactNode
对象,注意是一个,而不能是多个。
接下来就可以这样使用ShoppingList组件:
1 2 3 4
| ReactDom.render( <ShoppingList shoppingList='购物清单'/>, document.querySelect('#app') );
|
可以发现props
在调用组件的时候的传入,就好比我们调用函数时传入参数一样。
props
上面的组件是一个无状态(stateless)组件,只能接收props,返回reactNode。
所以,它可以简写为如下形式:
1 2 3 4 5 6 7 8 9 10
| const ShoppingList = (props) => ( <div className="shopping-list"> <h1>{this.porps.shoppingList}</h1> <ul> <li>显示器</li> <li>主机</li> <li>机械键盘</li> </ul> </div> );
|
这种react组件被称为functional component
。
另外还有以下几点需要注意:
- props是只读的
- 所有的组件必须表现地像
pure function
,不可以修改props。
state
当然,既然有stateless组件,肯定也有stateful组件。
stateful的组件就不能定义为functional component
,而必须是class component
。后面将要介绍的包含生命周期函数(lifecycle hooks)的组件也只能定义为class component
。
看这个时钟的案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| class Clock extend React.Component { constructor (props){ super(props); this.state = { date: new Date() } } componentDidMount () { this.timerId = serInterval( () => this.tick(), 1000 ); } componentWillUnmount () { clearInterval(this.timerId); } tick () { this.setState({ date: new Date() }); } render () { return ( <div> <h1>当前时间为:</h1> <p>{this.state.date}</p> </div> ); } }
ReactDom.render( <Clock/>, document.querySelector('#app') );
|
这个组件的运行过程如下:
- 在通过ReactDom.render来渲染Clock组件到DOM时,调用
constructor()
。
- 接着调用
render()
并更新DOM。
- 将Clock挂载到DOM后,调用
componentDidMount()
,它开启了一个定时器,让浏览器每隔1s调用tick()。
- 1s后,执行
tick()
,并且在tick中执行setState()
。
- state的改变会触发
render()
函数的再次调用(注意这时constructor,componentDidMount不会再调用)。
- 如果Clock组件被从DOM中移除,那么会调用
componentWillUnmount()
,停止定时器。
另外使用state时,还有以下几点需要注意:
- 不要直接修改state对象,而要通过
this.setState()
。
1 2 3 4 5 6 7
| this.state.commment = 'hello'; this.setState({ comment: 'hello' });
|
- state和props更新可能异步的,因为react出于性能考虑,可能将多个setState()调用打包到一起来执行。所以我们不能依赖与它当前的值来计算下一个state。
1 2 3 4 5 6 7 8
| this.setState({ counter: this.state.counter + this.props.increment }); this.setState((prevState, props) => ({ counter: prevState.counter + props.increment }));
|
- state对象的更新是通过merge来实现的。所以,你调用setState的时候,只需要改变你关心的state分片就可以。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| constructor (props) { super(props); this.state = { posts: [], comments: [] } } componentDidMount () { fetchPost().then(response => { this.setState({ posts: response.posts }); }); fetchComment().then(response => { this.setState({ comments: response.comments }); }); }
|
state的初始化,即可以在class component中的constructor()中使用this.state
来赋值,也可以在使用React.createClass()
中提供gitInitialState()
方法来初始化。
在应用中,所有数据的变化都源于一个source for truth
,通常就是state。如果多个子组件都需要这份数据,可以将state放在离这些子组件最近的公共祖先组件中,在祖先组件中,通过props将state(分片)分发给各个子组件,这也是react中数据流自上而下流动的体现。
组件生命周期
一个React组件的声明周期大致分为三个阶段:
- 挂载阶段(Mounting),发生在组件被挂载到真实DOM时
- 更新阶段(Updating),组件重新渲染成选你DOM并决定真实DOM是否需要更新时
- 卸载阶段(Unmounting),组件从DOM中卸载时
下图列出了在这三个阶段对应的方法以及执行顺序:
尤其注意各个lifecycle hooks在什么阶段会执行,比如constructor()只会在挂载阶段执行,更新阶段不执行。
列表项的key
我们常常有这样的需求,根据一个数组,渲染出一个列表。
这时,我们必须为每个列表项提供一个独一无二的属性key
,React可以用这个key来鉴别出哪个列表项被增加、修改或者删除了。
1 2 3 4 5 6 7 8 9 10 11
| render () { const numbers = [1,2,3,4,5]; const items = numbers.map((num) => ( <li key={num.toString()}>num</li> )); return ( <ul> {items} </ul> ); }
|
注意:在哪里生成reactNode数组,就在哪里使用key。
在原生的HTML中,<input>
,<textarea>
,select
这些表单元素维持着自己的状态value
,并且根据用户的输入来自动更新value。
而在react中,所有组件的状态应该在构造函数中声明,并且只能够通过setValue
来更新状态。
所以,在react中的表单元素会是下面这个样子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| class InfoForm extends React.Component { constructor (props) { super(props); this.state = { title: 'react', type: 'frontend', content: '' } } handleChange (event) { const target = event.target; const value = target.value; const name = target.name; this.setState({ [name]: value }); } handleSubmit (event) { event.preventDefault(); console.log(this.state); } render () { return ( <form onSubmit={this.handleSubmit.bind(this)}> <label> 标题: <input name='title' type='text' value={this.state.title} onChange={this.handleChange.bind(this)}/> </label> <br/> <label> 类型: <select name='type' value={this.state.type} onChange={this.handleChange.bind(this)}> <option value="frontend">前端</option> <option value="backend">后台</option> <option value="other">其他类型</option> </select> </label> <br/> <label> 内容: <textarea name='content' value={this.state.content} onChange={this.handleChange.bind(this)}/> </label> <br/> <input type='submit' value='提交'/> </form> ) } }
|
合成还是继承?
React给出的答案是合成(composition),而非继承(inheritance)。在React中,可以通过props提供了很强大的合成组件的功能。
props.children
我们常常有这样的需求,一个组件在定义的时候并不知道具体的内容是什么,比如SideBar
或者Dialog
。
这时候,我们使用特殊的props.children
来定义该组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| let SideBar = props => ( <div className="sidebar"> {props.children} </div> )
// 调用组件 ReactDom.render( <SideBar> <ul> <li>item1</li> <li>item2</li> <li>item3</li> </ul> </SideBar> );
|
多个slot
如果我们想定义一个包含多个插槽(slot)的组件,那么我们可以通过props来实现。
因为props可以向组件传递任何类型的数据,包括对象、函数、ReactNode。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| let SplitPane = props => ( <div className="split-pane"> <div className="split-pane-left"> {this.props.left} </div> <div className="split-pane-right"> {this.props.right} </div> </div> );
// 调用组件 ReactDom.render( <SplitPane left={ <Contracts/> } right={ <Chat/> } > );
|
thinking in react
组件架构方式有两种:从整体到局部和从局部到整体。在简单的应用中,推荐从整体到局部,在复杂的应用中,推荐从局部到整体。
让你的app只保存最小量的state数据。比如在todoList中,state中保存了todos的一个数组,就不需要保存todos数组的长度。因为这可以根据todos数组计算出来。
把app所需的所有数据罗列出来,并根据以下3条原则来判定哪些数据要当作state:
- 如果该数据可以通过props传递给其他组件,那么可能不是state。
- 如果该数据一直保持不变,可能不是state。
- 如果该数据可以根据其他state或者props计算得到,可能不是state。
最后
以上就是react的基础部分,后面再整理react的进阶知识以及redux,react-redux的实际应用。