React 基础
课程目标
- 入门 React,了解常规用法;
- 掌握面试中 React 的基础问题;
- 掌握 React 学习路线;
知识要点
React 简介
React 是一个声明式,高效且灵活的用于构建用户界面的 JavaScript 库,使用 React 可以将一些简短、独立的代码片段组合成复杂的 UI 界面,这些代码片段被称为“组件”
UI = render(data) -> 单向数据流
- MVC
1 | // model |
- MVVM
JSX 模版语法
JSX 称为 JS 的语法扩展,将 UI 与逻辑层耦合在组件⾥,⽤ {} 标识。
因为 JSX 语法上更接近 JS ⽽不是 HTML,所以使⽤ camelCase(⼩驼峰命名)来定义属性的名称;JSX ⾥的 class 变成了 className,⽽ tabindex 则变为 tabIndex。
JSX 支持表达式
支持 JS 表达式、变量、方法名
1 | // 变量 |
JSX 指定属性
1 | const element = <img src={user.avatarUrl}></img> |
JSX 表示对象
1 | const element = ( |
将 JSX 渲染为 DOM
1 | // 使用 ReactDOM.render |
JSX 转 JS
JSX 可以当做语法糖,可以在 babel 官⽹中尝试,https://babeljs.io/repl 可以使⽤官⽹提供的 create-react-app npm run eject 来看 babelrc 中的配置,主要使⽤https://www.babeljs.cn/docs/babel-preset-react
1 | 安装 babel 及 react 的依赖 |
.babelrc
1 | { |
组件 、 Props & state
组件,从概念上类似于 JavaScript 函数。它接受任意的⼊参(即 “props”),并返回⽤于描述⻚⾯展示内容的 React 元素。
组件
- 函数式组件
- Class 类组件
1 | function Welcome (props) { |
渲染组件
1 | function Welcome (props) { |
props
所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
state
1 | // 使用 props 形式 |
- setState
构造函数是唯一可以给 this.state 赋值的地方1
this.setState({comment: 'Hello'})
- setState 更新可能是异步的
出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。
因为 this.props 和 this.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。1
2
3
4
5
6
7
8// Wrong 此代码可能会无法更新计数器
this.setState({
counter: this.state.counter + this.props.increment,
});
// Correct 这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:
this.setState((state, props) => ({
counter: state.counter + props.increment
})); - State 的更新会被合并
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
componentDidMount() {
fetchPosts().then(response => {
// 相当于 {posts: response.posts, ...otherState}
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
} - 数据是向下流动的
state 只在当前的组件里生效,属于组件内的属性,重复实例化相同的组件,内部的内存地址也是不一样的;
例如 Clock 中计时器都是独立的
生命周期
render
是class组件必需的⽅法
获取最新的 props 和 state
在不修改组件 state 的情况下,每次调⽤时都返回相同的结果
constructor
如果不初始化 state 或不进⾏⽅法绑定,则不需要为 React 组件实现构造函数。
- 通过给 this.state 赋值对象来初始化内部 state。
- 为事件处理函数绑定实例
1 | constructor(props) { |
componentDidMount
会在组件挂载后(插⼊ DOM 树中)⽴即调⽤
依赖于 DOM 节点的初始化应该放在这⾥,如需通过⽹络请求获取数据;
可以在此⽣命周期⾥加 setState,但发⽣在浏览器更新屏幕之前,会导致性能问题;
有更新在render阶段的 constructor 中 init State,但有更新可以在此⽅法时 setState
componentDidUpdate
1 | componentDidUpdate(prevProps, prevState, snapshot) |
会在更新后会被立即调用。首次渲染不会执行此方法。
当组件更新后,可以在此处对 DOM 进行操作。如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,当 props 未发生变化时,则不会执行网络请求)。
1 | componentDidUpdate(prevProps) { |
如果组件实现了 getSnapshotBeforeUpdate() 生命周期(不常用),则它的返回值将作为 componentDidUpdate() 的第三个参数 “snapshot” 参数传递。否则此参数将为 undefined。
如果 shouldComponentUpdate() 返回值为 false,则不会调用 componentDidUpdate()。
componentWillUnmount
componentWillUnmount() 会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等。
componentWillUnmount() 中不应调用 setState(),因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。
shouldComponentUpdate
(不常用)
1 | shouldComponentUpdate(nextProps, nextState) |
根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。默认行为是 state 每次发生变化组件都会重新渲染。
作为性能优化使⽤,返回false可以跳过re-render
shouldComponentUpdate() 返回 false,不会调⽤ UNSAFE_componentWillUpdate(),render() 和 componentDidUpdate()。
getDerivedStateFromProps
(不常用)
是为了取代 componentWillReceiveProps 和 componentWillUpdate 设置的根据 props 的变化改变 state,它应返回⼀个对象来更新 state,如果返回 null 则不更新任何内容。
- 在使⽤此⽣命周期时,要注意把传⼊的 prop 值和之前传⼊的 prop 进⾏⽐较;
- 因为这个⽣命周期是静态⽅法,同时要保持它是纯函数,不要产⽣副作⽤;
getSnapshotBeforeUpdate
(不常用)
1 | getSnapshotBeforeUpdate(prevProps, prevState) |
getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate()。
1 | class ScrollingList extends React.Component { |
getDerivedStateFromError
(不常用)
配合Error boundaries使⽤
此⽣命周期会在后代组件抛出错误后被调⽤。 它将抛出的错误作为参数,并返回⼀个值以更新 state;
componentDidCatch
(不常用)
componentDidCatch() 会在“提交”阶段被调⽤,因此允许执⾏副作⽤。 它应该⽤于记录错误之类的情况;
1 | class ErrorBoundary extends React.Component { |
UNSAFE_componentWillMount
(不建议使⽤)
UNSAFE_componentWillMount() 在挂载之前被调⽤;
它在 render() 之前调⽤,因此在此⽅法中同步调⽤ setState() 不会⽣效;
需要的话⽤componentDidMount替代。
UNSAFE_componentWillReceiveProps
(不建议使⽤)
UNSAFE_componentWillReceiveProps() 会在已挂载的组件接收新的 props 之前被调⽤;
如果你需要更新状态以响应 prop 更改(例如,重置它),你可以⽐较 this.props 和 nextProps 并在此⽅法中使⽤ this.setState() 执⾏ state 转换。
UNSAFE_componentWillUpdate
(不建议使⽤)
- 当组件收到新的 props 或 state 时,会在渲染之前调⽤ UNSAFE_componentWillUpdate();
- 使⽤此作为在更新发⽣之前执⾏准备更新的机会;
- 初始渲染不会调⽤此⽅法;
如果 shouldComponentUpdate() 返回 false,则不会调⽤ UNSAFE_componentWillUpdate();
事件处理
语法格式
- 在JSX元素上添加事件,通过on*EventType这种内联⽅式添加,命名采⽤⼩驼峰式(camelCase)的形式,⽽不是纯⼩写(原⽣HTML中对DOM元素绑定事件,事件类型是⼩写的);
- ⽆需调⽤addEventListener进⾏事件监听,也⽆需考虑兼容性,React已经封装好了⼀些的事件类型属性;
- 使⽤ JSX 语法时你需要传⼊⼀个函数作为事件处理函数,⽽不是⼀个字符串;
- 不能通过返回 false 的⽅式阻⽌默认⾏为。你必须显式的使⽤ preventDefault;
1 | function ActionLink() { |
1 | class Toggle extends React.Component { |
为什么要绑定 this
1 | function createElement (dom, params) { |
绑定 this 的方法:
- 不显示使用 bind
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class LoggingButton extends React.Component {
// 此语法确保 `handleClick` 内的 `this` 已被绑定。
// 注意: 这是 *实验性* 语法。
handleClick = () => {
console.log('this is:', this);
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
}
} - 箭头函数,问题:每次 render 都会创建不同的回调函数,如果该回调函数作为 props 传入子组件,每次子组件都要 re-render
1
2
3
4
5
6
7
8
9
10
11
12
13
14class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
// 此语法确保 `handleClick` 内的 `this` 已被绑定。
return (
<button onClick={() => this.handleClick()}>
Click me
</button>
);
}
}
接收参数
事件对象 e 会被作为第⼆个参数传递;
通过箭头函数的⽅式,事件对象必须显式的进⾏传递;
通过 Function.prototype.bind 的⽅式,事件对象以及更多的参数将会被隐式的进⾏传递;
1 | <button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button> |
条件渲染
if else 渲染
1 | class LoginControl extends React.Component { |
与运算符 &&
1 | function Mailbox(props) { |
返回 false 的表达式,会跳过元素,但会返回该表达式
1 | render() { |
三元运算符
1 | render() { |
如何阻止组件渲染
1 | function WarningBanner(props) { |
列表
1 | function NumberList(props) { |
若没有 key,会 warning a key should be provided for list items;
key 可以帮助 react diff,最好不要用 index 作为 key,会导致性能变差;
如果不指定显式的 key 值,那么 React 将默认使用索引用作为列表项目的 key 值。
key 注意点
key 要保留在 map 的遍历元素上
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
28function ListItem(props) {
const value = props.value;
return (
// 错误!你不需要在这里指定 key:
<li key={value.toString()}>
{value}
</li>
);
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 正确!key 应该在数组的上下文中被指定
<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 只是在兄弟节点之间必须唯一
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
33function Blog(props) {
const sidebar = (
<ul>
{props.posts.map((post) =>
<li key={post.id}>
{post.title}
</li>
)}
</ul>
);
const content = props.posts.map((post) =>
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
);
return (
<div>
{sidebar}
<hr />
{content}
</div>
);
}
const posts = [
{id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
{id: 2, title: 'Installation', content: 'You can install React from npm.'}
];
ReactDOM.render(
<Blog posts={posts} />,
document.getElementById('root')
);在 JSX 中嵌入 map()
1
2
3
4
5
6
7
8
9
10function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{numbers.map((number) =>
<ListItem key={number.toString()} value={number} />
)}
</ul>
);
}
补充知识点
React 如何预防 XSS
1 | const title = response.potentialyMaliciousInput // 此时只是字符串 |
React DOM 在渲染所有输入内容之前,默认会进行转义。
部分源码:
1 | for (index = match.index; index < str.length; index++) { |
关于 setState 的异步
异步目的:batch 处理、性能优化
- 合成事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class App extends Component {
state = {val: 0}
increment = () => {
this.setState({val: this.state.val + 1})
console.log(this.state.val) // 0
}
render() {
return (
<div onClick={this.increment}>
{`Counter is: ${this.state.val}`}
</div>
)
}
} - 生命周期
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class App extends Component {
state = {val: 0}
componentDidiMount() {
this.setState({val: this.state.val + 1})
console.log(this.state.val) // 0
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
} - 原生事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class App extends Component {
state = {val: 0}
changeValue = () => {
this.setState({val: this.state.val + 1})
console.log(this.state.val) // 1
}
componentDidiMount() {
document.body.addEventListener('click', this.changeValue, false)
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
} - setTimeout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class App extends Component {
state = {val: 0}
componentDidiMount() {
setTimeout(_ => {
this.setState({val: this.state.val + 1})
console.log(this.state.val) // 1
}, 0)
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
} - 批处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class App extends Component {
state = {val: 0}
batchUpdates = () => {
this.setState({val: this.state.val + 1})
this.setState({val: this.state.val + 1})
this.setState({val: this.state.val + 1})
}
render() {
return (
<div onClick={this.batchUpdates}>
{`Counter is: ${this.state.val}`} // 1
</div>
)
}
}- setState 只在合成事件和⽣命周期中是“异步”的,在原⽣事件和 setTimeout 中都是同步的;
- setState的“异步”并不是说内部由异步代码实现,其实本身执⾏的过程和代码都是同步的, 只是合成事件和钩⼦函数的调⽤顺序在更新之前,导致在合成事件和钩⼦函数中没法⽴⻢拿到更新后的值,形式了所谓的“异步”, 当然可以通过第⼆个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
- setState 的批量更新优化也是建⽴在“异步”(合成事件、钩⼦函数)之上的,在原⽣事件和 setTimeout 中不会批量更新,在“异步”中如果对同⼀个值进⾏多次 setState , setState 的批量更新策略会对其进⾏覆盖,取Y后⼀次的执⾏,如果是同时 setState 多个不同的值,在更新时会对其进⾏合并批量更新。
create-react-class
自动绑定 this
1 | var createReactClass = require('create-react-class'); |