React数据流
react作为状态驱动的前端框架,十分注重数据和状态的管理。react的状态管理和组件通信主要包括:
- 变与不变:不可变的属性(props)和可变的状态(state);
- 组件间的通信:基于props的父子组件之间的数据传递;
- 上下文(context):组件间不直接相互依赖的数据传递;
- 复杂状态管理解决方案:MobX。
props与state
我们在开发应用时,必然是要与数据进行频繁交互的。React 的核心思想是组件化的思想,所有的页面会被切分成一些独立的、可复用的组件。 那么数据从何而来呢?React 的数据是自顶向下单向流动的,即从父组件到子组件中,组件的数据存储在 props 和 state 中。
UI = Component(props, state)
props 和 state 本质
props 是组件对外的接口,state 是组件对内的接口。 组件内可以引用其他组件,组件之间的引用形成了一个树状结构(组件树),如果下层组件需要使用上层组件的数据或方法,上层组件就可以通过下层组件的 props 属性进行传递,因此 props 是组件对外的接口。组件除了使用上层组件传递的数据外,自身也可能需要维护管理数据,这就是组件对内的接口 state。根据对外接口 props 和对内接口 state,组件计算出对应界面的UI。
<Component data="测试props"/>
import Item from "./item";
export default class ItemList extends React.Component{
render(){
return (
<div> <Item data="我是props值"/>, </div>
) }
}
export default class Item extends React.Component{
render(){
return (
<h1>{this.props.data}</h1>
) }
}
只读性
props 是组件的只读属性,组件内部不能直接修改 props,要想修改 props,只能在该组件的上层组件中修改。在组件状态上移的场景中,父组件正是通过子组件的 props,传递给子组件其所需要的状态。
props 经常被用作渲染组件和初始化状态,当一个组件被实例化之后,它的 props 是只读的,不可改变的。如果 props 在渲染过程中可以被改变,会导致这个组件显示的形态变得不可预测。只有通过父组件重新渲染的方式才可以把新的 props 传入组件中。
state
state 的主要作用是用于组件保存、控制以及修改自己的状态,它只能在 constructor 中初始化,它是组件的私有属性,不可通过外部访问和修改,只能通过组件内部的 this.setState 来修改,修改 state 属性会导致组件的重新渲染。
在组件初始化的时候,通过
this.state给组件设定一个初始的state,在第一次render的时候就会用这个数据来渲染组件。
我们既然要创建组件,那么就必然需要定义一个能代表一个组件UI呈现的完整状态集,也就是说:组件对应UI的任何改变,都可以从 state 的变化中反映出来;同时,state 还必须是代表一个组件UI呈现的最小状态集,即 state 中的所有状态都是用于反映组件UI的变化,没有任何多余的状态,也不需要通过其他状态计算而来的中间状态。
⚠️ 并不是组件中用到的所有变量都是组件的状态! 当存在多个组件共同依赖同一个状态时,一般的做法是状态上移,将这个状态放到这几个组件的公共父组件中。
正确修改state
1)首先,不能直接修改 State
state 不同于 props 的一点是:state 是可以被改变的。不过,不可以直接通过 this.state=xxx 的方式来修改,而需要通过 this.setState() 方法来修改 state。
this.setState({title: 'React'});
⚠️ 通过 this.state=xx来 初始化 state,使用 this.setState 来 修改state,constructor 是唯一能够初始化的地方。
2)setState 可以接收多个参数
setState接受一个对象或者函数作为第一个参数,只需要传入需要更新的部分即可,不需要传入整个对象。
export default class ItemList extends React.Component{
constructor(){
super();
this.state = {
name: '八了个戒',
age: 25,
}
}
componentDidMount(){
this.setState({age: 18})
}
}
在执行完 setState 之后的 state 应该是 {name: '八了个戒', age: 18}。
setState还可以接受第二个参数,它是一个函数,会在setState调用完成并且组件开始重新渲染时被调用,可以用来监听渲染是否完成。
this.setState({
name: '八戒'
},()=>console.log('setState finished')
)
3)State 的更新是异步的
调用 setState,组件的 state 并不会立即改变,setState 只是把要修改的状态放入一个队列中,React 会优化真正的执行时机,并且 React 会出于性能原因,可能会将多次 setState 的状态修改合并成一次状态修改。
所以不能依赖当前的 state,计算下个 state。当真正执行状态修改时,依赖的 this.state 并不能保证是最新的 state,因为 React 会把多次 state 的修改合并成一次,这时,this.state 还是等于这几次修改发生前的 state。另外需要注意的是,同样不能依赖当前的 props 计算下个 state,因为 props 的更新也是异步的。
组件通信
- 父组件——子组件
- 子组件——父组件
- 跨级组件
- 兄弟组件
父传子:props和自定义函数事件
父组件可以直接向子组件传递 props,子组件接收到数据后进行更新,再同步给父组件。
function Money() {
const [selected, setSelected] = useState('food');
return (
<TagsSection value={selected}/>
);
}
type Props = {
//声明props的数据类型
value: string;
}
const TagsSection: React.FC<Props> = (props) => {
const tagName = props.value;
return (
<div>the tag name is { tagName }</div>
);
};
子传父:props.onchange
props.onchange 相当于子组件发给父组件的一个提醒:我这里的数据变了,你那边也要更新!
const TagsSection: React.FC<Props> = (props) => {
const onToggleTag = (tagId: number) => {
const index = selectedTagIds.indexOf(tagId);
if (index >= 0) {
props.onChange(selectedTagIds.filter(t => t !== tagId));
} else {
props.onChange([...selectedTagIds, tagId]);
}
};
}
子组件 TagsSection 内部有个 onToggleTag 函数,判断传入的 tagId 索引是否大于或等于0。如果大于等于0,那么使用 selectedTagIds.filter 方法,然后用 props.onchange 把数据传给父组件,让父组件来改变数据。
注意:如果有需要同步给父组件的数据,请务必使用 props.onchange,否则子组件自己改了数据,父组件都不知道,拿就无法同步了。
跨级组件:Context
一般跨级组件都是一些多组件公用的数据,比如我们登录的用户信息、UI主题等等。如果组件的层级比较深,如果只是用 props ,只能通过一层一层的组件来接力下去,还挺麻烦的。
function App(){
return ( <Toolbar theme="dark"> )
}
function Toolbar(props) {
return ( <div> <ThemeButton theme={props.theme} /> </div> );
}
function ThemeButton(){
return ( <Button theme={props.theme} /> )
}
在上面的代码中,子组件 Button 想要拿到 themecolor ,那么就得把 theme 作为 props,从 App 一路传到 ThemeButton。
针对这样的情况,React 提供了Context,可以实现多层级的组件穿透。
const ThemeContext = React.createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider> );
}
function Toolbar() {
return (
<div> <ThemeButton /> </div> );
}
function ThemeButton() {
render() {
return (
<ThemeContext.Consumer>
{value=><Button theme={value} />}
</ThemeContext.Consumer> ); } }
React.createContext 创建了一个 context 对象,如果某个组件订阅了这个对象,在渲染的时候,会从离这个组件最近的一个 Provider 组件中获取当前 context。
Context 的使用方式:
- 使用
const xxx = createContext(null)创建上下文 - 使用
<xxx.Provider>圈定作用域 - 在作用域内使用
useContext(xxx)来使用上下文
MobX
MobX(MobX-state-tree)是一个状态管理库,用于管理JavaScript应用程序中的状态。它提供了一种简单而强大的方式来管理应用程序的状态和响应状态变化。与Redux等其他状态管理库不同,MobX更加自由和灵活,更强调直观性和可维护性。
- Observables(可观察对象):
- 可观察对象是应用程序中需要进行状态跟踪的对象。在MobX中,你可以使用
observable函数将对象转换为可观察对象。
import { observable } from 'mobx';
const appState = observable({
count: 0,
increment() {
this.count += 1;
},
});
- Actions(动作):
- 动作是修改可观察对象的方法。在MobX中,使用
@action装饰器或action函数来定义动作。
import { action } from 'mobx';
const appState = observable({
count: 0,
@action
increment() {
this.count += 1;
},
});
3.Reactions(反应):
- 反应是对可观察对象变化的响应。可以使用
autorun、reaction等函数来定义响应。
import { autorun } from 'mobx';
autorun(() => {
console.log(`Count: ${appState.count}`);
});
// 每次 count 发生变化时,上述 autorun 中的回调都会执行。
4.Computeds(计算属性):
- 计算属性是基于可观察对象的状态派生出的值。使用
computed函数定义计算属性。
import { computed } from 'mobx';
const appState = observable({
count: 0,
});
const doubleCount = computed(() => appState.count * 2);
5.使用MobX React:
mobx-react是一个用于将MobX和React集成的库。它提供了Provider和inject等组件,使得在React组件中可以更方便地使用MobX的状态。
import { Provider, inject, observer } from 'mobx-react';
// 注入状态并观察状态变化的 React 组件
@inject('appState')
@observer
class MyComponent extends React.Component {
render() {
const { appState } = this.props;
return <div>{appState.count}</div>;
}
}
// 使用 Provider 将状态提供给整个应用程序
ReactDOM.render(
<Provider appState={appState}>
<MyComponent />
</Provider>,
document.getElementById('root')
);
Mobx 6.x 中创建可观察状态
在Mobx 6.x或更高版本中,推荐使用makeObservable 和 makeAutoObservable 将普通 JavaScript 对象或类转换为可观察对象的函数。它们可以自动地将类的属性声明为可观察状态,并根据需要生成相应的 getter 和 setter 方法。
1. makeObservable
makeObservable 是 Mobx 中用来将普通对象转换成可观察对象的函数。需要手动定义每个属性和方法的可观察类型。
makeObservable(target, annotations?, options?)
-
target: 要转换成可观察对象的目标对象,一般是
this。 -
annotations: 一个对象,用来指定哪些属性需要被转换成可观察属性,以及它们的行为和类型。它将会为每一个成员映射 注解;以下是它可能的常用元注解(定义注解的注解):
deep: 表示该属性是否进行深度观察。如果值为true,那么当该属性是对象或数组时,其子属性也会被观察。默认为false。default: 该属性的默认值,在属性值未初始化时使用。ref: 表示该属性是否为一个可变的引用(mutable)。
-
options:配置对象,它可以设置一些选项来影响注解的行为。有两个常用的选项:
autoBind:这个选项默认为false,表示不自动绑定 action 方法的 this 指向。如果设置为 true,表示使 action 方法始终拥有正确的 this 指向,而不需要使用 bind 或箭头函数来绑定。proxy:这个选项默认为true,表示使用 Proxy 包装来创建 observable 属性。如果设置为 false,表示禁用 Proxy 包装,使用 defineProperty 来创建 observable 属性。这样可以提高兼容性,但是会失去一些特性,例如动态添加或删除属性。name: <string>: 为对象提供一个在错误消息和反射 API 中打印的调试名称。 注意:
observable.ref是一种禁用自动转换的注解,它只会创建一个可观察的引用,而不会改变赋值给字段的值本身。这意味着如果赋值的值是一个对象、数组、Map 或 Set,那么它们的属性或元素不会变成可观察的。
下面是一个使用 makeObservable 的完整示例:
import { makeObservable, observable, computed, action } from "mobx";
class Counter {
count = 0;
multiplier = 2;
address = { city: "Beijing", country: "China", };
constructor() {
makeObservable(this, {
count: observable,
multiplier: observable,
doubledCount: computed,
increment: action,
address: observable.shallow,
});
}
get doubledCount() {
return this.count * this.multiplier;
}
increment() {
this.count++;
}
}
const counter = new Counter();
2. makeAutoObservable
makeAutoObservable用于自动将对象的属性和方法标记为可观察的。这使得当这些属性或方法发生变化时,MobX 可以追踪并触发相关的副作用(如重新渲染 UI)。
makeAutoObservable(target, overrides?, options?)
-
target: 要转化的目标对象,通常是 this。
-
overrides: 覆盖默认的推断规则,可以指定某些属性或方法的注解,或者用 false 排除它们。
-
options: 配置对象,它可以设置一些选项来影响注解的行为。有以下常用的选项:
autoBind:这个选项默认为true,表示不自动绑定 action 方法的 this 指向。如果设置为 true,表示使 action 方法始终拥有正确的 this 指向,而不需要使用 bind 或箭头函数来绑定。proxy:这个选项默认为false,表示使用 Proxy 包装来创建 observable 属性。如果设置为 false,表示禁用 Proxy 包装,使用 defineProperty 来创建 observable 属性。这样可以提高兼容性,但是会失去一些特性,例如动态添加或删除属性。name: <string>: 为对象提供一个在错误消息和反射 API 中打印的调试名称。
以下是一个使用makeAutoObservable的示例:
class Person {
firstName = "Mob"
lastName = "X"
age = 0
hobbies = []
constructor() {
makeAutoObservable(this, {
// 排除 age 不转化为 observable
age: false,
// 排除 hobbies 不转化为 observable
hobbies: false
})
}
get fullName() {
return this.firstName + " " + this.lastName
}
setFirstName(name) {
this.firstName = name
}
setLastName(name) {
this.lastName = name
}
}
第二个参数overrides可能的属性或方法的注解有以下几种:
- observable: 定义一个存储状态的可追踪字段,可以是普通值,对象,数组,Map 或 Set。
- computed: 定义一个从状态派生出新值并缓存其输出的 getter。
- action: 定义一个可以修改状态的方法。
- autoAction: 定义一个可以修改状态的方法,但是不需要显式地使用 runInAction 包装。
- flow: 定义一个可以修改状态的生成器函数,可以使用 yield 暂停和恢复执行。
- false: 排除一个属性或方法,不转化为可观察的。
3、第三个参数 options
makeObservable 和 makeAutoObservable 都是用于创建可观察状态的函数,它们都可以接受第三个参数,也就是 options 参数。其中,autoBind 和 proxy 是两个常用的选项。
- autoBind:这个选项可以设置为 true,使 action 方法始终拥有正确的 this 指向,而不需要使用 bind 或箭头函数来绑定。
import { makeAutoObservable } from "mobx"
class CounterStore {
// 定义一个可观察的数值属性
count = 0
constructor() {
// 使用 makeAutoObservable 自动转化 this 对象的属性和方法,并设置 autoBind 为 true
makeAutoObservable(this, undefined, { autoBind: true })
}
// 定义一个 action 方法,不需要使用 bind 或箭头函数来绑定 this
increment() {
this.count++
}
}
// 或者使用 makeObservable 手动转化,并设置 autoBind 为 true
import { makeObservable, observable, action } from "mobx"
class CounterStore {
// 定义一个可观察的数值属性
count = 0
constructor() {
// 使用 makeObservable 转化 this 对象的属性和方法,并设置 autoBind 为 true
makeObservable(this, {
count: observable,
increment: action
}, { autoBind: true })
}
// 定义一个 action 方法,不需要使用 bind 或箭头函数来绑定 this
increment() {
this.count++
}
}
- proxy:这个选项可以设置为 false,禁用 Proxy 包装,使用 defineProperty 来创建 observable 属性。这样可以提高兼容性,但是会失去一些特性,例如动态添加或删除属性。
import { makeAutoObservable } from "mobx"
class BoxStore {
// 定义一个可观察的对象属性
box = { width: 0, height: 0 }
constructor() {
// 使用 makeAutoObservable 自动转化 this 对象的属性和方法,并设置 proxy 为 false
makeAutoObservable(this, undefined, { proxy: false })
}
}
// 或者使用 makeObservable 手动转化,并设置 proxy 为 false
import { makeObservable, observable } from "mobx"
class BoxStore {
// 定义一个可观察的对象属性
box = { width: 0, height: 0 }
constructor() {
// 使用 makeObservable 转化 this 对象的属性和方法,并设置 proxy 为 false
makeObservable(this, {
box: observable
}, { proxy: false })
}
}
- name: 表示给该属性一个名称,在控制台中,可以看到可观察的属性或方法的名称,方便在调试时更好地识别它。
class Person {
// 使用 name 注解给 firstName 属性指定一个名称
firstName = observable("Michel", { name: "myfirstName" });
// 不使用 name 注解
lastName = observable("Weststrate");
constructor() {
// 使用 makeObservable 函数来使类实例具有可观察性和反应性
// 使用 name 选项来给类实例指定一个名称
makeAutoObservable(
this,
undefined,
{ name: "myPerson" }
);
}
}
// 或者使用 makeObservable
class Person {
// 使用 name 注解给 firstName 属性指定一个名称
firstName = observable("Michel", { name: "myfirstName" });
// 不使用 name 注解
lastName = observable("Weststrate");
constructor() {
// 使用 makeObservable 函数来使类实例具有可观察性和反应性
// 使用 name 选项来给类实例指定一个名称
makeObservable(
this,
{ firstName: observable, lastName: observable },
{ name: "myPerson" }
);
}
}
name注解的作用:
- 在控制台中,可以看到可观察的属性或方法的名称,而不是默认的匿名函数或对象。这样可以更容易地定位问题或查看状态变化。
- 在 MobX 开发者工具中,可以看到可观察的属性或方法的名称,而不是默认的
observable或action。这样可以更清晰地展示状态树或动作日志。 - 在 MobX 跟踪器中,可以看到可观察的属性或方法的名称,而不是默认的
observable或action。这样可以更方便地分析性能或依赖关系。
注意事项
makeObservable需要手动指定要标记的属性和方法,并通过传递修饰器来标记。makeAutoObservable不需要手动指定要标记的属性和方法,会自动检测类中的所有属性并进行标记。- 它们都是在类的构造函数中使用的,并且只能在构造函数中调用一次。它们会修改原始对象并添加属性拦截器来实现 MobX 的响应式行为。
- 当使用
makeObservable或makeAutoObservable将类的属性声明为可观察状态时,它们会自动为每个属性生成相应的 getter 和 setter 方法。
以下是一个示例,演示了如何使用 makeObservable 和 makeAutoObservable 自动将属性声明为可观察状态并生成 getter 和 setter 方法:
import { makeObservable, makeAutoObservable, observable } from 'mobx';
class StoreWithMakeObservable {
value = 0;
constructor() {
makeObservable(this, {
value: observable,
});
}
}
class StoreWithMakeAutoObservable {
value = 0;
constructor() {
makeAutoObservable(this);
}
}
const store1 = new StoreWithMakeObservable();
const store2 = new StoreWithMakeAutoObservable();
console.log(store1.value); // 输出:0
store1.value = 10;
console.log(store1.value); // 输出:10
console.log(store2.value); // 输出:0
store2.value = 20;
console.log(store2.value); // 输出:20
Mobx 中的异步操作
在处理异步操作时,MobX 提供了 runInAction 函数,它用于确保在 action 中执行的异步操作能够正确追踪状态的变化。通常,定时器和异步操作包括网络请求等,这些操作可能在当前 action 执行结束后才完成。
// 请在较低版本中执行以下代码
import { observable, action, runInAction } from 'mobx';
class UserStore {
@observable user = null;
@action
async fetchUser() {
try {
const response = await fetch('https://api.example.com/user');
const userData = await response.json();
runInAction(() => {
this.user = userData;
});
} catch (error) {
runInAction(() => {
// 处理错误
console.error('Error fetching user:', error);
});
}
}
}
const userStore = new UserStore();
在上述示例中,runInAction 函数用于包装异步操作。在 runInAction 的回调函数内部,我们可以安全地修改状态。这确保了状态修改发生在 action 内部,以便 MobX 能够正确地追踪状态的变化。
注意: runInAction 可以嵌套使用,以包装多个状态修改操作。
Mobx 管理 React 中的状态
如果当前react项目需要启用mobx管理状态,则需要用到Provider和@inject两个API。用法如下:
Provider是mobx-react库中的一个 React 组件,用于在应用程序中提供 MobX 存储的状态。它通常与@inject装饰器一起使用,以确保组件可以访问和响应 MobX 存储的状态。
创建 Provider:
首先,我们需要使用 Provider 组件包装整个 React 应用的根组件。这通常发生在应用的入口文件中。例如:
import React from 'react';
import { Provider } from "mobx-react"
import todoStore from './todoStore'
class App extends Component{
render(){
return(
<Provider store={...todoStore}>
<ToDoApp/>
</Provider>
)
}
}
TodoStore示例
import { observable, action, makeObservable } from 'mobx';
class TodoStore {
todos = [];
constructor() {
makeObservable(this, {
todos: observable,
addTodo: action,
});
}
addTodo(text) {
this.todos.push({ text, completed: false });
}
}
const todoStore = new TodoStore();
export default todoStore;
注意事项:
- 我们可以同时提供多个存储给
Provider,只需添加多个属性即可,例如<Provider store1={store1} store2={store2}>。 @inject装饰器的参数需要与我们在Provider中提供的属性名称一致,以便注入正确的存储。- Mobx 需要我们使用 observer 高阶组件或者 useObserver 钩子函数来包裹需要响应数据变化的组件。如果忘记了这一步,可能会导致组件无法正确地重新渲染。
- Mobx 需要我们使用装饰器或者
makeAutoObservable等方法来标记可观察对象,动作和计算属性。 - Mobx 需要我们使用特定的 API 来修改可观察对象,例如
observable、action、computed等。如果直接使用赋值运算符或者原生数组方法,可能会导致 Mobx 无法检测到数据的变化。
mobx官方文档Creating observable state | MobX中文文档 | MobX中文网 (mobxjs.com)