React从入门到真香

1. 设计思想

  1. React 设计思想
  2. React的设计哲学 - 简单之美
  3. 颠覆式前端UI开发框架:React

    2. 安装

2.1 CDN的方式引入

1
2
3
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  • react.js:React核心库
  • react-dom.js:提供操作 DOM 的 react 扩展库
  • babel.min.js:解析 JSX 语法代码转为 JS 代码的库

2.2 create-react-app

安装create-react-app

1
npm isntall -g create-react-app

创建项目

1
create-react-app my-app

3. JSX

jsx 允许在模板中用{}插入JavaScript表达式,如果{}中的变量是数组,则会展开这个数组的所有成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var arr = [
<h1>Hello world!</h1>,
<h2>React is awesome</h2>,
];
ReactDOM.render(
<div>{arr}</div>,
document.getElementById('example')
);

var names = ['Alice', 'Emily', 'Kate'];

ReactDOM.render(
<div>
{
names.map(function (name) { // 列表渲染
return <div>Hello, {name}!</div>
})
}
</div>,
document.getElementById('example2')
);

ReactDOM.render第一个参数是渲染的组件,第二个参数是挂载实例的位置。

  • className替换class
  • 内联样式——style=
  • 只能含有一个最外层标签

对于最后一个问题,可以使用React.Fragment组件,它能够在不额外创建 DOM 元素的情况下,让render返回多个元素

1
2
3
4
5
6
7
8
render() {
return (
<React.Fragment>
Some text.
<h2>A heading</h2>
</React.Fragment>
);
}

4.组件

4.1 类组件(有状态)

组件类的第一个名字必须大写

通过继承React.Component来实现类组件,需要实现一个render方法,该方法返回一个模板

1
2
3
4
5
6
7
8
9
10
class Clock React.Component {
render () {
return (
<div onClick={this.clickFunc.bind(this)}> // 将clickFunc内部this指向组件实例
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}</h2>
</div>
)
}
}

通过React.createClass生成一个组件类

1
2
3
4
5
6
7
8
9
10
var Clock = React.createClass({
render: function() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}</h2>
</div>
)
}
});

两种方式的区别是,前者不会自动绑定this

4.2 函数式组件(无状态)

1
2
3
4
5
6
7
8
9
10
function Person (props) {
const {name, age, sex} = props;
return {
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
}
}

5. State

5.1 state初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
class Clock React.Component {
// 第一种 直接在实例上定义state属性
state = {
value: 6,
}

// 第二种 在构造器中设置state
constructor () {
this.state = {
value: 6
}
}
}

5.2 state修改

直接修改数据是不会触发视图更新的,只有使用setState来修改数据,会重新触发组件的render函数

setState第一个参数就是修改后的state对象,可以修改某一个值,setState是浅合并

如果setState的第一个参数不是一个对象而是一个函数,这个函数在执行时会通过参数被传入prevState,也就是之前的状态,而返回值就会和state进行合并

1
2
3
4
5
6
7
this.setState((prevState : any) => {
return {
num : prevState.num + 1
}
}, () => {
console.log(this.state.num)
})

但是setState有两个问题

  • setState对状态的修改,可能是异步执行的(如果改变状态的代码处于某个HTML元素的事件中,则其是异步的,否则是同步)
  • React会对异步的setState进行优化,将多次setState进行合并(将多次状态改变完成后,再统一对state进行改变,然后触发render)

下面给出第一个问题的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Comp extends React.Component {
state = {
num: 0
}

handleClick = () => {
this.setState({
num: this.state.num + 1
})
console.log(this.state.num) // 第一下点击输出 0
}

render () {
return (
<React.Fragment>
<p>{this.state.num}</p>
<button onClick={this.handleClick}>点我</button>
</React.Fragment>
)
}
}

在第一下点击按钮的时候,console.log输出 0,说明setState在DOM事件中是异步执行的。

解决方式:setState有第二个参数,来放置render执行后接着执行的回调函数

6. Props

6.1 传递参数

父组件在使用子组件的时候,通过给子组件元素设置属性的方式传递参数给子组件

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
// 父组件设置属性给子组件传参,这里属性名用value2用作区分
class Board extends React.Component {
render () {
return (
<Square value2={0}/>
);
}
}

// 类组件 通过 this.props.[属性] 来获取参数
class Square extends React.Component {
render() {
return (
<button className="square">
{this.props.value2}
</button>
);
}
}

// 函数式组件通过 props.[属性] 来获取参数
function Square (props) {
return (
<button className="square">
{props.value2}
</button>
)
}

6.2 props传递事件

通过this.props.[事件名]调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Board extends React.Component {
render () {
return (
<Square click={() => {alert("click event")}}/>
);
}
}

class Square extends React.Component {
render() {
return (
<button onClick={this.props.click} className="square">
button
</button>
);
}
}

6.3 props校验

1.需要引入一个库prop-types.js

  • cdn
1
<script src="https://unpkg.com/prop-types@15.6/prop-types.js"></script>
  • npm
1
npm install --save prop-types 

2.使用方式

在组件上定义 PropTypes对象,键就是props的名,值就是限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Square extends React.Component {
render() {
// ... do things with the props
}

// 方式一
static propTypes = {
value1: PropTypes.number.isRequired,
value2: PropTypes.string
}

// 指定props默认值
static defaultProps = {
value1: 666,
value2: "6",
}
}

// 方式二
Square.propTypes = {
value1: PropTypes.number.isRequired,
value2: PropTypes.string
}

更多使用方式参考:https://github.com/facebook/prop-types

6.4 this.props.children

获取组件所有子节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class NotesList extends React.Component {
render() {
return (
<ol>
{
React.Children.map(this.props.children, function (child) {
return <li>{child}</li>;
})
}
</ol>
);
}
}

ReactDOM.render(
<NotesList>
<span>hello</span>
<span>world</span>
</NotesList>,
document.getElementById('example')
);
  • 没有子节点:undefined
  • 一个子节点:object
  • 多个子节点:array

可以用React.Children.map来遍历子节点,则不需要担心子元素个数

6.5 props传递组件

React组件本质就是对象,当作props像其他数据一样传递也是可以的

7. React生命周期

感觉和vue差不多,直接简单带过把

componentWillMount()

组件即将被渲染到页面之前触发,此时可以进行开启定时器、向服务器发送请求等操作

componentDidMount()

组件已经被渲染到页面中后触发,可以通过this.getDOMNode()来进行访问DOM。

componentWillReceiveProps()

在组件接收到一个新的 prop (更新后)时被调用。这个方法在初始化render时不会被调用。

shouldComponentUpdate()

返回一个布尔值。在组件接收到新的props或者state时被调用。在初始化时或者使用forceUpdate时不被调用。

1
2
3
4
5
// 该钩子函数可以接收到两个参数,新的属性和状态,返回true/false来控制组件是否需要更新。
shouldComponentUpdate(newProps, newState) {
if (newProps.number < 5) return true;
return false
}

一个React项目需要更新一个小组件时,很可能需要父组件更新自己的状态。而一个父组件的重新更新会造成它旗下所有的子组件重新执行render()方法(即使没有使用父组件的state),形成新的虚拟DOM,再用diff算法对新旧虚拟DOM进行结构和属性的比较,决定组件是否需要重新渲染,还可以使用下面说了PureComponent

componentWillUpdate()

在组件接收到新的props或者state但还没有render时被调用。在初始化时不会被调用。

componentDidUpdate()

在组件完成更新后立即调用。在初始化时不会被调用。

componentWillUnMount()

组件被销毁时触发。这里我们可以进行一些清理操作,例如清理定时器,取消Redux的订阅事件等等。

getDerivedStateFromError()

这个生命周期方法在ErrorBoundary类中使用。实际上,如果使用这个生命周期方法,任何类都会变成ErrorBoundary。这用于在组件树中出现错误时呈现回退UI,而不是在屏幕上显示一些奇怪的错误。

componentDidCatch()

这个生命周期方法在ErrorBoundary类中使用。实际上,如果使用这个生命周期方法,任何类都会变成ErrorBoundary。这用于在组件树中出现错误时记录错误。

8. Ref

用来访问DOM元素或render中的react元素,Ref的使用规则如下

  1. ref作用于内置的html组件时,得到的将是真实的dom对象
  2. ref作用于类组件时,得到的将是类的实例
  3. ref不能作用于函数组件(因为没有实例),但是函数组件内部可以

8.1 创建ref

ref的可选值为

  1. 字符串(不建议)

    原因:react ref注意事项

  2. 回调函数

    触发时机:ref中的回调函数会在对应的普通组件(或元素)componentDidMountComponentDidUpdate之前,或者componentWillUnmount之后执行

    注意:如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的

  3. createRef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
focusTextInput() {
// 注意:我们通过 "current" 来访问 DOM 节点
this.textInput.current.focus();
}
render() {
return (
<React.Fragment>
<input type="text" ref={this.textInput} />
<input type="button" value="Focus the text input" onClick={this.focusTextInput.bind(this)}/>
</React.Fragment>
);
}
}

8.2 转发ref

用于获取组件内部的某个元素,有些高度复用的基础组件不可避免的需要在父组件获取,用以管理焦点等

React.forwardRef用以获取传递给它的ref,然后转发到渲染它的DOM。

对于函数式组件React.forwardRef直接包裹函数就可以接收到ref

1
2
3
4
5
6
7
8
9
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));

// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

使用 FancyButton 的组件可以获取底层 DOM 节点 button 的 ref

而对于类组件,需要使用HOC的形式,用React.forwardRef包裹返回的函数式组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function logProps(Component) {
class LogProps extends React.Component {
render() {
const {forwardedRef, ...rest} = this.props;

// 将自定义的 prop 属性 “forwardedRef” 定义为 ref
return <input ref={forwardedRef} {...rest} />;
}
}

// 注意 React.forwardRef 回调的第二个参数 “ref”。
// 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
// 然后它就可以被挂载到被 LogProps 包裹的子组件上。
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}

React.forwardRef包裹的组件在DevTools中显示为”ForwardRef”,可以把包裹的函数用普通函数的形式命名,DevTools也将包含其名称(例如 “ForwardRef(myFunction)”),也可以设置函数的displayName属性来设置DevTools中显示的名字

9.Context

跨组件的通信方式,等同Vue的provide、inject

使用

1.创建Context容器对象

1
2
// 只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效
const xxxContext = React.createContext(defaultValue)

2.渲染子组时,外面包裹xxxContext.Provider,通过value属性给后代组件传递数据

1
2
3
<xxxContext.Provider value={数据}>
<子组件></子组件>
</xxxContext.Provider>

ps:如果要传多个数据,需要套多层

3.后代组件获取数据

1
2
3
4
5
6
7
8
// 第一种:仅适用于类组件
static contextType = xxxContext // 声明接受context
this.context; // 读取context中的value数据

// 第二种:函数组件与类组件都可以
<xxxContext.Consumer>
{value}
</xxxContext.Consumer>

不建议使用该api,他提高了组件复用的难度

10.PureComponent

父组件setState会触发子组件render,PureComponent通过prop和state的浅比较来实现shouldComponentUpdate,算是一种语法糖,帮我们应该在shouldComponentUpdate中应该手动比较的给做了

如果是浅层state或prop没改变,那么不会触发视图更新,书写方式如下

1
2
3
class IndexPage extends PureComponent {
// ...
}

由于是浅比较,所以直接修改对象内部的值是无法更新视图的

在函数式组件则是用React.memo包裹函数式组件来做到PureComponent的效果

参考

React学习资源汇总
React 入门实例教程
react学习笔记2-react基本使用
react学习笔记3-react其他使用技巧
图解ES6中的React生命周期
你要的 React 面试知识点,都在这了

作者

Liang

发布于

2021-05-27

更新于

2021-12-24

许可协议


评论