React.js |核心概念
参照react官方文档与书籍程墨的《深入浅出React和Redux》,文章用于笔记整理。
初始化React项目
React技术依赖于一个很庞大的技术栈,比如,转译JavaScript代码需要使用Babel,模块打包工具又要使用Webpack……这些技术栈都需要各自的配置文件。现在通过create-react-app
工具就可以创建一个具有基本配置的应用。create-react-app
是一个通过npm发布的安装包,在确认Node.js和npm安装好之后安装这个工具:
npm install --global create-react-app
现在创建一个项目:
create-react-app myapp
它会生成一个名为myapp的目录,这个目录中会自动添加一个应用的框架,避免了大量的手工配置工作。运行项目:
cd myapp
npm start
创建完react应用之后,主要关注src目录,其中index.js
是入口文件。现在对默认的src目录内容做了一些删减,只保留以下三个文件:
App.js
index.css
index.js
App.js
import React from 'react';
function App() {
return (
<h1>hello world!</h1>
);
}
export default App;
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
//ES6语法导入模块。ES6语法的js代码会被webpack和babel转译成所有浏览器都支持的ES5语法,而这一切create-react-app已经替我们完成了这些工作。
import App from './App';
//渲染app组件
ReactDOM.render(
<App />,document.getElementById('root')
);
React组件
React的首要思想是通过组件(Component)来开发应用。所谓组件,指的是能完成某个特定功能的独立的、可重用的代码。把一个大应用分解成若干小的组件,每个组件只关注于某个小范围的特定功能。
组件类别
-
函数组件
function Welcome(props) { return <h1>Hello, {props.name}</h1>; }
-
class组件
class Welcome extends React.Component { //... render() { return <h1>Hello, {this.props.name}</h1>; } }
渲染组件
-
React 元素可以是DOM 标签
const element = <div />;
-
也可以是自定义的组件
const element = <Welcome name="Sara" />;
当 React 元素为用户自定义组件时,它会将 JSX 所接收的属性以及子组件转换为props对象传递给组件:
//1.定义组件 function Welcome(props) { return <h1>Hello, {props.name}</h1>; } //2.创建组件并将 {name: 'Sara'} 作为 props对象传入 const element = <Welcome name="Sara" />; //3.渲染组件 ReactDOM.render( element, document.getElementById('root') );
组合组件
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
//可以多次渲染 Welcome 组件的 App 组件
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
</div>
);
}
组件示例
ClickCounter.js(在src下创建一个计数组件)
//从react库中引入了React和Component
//Component作为所有组件的基类,提供了很多组件共有的功能
import React,{Component} from 'react';
//使用ES6语法创建一个叫ClickCounter的组件类,继承Component
class ClickCounter extends Component{
//构造函数
constructor(props){
super(props);
this.onClickButton=this.onClickButton.bind(this)
this.state={count:0}
}
onClickButton(){
this.setState({count:this.state.count+1})
}
//渲染
render(){
return(
<div>
<button onClick={this.onClickButton}>+1</button>
计数:{this.state.count}
</div>
)
}
}
export default ClickCounter
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import ClickCounter from './ClickCounter';
ReactDOM.render(
<ClickCounter />,document.getElementById('root')
);
JSX
所谓JSX,是JavaScript的语法扩展(eXtension),让我们可以在JavaScript中可以编写像HTML一样的代码。下面是一个jsx例子:
const element = <h1>Hello, world!</h1>;
大括号
可以在大括号内放置任何有效的js表达式:
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;
const element = <img src={user.avatarUrl}></img>;
自定义组件
用户定义的组件必须以大写字母开头
// 错误!React 会认为 <hello /> 是一个 HTML 标签
function hello() {...}
// 正确!
function HelloWorld() {...}
字符串字面量
//等价
<MyComponent message={'hello world'} />
<MyComponent message="hello world" />
//字符串字面量赋值给 prop 时是未转义的
<MyComponent message={'<3'} />
<MyComponent message="<3" />
原理
Babel 会把 JSX 转译成一个名为 React.createElement()
函数调用。实际上,JSX 仅仅只是 React.createElement(component, props, ...children)
函数的语法糖。
<MyButton color="blue" shadowSize={2}>
Click Me
</MyButton>
会编译为:
React.createElement(
MyButton,
{color: 'blue', shadowSize: 2},
'Click Me'
)
如果没有子节点:
<div className="sidebar" />
会编译为:
React.createElement(
'div',
{className: 'sidebar'}
)
由于 JSX 会编译为 React.createElement
调用形式,所以在有 JSX 的地方需要引入React 库:
//引入
import React from 'react';
function WarningButton() {
return <CustomButton color="red" />;
}
点语法
可以使用点语法引用React 组件
import React from 'react';
const MyComponents = {
DatePicker: function DatePicker(props) {
return <div>Imagine a {props.color} datepicker here.</div>;
}
}
//点语法引用组件
function BlueDatePicker() {
return <MyComponents.DatePicker color="blue" />;
}
空格与空行
JSX 会移除行首尾的空格以及空行。与标签相邻的空行均会被删除,文本字符串之间的新行会被压缩为一个空格。
<div>Hello World</div>
<div>
Hello World
</div>
等价
<div>
Hello
World
</div>
<div>
Hello World
</div>
JSX子元素
-
允许由多个 JSX 元素组成
const element = ( <div> <h1>Hello!</h1> <h2>Good to see you here.</h2> </div> );
<MyContainer> <MyFirstComponent /> <MySecondComponent /> </MyContainer>
-
JS表达式
<MyComponent>{'foo'}</MyComponent>
function Item(props) { return <li>{props.message}</li>; } function TodoList() { const todos = ['finish doc', 'submit pr', 'nag dan to review']; return ( <ul> {todos.map((message) => <Item key={message} message={message} />)} </ul> ); }
-
函数
<Repeat numTimes={10}> {(index) => <div key={index}>This is item {index} in the list</div>} </Repeat>
-
布尔类型、Null 以及 Undefined 将会忽略
//结果相同 <div /> <div></div> <div>{false}</div> <div>{null}</div> <div>{undefined}</div> <div>{true}</div>
运行时选择类型
不能将通用表达式作为 React 元素类型。如果想通过通用表达式来动态决定元素类型,需要首先将它赋值给大写字母开头的变量:
const components = {
photo: PhotoStory,
video: VideoStory
};
function Story(props) {
// 错误!
return <components[props.storyType] story={props.story} />;
}
function Story(props) {
// 正确!JSX 类型可以是大写字母开头的变量
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />;
}
包含在开始和结束标签之间的 JSX 表达式内容将作为特定属性 props.children
传递给外层组件。
<MyComponent>Hello world!</MyComponent>
MyComponent
中的 props.children
是一个简单的未转义字符串 "Hello world!"
。
组件数据
React组件的数据分为两种,prop和state。无论prop或者state的改变,都可能引发组件的重新渲染。prop是组件的对外接口,state是组件的内部状态,对外用prop,内部用state。下面将用一个例子演示:App父组件与ShowScore子组件
App.js
import React, { Component } from 'react';
import ShowScore from './ShowScore'
class App extends Component{
render(){
return(
<div>
<ShowScore name="李华" initValue={80}/>
<ShowScore name="小明" initValue={90}/>
</div>
)
}
}
export default App
ShowScore.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class ShowScore extends Component{
constructor(props){
super(props)
this.onClickAdd=this.onClickAdd.bind(this)
this.onClickDec=this.onClickDec.bind(this)
this.state={
score:props.initValue||0
}
}
onClickAdd(){this.setState({score:this.state.score+1})}
onClickDec(){this.setState({score:this.state.score-1})}
render(){
const{name}=this.props
//等同于:const name =this.props.name
return(
<div>
<button onClick={this.onClickAdd}>+</button>
<button onClick={this.onClickDec}>-</button>
<span>{name}的分数为:{this.state.score}</span>
</div>
)
}
}
ShowScore.propTypes={
name:PropTypes.string.isRequired,
score:PropTypes.number
}
export default ShowScore
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
ReactDOM.render(
<App/>,
document.getElementById('root')
);
Prop
prop(property的简写)是从外部传递给组件的数据。每个React组件都是独立存在的模块,组件之外的一切都是外部世界,外部世界就是通过prop来和组件对话的。
给Prop赋值
父组件App首先引入了子组件ShowScore,并给prop赋值:
import ShowScore from './ShowScore'
<ShowScore name="李华" initValue={80}/>
<ShowScore name="小明" initValue={90}/>
读取Prop
对于子组件ShowScore来说,父组件App就是外部世界。子组件ShowScore接受了外部的prop值。把prop中的initValue赋值给子组件内部状态state中的score:
constructor(props){
//...
this.state={
score:props.initValue||0
}
}
把prop中的name赋值在视图中渲染:
render(){
//...
return(
<div>
...
<span>{name}的分数为:{this.state.score}</span>
</div>
)
}
propTypes检查
React通过propTypes来声明自己的接口规范。可以通过增加类的propTypes属性来定义prop规格。在运行时和静态代码检查时根据propTypes判断外部世界是否正确地使用了组件的属性。在下面例子中,如果没有按照规范传prop,会看到控制台会出现警告。
import PropTypes from 'prop-types';
ShowScore.propTypes={
name:PropTypes.string.isRequired,
score:PropTypes.number
}
Prop只读性
//纯函数-不会更改入参-多次调用下相同的入参始终返回相同的结果
function sum(a, b) {
return a + b;
}
//非纯函数-更改了入参
function withdraw(account, amount) {
account.total -= amount;
}
Prop默认值
//Props 默认值为 True
//等价
<MyTextBox autocomplete />
<MyTextBox autocomplete={true} />
属性展开
function App1() {
return <Greeting firstName="Ben" lastName="Hector" />;
}
//如果你已经有了一个 props 对象,你可以使用展开运算符 ... 来在 JSX 中传递整个 props 对象
function App2() {
const props = {firstName: 'Ben', lastName: 'Hector'};
return <Greeting {...props} />;
}
可以选择只保留当前组件需要接收的 props,并使用展开运算符将其他 props 传递下去
//将一个函数组件赋值给Button
const Button = props => {
//2.解构prop,保留kind
const { kind, ...other } = props;
//3.根据保留的kind的值选择className
const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
//4.其他的 props通过 ...other 对象传递
return <button className={className} {...other} />;
};
const App = () => {
return (
<div>
//1.传prop,有kind和onClick
<Button kind="primary" onClick={() => console.log("clicked!")}>
Hello World!
</Button>
</div>
);
};
props.children
包含在开始和结束标签之间的 JSX 表达式内容将作为特定属性 props.children
传递给外层组件。下面例子中,MyComponent 的 props.children 是一个简单的未转义字符串 "Hello world!"
<MyComponent>Hello world!</MyComponent>
State
state代表组件的内部状态。state记录自身数据变化。由于React组件不能修改传入的prop,所以常常会把prop数据保存在state中。
初始化State
constructor(props){
...
this.state={
//后面的0表示:若没有传prop值,则初始值为0
score:props.initValue||0
}
}
更新State
//通过this.setState函数更新内部状态state的score值
onClickAdd(){
this.setState({score:this.state.score+1})
}
读取State
//通过this.state.score可以读取到内部状态state的score值
render(){
//...
return(
<div>
...
<span>{name}的分数为:{this.state.score}</span>
</div>
)
}
注意
不要直接修改 State:
this.state.score = 90;
而是应该:
this.setState({score: 90});
State 的更新可能是异步的。this.props 和 this.state 可能会异步更新,所以不要依赖他们的值来更新下一个状态。比如:
this.setState({
counter: this.state.counter + this.props.increment,
});
可以让 setState() 接收一个函数而不是一个对象来解决:
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
或者
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});
生命周期
装载过程
装载过程(Mount):把组件第一次在DOM树中渲染的过程
1.constructor
constructor是ES6中每个类的构造函数。要创造一个组件类的实例,都会调用对应的构造函数。注意,无状态的React组件往往就不需要定义构造函数。React组件需要构造函数的目的是:
- 初始化state
- 绑定成员函数的this环境
在ES6语法下,构造函数的this是当前组件实例,它不会与类的成员函数自动绑定。所以,为了方便调用,会在构造函数中将这个实例的特定函数绑定this为当前实例。
constructor(props){
//...
this.onClickAdd=this.onClickAdd.bind(this)
this.onClickDec=this.onClickDec.bind(this)
}
2.getInitialState和getDefaultProps
getInitialState的返回值会用来初始化组件的this.state。但只有用React.createClass
方法创造的组件类中才会起作用;getDefaultProps函数的返回值可以作为props的初始值。这个函数只在React.createClass
方法创造的组件类才会用到
const Sample=React.createClass({
getInitialState:function(){
return{foo:'bar'}
},
getDefaultProps:function(){
return{sampleProp:0}
},
})
用ES6的话,在构造函数中通过给this.state赋值、通过给类属性defaultProps赋值指定props初始值来进行初始化。代码如下:
class Sample extends React.Component{
constructor(props){
super(props);
this.state={foo:'bar'}
}
}
Sample.defaultProps={
return{sampleProp:0}
}
3.render
render函数是React组件中最重要的函数。render函数并不做实际的渲染动作,它只返回一个JSX描述的结构,最终由React来操作渲染过程。有时候组件的作用不是渲染界面,或者,在某些情况下没有需要渲染的,那就让render函数返回一个null或者false。render函数是一个纯函数,完全根据this.state和this.props来决定返回的结果,而且不要产生任何副作用。一个纯函数不应该引起状态的改变
4.componentWillMount和componentDidMount
在装载过程中,componentWillMount会在调用render函数之前被调用,component-DidMount会在调用render函数之后被调用。通常不用定义componentWillMount函数,可以认为这个函数存在的主要目的就是为了和componentDidMount对称。注意,render函数被调用完之后,componentDidMount函数需要等到render返回的东西已经引发了渲染,组件已经被“装载”到了DOM树上之后才执行。
更新过程
更新过程(Update):当组件被重新渲染的过程。当props或者state被修改的时候,就会引发组件的更新过程。
1.componentWillReceiveProps(nextProps)
只要是父组件的render被调用,在render里面被渲染的子组件就会调用函数。不管父组件传给子组件的props有没有改变,都会触发子组件的componentWillReceiveProps函数。注意,通过this.setState方法触发的更新过程不会调用这个函数,这个函数适合根据新的props值(也就是参数nextProps)来计算出是不是要更新内部状态state。这个函数有必要把传入参数nextProps和this.props作必要对比。nextProps代表的是这一次渲染传入的props值,this.props代表的上一次渲染时的props值,只有两者有变化的时候才有必要调用this.setState更新内部状态。
2.shouldComponentUpdate(nextProps, nextState)
shouldComponentUpdate也是React组件中重要的生命周期函数。它决定了一个组件什么时候不需要渲染。
在更新过程中,React库首先调用shouldComponentUpdate函数,如果这个函数返回true,那就会继续更新过程,接下来调用render函数;反之,如果得到一个false,那就立刻停止更新过程,也就不会引发后续的渲染了。在执行到到函数shouldComponentUpdate的时候,this.state依然是this.setState函数执行之前的值,所以我们要做的实际上就是在nextProps、nextState、this.props和this. state中互相比对。即shouldComponentUpdate的参数是接下来的props和state值,对比this.props和this.state来判断出是返回true还是返回false。
3.componentWillUpdate和componentDidUpdate
如果组件的shouldComponentUpdate函数返回true, React接下来就会依次调用对应组件的componentWillUpdate、render和componentDidUpdate函数。
卸载过程
卸载过程(Unmount):组件从DOM中删除的过程。
当React组件要从DOM树上删除掉之前,对应的componentWillUnmount函数会被调用,所以这个函数适合做一些清理性的工作。componentWillUnmount中的工作往往和componentDidMount有关,比如,在componentDidMount中用非React的方法创造了一些DOM元素,如果撒手不管可能会造成内存泄露,那就需要在componentWillUnmount中把这些创造的DOM元素清理掉。
事件处理
驼峰式命名
React 事件的命名采用小驼峰式(camelCase)
<button onClick={handleClick}>
...
</button>
传入函数
使用 JSX 语法时需要传入一个函数作为事件处理函数,而不是一个字符串
//传统html
<button onclick="activateLasers()">
Activate Lasers
</button>
//React
<button onClick={activateLasers}>
Activate Lasers
</button>
阻止默认行为
不能通过返回 false 的方式阻止默认行为。必须显式的使用 preventDefault 。
//传统html
<a href="#" onclick="console.log('...'); return false">
Click me
</a>
//react
function ActionLink() {
function handleClick(e) {
e.preventDefault();
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
);
}
声明事件
用 ES6 class 语法定义一个组件时,通常将事件处理函数声明为 class 中的方法
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
this.handleClick = this.handleClick.bind(this);
}
//在类中定义
handleClick() {
this.setState(state => ({isToggleOn: !state.isToggleOn}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
在 JavaScript 中,class 的方法默认不会绑定 this。这其实与 JavaScript 函数工作原理有关。通常情况下,如果没有在方法后面添加 (),例如 onClick={this.handleClick}
,就应该为这个方法绑定 this。如果不想使用bind,可以采取下面的方法:
1.public class fields 语法:
class LoggingButton extends React.Component {
constructor(props) {...}
//...
handleClick = () => {...}
}
2.在回调中使用箭头函数:
<button onClick={() => this.handleClick()}>
传递参数
//删除指定行
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
在这两种情况下,React 的事件对象 e 会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递;而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。
条件渲染
function UserGreeting() {return <h1>Welcome back!</h1>}
function GuestGreeting() {return <h1>Please sign up.</h1>}
function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
if (isLoggedIn) {return <UserGreeting />}
return <GuestGreeting />;
}
ReactDOM.render(
<Greeting isLoggedIn={false} />,
document.getElementById('root')
);
元素变量
使用变量来储存元素,可以有条件地渲染组件的一部分
import React, { Component } from 'react';
import Greeting from './Greeting'
//按钮切换组件
function LoginButton(props) {return <button onClick={props.onClick}>Login</button>}
function LogoutButton(props) {return <button onClick={props.onClick}> Logout</button>}
class LoginControl extends React.Component {
constructor(props) {
super(props);
this.handleLoginClick = this.handleLoginClick.bind(this);
this.handleLogoutClick = this.handleLogoutClick.bind(this);
this.state = {isLoggedIn: false};
}
handleLoginClick() {this.setState({isLoggedIn: true})}
handleLogoutClick() {this.setState({isLoggedIn: false})}
render() {
const isLoggedIn = this.state.isLoggedIn;
let button;
if (isLoggedIn) {button = <LogoutButton onClick={this.handleLogoutClick} />}
else { button = <LoginButton onClick={this.handleLoginClick} />}
return (
<div>
<Greeting isLoggedIn={isLoggedIn} />
{button}
</div>
);
}
}
export default LoginControl
元素符&&
可以在JSX 中用内联条件渲染——元素符&&。true && expression 总是会返回 expression, 而 false && expression 总是会返回 false,React 会忽略并跳过它。
function Mailbox(props) {
const unreadMessages = props.unreadMessages;
return <div>
{unreadMessages.length > 0 &&<h2>You have {unreadMessages.length} unread messages.</h2>}
</div>
}
const messages = ['快起床!', '快吃饭!', '快写作业!'];
ReactDOM.render(
<Mailbox unreadMessages={messages} />,
document.getElementById('root')
);
三目运算符
condition ? true : false
function IsLogin(props) {
const isLoggedIn = props.isLoggedIn;
return (
<div>The user is {isLoggedIn ? 'currently' : 'not'} logged in.</div>
);
}
ReactDOM.render(
<IsLogin isLoggedIn={false}/>,
document.getElementById('root')
);
阻止组件渲染
让组件直接返回 null
function WarningBanner(props) {
if (!props.warn) {
return null;
}
return (
<div className="warning">
Warning!
</div>
);
}
列表 & Key
渲染多个组件
使用 {} 在 JSX 内构建一个元素集合:
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li>{number}</li>
);
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);
基础列表组件
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map(number =><li>{number}</li>);
return <ul>{listItems}</ul>
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
Key
key 帮助 React 识别哪些元素改变了。一般情况下,应当给数组中的每一个元素赋予一个确定的标识,否则控制台会出现警告。
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
key 最好是这个元素在列表中唯一的字符串
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
当元素没有确定 id 的时候,使用元素索引 index 作为 key(不推荐)
const todoItems = todos.map((todo, index) =>
<li key={index}>
{todo.text}
</li>
);
元素的 key 只有放在就近的数组上下文中才有意义
//错误
function ListItem(props) {
const value = props.value;
return <li key={value.toString()}>{value}</li>
);
}
//正确
function ListItem(props) {
return <li>{props.value}</li>;
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<ListItem key={number.toString()} value={number} />
);
return <ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
key 在其兄弟节点之间应该是唯一的。但不需要是全局唯一
在 JSX 中嵌入 map()
//原始
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<ListItem key={number.toString()} value={number} />
);
return <ul>{listItems}</ul>
);
}
//JSX
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>{numbers.map((number) =><ListItem key={number.toString()}value={number}/>)}</ul>
);
}
表单
受控组件
在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。渲染表单的 React 组件控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”
class NameForm extends Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange=this.handleChange
this.handleSubmit=this.handleSubmit
}
handleChange=(e)=> {this.setState({value: e.target.value})}
handleSubmit=(e)=> {
alert('提交的名字: ' + this.state.value);
e.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label> 名字:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}
textarea
在 React 中,<textarea>
使用 value
属性代替。和使用单行 input 的表单非常类似
<textarea value={this.state.value} onChange={this.handleChange} />
select
React 并不会使用 selected
属性,而是在根 select
标签上使用 value
属性
class FlavorForm extends React.Component {
constructor(props) {
super(props);
//默认选中
this.state = {value: 'coconut'};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {this.setState({value: event.target.value});}
handleSubmit(event) {
alert('你喜欢的风味是: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
选择你喜欢的风味:
<select value={this.state.value} onChange={this.handleChange}>
<option value="grapefruit">葡萄柚</option>
<option value="lime">酸橙</option>
<option value="coconut">椰子</option>
<option value="mango">芒果</option>
</select>
</label>
<input type="submit" value="提交" />
</form>
);
}
}
可以将数组传递到 value
属性中,以支持在 select
标签中选择多个选项:
<select multiple={true} value={['grapefruit', 'lime']}>
多个input
class Reservation extends React.Component {
constructor(props) {
super(props);
this.state = {
isGoing: true,
Guests: 2
};
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange(e) {
const target = e.target;
//先获取值
const value = target.name === 'isGoing' ? target.checked : target.value;
//获取input的名称
const name = target.name;
//这样写的前提:input的value值与name保持一致
this.setState({
[name]: value
});
}
render() {
return (
<form>
<label>
参与:
<input name="isGoing" type="checkbox" checked={this.state.isGoing} onChange={this.handleInputChange} />
</label>
<br />
<label>
来宾人数:
<input name="Guests" type="number" value={this.state.Guests} onChange={this.handleInputChange} />
</label>
</form>
);
}
}
空值
在受控组件指定 value 的 prop 会阻止用户更改输入。但如果你的prop传undefined
或 null
,输入仍可编辑。
ReactDOM.render(<input value="hi" />, mountNode);
setTimeout(function() {
ReactDOM.render(<input value={null} />, mountNode);
}, 1000);
React特点
- React理念
React的理念归结为一个公式:UI=render(data)
。用户看到的界面(UI),应该是一个函数(在这里叫render)的执行结果,只接受数据(data)作为参数。如此一来,最终的用户界面,在render函数确定的情况下完全取决于输入数据。想要更新用户界面,要做的就是更新data,用户界面自然会做出响应,所以React实践的也是“响应式编程”(Reactive Programming)的思想。
- Virtual DOM
React利用Virtual DOM,让每次渲染都只重新渲染最少的DOM元素。
要了解Virtual DOM,就要先了解DOM, DOM是结构化文本的抽象表达形式,特定于Web环境中,这个结构化文本就是HTML文本。HTML中的每个元素都对应DOM中某个节点,这样,因为HTML元素的逐级包含关系,DOM节点自然就构成了一个树形结构,称为DOM树。
浏览器为了渲染HTML格式的网页,会先将HTML文本解析以构建DOM树,然后根据DOM树渲染出用户看到的界面,当要改变界面内容的时候,就去改变DOM树上的节点。
Web前端开发关于性能优化有一个原则:尽量减少DOM操作。虽然DOM操作也只是一些简单的JavaScript语句,但是DOM操作会引起浏览器对网页进行重新布局,重新绘制,这就是一个比JavaScript语句执行慢很多的过程。
虽然JSX看起来很像是一个模板,但是最终会被Babel解析为一条条创建React组件或者HTML元素的语句,神奇之处在于,React并不是通过这些语句直接构建DOM树,而是首先构建Virtual DOM。
既然DOM树是对HTML的抽象,那Virtual DOM就是对DOM树的抽象。VirutalDOM不会触及浏览器的部分,只是存在于JavaScript空间的树形结构,每次自上而下渲染React组件时,会对比这一次产生的Virtual DOM和上一次渲染的VirtualDOM,对比就会发现差别,然后修改真正的DOM树时就只需要触及差别中的部分。
以ClickCounter为例,一开始点击计数为0,用户点击按钮让点击计数变成1,这一次重新渲染,React通过Virtual DOM的对比发现其实只是id为clickCount的span元素中内容从0变成了1而已:
<span id="clickCount">{this.state.count}</span>
React发现这次渲染要做的事情只是更换这个span元素的内容而已,其他DOM元素都不需要触及,于是执行类似下面的语句,就完成了任务:
document.getElementById("clickCount").innerHTML="1";
- 工作方式的优点
对比jQuery的方式直观易懂,当项目逐渐变得庞大时,用jQuery写出的代码往往互相纠缠,难以维护:
而React可以避免构建这样复杂的程序结构,无论何种事件,引发的都是React组件的重新渲染,至于如何只修改必要的DOM部分,则完全交给React去操作:
组件设计
作为软件设计的通则,组件的划分要满足高内聚(High Cohesion)和低耦合(LowCoupling)的原则:
高内聚指的是把逻辑紧密相关的内容放在一个组件中。传统上,内容由HTML表示,交互行放在JavaScript代码文件中,样式放在CSS文件中定义。一个功能模块却要放在三个不同的文件中,这其实不满足高内聚的原则。React中展示内容的JSX、定义行为的JavaScript代码,甚至定义样式的CSS,都可以放在一个JavaScript文件中。
低耦合指的是不同组件之间的依赖关系要尽量弱化,也就是每个组件要尽量独立。React组件的对外接口非常规范,方便开发者设计低耦合的系统。
组件向外传递数据
组件之间的交流是相互的,子组件某些情况下也需要把数据传递给父组件。
在上面例子中,父组件App包含两个子组件ShowScore。每个ShowScore都有一个可以动态改变的分数值,现在希望父组件App能够即时显示出这两个子组件当前分数值之和。
ShowScore.js
//改装一下函数
onClickAdd(){this.updateScore(true)}
onClickDec(){this.updateScore(false)}
updateScore(isIncrement){
const previousScore=this.state.score;
const newScore=isIncrement?previousScore+1:previousScore-1
//不仅可以改变内部状态
this.setState({score:newScore})
//还能通过this.props.onUpdate将状态传递给父组件
this.props.onUpdate(newScore,previousScore)
}
App.js
//添加构造函数。使其可以绑定成员函数的this环境
constructor(props){
super(props);
//初始状态数据与初始状态总和
this.initValue=[10,20]
const initSum=this.initValue.reduce((a,b)=>a+b,0)
this.onScoreUpdate=this.onScoreUpdate.bind(this)
this.state={
sum:initSum
}
}
//定义函数
onScoreUpdate(newScore,previousScore){
const ScoreChange=newScore-previousScore;
//让初始总和+/-变化的值
this.setState({sum:this.state.sum+ScoreChange})
}
//给子组件传递prop。现在只要子组件ShowScore触发了onUpdate,就会调用父组件的onScoreUpdate
<ShowScore name="李华" initValue={this.initValue[0]} onUpdate={this.onScoreUpdate}/>
<ShowScore name="小明" initValue={this.initValue[1]} onUpdate={this.onScoreUpdate}/>
<div>总分数:{this.state.sum}</div>
state和prop的局限
是时候重新思考一下多个组件之间的数据管理问题了。上面的例子中每个ShowScore组件有自己的状态记录当前分数,而父组件App也有一个状态来存储所有ShowScore分数总和。也就是数据发生了重复。如果数据重复,带来的一个问题就是如何保证重复的数据一致。
另一种思路,就是干脆不要让任何一个React组件储存数据,而是把数据源放在React组件之外形成全局状态,让各个组件保持和全局状态的一致,这样更容易控制:
这就是Flux和Redux中Store的概念。
所涉及的示例源码可在这里获取(不完全)