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')
);

image.png

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')
);

image.png

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="&lt;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传undefinednull,输入仍可编辑。

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写出的代码往往互相纠缠,难以维护:

image.png

而React可以避免构建这样复杂的程序结构,无论何种事件,引发的都是React组件的重新渲染,至于如何只修改必要的DOM部分,则完全交给React去操作:

image.png

组件设计

作为软件设计的通则,组件的划分要满足高内聚(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>

image.png

state和prop的局限

是时候重新思考一下多个组件之间的数据管理问题了。上面的例子中每个ShowScore组件有自己的状态记录当前分数,而父组件App也有一个状态来存储所有ShowScore分数总和。也就是数据发生了重复。如果数据重复,带来的一个问题就是如何保证重复的数据一致。

image.png

另一种思路,就是干脆不要让任何一个React组件储存数据,而是把数据源放在React组件之外形成全局状态,让各个组件保持和全局状态的一致,这样更容易控制:

image.png

这就是Flux和Redux中Store的概念。

所涉及的示例源码可在这里获取(不完全)

posted @ 2020-09-24 11:05  sanhuamao  阅读(277)  评论(0编辑  收藏  举报