React

React 设计思想

译者序:本文是 React 核心开发者、有 React API 终结者之称的 Sebastian Markbåge 撰写,阐述了他设计 React 的初衷。阅读此文,你能站在更高的高度思考 React 的过去、现在和未来。原文地址:https://github.com/reactjs/react-basic

我写此文是想正式地阐述我心中 React 的心智模型。目的是解释为什么我们会这样设计 React,同时你也可以根据这些论点反推出 React。

不可否认,此文中的部分论据或前提尚存争议,而且部分示例的设计可能存在 bug 或疏忽。这只是正式确定它的最初阶段。如果你有更好的完善它的想法可以随时提交 pull request。本文不会介绍框架细节中的奇技淫巧,相信这样能提纲挈领,让你看清 React 由简单到复杂的设计过程。

React.js 的真实实现中充满了具体问题的解决方案,渐进式的解法,算法优化,历史遗留代码,debug 工具以及其他一些可以让它真的具有高可用性的内容。这些代码可能并不稳定,因为未来浏览器的变化和功能权重的变化随时面临改变。所以具体的代码很难彻底解释清楚。

我偏向于选择一种我能完全 hold 住的简洁的心智模型来作介绍。

1. 变换(Transformation)

设计 React 的核心前提是认为 UI 只是把数据通过映射关系变换成另一种形式的数据。同样的输入必会有同样的输出。这恰好就是纯函数。

1
2
3
4
5
function NameBox(name) {
return { fontWeight: 'bold', labelContent: name };
}
'Sebastian Markbåge' ->
{ fontWeight: 'bold', labelContent: 'Sebastian Markbåge' };

2. 抽象(Abstraction)

你不可能仅用一个函数就能实现复杂的 UI。重要的是,你需要把 UI 抽象成多个隐藏内部细节,又可复用的函数。通过在一个函数中调用另一个函数来实现复杂的 UI,这就是抽象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function FancyUserBox(user) {
return {
borderStyle: '1px solid blue',
childContent: [
'Name: ',
NameBox(user.firstName + ' ' + user.lastName)
]
};
}
{ firstName: 'Sebastian', lastName: 'Markbåge' } ->
{
borderStyle: '1px solid blue',
childContent: [
'Name: ',
{ fontWeight: 'bold', labelContent: 'Sebastian Markbåge' }
]
};

3. 组合(Composition)

为了真正达到重用的特性,只重用叶子然后每次都为他们创建一个新的容器是不够的。你还需要可以包含其他抽象的容器再次进行组合。我理解的“组合”就是将两个或者多个不同的抽象合并为一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
function FancyBox(children) {
return {
borderStyle: '1px solid blue',
children: children
};
}

function UserBox(user) {
return FancyBox([
'Name: ',
NameBox(user.firstName + ' ' + user.lastName)
]);
}

4. 状态(State)

UI 不单单是对服务器端或业务逻辑状态的复制。实际上还有很多状态是针对具体的渲染目标。举个例子,在一个 text field 中打字。它不一定要复制到其他页面或者你的手机设备。滚动位置这个状态是一个典型的你几乎不会复制到多个渲染目标的。

我们倾向于使用不可变的数据模型。我们把可以改变 state 的函数串联起来作为原点放置在顶层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function FancyNameBox(user, likes, onClick) {
return FancyBox([
'Name: ', NameBox(user.firstName + ' ' + user.lastName),
'Likes: ', LikeBox(likes),
LikeButton(onClick)
]);
}

// 实现细节

var likes = 0;
function addOneMoreLike() {
likes++;
rerender();
}

// 初始化

FancyNameBox(
{ firstName: 'Sebastian', lastName: 'Markbåge' },
likes,
addOneMoreLike
);

注意:本例更新状态时会带来副作用(addOneMoreLike 函数中)。我实际的想法是当一个“update”传入时我们返回下一个版本的状态,但那样会比较复杂。此示例待更新

5. Memoization

对于纯函数,使用相同的参数一次次调用未免太浪费资源。我们可以创建一个函数的 memorized 版本,用来追踪最后一个参数和结果。这样如果我们继续使用同样的值,就不需要反复执行它了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function memoize(fn) {
var cachedArg;
var cachedResult;
return function(arg) {
if (cachedArg === arg) {
return cachedResult;
}
cachedArg = arg;
cachedResult = fn(arg);
return cachedResult;
};
}

var MemoizedNameBox = memoize(NameBox);

function NameAndAgeBox(user, currentTime) {
return FancyBox([
'Name: ',
MemoizedNameBox(user.firstName + ' ' + user.lastName),
'Age in milliseconds: ',
currentTime - user.dateOfBirth
]);
}

6. 列表(Lists)

大部分 UI 都是展示列表数据中不同 item 的列表结构。这是一个天然的层级。

为了管理列表中的每一个 item 的 state ,我们可以创造一个 Map 容纳具体 item 的 state。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function UserList(users, likesPerUser, updateUserLikes) {
return users.map(user => FancyNameBox(
user,
likesPerUser.get(user.id),
() => updateUserLikes(user.id, likesPerUser.get(user.id) + 1)
));
}

var likesPerUser = new Map();
function updateUserLikes(id, likeCount) {
likesPerUser.set(id, likeCount);
rerender();
}

UserList(data.users, likesPerUser, updateUserLikes);

注意:现在我们向 FancyNameBox 传了多个不同的参数。这打破了我们的 memoization 因为我们每次只能存储一个值。更多相关内容在下面。

7. 连续性(Continuations)

不幸的是,自从 UI 中有太多的列表,明确的管理就需要大量的重复性样板代码。

我们可以通过推迟一些函数的执行,进而把一些模板移出业务逻辑。比如,使用“柯里化”(JavaScript 中的 bind)。然后我们可以从核心的函数外面传递 state,这样就没有样板代码了。

下面这样并没有减少样板代码,但至少把它从关键业务逻辑中剥离。

1
2
3
4
5
6
7
8
9
10
11
12
function FancyUserList(users) {
return FancyBox(
UserList.bind(null, users)
);
}

const box = FancyUserList(data.users);
const resolvedChildren = box.children(likesPerUser, updateUserLikes);
const resolvedBox = {
...box,
children: resolvedChildren
};

8. State Map

之前我们知道可以使用组合避免重复执行相同的东西这样一种重复模式。我们可以把执行和传递 state 逻辑挪动到被复用很多的低层级的函数中去。

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
function FancyBoxWithState(
children,
stateMap,
updateState
) {
return FancyBox(
children.map(child => child.continuation(
stateMap.get(child.key),
updateState
))
);
}

function UserList(users) {
return users.map(user => {
continuation: FancyNameBox.bind(null, user),
key: user.id
});
}

function FancyUserList(users) {
return FancyBoxWithState.bind(null,
UserList(users)
);
}

const continuation = FancyUserList(data.users);
continuation(likesPerUser, updateUserLikes);

9. Memoization Map

一旦我们想在一个 memoization 列表中 memoize 多个 item 就会变得很困难。因为你需要制定复杂的缓存算法来平衡调用频率和内存占有率。

还好 UI 在同一个位置会相对的稳定。相同的位置一般每次都会接受相同的参数。这样以来,使用一个集合来做 memoization 是一个非常好用的策略。

我们可以用对待 state 同样的方式,在组合的函数中传递一个 memoization 缓存。

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
function memoize(fn) {
return function(arg, memoizationCache) {
if (memoizationCache.arg === arg) {
return memoizationCache.result;
}
const result = fn(arg);
memoizationCache.arg = arg;
memoizationCache.result = result;
return result;
};
}

function FancyBoxWithState(
children,
stateMap,
updateState,
memoizationCache
) {
return FancyBox(
children.map(child => child.continuation(
stateMap.get(child.key),
updateState,
memoizationCache.get(child.key)
))
);
}

const MemoizedFancyNameBox = memoize(FancyNameBox);

10. 代数效应(Algebraic Effects)

多层抽象需要共享琐碎数据时,一层层传递数据非常麻烦。如果能有一种方式可以在多层抽象中快捷地传递数据,同时又不需要牵涉到中间层级,那该有多好。React 中我们把它叫做“context”。

有时候数据依赖并不是严格按照抽象树自上而下进行。举个例子,在布局算法中,你需要在实现他们的位置之前了解子节点的大小。

现在,这个例子有一点超纲。我会使用 代数效应 这个由我发起的 ECMAScript 新特性提议。如果你对函数式编程很熟悉,它们 在避免由 monad 强制引入的仪式一样的编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function ThemeBorderColorRequest() { }

function FancyBox(children) {
const color = raise new ThemeBorderColorRequest();
return {
borderWidth: '1px',
borderColor: color,
children: children
};
}

function BlueTheme(children) {
return try {
children();
} catch effect ThemeBorderColorRequest -> [, continuation] {
continuation('blue');
}
}

function App(data) {
return BlueTheme(
FancyUserList.bind(null, data.users)
);
}

本节转载:https://github.com/react-guide/react-basic

英文版:https://github.com/reactjs/react-basic

组件的生命周期(v16.4版本)

react 16版之前的生命周期函数

先来了解一下 react 16版之前的生命周期

react16前的生命周期

react16版本前的直观版

  • 初始化阶段:
    • getDefaultProps:获取实例的默认属性
    • getInitialState:获取每个实例的初始化状态
    • componentWillMount:组件即将被装载、渲染到页面上
    • render:组件在这里生成虚拟的 DOM 节点
    • componentDidMount:组件真正在被装载之后
  • 运行中状态:
    • componentWillReceiveProps:组件将要接收到属性的时候调用
    • shouldComponentUpdate:组件接受到新属性或者新状态的时候(可以返回 false,接收数据后不更新,阻止 render 调用,后面的函数不会被继续执行了)
    • componentWillUpdate:组件即将更新不能修改属性和状态
    • render:组件重新描绘
    • componentDidUpdate:组件已经更新
  • 销毁阶段:
    • componentWillUnmount:组件即将销毁

getDefaultProps 和 getInitialState

getDefaultProps 函数的返回值可以作为 props 的初始值,这个函数只在 React.createClass 方法创造的组件类才会用到。

getInitialState 这个函数的返回值会用来初始化组件的 this.state,但是,这个方法只有用 React.createClass 方法创造的组件类才会发生作用。

假如我们用 React.createClass 定义一个组件 Sample,设定内部状态 foo 的初始值为字符串 bar,同时设定一个叫 sampleProp 的 prop 初始值为数字值 0,代码如下:

1
2
3
4
5
6
7
8
const Sample = React.createClass({
getInitialState: function() {
return { foo: 'bar' };
},
getDefaultProps: function() {
return { sapmpleProp: 0 };
}
})

实际上,getDefaultProps 和 getInitialState两个方法在 ES6 的方法定义的 React 组件根本不会用到。所以这里只作为了解。

其他16版之前的生命周期将会在下文的比较中讨论。

React16.4版之后的生命周期

react 16.4版之后的生命周期函数

挂载

当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:

  • constructor()
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()

注意:

下述生命周期方法即将过时,在新代码中应该避免使用它们

  • UNSAFE_componentWillMount()

更新

当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:

  • static getDerivedStateFromProps()
  • shouldComponentUpdate()
  • render()
  • getSnapshotBeforeUpdate()
  • componentDidUpdate()

注意:

下述方法即将过时,在新代码中应该避免使用它们

  • UNSAFE_componentWillUpdate()
  • UNSAFE_componentWillReceiveProps()

卸载

当组件从 DOM 中移除时会调用如下方法:

  • componentWillUnmount()

错误处理

当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:

  • static getDerivedStateFromError()
  • componentDidCatch()

其他 APIs

组件还提供了一些额外的 API:

  • setState()
  • forceUpdate()

class 属性

  • defaultProps
  • displayName

实例属性

  • props
  • state

constructor()

1
constructor(props)

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。

在 React 组件挂载之前,会调用它的构造函数。在为 React.Component 子类实现构造函数时,应在其他语句之前前调用 super(props)。否则,this.props 在构造函数中可能会出现未定义的 bug。

通常,在 React 中,构造函数仅用于以下两种情况:

constructor() 函数中不要调用 setState() 方法。如果你的组件需要使用内部 state,请直接在构造函数中为 this.state 赋值初始 state

1
2
3
4
5
6
7
constructor(props) {
super(props);
// 不要在这里调用 this.setState()
this.state = { counter: 0 };
// 为事件处理函数绑定实例
this.handleClick = this.handleClick.bind(this);
}

只能在构造函数中直接为 this.state 赋值。如需在其他方法中赋值,你应使用 this.setState() 替代。

要避免在构造函数中引入任何副作用或订阅。如遇到此场景,请将对应的操作放置在 componentDidMount 中。

注意

避免将 props 的值复制给 state!这是一个常见的错误:

1
2
3
4
5
6
> constructor(props) {
> super(props);
> // 不要这样做
> this.state = { color: props.color };
> }
>

如此做毫无必要(你可以直接使用 this.props.color),同时还产生了 bug(更新 prop 中的 color 时,并不会影响 state,因为挂载的生命周期函数只执行一次,只起到初始化的作用,没有更新的作用)。

**只有在你刻意忽略 prop 更新的情况下使用。**此时,应将 prop 重命名为 initialColordefaultColor。必要时,你可以修改它的 key,以强制“重置”其内部 state。

请参阅关于避免派生状态的博文,以了解出现 state 依赖 props 的情况该如何处理。

static getDerivedStateFromProps()

1
2
3
static getDerivedStateFromProps(nextProps, prevState)
// 官方文档的是 static getDerivedStateFromProps(props, state),此处的 pros 和 state 不太明显
// 所以改为 nextProps(更新后的 props)和 prevState(要更新前的 state)

getDerivedStateFromProps 从语义上理解是从更新后 props 中获取派生 state,它会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。

此方法适用于罕见的用例,即 state 的值在任何时候都取决于 props。例如,实现 <Transition> 组件可能很方便,该组件会比较当前组件与下一组件,以决定针对哪些组件进行转场动画。

派生状态会导致代码冗余,并使组件难以维护。 确保你已熟悉这些简单的替代方案:

此方法无权访问组件实例,即这个函数不能通过 this 访问到class的属性,也并不推荐直接访问属性。而是应该通过参数提供的 nextProps 以及 prevState 来进行判断,根据新传入的props来映射到state。如果你需要,可以通过提取组件 props 的纯函数及 class 之外的状态,在getDerivedStateFromProps()和其他 class 方法之间重用代码。

请注意,不管原因是什么,都会在每次渲染前触发此方法。这与 UNSAFE_componentWillReceiveProps 形成对比,后者是如果父组件导致组件重新渲染,即使 props 没有更改,也会调用此方法。如果只想处理更改,请确保进行当前值与变更值的比较。

需要注意的是,如果props传入的内容不需要影响到你的state,那么就需要返回一个null,这个返回值是必须的,所以尽量将其写到函数的末尾。

关于适不适合使用派生 state,可以参考 你可能不需要使用派生 state

render()

1
render()

render() 方法是 class 组件中唯一必须实现的方法。

render 被调用时,它会检查 this.propsthis.state 的变化并返回以下类型之一:

  • React 元素。通常通过 JSX 创建。例如,<div /> 会被 React 渲染为 DOM 节点,<MyComponent /> 会被 React 渲染为自定义组件,无论是 <div /> 还是 <MyComponent /> 均为 React 元素。
  • 数组或 fragments。 使得 render 方法可以返回多个元素。欲了解更多详细信息,请参阅 fragments 文档。
  • Portals。可以渲染子节点到不同的 DOM 子树中。欲了解更多详细信息,请参阅有关 portals 的文档
  • 字符串或数值类型。它们在 DOM 中会被渲染为文本节点
  • 布尔类型或 null。什么都不渲染。(主要用于支持返回 test && <Child /> 的模式,其中 test 为布尔类型。)

render() 函数应该为纯函数,这意味着在不修改组件 state 的情况下,每次调用时都返回相同的结果,并且它不会直接与浏览器交互。

如需与浏览器进行交互,请在 componentDidMount() 或其他生命周期方法中执行你的操作。保持 render() 为纯函数,可以使组件更容易思考。

注意

如果 shouldComponentUpdate() 返回 false,则不会调用 render()

componentDidMount()

1
componentDidMount()

componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。

这个方法是比较适合添加订阅的地方。如果添加了订阅,请不要忘记在 componentWillUnmount() 里取消订阅

你可以在 componentDidMount()可以直接调用 setState()它将触发额外渲染,但此渲染会发生在浏览器更新屏幕之前。如此保证了即使在 render() 两次调用的情况下,用户也不会看到中间状态。请谨慎使用该模式,因为它会导致性能问题。通常,你应该在 constructor() 中初始化 state。如果你的渲染依赖于 DOM 节点的大小或位置,比如实现 modals 和 tooltips 等情况下,你可以使用此方式处理

shouldComponentUpdate()

1
shouldComponentUpdate(nextProps, nextState)

根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。默认行为是 state 每次发生变化组件都会重新渲染。大部分情况下,你应该遵循默认行为。

当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。返回值默认为 true。首次渲染或使用 forceUpdate() 时不会调用该方法。

此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。你应该考虑使用内置的 PureComponent 组件,而不是手动编写 shouldComponentUpdate()PureComponent 会对 props 和 state 进行浅层比较,并减少了跳过必要更新的可能性。

如果你一定要手动编写此函数,可以将 this.propsnextProps 以及 this.statenextState 进行比较,并返回 false 以告知 React 可以跳过更新。请注意,返回 false 并不会阻止子组件在 state 更改时重新渲染。

我们不建议在 shouldComponentUpdate() 中进行深层比较或使用 JSON.stringify()。这样非常影响效率,且会损害性能。

目前,如果 shouldComponentUpdate() 返回 false,则不会调用 UNSAFE_componentWillUpdate()render()componentDidUpdate()后续版本,React 可能会将 shouldComponentUpdate 视为提示而不是严格的指令,并且,当返回 false 时,仍可能导致组件重新渲染。

getSnapshotBeforeUpdate()

1
getSnapshotBeforeUpdate(prevProps, prevState)

getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate()

此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。

应返回 snapshot 的值(或 null)。

例如:

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
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}

getSnapshotBeforeUpdate(prevProps, prevState) {
// 我们是否在 list 中添加新的 items ?
// 捕获滚动位置以便我们稍后调整滚动位置。
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}

componentDidUpdate(prevProps, prevState, snapshot) {
// 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
// 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
//(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}

render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}

在上述示例中,重点是从 getSnapshotBeforeUpdate 读取 scrollHeight 属性,因为 “render” 阶段生命周期(如 render)和 “commit” 阶段生命周期(如 getSnapshotBeforeUpdatecomponentDidUpdate)之间可能存在延迟。

componentDidUpdate()

1
componentDidUpdate(prevProps, prevState, snapshot)

componentDidUpdate() 会在更新后会被立即调用。首次渲染不会执行此方法。

当组件更新后,可以在此处对 DOM 进行操作。如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,当 props 未发生变化时,则不会执行网络请求)。

1
2
3
4
5
6
componentDidUpdate(prevProps) {
// 典型用法(不要忘记比较 props):
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}

你也可以在 componentDidUpdate()直接调用 setState(),但请注意它必须被包裹在一个条件语件里,正如上述的例子那样进行处理,否则会导致死循环。它还会导致额外的重新渲染,虽然用户不可见,但会影响组件性能。不要将 props 直接赋给 state(官方:不要将 props “镜像”给 state),请考虑直接使用 props。 欲了解更多有关内容,请参阅为什么 props 复制给 state 会产生 bug

如果组件实现了 getSnapshotBeforeUpdate() 生命周期(不常用),则它的返回值将作为 componentDidUpdate() 的第三个参数 “snapshot” 参数传递。否则此参数将为 undefined。

注意

如果 shouldComponentUpdate() 返回值为 false,则不会调用 componentDidUpdate()

componentWillUnmount()

1
componentWillUnmount()

componentWillUnmount() 会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer取消网络请求清除在 componentDidMount() 中创建的订阅等。

componentWillUnmount()不应调用 setState(),因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。

static getDerivedStateFromError()

1
static getDerivedStateFromError(error)

此生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染可以显示降级 UI
return { hasError: true };
}

render() {
if (this.state.hasError) {
// 你可以渲染任何自定义的降级 UI
return <h1>Something went wrong.</h1>;
}

return this.props.children;
}
}

注意

getDerivedStateFromError() 会在渲染阶段调用,因此不允许出现副作用。 如遇此类情况,请改用 componentDidCatch()

componentDidCatch()

1
componentDidCatch(error, info)

此生命周期在后代组件抛出错误后被调用。 它接收两个参数:

  1. error —— 抛出的错误。
  2. info —— 带有 componentStack key 的对象,其中包含有关组件引发错误的栈信息

componentDidCatch() 会在“提交”阶段被调用,因此允许执行副作用。 它应该用于记录错误之类的情况:

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
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染可以显示降级 UI
return { hasError: true };
}

componentDidCatch(error, info) {
// "组件堆栈" 例子:
// in ComponentThatThrows (created by App)
// in ErrorBoundary (created by App)
// in div (created by App)
// in App
logComponentStackToMyService(info.componentStack);
}

render() {
if (this.state.hasError) {
// 你可以渲染任何自定义的降级 UI
return <h1>Something went wrong.</h1>;
}

return this.props.children;
}
}

注意

如果发生错误,你可以通过调用 setState 使用 componentDidCatch() 渲染降级 UI,但在未来的版本中将不推荐这样做。 可以使用静态 getDerivedStateFromError() 来处理降级渲染。

过时的生命周期方法

以下生命周期方法标记为“过时”。这些方法仍然有效,但不建议在新代码中使用它们。参阅此博客文章以了解更多有关迁移旧版生命周期方法的信息。

UNSAFE_componentWillMount()
1
UNSAFE_componentWillMount()

注意

此生命周期之前名为 componentWillMount。该名称将继续使用至 React 17。可以使用 rename-unsafe-lifecycles codemod 自动更新你的组件。

UNSAFE_componentWillMount() 在挂载之前被调用。它在 render() 之前调用,因此在此方法中同步调用 setState() 不会触发额外渲染。通常,我们建议使用 constructor() 来初始化 state。

避免在此方法中引入任何副作用或订阅。如遇此种情况,请改用 componentDidMount()

此方法是服务端渲染唯一会调用的生命周期函数

UNSAFE_componentWillReceiveProps()
1
UNSAFE_componentWillReceiveProps(nextProps)

注意

此生命周期之前名为 componentWillReceiveProps。该名称将继续使用至 React 17。可以使用 rename-unsafe-lifecycles codemod 自动更新你的组件。

注意:

使用此生命周期方法通常会出现 bug 和不一致性:

对于其他使用场景,请遵循此博客文章中有关派生状态的建议

UNSAFE_componentWillReceiveProps() 会在已挂载的组件接收新的 props 之前被调用。如果你需要更新状态以响应 prop 更改(例如,重置它),你可以比较 this.propsnextProps 并在此方法中使用 this.setState() 执行 state 转换。

请注意,**如果父组件导致组件重新渲染,即使 props 没有更改,也会调用此方法。**如果只想处理更改,请确保进行当前值与变更值的比较。

挂载过程中,React 不会针对初始 props 调用 UNSAFE_componentWillReceiveProps()组件只会在组件的 props 更新时调用此方法。调用 this.setState() 通常不会触发 UNSAFE_componentWillReceiveProps()

UNSAFE_componentWillUpdate()
1
UNSAFE_componentWillUpdate(nextProps, nextState)

注意

此生命周期之前名为 componentWillUpdate。该名称将继续使用至 React 17。可以使用 rename-unsafe-lifecycles codemod 自动更新你的组件。

当组件收到新的 props 或 state 时,会在渲染之前调用 UNSAFE_componentWillUpdate()。使用此作为在更新发生之前执行准备更新的机会。初始渲染不会调用此方法。

注意,你不能此方法中调用 this.setState();在 UNSAFE_componentWillUpdate() 返回之前,你也不应该执行任何其他操作(例如,dispatch Redux 的 action)触发对 React 组件的更新

通常,此方法可以替换为 componentDidUpdate()。如果你在此方法中读取 DOM 信息(例如,为了保存滚动位置),则可以将此逻辑移至 getSnapshotBeforeUpdate() 中。

注意

如果 shouldComponentUpdate() 返回 false,则不会调用 UNSAFE_componentWillUpdate()

参考

React.Component

setState

setState()更新状态的2种写法

​ 1). setState(updater, [callback]),

​ updater为返回stateChange对象的函数: (state, props) => stateChange

​ 接收的state和props被保证为最新的

​ 2). setState(stateChange, [callback])

​ stateChange为对象,

​ callback是可选的回调函数, 在状态更新且界面更新后才执行

​ 3). 总结:

​ 对象方式是函数方式的简写方式

​ 如果新状态不依赖于原状态 ===> 使用对象方式

​ 如果新状态依赖于原状态 ===> 使用函数方式

​ 如果需要在setState()后获取最新的状态数据, 在第二个callback函数中读取

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
49
50
51
<div id="example"></div>

<script type="text/javascript" src="./js/react.development.js"></script>
<script type="text/javascript" src="./js/react-dom.development.js"></script>
<script type="text/javascript" src="./js/babel.min.js"></script>

<script type="text/babel">

class A extends React.Component {

state = {
count: 1
}

test1 = () => {
this.setState(state => ({count: state.count + 1}))
console.log('test1 setState()之后', this.state.count)
}

test2 = () => {
/*const count = this.state.count + 1
this.setState({
count
})*/
this.setState({
count: 3
})
console.log('test2 setState()之后', this.state.count)
}

test3 = () => {
this.setState(state => ({count: state.count + 1}), () => { // 在状态更新且界面更新之后回调
console.log('test3 setState callback()', this.state.count)
})
}

render() {
console.log('A render()')
return (
<div>
<h1>A组件: {this.state.count}</h1>
<button onClick={this.test1}>A 测试1</button>&nbsp;&nbsp;
<button onClick={this.test2}>A 测试2</button>&nbsp;&nbsp;
<button onClick={this.test3}>A 测试3</button>&nbsp;&nbsp;
</div>
)
}
}

ReactDOM.render(<A/>, document.getElementById('example'))
</script>

setState()更新状态是异步还是同步的?
1). 执行setState()的位置?
在react控制的回调函数中: 生命周期勾子 / react事件监听回调
非react控制的异步回调函数中: 定时器回调 / 原生事件监听回调 / promise回调 /…
2). 异步 OR 同步?
react相关回调中: 表现为“异步”
其它异步回调中: 同步

关于异步的setState()
1). 多次调用, 如何处理?
setState({}): 合并更新一次状态, 只调用一次render()更新界面 —状态更新和界面更新都合并了
setState(fn): 更新多次状态, 但只调用一次render()更新界面 —状态更新没有合并, 但界面更新合并了
2). 如何得到异步更新后的状态数据?
在setState()的callback回调函数中

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<div id="example"></div>

<script type="text/javascript" src="./js/react.development.js"></script>
<script type="text/javascript" src="./js/react-dom.development.js"></script>
<script type="text/javascript" src="./js/babel.min.js"></script>

<script type="text/babel">
class StateTest extends React.Component {

state = {
count: 0,
}

/*
react事件监听回调中, setState()是异步更新状态
*/
update1 = () => {
console.log('update1 setState()之前', this.state.count)
this.setState(state => ({count: state.count + 1}))
console.log('update1 setState()之后', this.state.count)
}

/*
react生命周期勾子中, setState()是异步更新状态
*/
componentDidMount () {
console.log('componentDidMount setState()之前', this.state.count)
this.setState(state => ({count: state.count + 1}))
console.log('componentDidMount setState()之后', this.state.count)
}

/*
定时器回调 / 原生事件监听回调 / promise回调 /...
*/
update2 = () => {
setTimeout(() => {
console.log('setTimeout setState()之前', this.state.count)
this.setState(state => ({count: state.count + 1}))
console.log('setTimeout setState()之后', this.state.count)
})
}
update3 = () => {
const h2 = this.refs.count
h2.onclick = () => {
console.log('onclick setState()之前', this.state.count)
this.setState(state => ({count: state.count + 1}))
console.log('onclick setState()之后', this.state.count)
}
}
update4 = () => {
Promise.resolve().then(value => {
console.log('Promise setState()之前', this.state.count)
this.setState(state => ({count: state.count + 1}))
console.log('Promise setState()之后', this.state.count)
})
}


update5 = () => {
console.log('onclick setState()之前', this.state.count)
this.setState(state => ({count: state.count + 1}))
console.log('onclick setState()之后', this.state.count)
console.log('onclick setState()之前2', this.state.count)
this.setState(state => ({count: state.count + 1}))
console.log('onclick setState()之后2', this.state.count)
}

update6 = () => {
console.log('onclick setState()之前', this.state.count)
this.setState({count: this.state.count + 1})
console.log('onclick setState()之后', this.state.count)
console.log('onclick setState()之前2', this.state.count)
this.setState({count: this.state.count + 1})
console.log('onclick setState()之后2', this.state.count)
}

update7 = () => {
console.log('onclick setState()之前', this.state.count)
this.setState({count: this.state.count + 1})
console.log('onclick setState()之后', this.state.count)

console.log('onclick setState()之前2', this.state.count)
this.setState(state => ({count: state.count + 1}))
console.log('onclick setState()之后2', this.state.count)
}


render() {
const {count} = this.state
console.log('render()', count)
return (
<div>
<h2 ref='count'>{count}</h2>
<button onClick={this.update1}>更新1</button> ---
<button onClick={this.update2}>更新2</button> &nbsp;
<button onClick={this.update3}>更新3</button> &nbsp;
<button onClick={this.update4}>更新4</button> ---
<button onClick={this.update5}>更新5</button> &nbsp;
<button onClick={this.update6}>更新6</button> &nbsp;
<button onClick={this.update7}>更新7</button> &nbsp;
</div>
)
}
}

ReactDOM.render(<StateTest/>, document.getElementById('example')) // 渲染组件标签, 内部会调用组件标签对象的render()虚拟DOM

</script>

setState()更新状态是异步还是同步的?
1). 执行setState()的位置?
在react控制的回调函数中: 生命周期勾子 / react事件监听回调
非react控制的异步回调函数中: 定时器回调 / 原生事件监听回调 / promise回调 /…
2). 异步 OR 同步?
react相关回调中: 异步
其它异步回调中: 同步

关于异步的setState()
1). 多次调用, 如何处理?
setState(fn): 更新多次状态, 但只调用一次render()更新界面 —状态更新没有合并, 但界面更新合并了
setState({}): 合并更新一次状态, 只调用一次render()更新界面 —状态更新和界面更新都合并了
2). 如何得到异步更新后的状态数据?
在setState()的callback回调函数中

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<script type="text/babel">

class StateTest extends React.Component {

state = {
count: 0,
}

/*
react事件监听回调中, setState()是异步更新状态
*/
update1 = () => {
console.log('update1 setState()之前', this.state.count)
this.setState(state => ({count: state.count + 1}))
console.log('update1 setState()之后', this.state.count)
}

/*
react生命周期勾子中, setState()是异步更新状态
*/
componentDidMount () {
console.log('componentDidMount setState()之前', this.state.count)
this.setState(state => ({count: state.count + 1}))
console.log('componentDidMount setState()之后', this.state.count)
}

/*
定时器回调 / 原生事件监听回调 / promise回调 /...
*/
update2 = () => {
setTimeout(() => {
console.log('setTimeout setState()之前', this.state.count)
this.setState(state => ({count: state.count + 1}))
console.log('setTimeout setState()之后', this.state.count)
})
}
update3 = () => {
const h2 = this.refs.count
h2.onclick = () => {
console.log('onclick setState()之前', this.state.count)
this.setState(state => ({count: state.count + 1}))
console.log('onclick setState()之后', this.state.count)
}
}
update4 = () => {
Promise.resolve().then(value => {
console.log('Promise setState()之前', this.state.count)
this.setState(state => ({count: state.count + 1}))
console.log('Promise setState()之后', this.state.count)
})
}


update5 = () => { // 这里是通过回调函数传入 state 来更新 count
// 而传入的 state 始终是保持最新状态,所以这里的 count 状态的改变没有合并
// 此处 count = state.count + 1,得到新的 count,然后用新的 count 再加一
console.log('onclick setState()之前', this.state.count)
this.setState(state => ({count: state.count + 1}))
console.log('onclick setState()之后', this.state.count)
console.log('onclick setState()之前2', this.state.count)
this.setState(state => ({count: state.count + 1}))
console.log('onclick setState()之后2', this.state.count)
}

update6 = () => { // 这里是直接取this.state, 又因为异步调用,
// 所以 count 状态的改变会合并,会以最后一次的状态改变为准,
// 此处的 count = this.state.count + 10
console.log('onclick setState()之前', this.state.count)
this.setState({count: this.state.count + 1})
console.log('onclick setState()之后', this.state.count)
console.log('onclick setState()之前2', this.state.count)
this.setState({count: this.state.count + 10})
console.log('onclick setState()之后2', this.state.count)
}

update7 = () => {
console.log('onclick setState()之前', this.state.count)
this.setState({count: this.state.count + 1})
console.log('onclick setState()之后', this.state.count)

console.log('onclick setState()之前2', this.state.count)
this.setState(state => ({count: state.count + 1}))
console.log('onclick setState()之后2', this.state.count)
}

update8 = () => {
console.log('onclick setState()之前2', this.state.count)
this.setState(state => ({count: state.count + 1}))
console.log('onclick setState()之后2', this.state.count)

console.log('onclick setState()之前', this.state.count)
this.setState({count: this.state.count + 1})
console.log('onclick setState()之后', this.state.count)
}


render() {
const {count} = this.state
console.log('render()', count)
return (
<div>
<h2 ref='count'>{count}</h2>
<button onClick={this.update1}>更新1</button> ---
<button onClick={this.update2}>更新2</button> &nbsp;
<button onClick={this.update3}>更新3</button> &nbsp;
<button onClick={this.update4}>更新4</button> ---
<button onClick={this.update5}>更新5</button> &nbsp;
<button onClick={this.update6}>更新6</button> &nbsp;
<button onClick={this.update7}>更新7</button> &nbsp;
<button onClick={this.update8}>更新8</button> &nbsp;
</div>
)
}
}

ReactDOM.render(<StateTest/>, document.getElementById('example')) // 渲染组件标签, 内部会调用组件标签对象的render()虚拟DOM

</script>
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
<script type="text/babel">
class StateTest extends React.Component {

state = {
count: 0,
}

componentDidMount() {
this.setState({count: this.state.count + 1})
this.setState({count: this.state.count + 1})
console.log(this.state.count) // 2 ==> 0

this.setState(state => ({count: state.count + 1}))
this.setState(state => ({count: state.count + 1}))
console.log(this.state.count) // 3 ==> 0

setTimeout(() => {
this.setState({count: this.state.count + 1})
console.log('timeout', this.state.count) // 10 ==> 6

this.setState({count: this.state.count + 1})
console.log('timeout', this.state.count) // 12 ==> 7
}, 0)

Promise.resolve().then(value => {
this.setState({count: this.state.count + 1})
console.log('promise', this.state.count) // 6 ==>4

this.setState({count: this.state.count + 1})
console.log('promise', this.state.count) // 8 ==> 5
})
}

render() {
const count = this.state.count
console.log('render', count) // 1 ==> 0 4 ==>3 5 ==>4 7 ==>5 9 ==>6 11 ==>7
return (
<div>
<p>{count}</p>
</div>
)
}
}

ReactDOM.render(<StateTest/>, document.getElementById('example'))

</script>

PureComponent

  1. Component存在的问题?
    1). 父组件重新render(), 当前组件也会重新执行render(), 即使没有任何变化
    2). 当前组件setState(), 重新执行render(), 即使state没有任何变化

  2. 解决Component存在的问题
    1). 原因: 组件的componentShouldUpdate()默认返回true, 即使数据没有变化render()都会重新执行
    2). 办法1: 重写shouldComponentUpdate(), 判断如果数据有变化返回true, 否则返回false
    3). 办法2: 使用PureComponent代替Component
    4). 说明: 一般都使用PureComponent来优化组件性能

  3. PureComponent的基本原理
    1). 重写实现shouldComponentUpdate()
    2). 对组件的新/旧state和props中的数据进行浅比较, 如果都没有变化, 返回false, 否则返回true
    3). 一旦componentShouldUpdate()返回false不再执行用于更新的render()

  4. 面试题:
    组件的哪个生命周期勾子能实现组件优化?
    PureComponent的原理?
    区别Component与PureComponent?

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<script type="text/babel">

class A extends React.Component {

state = {
m1: {count: 1}
}

test1 = () => {
/*this.setState(state => ({
m1: {count: state.m1.count + 1}
}))*/

// const m1 = this.state.m1
// m1.count = 2
// 如果用了 PureComponent,就不能这样更新 m1对象,否则不会重新 render(),
// this.setState({m1}) // 因为 PureComponent 的 shouldComponentUpdate()只是浅比较,只改变对象 m1的内部数据不会引发state的改变

// this.setState({m1: {...m1}})

this.setState({})
}

render() {
console.log('A render()')
return (
<div>
<h1>A组件: m1={this.state.m1.count}</h1>
<button onClick={this.test1}>A 测试1</button>
<B m1={this.state.m1}/>
</div>
)
}
}

class B extends React.PureComponent {

state = {
m2: 1
}

test2 = () => {
this.setState({})
}

/*
用来决定当前组件是否应该重新render(), 如果返回true, 就会去重新render(), 否则结束
*/
/*shouldComponentUpdate (nextProps, nextState) {
console.log('shouldComponentUpdate()')
// 比较新旧props中的和state数据, 如果没有一个变化的返回false, 否则true
if(this.props.m1===nextProps.m1 && this.state.m2===nextState.m2) {
return false
} else {
return true
}

// return true // Component中的默认为true
}*/

render() {
console.log('B render()')
return (
<div>
<h1>B组件: m2={this.state.m2}, m1.count={this.props.m1.count}</h1>
<button onClick={this.test2}>B 测试2</button>
</div>
)
}
}

ReactDOM.render(<A/>, document.getElementById('example'))
</script>

列表 key 的问题

面试题:
1). react/vue中的key的作用/内部原理
2). 为什么列表的key尽量不要用index

  1. 虚拟DOM的key的作用?
    1). 简单的说: key是虚拟DOM对象的标识, 在更新显示时key起着极其重要的作用
    2). 详细的说: 当列表数组中的数据发生变化生成新的虚拟DOM后, React进行新旧虚拟DOM的diff比较
    a. key没有变
    item数据没变, 直接使用原来的真实DOM
    item数据变了, 对原来的真实DOM进行数据更新
    b. key变了
    销毁原来的真实DOM, 根据item数据创建新的真实DOM显示(即使item数据没有变)

  2. key为index的问题
    1). 添加/删除/排序 => 产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低
    2). 如果item界面还有输入框 => 产生错误的真实DOM更新 ==> 界面有问题
    注意: 如果不存在添加/删除/排序操作, 用index没有问题

  3. 解决:
    使用item数据的标识数据作为key, 比如id属性值

  4. 什么时候可以用index作为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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
const time = Date.now()
class PersonList extends React.Component {

state = {
persons: [
{id: time, name: 'Tom', age: 13},
{id: time +1, name: 'Jack', age: 12},
]
}

add = () => {
const persons = [...this.state.persons]
persons.unshift({id: Date.now(), name: 'Bob', age: 14})
this.setState({persons})
}

remove = () => {
const persons = [...this.state.persons]
persons.shift()
this.setState({persons})
}

sort = () => {
const persons = [...this.state.persons]
persons.sort((p1, p2) => p1.age - p2.age)
this.setState({persons})
}

render() {
const persons = this.state.persons
return (
<div>
<div>
<button onClick={this.add}>add first</button>
&nbsp;
<button onClick={this.remove}>remove first</button>
&nbsp;
<button onClick={this.sort}>sort</button>
</div>

<h2>使用id作为key</h2>
<ul>
{
persons.map((p, index) =>
<li key={p.id}>{p.id}--{p.name}--{p.age}--<input/></li>
)
}
</ul>

<h2>使用index作为key</h2>
<ul>
{
persons.map((p, index) =>
<li key={index}>{p.id}--{p.name}--{p.age}--<input/></li>
)
}
</ul>
</div>
)
}
}

ReactDOM.render(
<PersonList/>,
document.getElementById('root')
)

React 入门

1.1. React 的基本认识

1.1.1. 官网

  1. 英文官网: https://reactjs.org/

  2. 中文官网: https://doc.react-china.org/
    1.1.2. 介绍描述

  3. 用于构建用户界面的 JavaScript 库(只关注于 View)

  4. 由 Facebook 开源
    1.1.3. React 的特点

  5. Declarative(声明式编码)

  6. Component-Based(组件化编码)

  7. Learn Once, Write Anywhere(支持客户端与服务器渲染)

  8. 高效

  9. 单向数据流
    1.1.4. React 高效的原因

  10. 虚拟(virtual)DOM, 不总是直接操作 DOM

  11. DOM Diff 算法, 最小化页面重绘

1.2. React 的基本使用

注意: 此时只是测试语法使用, 并不是真实项目开发使用

效果

1581239836317

1.2.2. 相关 js 库

  1. react.js: React 的核心库
  2. react-dom.js: 提供操作 DOM 的 react 扩展库
  3. babel.min.js: 解析 JSX 语法代码转为纯 JS 语法代码的库
    1.2.3. 在页面中导入 js
1
2
3
<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>

1.2.4. 编码

1
2
3
4
5
6
<script type="text/babel"> //必须声明 babel
// 创建虚拟 DOM 元素
const vDom = <h1>Hello React</h1> // 千万不要加引号
// 渲染虚拟 DOM 到页面真实 DOM 容器中
ReactDOM.render(vDom, document.getElementById('test'))
</script>

1.3. React JSX

1.3.1. 效果

1581249535409

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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>02_JSX</title>
</head>
<body>
<div id="test1"></div>
<div id="test2"></div>

<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/javascript">
// 1. 创建虚拟DOM
/*方式一: 纯JS(一般不用)创建虚拟DOM元素对象*/
const msg = 'I Like You!'
const myId = 'Atguigu'

const vDOM1 = React.createElement('h2', {id: myId}, msg.toUpperCase())
// 2. 渲染到真实页面
const domContainer = document.getElementById('test1')
// debugger
ReactDOM.render(vDOM1, domContainer)
</script>

<script type="text/babel">
// 1. 创建虚拟DOM
/*方式二: JSX创建虚拟DOM元素对象*/
const vDOM2 = <h3 id={myId.toUpperCase()}>{msg.toLowerCase()}</h3>
// debugger
// 2. 渲染到真实页面
ReactDOM.render(vDOM2, document.getElementById('test2'))
</script>
</body>
</html>

1.3.2. 虚拟 DOM

  1. React 提供了一些 API 来创建一种 特别 的一般 js 对象
    a. var element = React.createElement(‘h1’, {id:‘myTitle’},‘hello’)
    b. 上面创建的就是一个简单的虚拟 DOM 对象
  2. 虚拟 DOM 对象最终都会被 React 转换为真实的 DOM
  3. 我们编码时基本只需要操作 react 的虚拟 DOM 相关数据, react 会转换为真实 DOM 变化而更新界面

1.3.3. JSX

  1. 全称: JavaScript XML

  2. react 定义的一种类似于 XML 的 JS 扩展语法: XML+JS

  3. 作用: 用来创建 react 虚拟 DOM(元素)对象

​ a. var ele = Hello JSX!

​ b. 注意 1: 它不是字符串, 也不是 HTML/XML 标签

​ c. 注意 2: 它最终产生的就是一个 JS 对象

  1. 标签名任意: HTML 标签或其它标签

  2. 标签属性任意: HTML 标签属性或其它

  3. 基本语法规则

​ a. 遇到 <开头的代码, 以标签的语法解析: html 同名标签转换为 html 同名元素, 其它 标签需要特别解析

​ b. 遇到以 { 开头的代码,以 JS 语法解析: 标签中的 js 代码必须用{ }包含

  1. babel.js 的作用

​ a. 浏览器不能直接解析 JSX 代码, 需要 babel 转译为纯 JS 的代码才能运行

​ b. 只要用了 JSX,都要加上 type=“text/babel”, 声明需要 babel 来处理

1.3.4. 渲染虚拟 DOM(元素)

  1. 语法: ReactDOM.render(virtualDOM, containerDOM)

  2. 作用: 将虚拟 DOM 元素渲染到页面中的真实容器 DOM 中显示

  3. 参数说明

a. 参数一: 纯 js 或 jsx 创建的虚拟 dom 对象

b. 参数二: 用来包含虚拟 DOM 元素的真实 dom 元素对象(一般是一个 div)

1.3.5. 建虚拟 DOM 的 2 种方式

  1. 纯 JS(一般不用) React.createElement(‘h1’, {id:‘myTitle’}, title)

  2. JSX: <h1 id="myTitle">{title}</h1>

1.3.6. JSX 练习
需求: 动态展示列表数据

1581250319549

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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>02_JSX_DEMO</title>
</head>
<body>
<h2>前端JS框架列表</h2>
<div id="example1"></div>
<div id="example2"></div>

<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>

<script type="text/babel">
/*
功能: 动态展示列表数据
*/
/*
技术点:
1). 使用JSX创建虚拟DOM
2). React能自动遍历显示数组中所有的元素
3). array.map()的使用
*/

// 数据的数组
var names = ['jquery', 'zeptoo', 'angular', 'react全家桶', 'vue全家桶']

// 数据的数组-->标签的数组
var lis = []
names.forEach((name, index) => lis.push(<li key={index}>{name}</li>))
// 创建虚拟DOM
const ul = <ul>{lis}</ul>
// 渲染虚拟DOM
ReactDOM.render(ul, document.getElementById('example1'))
const ul2 = <ul>{
names.map((name, index) => <li key={index}>{name}</li>)
}</ul>
ReactDOM.render(ul2, document.getElementById('example2'))
</script>
</body>
</html>

1.4. 模块与组件和模块化与组件化的理解

1.4.1. 模块

  1. 理解: 向外提供特定功能的 js 程序, 一般就是一个 js 文件
  2. 为什么: js 代码更多更复杂
  3. 作用: 复用 js, 简化 js 的编写, 提高 js 运行效率
    1.4.2. 组件
  4. 理解: 用来实现特定(局部)功能效果的代码集合(html/css/js)
  5. 为什么: 一个界面的功能更复杂
  6. 作用: 复用编码, 简化项目编码, 提高运行效率
    1.4.3. 模块化
    当应用的 js 都以模块来编写的, 这个应用就是一个模块化的应用

1.4.4. 组件化
当应用是以多组件的方式实现, 这个应用就是一个组件化的应用

1581250594329

React 面向组件编程

2.1. 基本理解和使用

2.1.1. 效果

1581252879400

2.1.2. 自定义组件(Component) :

  1. 定义组件(2 种方式)
1
2
3
4
5
6
7
8
9
10
11
/*方式 1: 工厂函数组件(简单组件)*/
function MyComponent () {
return <h2>工厂函数组件(简单组件)</h2>
}
/*方式 2: ES6 类组件(复杂组件)*/
class MyComponent2 extends React.Component {
render () {
console.log(this) // MyComponent2 的实例对象
return <h2>ES6 类组件(复杂组件)</h2>
}
}
  1. 渲染组件标签
1
ReactDOM.render(<MyComponent />, document.getElementById('example1'))

2.1.3. 注意

  1. 组件名必须首字母大写
  2. 虚拟 DOM 元素只能有一个根元素
  3. 虚拟 DOM 元素必须有结束标签

2.1.4. render()渲染组件标签的基本流程

  1. React 内部会创建组件实例对象
  2. 得到包含的虚拟 DOM 并解析为真实 DOM
  3. 插入到指定的页面元素内部

2.2. 组件三大属性 1: state

2.2.2. 理解

  1. state 是组件对象最重要的属性, 值是对象(可以包含多个数据)
  2. 组件被称为"状态机", 通过更新组件的 state 来更新对应的页面显示(重新渲染组件)
    2.2.3. 编码操作
  3. 初始化状态:
1
2
3
4
5
6
constructor (props) {
super(props)
this.state = {
stateProp1 : value1, stateProp2 : value2
}
}
  1. 读取某个状态值
1
this.state.statePropertyName
  1. 更新状态---->组件界面更新
1
2
3
this.setState({
stateProp1 : value1, stateProp2 : value2
})

2.3. 组件三大属性 2: props

2.3.1. 效果
需求: 自定义用来显示一个人员信息的组件
1). 姓名必须指定
2). 如果性别没有指定, 默认为男
3). 如果年龄没有指定, 默认为 18

1581253316769

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>04_component_props</title>
</head>
<body>

<div id="example1"></div>
<hr>
<div id="example2"></div>

<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>

<script type="text/babel">

/*
需求: 自定义用来显示一个人员信息的组件, 效果如页面. 说明
1). 如果性别没有指定, 默认为男
2). 如果年龄没有指定, 默认为18
*/

//1. 定义组件类
class Person extends React.Component {
render() {
console.log(this)
return (
<ul>
<li>姓名: {this.props.name}</li>
<li>性别: {this.props.sex}</li>
<li>年龄: {this.props.age}</li>
</ul>
)
}
}
// 对标签属性进行限制
Person.propTypes = {
name: PropTypes.string.isRequired,
sex: PropTypes.string,
age: PropTypes.number
}
// 指定属性的默认值
Person.defaultProps = {
sex: '男',
age: 18
}

//2. 渲染组件标签
const person = {
name: 'Tom',
sex: '女',
age: 18
}
ReactDOM.render(<Person {...person}/>, document.getElementById('example1'))
const person2 = {
myName: 'JACK',
age: 17
}
ReactDOM.render(<Person name={person2.myName} age={person2.age}/>,
document.getElementById('example2'))
</script>
</body>
</html>

2.3.2. 理解

  1. 每个组件对象都会有 props(properties 的简写)属性
  2. 组件标签的所有属性都保存在 props 中
    2.3.3. 作用
  3. 通过标签属性从组件外向组件内传递变化的数据
  4. 注意: 组件内部不要修改 props 数据
    2.3.4. 编码操作
  5. 内部读取某个属性值
1
this.props.propertyName
  1. 对 props 中的属性值进行类型限制和必要性限制
1
2
3
4
Person.propTypes = {
name: React.PropTypes.string.isRequired,
age: React.PropTypes.number.isRequired
}
  1. 扩展属性: 将对象的所有属性通过 props 传递

  2. 默认属性值

1
Person.defaultProps = { name: 'Mary' }
  1. 组件类的构造函数
1
2
3
4
constructor (props) {
super(props)
console.log(props) // 查看所有属性
}

2.3.5. 面试题
问题: 请区别一下组件的 props 和 state 属性

  1. state: 组件自身内部可变化的数据
  2. props: 从组件外部向组件内部传递数据, 组件内部只读不修改

2.4. 组件三大属性 3: refs 与事件处理

2.4.1. 效果
需求: 自定义组件, 功能说明如下:

  1. 点击按钮, 提示第一个输入框中的值
  2. 当第 2 个输入框失去焦点时, 提示这个输入框中的值
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
49
50
51
52
53
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>07_component_refs</title>
</head>
<body>
<br>
<div id="example"></div>

<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>

<script type="text/babel">
/*
需求: 自定义组件, 功能说明如下:
1. 界面如果页面所示
2. 点击按钮, 提示第一个输入框中的值
3. 当第2个输入框失去焦点时, 提示这个输入框中的值
*/
//定义组件
class MyComponent extends React.Component {
constructor(props) {
super(props) // 调用父类(Component)的构造函数
//console.log(this)
// 将自定义的函数强制绑定为组件对象
this.handleClick = this.handleClick.bind(this) // 将返回函数中的this强制绑定为指定的对象, 并没有改变原来的函数中的this
}
// 自定义的方法中的this默认为null
handleClick () {
// alert(this) //this默认是null, 而不是组件对象
// 得到绑定在当前组件对象上的input的值
alert(this.msgInput.value)
}
handleBlur (event) {
alert(event.target.value)
}
render () {
return (
<div>
<input type="text" ref={input => this.msgInput = input}/>{' '}
<button onClick={this.handleClick}>提示输入数据</button>{' '}
<input type="text" placeholder="失去焦点提示数据" onBlur={this.handleBlur}/>
</div>
)
}
}
// 渲染组件标签
ReactDOM.render(<MyComponent />, document.getElementById('example'))
</script>
</body>
</html>

2.4.2. 组件的 3 大属性之二: refs 属性

  1. 组件内的标签都可以定义 ref 属性来标识自己

​ a. this.msgInput = input}/>
​ b. 回调函数在组件初始化渲染完或卸载时自动调用
2) 在组件中可以通过 this.msgInput 来得到对应的真实 DOM 元素
3) 作用: 通过 ref 获取组件内容特定标签对象, 进行读取其相关数据

2.4.3. 事件处理

  1. 通过 onXxx 属性指定组件的事件处理函数(注意大小写)
    a. React 使用的是自定义(合成)事件, 而不是使用的原生 DOM 事件
    b. React 中的事件是通过事件委托方式处理的(委托给组件最外层的元素)
  2. 通过 event.target 得到发生事件的 DOM 元素对象
1
2
3
handleFocus(event) {
event.target //返回 input 对象
}

2.4.4. 强烈注意

  1. 组件内置的方法中的 this 为组件对象
  2. 在组件类中自定义的方法中 this 为 null
    a. 强制绑定 this: 通过函数对象的 bind()
    b. 箭头函数(ES6 模块化编码时才能使用)

2.5. 组件的组合

2.5.1. 效果
功能: 组件化实现此功能

  1. 显示所有 todo 列表
  2. 输入文本, 点击按钮显示到列表的首位, 并清除输入的文本

1581253919919

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>05_components_composing</title>
</head>
<body>

<div id="example"></div>

<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel">
/*
1)拆分组件: 拆分界面,抽取组件
2)实现静态组件: 使用组件实现静态页面效果
3)实现动态组件
① 动态显示初始化数据
② 交互功能(从绑定事件监听开始)
*/
// 应用组件
class App extends React.Component {
constructor (props) {
super(props)
// 初始化状态
this.state = {
todos: ['吃饭', '睡觉', '打豆豆']
}
this.add = this.add.bind(this)
}
add (todo) {
const {todos} = this.state
todos.unshift(todo)
//更新状态
this.setState({todos})
}
render () {
const {todos} = this.state
return (
<div>
<TodoAdd add={this.add} count={todos.length} />
<TodoList todos={todos} />
</div>
)
}
}

// 添加todo组件
class TodoAdd extends React.Component {
constructor (props) {
super(props)
this.addTodo = this.addTodo.bind(this)
}
addTodo () {
// 读取输入数据
const text = this.input.value.trim()
// 查检
if(!text) {
return
}
// 保存到todos
this.props.add(text)
// 清除输入
this.input.value = ''
}
render () {
return (
<div>
<h2>Simple TODO List</h2>
<input type="text" ref={input => this.input=input}/>
<button onClick={this.addTodo}>Add #{this.props.count}</button>
</div>
)
}
}
TodoAdd.propTypes = {
add: PropTypes.func.isRequired,
count: PropTypes.number.isRequired
}

// todo列表组件
class TodoList extends React.Component {
render () {
const {todos} = this.props
return (
<ul>
{
todos.map((todo, index) => <li key={index}>{todo}</li>)
}
</ul>
)
}
}
TodoList.propTypes = {
todos: PropTypes.array.isRequired
}

// 渲染应用组件标签
ReactDOM.render(<App />, document.getElementById('example'))

</script>
</body>
</html>

2.5.2. 功能界面的组件化编码流程(无比重要)

  1. 拆分组件: 拆分界面,抽取组件
  2. 实现静态组件: 使用组件实现静态页面效果
  3. 实现动态组件
    a. 动态显示初始化数据
    b. 交互功能(从绑定事件监听开始)

2.6. 收集表单数据

2.6.1. 效果
需求: 自定义包含表单的组件

  1. 输入用户名密码后, 点击登陆提示输入信息
  2. 不提交表单
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>09_form</title>
</head>
<body>
<div id="example"></div>

<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel">
/*
1. 问题: 在react应用中, 如何收集表单输入数据
2. 包含表单的组件分类
受控组件
非受控组件
*/
/*
需求: 自定义包含表单的组件
1. 界面如下所示
2. 输入用户名密码后, 点击登陆提示输入信息
3. 不提交表单
*/
class LoginForm extends React.Component {
constructor(props) {
super(props)
this.state = {
username: ''
}
this.handleSubmit = this.handleSubmit.bind(this)
this.handleChange = this.handleChange.bind(this)
}

handleChange(event) {
this.setState({username: event.target.value})
}

handleSubmit(event) {
alert(`准备提交的用户名为: ${this.state.username}, 密码:${this.pwdInput.value}`)

// 阻止事件的默认行为: 提交表单
event.preventDefault()
}
render () {

return (
<form onSubmit={this.handleSubmit} action="/test">
<label>
用户名:
<input type="text" value={this.state.username} onChange={this.handleChange} />
</label>&nbsp;
<label>
密码:
<input type="password" ref={(input) => this.pwdInput = input} />
</label>&nbsp;
<input type="submit" value="登陆" />
</form>
)
}
}

ReactDOM.render(<LoginForm />, document.getElementById('example'))
</script>
</body>
</html>

2.6.2. 理解

  1. 问题: 在 react 应用中, 如何收集表单输入数据
  2. 包含表单的组件分类
    a. 受控组件: 表单项输入数据能自动收集成状态
    b. 非受控组件: 需要时才手动读取表单输入框中的数据

2.7. 组件生命周期

2.7.1. 效果
需求: 自定义组件

  1. 让指定的文本做显示/隐藏的渐变动画
  2. 切换持续时间为 2S
  3. 点击按钮从界面中移除组件界面
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>10_ComponentLife</title>
</head>
<body>
<div id="example"></div>

<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel">
/*
需求: 自定义组件
1. 让指定的文本做显示/隐藏的动画
2. 切换时间为2S
3. 点击按钮从界面中移除组件界面
*/
class Fade extends React.Component {

constructor (props) {
super(props)
console.log('constructor(): 创建组件对象')
this.state = {
opacity: 1
}
this.removeComponent = this.removeComponent.bind(this)
}

componentWillMount () {
console.log('componentWillMount(): 初始化将要挂载')
}

componentDidMount () {// 在此方法中启动定时器/绑定监听/发送ajax请求
console.log('componentDidMount(): 初始化已经挂载')
// 保存到当前组件对象中
this.intervalId = setInterval(function () {
console.log('--------')
// 得到当前opacity
let {opacity} = this.state
// 更新opacity
opacity -= 0.1
if(opacity<=0) {
opacity = 1
}
// 更新状态
this.setState({opacity})
}.bind(this), 200)
}

componentWillUpdate () {
console.log('componentWillUpdate(): 将要更新')
}
componentDidUpdate () {
console.log('componentDidUpdate(): 已经更新')
}

componentWillUnmount () {// 清除定时器/解除监听
console.log('componentWillUnmount(): 将要被移除')
clearInterval(this.intervalId)
}

removeComponent () {
ReactDOM.unmountComponentAtNode(document.getElementById('example'))
}

render() {
console.log('render() 渲染组件')
return (
<div>
<h2 style={{opacity:this.state.opacity}}>{this.props.content}</h2>
<button onClick={this.removeComponent}>不活了</button>
</div>
)
}
}
ReactDOM.render(<Fade content="react学不会, 怎么办?"/>, document.getElementById('example'))
</script>
</body>
</html>

2.7.2. 理解

  1. 组件对象从创建到死亡它会经历特定的生命周期阶段
  2. React 组件对象包含一系列的勾子函数(生命周期回调函数), 在生命周期特定时刻回调
  3. 我们在定义组件时, 可以重写特定的生命周期回调函数, 做特定的工作

2.7.3. 生命周期流程图

1581254292664

2.7.4. 生命周期详述

  1. 组件的三个生命周期状态:
  • Mount:插入真实 DOM
  • Update:被重新渲染
  • Unmount:被移出真实 DOM
  1. React 为每个状态都提供了勾子(hook)函数
  • componentWillMount()

  • componentDidMount() * componentWillUpdate() * componentDidUpdate() * componentWillUnmount()

  1. 生命周期流程:
    a. 第一次初始化渲染显示: ReactDOM.render()
  • constructor(): 创建对象初始化 state

  • componentWillMount() : 将要插入回调

  • render() : 用于插入虚拟 DOM 回调

  • componentDidMount() : 已经插入回调

b. 每次更新 state: this.setSate()

  • componentWillUpdate() : 将要更新回调

  • render() : 更新(重新渲染)

  • componentDidUpdate() : 已经更新回调

c. 移除组件: ReactDOM.unmountComponentAtNode(containerDom)

  • componentWillUnmount() : 组件将要被移除回调

2.7.5. 重要的勾子

  1. render(): 初始化渲染或更新渲染调用
  2. componentDidMount(): 开启监听, 发送 ajax 请求
  3. componentWillUnmount(): 做一些收尾工作, 如: 清理定时器
  4. componentWillReceiveProps(): 后面需要时讲

2.8. 虚拟 DOM 与 DOM Diff 算法

2.8.1. 效果

1581254596169

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
49
50
51
52
53
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>04_component</title>
</head>
<body>
<div id="example"></div>
<br>

<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel">
/*
验证:
虚拟DOM+DOM Diff算法: 最小化页面重绘
*/

class HelloWorld extends React.Component {
constructor(props) {
super(props)
this.state = {
date: new Date()
}
}

componentDidMount () {
setInterval(() => {
this.setState({
date: new Date()
})
}, 1000)
}

render () {
console.log('render()')
return (
<p>
Hello, <input type="text" placeholder="Your name here"/>!&nbsp;
<span>It is {this.state.date.toTimeString()}</span>
</p>
)
}
}

ReactDOM.render(
<HelloWorld/>,
document.getElementById('example')
)
</script>
</body>
</html>

2.8.2. 基本原理图

1581254713009

react 应用(基于 react 脚手架)

3.1. 使用 create-react-app 创建 react 应用

3.1.1. react 脚手架

  1. xxx 脚手架: 用来帮助程序员快速创建一个基于 xxx 库的模板项目
    a. 包含了所有需要的配置
    b. 指定好了所有的依赖
    c. 可以直接安装/编译/运行一个简单效果
  2. react 提供了一个用于创建 react 项目的脚手架库: create-react-app
  3. 项目的整体技术架构为: react + webpack + es6 + eslint
  4. 使用脚手架开发的项目的特点: 模块化, 组件化, 工程化

3.1.2. 创建项目并启动

1
2
3
4
npm install -g create-react-app
create-react-app hello-react
cd hello-react
npm start

3.1.3. react 脚手架项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
ReactNews
|--node_modules---第三方依赖模块文件夹
|--public
|-- index.html-----------------主页面
|--scripts
|-- build.js-------------------build 打包引用配置
|-- start.js-------------------start 运行引用配置
|--src------------源码文件夹
|--components-----------------react 组件
|--index.js-------------------应用入口 js
|--.gitignore------git 版本管制忽略的配置
|--package.json----应用包配置文件
|--README.md-------应用描述说明的 readme 文件

3.2. demo: 评论管理

3.2.1. 效果

示例代码:https://github.com/cheungww/React_practice/tree/master/react_demo

3.2.2. 拆分组件
应用组件: App

  • state: comments/array
    添加评论组件: CommentAdd
  • state: username/string, content/string * props: add/func
    评论列表组件: CommentList * props: comment/object, delete/func, index/number
    评论项组件: CommentItem
  • props: comments/array, delete/func

3.2.3. 实现静态组件
3.2.4. 实现动态组件
动态展示初始化数据

  • 初始化状态数据
  • 传递属性数据

响应用户操作, 更新组件界面

  • 绑定事件监听, 并处理
  • 更新 state

react ajax

4.1. 理解

4.1.1. 前置说明

  1. React 本身只关注于界面, 并不包含发送 ajax 请求的代码
  2. 前端应用需要通过 ajax 请求与后台进行交互(json 数据)
  3. react 应用中需要集成第三方 ajax 库(或自己封装)
    4.1.2. 常用的 ajax 请求库
  4. jQuery: 比较重, 如果需要另外引入不建议使用
  5. axios: 轻量级, 建议使用
    a. 封装 XmlHttpRequest 对象的 ajax
    b. promise 风格
    c. 可以用在浏览器端和 node 服务器端
  6. fetch: 原生函数, 但老版本浏览器不支持
    a. 不再使用 XmlHttpRequest 对象提交 ajax 请求
    b. 为了兼容低版本的浏览器, 可以引入兼容库 fetch.js
    4.1.3. 效果
    需求:
  1. 界面效果如下
  2. 根据指定的关键字在 github 上搜索匹配的最受关注的库
  3. 显示库名, 点击链接查看库
  4. 测试接口: https://api.github.com/search/repositories?q=r&sort=stars

4.2. axios

4.2.1. 文档
https://github.com/axios/axios
4.2.2. 相关 API

  1. GET 请求
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
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
axios.get('/user', {
params: {
ID: 12345
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});

axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});

4.3. Fetch

4.3.1. 文档

  1. https://github.github.io/fetch/
  2. https://segmentfault.com/a/1190000003810652
    4.3.2. 相关 API
  3. GET 请求
1
2
3
4
5
6
7
fetch(url).then(function(response) {
return response.json()
}).then(function(data) {
console.log(data)
}).catch(function(e) {
console.log(e)
});
  1. POST 请求
1
2
3
4
5
6
7
8
fetch(url, {
method: "POST",
body: JSON.stringify(data),
}).then(function(data) {
console.log(data)
}).catch(function(e) {
console.log(e)
})

4.4. demo: github users
4.4.1. 效果

示例代码:https://github.com/cheungww/React_practice/tree/master/react_demo

4.4.2. 拆分组件
App

  • state: searchName/string

Search

  • props: setSearchName/func

List

  • props: searchName/string * state: firstView/bool, loading/bool, users/array, errMsg/string

4.4.3. 编写静态组件
4.4.4. 编写动态组件
componentWillReceiveProps(nextProps): 监视接收到新的 props, 发送 ajax 使用 axios 库发送 ajax 请求

几个重要技术总结

5.1. 组件间通信

5.1.1. 方式一: 通过 props 传递

  1. 共同的数据放在父组件上, 特有的数据放在自己组件内部(state)
  2. 通过 props 可以传递一般数据和函数数据, 只能一层一层传递
  3. 一般数据–>父组件传递数据给子组件–>子组件读取数据
  4. 函数数据–>子组件传递数据给父组件–>子组件调用函数
    5.1.2. 方式二: 使用消息订阅(subscribe)-发布(publish)机制
  5. 工具库: PubSubJS
  6. 下载: npm install pubsub-js --save
  7. 使用:
1
2
3
import PubSub from 'pubsub-js' //引入
PubSub.subscribe('delete', function(data){ }); //订阅
PubSub.publish('delete', data) //发布消息

5.1.3. 方式三: redux
后面专门讲解

5.2. 事件监听理解

5.2.1. 原生 DOM 事件

  1. 绑定事件监听
    a. 事件名(类型): 只有有限的几个, 不能随便写

​ b. 回调函数
2) 触发事件
​ a. 用户操作界面
​ b. 事件名(类型)
​ c. 数据()
5.2.2. 自定义事件(消息机制)

  1. 绑定事件监听
    ​ a. 事件名(类型): 任意
    ​ b. 回调函数: 通过形参接收数据, 在函数体处理事件
  2. 触发事件(编码)
    ​ a. 事件名(类型): 与绑定的事件监听的事件名一致
    ​ b. 数据: 会自动传递给回调函数

5.3. ES6 常用新语法

  1. 定义常量/变量: const/let
  2. 解构赋值: let {a, b} = this.props import {aa} from ‘xxx’ 3) 对象的简洁表达: {a, b}
  3. 箭头函数:
    a. 常用场景
  • 组件的自定义方法: xxx = () => {}
  • 参数匿名函数

b. 优点:

  • 简洁

  • 没有自己的 this,使用引用 this 查找的是外部 this

  1. 扩展(三点)运算符: 拆解对象(const MyProps = {}, )
  2. 类: class/extends/constructor/super
  3. ES6 模块化: export default | import

react-router4

6.1. 相关理解

6.1.1. react-router 的理解

  1. react 的一个插件库
  2. 专门用来实现一个 SPA 应用
  3. 基于 react 的项目基本都会用到此库

6.1.2. SPA 的理解

  1. 单页 Web 应用(single page web application,SPA)
  2. 整个应用只有一个完整的页面
  3. 点击页面中的链接不会刷新页面, 本身也不会向服务器发请求
  4. 当点击路由链接时, 只会做页面的局部更新
  5. 数据都需要通过 ajax 请求获取, 并在前端异步展现

6.1.3. 路由的理解

  1. 什么是路由?
    a. 一个路由就是一个映射关系(key:value)
    b. key 为路由路径, value 可能是 function/component
  2. 路由分类
    a. 后台路由: node 服务器端路由, value 是 function, 用来处理客户端提交的请求并返回一个响应数据

​ b. 前台路由: 浏览器端路由, value 是 component, 当请求的是路由 path 时, 浏览器端前没有发送 http 请求, 但界面会更新显示对应的组件
3) 后台路由
a. 注册路由: router.get(path, function(req, res))
b. 当 node 接收到一个请求时, 根据请求路径找到匹配的路由, 调用路由中的函数来处理请求, 返回响应数据
4) 前端路由
a. 注册路由:
b. 当浏览器的 hash 变为#about 时, 当前路由组件就会变为 About 组件

6.1.4. 前端路由的实现

  1. history 库
    a. 网址: https://github.com/ReactTraining/history
    b. 管理浏览器会话历史(history)的工具库
    c. 包装的是原生 BOM 中 window.history 和 window.location.hash

  2. history API
    a. History.createBrowserHistory(): 得到封装 window.history 的管理对象
    b. History.createHashHistory(): 得到封装 window.location.hash 的管理对象
    c. history.push(): 添加一个新的历史记录
    d. history.replace(): 用一个新的历史记录替换当前的记录
    e. history.goBack(): 回退到上一个历史记录
    f. history.goForword(): 前进到下一个历史记录
    g. history.listen(function(location){}): 监视历史记录的变化

  3. 测试

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>history test</title>
</head>
<body>
<p><input type="text"></p>
<a href="/test1" onclick="return push('/test1')">test1</a><br><br>
<button onClick="push('/test2')">push test2</button><br><br>
<button onClick="back()">回退</button><br><br>
<button onClick="forword()">前进</button><br><br>
<button onClick="replace('/test3')">replace test3</button><br><br>
<script type="text/javascript" src="https://cdn.bootcss.com/history/4.7.2/history.js"></script>
<script type="text/javascript">
let history = History.createBrowserHistory() // 方式一
// history = History.createHashHistory() // 方式二
// console.log(history)
function push (to) {
history.push(to)
return false
}
function back() {
history.goBack()
}
function forword() {
history.goForward()
}
function replace (to) {
history.replace(to)
}
history.listen((location) => {
console.log('请求路由路径变化了', location)
})
</script>
</body>
</html>

6.2. react-router 相关 API

6.2.1. 组件

  1. <BrowserRouter>
  2. <HashRouter>
  3. <Route>
  4. <Redirect>
  5. <Link>
  6. <NavLink>
  7. <Switch>
    6.2.2. 其它
  8. history 对象
  9. match 对象
  10. withRouter 函数

6.3. 基本路由使用

6.3.1. 效果

6.3.2. 准备

  1. 下载 react-router: npm install --save react-router@4
  2. 引入 bootstrap.css:
    6.3.3. 路由组件: views/about.jsx
1
2
3
4
import React from 'react'
export default function About() {
return <div>About 组件内容</div>
}

6.3.4. 路由组件: views/home.jsx

1
2
3
4
import React from 'react'
export default function About() {
return <div>Home 组件内容</div>
}

6.3.5. 包装 NavLink 组件: components/my-nav-link.jsx

1
2
3
4
5
import React from 'react'
import {NavLink} from 'react-router-dom'
export default function MyNavLink(props) {
return <NavLink {...props} activeClassName='activeClass'/>
}

6.3.6. 应用组件: components/app.jsx

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
import React from 'react'
import { Route, Switch, Redirect } from 'react-router-dom'
import MyNavLink from './components/my-nav-link'
import About from './views/about'
import Home from './views/home'
export default class App extends React.Component {
render() {
return (
<div>
<div className="row">
<div className="col-xs-offset-2 col-xs-8">
<div className="page-header">
<h2>React Router Demo</h2>
</div>
</div>
</div>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
{/*导航路由链接*/}
<MyNavLink className="list-group-item" to='/about' >About</MyNavLink>
<MyNavLink className="list-group-item" to='/home'>Home</MyNavLink>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/*可切换的路由组件*/}
<Switch>
<Route path='/about' component={About} />
<Route path='/home' component={Home} />
<Redirect to='/about' />
</Switch>
</div>
</div>
</div>
</div>
</div>
)
}
}

6.3.7. 自定义样式: index.css

1
2
3
.activeClass {
color: red !important;
}

6.3.8. 入口 JS: index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react'
import ReactDOM from 'react-dom'
import {BrowserRouter, HashRouter} from 'react-router-dom'
import App from './components/app'
import './index.css'
ReactDOM.render(
(
<BrowserRouter>
<App />
</BrowserRouter>
/*<HashRouter>
<App />
</HashRouter>*/
),
document.getElementById('root')
)

6.4. 嵌套路由使用

6.4.1. 效果

6.4.2. 二级路由组件: views/news.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react'
export default class News extends React.Component {
state = {
newsArr: ['news001', 'news002', 'news003']
}
render () {
return (
<div>
<ul>
{
this.state.newsArr.map((news, index) => <li key={index}>{news}</li>)
}
</ul>
</div>
)
}
}

6.4.3. 二级路由组件: views/message.jsx

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
import React from 'react'
import {Link, Route} from 'react-router-dom'
export default class Message extends React.Component {
state = {
messages: []
}
componentDidMount () {
// 模拟发送 ajax 请求
setTimeout(() => {
const data = [
{id: 1, title: 'Message001'},
{id: 3, title: 'Message003'},
{id: 6, title: 'Message006'},
]
this.setState({
messages: data
})
}, 1000)
}
render () {
const path = this.props.match.path
return (
<div>
<ul>
{
this.state.messages.map((m, index) => {
return (
<li key={index}>
<Link to='???'>{m.title}</Link>
</li>
)
})
}
</ul>
</div>
)
}
}

6.4.4. 一级路由组件: views/home.jsx

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
import React from 'react'
import { Switch, Route, Redirect } from 'react-router-dom'
import MyNavLink from './components/my-nav-link'
import News from './views/news'
import Message from './views/message'
export default function Home() {
return (
<div>
<h2>Home 组件内容</h2>
<div>
<ul className="nav nav-tabs">
<li>
<MyNavLink to='/home/news'>News</MyNavLink>
</li>
<li>
<MyNavLink to="/home/message">Message</MyNavLink>
</li>
</ul>
<Switch>
<Route path='/home/news' component={News} />
<Route path='/home/message' component={Message} />
<Redirect to='/home/news' />
</Switch>
</div>
</div>
)
}

6.5. 向路由组件传递参数数据

6.5.1. 效果

6.5.2. 三级路由组件: views/message-detail.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react'
const messageDetails = [
{ id: 1, title: 'Message001', content: '我爱你, 中国' },
{ id: 3, title: 'Message003', content: '我爱你, 老婆' },
{ id: 6, title: 'Message006', content: '我爱你, 孩子' },
]
export default function MessageDetail(props) {
const id = props.match.params.id
const md = messageDetails.find(md => md.id === id * 1)
return (
<ul>
<li>ID: {md.id}</li>
<li>TITLE: {md.title}</li>
<li>CONTENT: {md.content}</li>
</ul>
)
}

6.5.3. 二级路由组件: views/message.jsx

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
import React from 'react'
import {Link, Route} from 'react-router-dom'
import MessageDetail from "./views/message-detail"
export default class Message extends React.Component {
state = {
messages: []
}
componentDidMount() {
// 模拟发送 ajax 请求
setTimeout(() => {
const data = [
{ id: 1, title: 'Message001' },
{ id: 3, title: 'Message003' },
{ id: 6, title: 'Message006' },
]
this.setState({
messages: data
})
}, 1000)
}
render() {
const path = this.props.match.path
return (
<div>
<ul>
{
this.state.messages.map((m, index) => {
return (
<li key={index}>
<Link to={`${path}/${m.id}`}>{m.title}</Link>
</li>
)
})
}
</ul>
<hr />
<Route path={`${path}/:id`} component={MessageDetail}></Route>
</div>
)
}
}

6.6. 多种路由跳转方式

6.6.1. 效果

6.6.2. 二级路由: views/message.jsx

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import React from 'react'
import { Link, Route } from 'react-router-dom'
import MessageDetail from "./views/message-detail"
export default class Message extends React.Component {
state = {
messages: []
}
componentDidMount() {
// 模拟发送 ajax 请求
setTimeout(() => {
const data = [
{ id: 1, title: 'Message001' },
{ id: 3, title: 'Message003' },
{ id: 6, title: 'Message006' },
]
this.setState({
messages: data
})
}, 1000)
}
ShowDetail = (id) => {
this.props.history.push(`/home/message/${id}`)
}
ShowDetail2 = (id) => {
this.props.history.replace(`/home/message/${id}`)
}
back = () => {
this.props.history.goBack()
}
forward = () => {
this.props.history.goForward()
}
render() {
const path = this.props.match.path
return (
<div>
<ul>
{
this.state.messages.map((m, index) => {
return (
<li key={index}>
<Link to={`${path}/${m.id}`}>{m.title}</Link>
&nbsp;
<button onClick={() => this.ShowDetail(m.id)}>查看详情
(push)</button>&nbsp;
<button onClick={() => this.ShowDetail2(m.id)}>查看详情
(replace)</button>
</li>
)
})
}
</ul>
<p>
<button onClick={this.back}>返回</button>&nbsp;
<button onClick={this.forward}>前进</button>&nbsp;
</p>
<hr />
<Route path={`${path}/:id`} component={MessageDetail}></Route>
</div>
)
}
}

react-ui

7.1. 最流行的开源 React UI 组件库

7.1.1. material-ui(国外)

  1. 官网: http://www.material-ui.com/#/
  2. github: https://github.com/callemall/material-ui
    7.1.2. ant-design(国内蚂蚁金服)
  3. PC 官网: https://ant.design/index-cn
  4. 移动官网: https://mobile.ant.design/index-cn
  5. Github: https://github.com/ant-design/ant-design/
  6. Github: https://github.com/ant-design/ant-design-mobile/

7.2. ant-design-mobile 使用入门

7.2.1. 效果

7.2.2. 使用 create-react-app 创建 react 应用

1
2
3
4
npm install create-react-app -g
create-react-app antm-demo
cd antm-demo
npm start

7.2.3. 搭建 antd-mobile 的基本开发环境

  1. 下载
1
npm install antd-mobile --save
  1. src/App.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { Component } from 'react'
// 分别引入需要使用的组件
import Button from 'antd-mobile/lib/button'
import Toast from 'antd-mobile/lib/toast'
export default class App extends Component {
handleClick = () => {
Toast.info('提交成功', 2)
}
render() {
return (
<div>
<Button type="primary" onClick={this.handleClick}>提交</Button>
</div>
)
}
}
  1. src/index.js
1
2
3
4
5
6
import React from 'react';
import ReactDOM from 'react-dom'
import App from "./App"
// 引入整体 css
import 'antd-mobile/dist/antd-mobile.css'
ReactDOM.render(<App />, document.getElementById('root'))
  1. index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1,
minimum-scale=1, user-scalable=no" />
<script src="https://as.alipayobjects.com/g/component/fastclick/1.0.6/fastclick.js"></script>
<script>
if ('addEventListener' in document) {
document.addEventListener('DOMContentLoaded', function () {
FastClick.attach(document.body);
}, false);
}
if (!window.Promise) {
document.writeln('<script src = "https://as.alipayobjects.com/g/component/es6-promise/3.2.2/es6-promise.min.js"
'+' > '+' < '+' / '+'script > ');
}
</script>

7.2.4. 实现按需打包(组件 js/css)

  1. 下载依赖包
1
2
yarn add react-app-rewired --dev
yarn add babel-plugin-import --dev
  1. 修改默认配置:
  • package.json

    1
    2
    3
    4
    5
    "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test --env=jsdom"
    }
  • config-overrides.js

    1
    2
    3
    4
    5
    6
    const {injectBabelPlugin} = require('react-app-rewired');
    module.exports = function override(config, env) {
    config = injectBabelPlugin(['import', {libraryName: 'antd-mobile', style: 'css'}],
    config);
    return config;
    };
  1. 编码
1
2
3
4
// import 'antd-mobile/dist/antd-mobile.css'
// import Button from 'antd-mobile/lib/button'
// import Toast from 'antd-mobile/lib/toast'
import {Button, Toast} from 'antd-mobile'

redux

8.1. redux 理解

8.1.1. 学习文档

  1. 英文文档: https://redux.js.org/
  2. 中文文档: http://www.redux.org.cn/
  3. Github: https://github.com/reactjs/redux
    8.1.2. redux 是什么?
  4. redux 是一个独立专门用于做状态管理的 JS 库(不是 react 插件库)
  5. 它可以用在 react, angular, vue 等项目中, 但基本与 react 配合使用
  6. 作用: 集中式管理 react 应用中多个组件共享的状态
    8.1.3. redux 工作流程

1581259109591

8.1.4. 什么情况下需要使用 redux

  1. 总体原则: 能不用就不用, 如果不用比较吃力才考虑使用
  2. 某个组件的状态,需要共享
  3. 某个状态需要在任何地方都可以拿到
  4. 一个组件需要改变全局状态
  5. 一个组件需要改变另一个组件的状态

8.2. redux 的核心 API

8.2.1. createStore()

  1. 作用:
    创建包含指定 reducer 的 store 对象
  2. 编码:
1
2
3
import {createStore} from 'redux'
import counter from './reducers/counter'
const store = createStore(counter)

8.2.2. store 对象

  1. 作用: redux 库最核心的管理对象
  2. 它内部维护着:
    state
    reducer
  3. 核心方法:
    getState()
    dispatch(action)
    subscribe(listener)
  4. 编码:
    store.getState()
    store.dispatch({type:‘INCREMENT’, number})

​ store.subscribe(render)

8.2.3. applyMiddleware()

  1. 作用:
    应用上基于 redux 的中间件(插件库)
  2. 编码:
1
2
3
4
5
6
import {createStore, applyMiddleware} from 'redux' 
import thunk from 'redux-thunk' // redux 异步中间件
const store = createStore(
counter,
applyMiddleware(thunk) // 应用上异步中间件
)

8.2.4. combineReducers()

  1. 作用:
    `合并多个 reducer 函数
  2. 编码:
1
2
3
4
5
export default combineReducers({
user,
chatUser,
chat
})

8.3. redux 的三个核心概念

8.3.1. action

  1. 标识要执行行为的对象
  2. 包含 2 个方面的属性
    a. type: 标识属性, 值为字符串, 唯一, 必要属性
    b. xxx: 数据属性, 值类型任意, 可选属性
  3. 例子:
1
2
3
const action = {
type: 'INCREMENT', data: 2
}
  1. Action Creator(创建 Action 的工厂函数)
1
const increment = (number) => ({type: 'INCREMENT', data: number})

8.3.2. reducer

  1. 根据老的 state 和 action, 产生新的 state 的纯函数
  2. 样例
1
2
3
4
5
6
7
8
9
10
export default function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + action.data
case 'DECREMENT':
return state - action.data
default:
return state
}
}
  1. 注意
    a. 返回一个新的状态
    b. 不要修改原来的状态

8.3.3. store

  1. 将 state,action 与 reducer 联系在一起的对象
  2. 如何得到此对象?
1
2
3
import {createStore} from 'redux' 
import reducer from './reducers'
const store = createStore(reducer)
  1. 此对象的功能?
    getState(): 得到 state
    dispatch(action): 分发 action, 触发 reducer 调用, 产生新的 state

subscribe(listener): 注册监听, 当产生了新的 state 时, 自动调用

8.4. 使用 redux 编写应用

8.4.1. 效果

8.4.2. 下载依赖包
npm install --save redux
8.4.3. redux/action-types.js

1
2
3
4
5
/*
action 对象的 type 常量名称模块
*/
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement

redux/actions.js

1
2
3
4
5
6
/*
action creator 模块
*/
import {INCREMENT, DECREMENT} from './action-types'
export const increment = number => ({type: INCREMENT, number})
export const decrement = number => ({type: DECREMENT, number})

8.4.5. redux/reducers.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*根据老的 state 和指定 action, 处理返回一个新的 state
*/
import { INCREMENT, DECREMENT } from '../constants/ActionTypes'
import { INCREMENT, DECREMENT } from './action-types'
export function counter(state = 0, action) {
console.log('counter', state, action)
switch (action.type) {
case INCREMENT:
return state + action.number
case DECREMENT:
return state - action.number
default:
return state
}
}

8.4.6. components/app.jsx

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
49
50
51
52
/*应用组件
*/
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import * as actions from '../redux/actions'
export default class App extends Component {
static propTypes = {
store: PropTypes.object.isRequired,
}
increment = () => {
const number = this.refs.numSelect.value * 1
this.props.store.dispatch(actions.increment(number))
}
decrement = () => {
const number = this.refs.numSelect.value * 1
this.props.store.dispatch(actions.decrement(number))
}
incrementIfOdd = () => {
const number = this.refs.numSelect.value * 1
let count = this.props.store.getState()
if (count % 2 === 1) {
this.props.store.dispatch(actions.increment(number))
}
}
incrementAsync = () => {
const number = this.refs.numSelect.value * 1
setTimeout(() => {
this.props.store.dispatch(actions.increment(number))
}, 1000)
}
render() {
return (
<div>
<p>
click {this.props.store.getState()} times {' '}
</p>
<select ref="numSelect">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>{' '}
<button onClick={this.increment}>+</button>
{' '}
<button onClick={this.decrement}>-</button>
{' '}
<button onClick={this.incrementIfOdd}>increment if odd</button>
{' '}
<button onClick={this.incrementAsync}>increment async</button>
</div>
)
}
}

8.4.7. index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import App from './components/app'
import { counter } from './redux/reducers'
// 根据 counter 函数创建 store 对象
const store = createStore(counter)
// 定义渲染根组件标签的函数
const render = () => {
ReactDOM.render(
<App store={store} />,
document.getElementById('root')
)
}
// 初始化渲染
render()
// 注册(订阅)监听, 一旦状态发生改变, 自动重新渲染
store.subscribe(render)

8.4.8. 问题

  1. redux 与 react 组件的代码耦合度太高
  2. 编码不够简洁

8.5. react-redux

8.5.1. 理解

  1. 一个 react 插件库
  2. 专门用来简化 react 应用中使用 redux

8.5.2. React-Redux 将所有组件分成两大类

  1. UI 组件
    a. 只负责 UI 的呈现,不带有任何业务逻辑
    b. 通过 props 接收数据(一般数据和函数)
    c. 不使用任何 Redux 的 API
    d. 一般保存在 components 文件夹下
  2. 容器组件
    a. 负责管理数据和业务逻辑,不负责 UI 的呈现
    b. 使用 Redux 的 API
    c. 一般保存在 containers 文件夹下
    8.5.3. 相关 API
  3. Provider
1
2
3
4
// 让所有组件都可以得到 state 数据
<Provider store={store}>
<App />
</Provider>
  1. connect()
1
2
3
4
5
6
用于包装 UI 组件生成容器组件
import { connect } from 'react-redux'
connect(
mapStateToprops,
mapDispatchToProps
)(Counter)
  1. mapStateToprops()
1
2
3
4
5
6
// 将外部的数据(即 state 对象)转换为 UI 组件的标签属性
const mapStateToprops = function (state) {
return {
value: state
}
}
  1. mapDispatchToProps()
    将分发 action 的函数转换为 UI 组件的标签属性
    简洁语法可以直接指定为 actions 对象或包含多个 action 方法的对象

8.5.4. 使用 react-redux

  1. 下载依赖包
1
npm install --save react-redux
  1. redux/action-types.js
    不变
  2. redux/actions.js
    不变
  3. redux/reducers.js
    不变
  4. components/counter.jsx
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
49
50
51
52
53
54
/*
UI 组件: 不包含任何 redux API
*/
import React from 'react'
import PropTypes from 'prop-types'
export default class Counter extends React.Component {
static propTypes = {
count: PropTypes.number.isRequired,
increment: PropTypes.func.isRequired,
decrement: PropTypes.func.isRequired
}
increment = () => {
const number = this.refs.numSelect.value * 1
this.props.increment(number)
}
decrement = () => {
const number = this.refs.numSelect.value * 1
this.props.decrement(number)
}
incrementIfOdd = () => {
const number = this.refs.numSelect.value * 1
let count = this.props.count
if (count % 2 === 1) {
this.props.increment(number)
}
}
incrementAsync = () => {
const number = this.refs.numSelect.value * 1
setTimeout(() => {
this.props.increment(number)
}, 1000)
}
render() {
return (
<div>
<p>
click {this.props.count} times {' '}
</p>
<select ref="numSelect">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>{' '}
<button onClick={this.increment}>+</button>
{' '}
<button onClick={this.decrement}>-</button>
{' '}
<button onClick={this.incrementIfOdd}>increment if odd</button>
{' '}
<button onClick={this.incrementAsync}>increment async</button>
</div>
)
}
}
  1. containters/app.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
/*包含 Counter 组件的容器组件
*/
import React from 'react'
// 引入连接函数
import { connect } from 'react-redux'
// 引入 action 函数
import { increment, decrement } from '../redux/actions'
import Counter from '../components/counter'
// 向外暴露连接 App 组件的包装组件
export default connect(
state => ({ count: state }),
{ increment, decrement }
)(Counter)
  1. index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './containers/app'
import { counter } from './redux/reducers'
// 根据 counter 函数创建 store 对象
const store = createStore(counter)
// 定义渲染根组件标签的函数
ReactDOM.render(
(
<Provider store={store}>
<App />
</Provider>
),
document.getElementById('root')
)

8.5.5. 问题

  1. redux 默认是不能进行异步处理的, 2) 应用中又需要在 redux 中执行异步任务(ajax, 定时器)

8.6. redux 异步编程

8.6.1. 下载 redux 插件(异步中间件)

1
npm install --save redux-thunk

8.6.2. index.js

1
2
3
4
5
6
7
import {createStore, applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
// 根据 counter 函数创建 store 对象
const store = createStore(
counter,
applyMiddleware(thunk) // 应用上异步中间件
)

8.6.3. redux/actions.js

1
2
3
4
5
6
7
8
// 异步 action creator(返回一个函数)
export const incrementAsync = number => {
return dispatch => {
setTimeout(() => {
dispatch(increment(number))
}, 1000)
}
}

8.6.4. components/counter.jsx

1
2
3
4
incrementAsync = () => {
const number = this.refs.numSelect.value*1
this.props.incrementAsync(number)
}

8.6.5. containers/app.jsx

1
2
3
4
5
6
import {increment, decrement, incrementAsync} from '../redux/actions'
// 向外暴露连接 App 组件的包装组件
export default connect(
state => ({count: state}),
{increment, decrement, incrementAsync}
)(Counter)

8.7. 使用上 redux 调试工具

8.7.1. 安装 chrome 浏览器插件

1
redux-devtools

8.7.2. 下载工具依赖包

1
npm install --save-dev redux-devtools-extension

8.7.3. 编码

1
2
3
4
5
import { composeWithDevTools } from 'redux-devtools-extension'
const store = createStore(
counter,
composeWithDevTools(applyMiddleware(thunk))
)

8.8. 相关重要知识: 纯函数和高阶函数

8.8.1. 纯函数

  1. 一类特别的函数: 只要是同样的输入,必定得到同样的输出
  2. 必须遵守以下一些约束
    a. 不得改写参数
    b. 不能调用系统 I/O 的 API
    c. 能调用 Date.now()或者 Math.random()等不纯的方法
  3. reducer 函数必须是一个纯函数

8.8.2. 高阶函数
4) 理解: 一类特别的函数
a. 情况 1: 参数是函数
b. 情况 2: 返回是函数

  1. 常见的高阶函数:
    a. 定时器设置函数
    b. 数组的 map()/filter()/reduce()/find()/bind()
    c. react-redux 中的 connect 函数
  2. 作用:
    a. 能实现更加动态, 更加可扩展的功能
-------文章到此结束  感谢您的阅读-------