Redux专题内容简介:
combineReducers({
counter: CounterReducer,
modal: ModalReducer
})
// 最后的reducer返回对象为: { counetr: {count: 0}, modal: {show: false}}
// 柯里化函数形式
export default store => next => action => {}
只有把创建好的中间件注册给store,那他才能够在redux的工作流程中生效。它的注册需要使用到redux的一个 applyMiddleware 方法,这个方法就是用来注册中间件的,该方法的返回值放在 createStore 方法的第二个参数中,这样一来,当组件去触发action的时候,这个中间件的代码就可以得到执行了。
如果注册的中间件有多个,那么它的执行顺序是如何的呢?
它会按照注册的顺序分别调用相对应的中间件,当第一个中间件调用执行完成以后,它里面会调用next方法返回action对象传递给下一个中间件,依次类推。
注意:中间件必须要调用next(action),把action 外后面传递,后面的中间件以及最后的reducer才能正确的接收到action并执行。
值得注意的是:
1、当前的中间件函数是不关心你想执行什么样的异步操作的,只关心你执行的是不是异步操作。
2、如果你执行的是异步操作,你在触发action的时候给中间件传递一个函数, 如果执行的是同步操作,就传递action对象。
3、异步操作代码要写在传递进来的函数当中。
4、当前这个中间件函数在调用你传递进来的函数时,要将dispatch 方法传递进来。
export default ({dispatch}) => next => action => {
if(typeof action === 'function') {
return action(dispatch)
}
next(action)
}
// 使用 redux-actions 来创建 action
import { createAction } from 'redux-actions'
const increment_action = createAction('increment') // 第一个字符串就是原来我们所创建的action对象中 type 的属性值
const decrement_action = createAction('decrement')
这里的 createAction就相当于我们之前自己所定义ActionCreator 函数。// counter reducer 使用 redux-actions 来实现
import { handleActions as createReducer } from 'redux-actions'
import { increment_action, decrement_action } from '../actions/counter.action'
const initialState = {count: 0}
const counterReducer = createReducer({
[increment_action]: (state, action) => ({ count: state.count + 1}),
[decrement_action]: (state, action) => ({ count: state.count - 1})
}, initialState)
export default counterReducer
shopping购物车案例进本实现思路:Redux源码实现:核心逻辑
redux中主要是一个createStore方法,该方法接收三个参数,createStore(reducer, preloaddedState, enhancer)。
reducer 是根据action的 类型来对 store 当中的状态进行更改。
preloadedState 是预先存储的初始化state 状态对象。
enhancer 是对store的功能进行增强,是我们所说的插件应用。
// redux的核心逻辑 createStore 方法
function createStore(reducer, preloadedState, enhancer) {
// 保存初始化store 状态对象, 需要在后续的方法中使用到,所需需要在方法中形成闭包,以便该值不会在createStore方法调用后被回收
var currentState = preloadedState;
var listeners = [];
// 定义getState 方法,返回当前store 中的currentState 对象
function getState() {
return currentState;
}
// 定义 dispatch 方法,用于触发action, 从而把action 交给 reducer 进行处理,并且将reducer处理过后的返回值更新到 currentState 的引用上,最后遍历所有已注册的订阅者,触发相对应的后续操作,如页面更新等
function dispatch (action) {
// 把action 交给 reducer 进行处理,并且将reducer处理过后的返回值更新到 currentState 的引用上
currentState = reducer(action);
// 遍历所有已注册的订阅者,触发相对应的后续操作,如页面更新等
for(let i = 0; i<= listeners.length; i++) {
var listener = listeners[i]
listener()
}
}
// 定义subscribe 方法,用于为store 添加的订阅者
function subscribe(listener) {
listeners.push(listener)
}
return {
getState, // 获取store的状态
dispatch, // 触发action
subscribe // 订阅状态
}
}
上面的代码实现过去简单粗暴,对参数的类型没有做限制,但参数不符合的时候没有错误提示,整个运行会直接挂掉。所以这里我们需要改进一下,对一些细节问题作容错处理。
1、 reducer 参数的类型必须是一个函数,该函数有两个参数,state 和 action 分别指示 store 中的状态以及本次action的对象。
2、是判断 dispatch 方法中的参数对象,action 中是否是对象,且对象中包含type 属性。
// redux的核心逻辑 createStore 方法
function createStore(reducer, preloadedState, enhancer) {
// 约束reducer 参数类型,它必须是一个function 类型
if(typeof reducer !== 'function') throw new TypeError('reducer must be a function');
// 保存初始化store 状态对象, 需要在后续的方法中使用到,所需需要在方法中形成闭包,以便该值不会在createStore方法调用后被回收
var currentState = preloadedState;
var listeners = [];
// 定义getState 方法,返回当前store 中的currentState 对象
function getState() {
return currentState;
}
// 定义 dispatch 方法,用于触发action, 从而把action 交给 reducer 进行处理,并且将reducer处理过后的返回值更新到 currentState 的引用上,最后遍历所有已注册的订阅者,触发相对应的后续操作,如页面更新等
function dispatch (action) {
// 判断action 是否为对象
if(!isPlainObject(action)) throw new TypeError('action must be a plain object');
// 判断action 对象中是否有type 属性
if(typeof action.type === 'undefined') throw new Error('action object must contain a type property');
// 把action 交给 reducer 进行处理,并且将reducer处理过后的返回值更新到 currentState 的引用上
currentState = reducer(action);
// 遍历所有已注册的订阅者,触发相对应的后续操作,如页面更新等
for(let i = 0; i<= listeners.length; i++) {
var listener = listeners[i]
listener()
}
}
// 定义subscribe 方法,用于为store 添加的订阅者
function subscribe(listener) {
listeners.push(listener)
}
return {
getState, // 获取store的状态
dispatch, // 触发action
subscribe // 订阅状态
}
}
// 判断obj参数是否为对象
function isPlainObject(obj) {
// 排除基本数据类型和null
if(typeof obj !== 'object' || obj === null) return false;
// 区分数组和对象。采用原型对象对比的方式,对象的原型和其最顶层的原型类型是相同的,都是 Object
var proto = obj;
while(Object.getPrototypeOf(proto)!==null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(obj) === proto;
}
Rudex 源码之 enhancer 参数:
可以让createStore 这个方法的调用者对 返回的store 对象进行功能上的增强。 enhancer 参数可以不传,但是如果传了的话,那它必须得是一个具有固定格式的函数。
// redux的核心逻辑 createStore 方法
function createStore(reducer, preloadedState, enhancer) {
// 约束reducer 参数类型,它必须是一个function 类型
if(typeof reducer !== 'function') throw new TypeError('reducer must be a function');
// 判断enhancer 参数有没有传递
if(typeof enhancer !== 'undefined') {
// 判断enhancer 是不是一个函数
if(typeof enhancer !== 'function') throw new TypeError('enhancer must be a function');
return enhancer(createStore)(reducer, preloadedStore);
}
// 保存初始化store 状态对象, 需要在后续的方法中使用到,所需需要在方法中形成闭包,以便该值不会在createStore方法调用后被回收
var currentState = preloadedState;
var listeners = [];
// 定义getState 方法,返回当前store 中的currentState 对象
function getState() {
return currentState;
}
// 定义 dispatch 方法,用于触发action, 从而把action 交给 reducer 进行处理,并且将reducer处理过后的返回值更新到 currentState 的引用上,最后遍历所有已注册的订阅者,触发相对应的后续操作,如页面更新等
function dispatch (action) {
// 判断action 是否为对象
if(!isPlainObject(action)) throw new TypeError('action must be a plain object');
// 判断action 对象中是否有type 属性
if(typeof action.type === 'undefined') throw new Error('action object must contain a type property');
// 把action 交给 reducer 进行处理,并且将reducer处理过后的返回值更新到 currentState 的引用上
currentState = reducer(action);
// 遍历所有已注册的订阅者,触发相对应的后续操作,如页面更新等
for(let i = 0; i<= listeners.length; i++) {
var listener = listeners[i]
listener()
}
}
// 定义subscribe 方法,用于为store 添加的订阅者
function subscribe(listener) {
listeners.push(listener)
}
return {
getState, // 获取store的状态
dispatch, // 触发action
subscribe // 订阅状态
}
}
// 判断obj参数是否为对象
function isPlainObject(obj) {
// 排除基本数据类型和null
if(typeof obj !== 'object' || obj === null) return false;
// 区分数组和对象。采用原型对象对比的方式,对象的原型和其最顶层的原型类型是相同的,都是 Object
var proto = obj;
while(Object.getPrototypeOf(proto)!==null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(obj) === proto;
}
enhancer 参数的例子:模仿实现了类似redux-thunk
function enhancer(createStore) {
return function(reducer, preloadedState) {
var store = createStore(reducer, preloadedStore);
var dispatch = store.dispatch
function _dispatch(action) {
if(typeof action === 'function') {
action(dispatch)
}
dispatch(action)
}
return {
...store,
dispatch: _dispatch
}
}
}
enhancer的目的地是让用redux这个库的人可以对返回的store 进行功能上的一些增强操作,例如允许加入异步操作代码。
Redux源码之 applyMiddleware
中间件就是允许我们在action 发出之后, reducer 接收到action 之前,让我们去做一些事情。
本质上 redux 中间件 就是对 dispatch 这个方法进行增强。
例如logger 日志中间件
// logger.middleware.js
function logger(store) {
return (next) => {
return (action) => {
console.log(action)
next(action)
}
}
}
又如thunk 中间件允许我们执行异步代码操作
// thunk.middleware.js
function thunk(store) {
return (next) => {
return (action) => {
console.log(action)
next(action)
}
}
}
applyMiddleware 方法是如何让中间件先起作用的。
function applyMiddleware(...middlewares) {
return function (createStore) {
return function (reducer, proloadedState) {
// 这里的返回应该是跟上边提到过的enhancer是一样的
// 创建 store
var store = createStore(reducer, preloadedState);
// 构建中间件所需的阉割版的store API
var middlewareAPI = {
getState: store.getState,
dispatch: store.dispatch
};
// 调用中间件的第一层函数,传递阉割版的store 对象
var chain = middlewares.map(middleware => middleware(middlewareAPI));
var dispatch = compose(...chain)(store.dispatch);
// 返回增强过后的store
return {
...store,
dispatch
}
}
}
}
function compose() {
var funcs = [...arguments];
return function (dispatch) {
// 中间件需要按顺序调用,所以在座位参数传递的时候需要进行倒序处理一下。
for(var i = funcs.length; i>=0; i--) {
dispatch = funcs[i](dispatch);
}
return dispatch;
}
}
Redux源码实现之 bindActionCreators
function increment() {
return { type: 'increment'}
}
function decrement() {
return { type: 'decrement'}
}
var actions = bindActionsCreators({increment, decrement}, store.dispatch);
// 这样处理过后,就可以在订阅者侦听处理函数中,使用actions.increment() 或者 actions.decrement() 来触发action 显得更加的自然。
// bindActionsCreators 方法定义
function bindActionsCreators(actionCreators, dispatch) {
var boundActionCreators = {};
for (var key in actionCreators) {
// 为 key 构造闭包保护
(function(key){
boundActionCreators[key] = function() {
// 对外触发action
dispatch(actionCreators[key]())
}
})(key);
}
return boundActionCreators;
}
Redux源码实现之 combineReducers
我们可以把大的reducer 拆分为一个个 小的 reducer,然后再让我们通过combineReducers 这个 API 把一个个小的 reducer 组合成一个大的的 reducer。它的用法是: var rootReducer = combineReducers({ couter: counterReducer, model: modelReducer }); 他的最终返回值是一个 reducer 函数。它有两个参数,一个是state, 一个是action。
function combineReducers(reducers) {
// 需要完成两件事:
// 1、检查reducer 类型, 必须为函数
var reducerKeys = Object.keys(reducers);
for(var i = 0; i<= reducerKey.length; i++){
var key = reducerKey[i];
if(typeof reducers[key] !== 'function') throw new TypeError('reducer must be a function');
}
// 2、调用一个一个小的reducer, 将reducer中 返回的状态存储在一个新的大的对象中。
return function(state, action) {
var nextState = {};
// 循环执行一个个reducer
for(var i = 0; i<= reducerKey.length; i++){
var key = reducerKey[i];
var reducer = reducers[key];
var previousStateForKey = state[key];
// 把处理后的返回值存储在nextState中
nextState[key] = reducer(previousStateForKey, action);
}
return nextState;
}
}
Redux Toolkit (redux工具集)
它是官方对redux的二次封装,用于高效Redux开发,使得Redux的使用变得更简单。
npm install @reduxjs/toolkit
npm install react-redux
状态切片的概念:stateSlice
对于状态切片,我们可以认为它就是原本我们在redux中的那一个个的小的reducer函数。
在Redux 中,原本 Reducer 函数和 Action 对象需要分别创建,现在则通过状态切片替代,它会返回Reducer函数和 Action对象。
// 创建todos 状态切片
import { createSlice } from '@reduxjs/toolkit'
const TODO_SLICE_KEY = 'todos'
const { reducer: TodosReducer, actions } = createSlice({
name: TODO_SLICE_KEY,
initialState: [],
reducers: {
addTodo: (state, action) => {
state.push(action.payload)
}
}
})
export const { addTodo } = actions
export default TodosReducer
使用工具集创建Store
import { configureStore } from '@reduxjs/toolkit'
import TodosReducer, { TODO_SLICE_KEY } from './todos.slice'
export default configureStore({
reducer: {
[TODO_SLICE_KEY]: TodosReducer
},
devTools: process.env.NODE_ENV !== 'production'
})
配置Provider触发Action
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { addTodo, TODO_SLICE_KEY } from '../../store/todos.slice'
import { deleteTodo } from '../../store/todos.slice'
export default function (props) {
const dispatch = useDispatch()
const todos = useSelector(state => state[TODO_SLICE_KEY])
return (
{todos.map((todo, index) => (
-
{todo.id}
))}
)
}
Action预处理
当action 被触发后,可以通过prepare 方法对action 进行预处理,处理完成之后再交给Reducer,prepare 方法必须返回对象。
// 创建todos 状态切片
import { createSlice } from '@reduxjs/toolkit'
export const TODO_SLICE_KEY = 'todos'
const { reducer: TodosReducer, actions } = createSlice({
name: TODO_SLICE_KEY,
initialState: [],
reducers: {
addTodo: {
reducer: (state, action) => {
state.push(action.payload)
},
prepare: todo => {
console.log('addTodo>>>', todo)
return {
payload: { id: Math.random(), ...todo }
}
}
},
deleteTodo: {
reducer: (state, action) => {
console.log('deleteTodo reducer>>>', state, action)
state.splice(action.payload.deleted, 1)
},
prepare: todo => {
console.log('deleteTodo>>>', todo)
return {
payload: { deleted: todo.index }
}
}
}
}
})
export const { addTodo, deleteTodo } = actions
export default TodosReducer
执行异步操作方式:
1)createAsyncThunk方法:该方法的作用是用来创建用于执行异步操作的Action Creator函数。
在第二个参数中通过thunkAPI的dispatch方法来在获取数据后触发保存数据到本地的操作。
// 创建todos 状态切片
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios'
export const TODO_SLICE_KEY = 'todos'
export const loadTodos = createAsyncThunk(
'todos/loads',
(payload, thunkAPI) => {
axios
.get(payload)
.then(response => thunkAPI.dispatch(setTodos(response.data)))
}
)
const { reducer: TodosReducer, actions } = createSlice({
name: TODO_SLICE_KEY,
initialState: [],
reducers: {
addTodo: {
reducer: (state, action) => {
state.push(action.payload)
},
prepare: todo => {
console.log('addTodo>>>', todo)
return {
payload: { id: Math.random(), ...todo }
}
}
},
deleteTodo: {
reducer: (state, action) => {
console.log('deleteTodo reducer>>>', state, action)
state.splice(action.payload.deleted, 1)
},
prepare: todo => {
console.log('deleteTodo>>>', todo)
return {
payload: { deleted: todo.index }
}
}
},
setTodos: (state, action) => {
console.log('setTodos reducer>>>', state, action)
action.payload.forEach(todo => state.push(todo))
}
}
})
export const { addTodo, deleteTodo, setTodos } = actions
export default TodosReducer
2)createAsyncThunk方法:因为方法本身返回的就是一个Action Creator函数,实际上我们是可以接收这个Action的,而不需要另外再触发一个新的Action。所以可以在第二个参数的函数中返回一个Promise。
这里我们需要在创建状态切片的时候,配置一个extraReducers 属性,用于创建接收异步操作结果的Reducer。 The recommended way of using extraReducers is to use a callback that receives a ActionReducerMapBuilder instance.
// 创建todos 状态切片
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios'
export const TODO_SLICE_KEY = 'todos'
export const loadTodos = createAsyncThunk(
'todos/loads',
// (payload, thunkAPI) => {
// axios
// .get(payload)
// .then(response => thunkAPI.dispatch(setTodos(response.data)))
// }
payload => axios.get(payload).then(response => response.data)
)
const { reducer: TodosReducer, actions } = createSlice({
name: TODO_SLICE_KEY,
initialState: [],
reducers: {
addTodo: {
reducer: (state, action) => {
state.push(action.payload)
},
prepare: todo => {
console.log('addTodo>>>', todo)
return {
payload: { id: Math.random(), ...todo }
}
}
},
deleteTodo: {
reducer: (state, action) => {
console.log('deleteTodo reducer>>>', state, action)
state.splice(action.payload.deleted, 1)
},
prepare: todo => {
console.log('deleteTodo>>>', todo)
return {
payload: { deleted: todo.index }
}
}
},
setTodos: (state, action) => {
console.log('setTodos reducer>>>', state, action)
action.payload.forEach(todo => state.push(todo))
}
},
extraReducers: {
[loadTodos.fulfilled]: (state, action) => {
console.log('loadTodos.fulfilled reducer>>>', state, action)
action.payload.forEach(todo => state.push(todo))
},
[loadTodos.pending]: (state, action) => {
console.log('loadTodos.pending reducer>>>', state, action)
return state
}
}
})
export const { addTodo, deleteTodo, setTodos } = actions
export default TodosReducer
为工具集@reduxjs/toolkit 配置中间件:configureStore 和 getDefaultMiddlware
通过配置,可以添加我们自己的中间件。值得注意的是reduxjs/toolkit 它自己本身就已经内置了一些中间间,所以我们在配置自己的中间件时需要把本来内置的中间件获取到再跟我们自己要添加的中间件组合到一起,设置给 store 的 middleware 属性,它的值是一个数组。
customizing the included middleware
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
import TodosReducer, { TODO_SLICE_KEY } from './todos.slice'
import logger from 'redux-logger'
export default configureStore({
reducer: {
[TODO_SLICE_KEY]: TodosReducer
},
middleware: [...getDefaultMiddleware(), logger],
devTools: process.env.NODE_ENV !== 'production'
})
实体适配器 与 createEntityAdapter 方法
实体就是我们抽象出来的数据,我们可以理解实体适配器就是一个放置数据的容器,将状态放入实体适配器中,实体适配器提供操作状态的各种方法,简化操作。简化我们对数据的常规操作,增删改查等。
如何创建实体适配器 createEntityAdapter
createEntityAdapter 方法的返回值就是一个实体适配器。在实体适配器中它给我们提供了一些方法,如:getInitialState() ,addOne(state, action.payload) ,addMany(state, action.payload) 等。
Object.keys(entityAdapter)
[
'selectId',
'sortComparer',
'getInitialState',
'getSelectors',
'removeAll',
'addOne',
'addMany',
'setOne',
'setMany',
'setAll',
'updateOne',
'updateMany',
'upsertOne',
'upsertMany',
'removeOne',
'removeMany'
]
我们在创建状态切片时配置的状态初始值就应该设置成该方法的放回值。
实体适配器要求每一个实体必须拥有id属性作为唯一标识,如果实体中的唯一标识字段不叫做 id,需要使用 selectId 进行声明。selectId 它的值是一个函数,参数是一个实体,返回值是实体中唯一标识字段的值。
const todosAdapter = createEntityAdapter({ selectId: todo => todo.cid })
状态选择器
提供从实体适配器中获取状态的快捷途径。
import {
createSelector
} from '@reduxjs/toolkit'
const { selectAll } = todosAdapter.getSelectors()
export const selectTodos = createSelector(
state => state[TODO_SLICE_KEY],
selectAll
)
Mobx专题简介
核心概念:
observable:被MobX 跟踪的状态。
action:允许修改状态的方法,在严格模式下只有action 方法被允许修改状态。
computed:根据现有状态衍生出来的状态。
flow:执行副作用,它是generator 函数,可以更改状态值。
工作流程:
资源下载:
mobx 核心库
mobx-react-lite 仅支持函数组件
mobx-react 既支持函数组件,也支持类组件
yarn add mobx mobx-react-lite
计数器案例: 在组件中显示数值状态,单击【+1】按钮使数值加1,单击【-1】按钮使数值减1。
1、创建状态的类,及其修改状态数值的方法
export default class CounterStore {
constructor() {
this.count = 0;
}
increment() {
this.count += 1;
}
decrement() {
this.count -= 1;
}
}
2、让MobX 可以追踪状态的变化
2.1 通过使用 observable 标识状态,使状态可观察。
2.2 通过 action 标识局改状态的方法,状态只有通过action 方法修改后才会通知视图更新。
这里就需要用到 mobx库中的 makeObservable 方法。以及 action 和 observable 标识。
import { action, observable, makeObservable} from 'mobx'
export default class CounterStore {
constructor() {
this.count = 0;
makeObservable(this, {
count: observable,
increment: action,
decrement: action
})
}
increment() {
this.count += 1;
}
decrement() {
this.count -= 1;
}
}
makeObservable方法的第一个参数是this, 指向的是类的实例对象本身。标识类实例对象中的属性及方法。 第二个参数是一个配置对象,在配置中指定哪些属性是状态可以被观察,哪些方法是action。
3、创建Store 类的实例对象,并将实例对象传递给组件使用。
4、在组件中通过store 实例对象获取状态以及操作状态的方法。
5、在组件中使用到的 MobX 管理的状态发生变化后,使视图更新。通过 observer 方法包裹组件实现该目的。
import React from 'react'
import { observer } from 'mobx-react-lite'
function Counter ({ counterStore }) {
return (
计数器案例
{counterStore.count}
)
}
export default observer(Counter)
// import * as React from 'react'
import React from 'react'
import Todos from './components/Todos'
import Counter from './components/Counter'
import CounterStore from './store/CouterStore'
const counterStore = new CounterStore()
function App () {
React.useEffect(() => {
console.log('useEffect')
}, [])
return (
)
}
export default App
如果在组件中将store 进行简化,结构出来每一个属性方法单独使用,那么就需要我们方法中的this指向要始终指向store 实例对象。因为简化后结构出来的修改状态的方法的this 指向出现了问题,我们可以通过在makeObservable的时候,使用action.bound 标识 修改状态的方法,让其强制绑定this,使得 this 指向 Store 实例对象。
import { action, observable, makeObservable } from 'mobx'
export default class CounterStore {
constructor () {
this.count = 0
makeObservable(this, {
count: observable,
increment: action.bound,
decrement: action.bound
})
}
increment () {
this.count += 1
}
decrement () {
this.count -= 1
}
}
import React from 'react'
import { observer } from 'mobx-react-lite'
function Counter ({ counterStore }) {
const { count, increment, decrement } = counterStore
return (
计数器案例
{count}
)
}
export default observer(Counter)
这样一来,this 的指向问题解决了,我们在组件当中无论怎么使用修改状态的方法都不会出现问题了。
总结:状态变化更新视图的必要条件
1、状态属性必须被标记为 observable
2、更改状态的方法必须被标记为 action
3、组件必须通过 observer 方法进行包裹
三个条件缺一不可。
如何创建和管理 RootStore
为什么需要:因为一个应用中可存在多个 Store ,多个 Store 最终要通过 RootStore 管理,在每个组件都需要用到 RootStore 。
这个需求我们可以通过 react 提供给我们的 context 上下文管理的相关方法来配合实现。
它们是 createContext 和 useContext 方法。
// root.store.js
import React from 'react'
import CounterStore from './CouterStore'
import { createContext, useContext } from 'react'
class RootStore {
constructor () {
this.counterStore = new CounterStore()
}
}
const rootStore = new RootStore()
const RootContext = createContext()
export const RootStoreProvider = ({ children }) => {
return (
{children}
)
}
export const useRootStore = () => {
return useContext(RootContext)
}
// import * as React from 'react'
import React from 'react'
import Todos from './components/Todos'
import Counter from './components/Counter'
import CounterStore from './store/CouterStore'
import { RootStoreProvider } from './store/root'
// const counterStore = new CounterStore()
function App () {
React.useEffect(() => {
console.log('useEffect')
}, [])
return (
)
}
export default App
Todo 任务列表案例
异步获取服务器数据或者进行其他异步操作时,需要把方法设置成generator 函数*开头的异步处理函数, 函数内部使用 yield 等待处理结果返回。 并且在构建状态管理库的时候,把实例对象的这些异步处理函数用 flow 标识为会产生副作用的函数。
// store/Todo.js
import { action, makeObservable, observable } from 'mobx'
import axios from 'axios'
export default class Todo {
constructor (todo) {
this.id = todo.id
this.title = todo.title
this.isEditing = false
this.isCompleted = todo.isCompleted || false
makeObservable(this, {
title: observable,
isCompleted: observable,
isEditing: observable,
modifyTodoIsCompleted: action.bound,
modifyTodoIsEditing: action.bound,
modifyTodoTitle: action.bound
})
}
modifyTodoIsCompleted () {
this.isCompleted = !this.isCompleted
}
modifyTodoIsEditing () {
this.isEditing = !this.isEditing
}
modifyTodoTitle (title) {
this.title = title
this.isEditing = false
}
// 修改远端服务器数据
* modifyTodoFromServer (todo) {
let response = yield axios.put(
`http://localhost:3001/todos/${todo.id}`,
todo
)
this.title = response.data.title
this.id = response.data.id
}
}
// store/TodoStore.js
import { action, computed, flow, observable, makeObservable } from 'mobx'
import axios from 'axios'
import Todo from './Todo'
export default class TodoStore {
constructor () {
this.todos = []
this.filterCondition = 'All'
makeObservable(this, {
todos: observable,
filterCondition: observable,
loadTodos: flow,
addTodo: action.bound,
createdId: action.bound,
removeTodo: action.bound,
unCompletedTodosCount: computed,
changeFilterCondition: action.bound,
filterTodos: computed,
clearCompletedTodos: action.bound
})
this.loadTodos()
}
* loadTodos () {
let response = yield axios.get('http://localhost:3001/todos')
response.data.forEach(todo => this.todos.push(new Todo(todo)))
}
addTodo (title) {
this.todos.push(new Todo({ title, id: this.createdId() }))
}
// 创建自增ID
createdId () {
console.log(this.todos.length)
if (!this.todos.length) return 1
return this.todos.reduce((id, todo) => (id < todo.id ? todo.id : id), 0) + 1
}
removeTodo (id) {
this.todos = this.todos.filter(todo => todo.id !== id)
}
get unCompletedTodosCount () {
return this.todos.filter(todo => !todo.isCompleted).length
}
changeFilterCondition (condition) {
this.filterCondition = condition
}
get filterTodos () {
switch (this.filterCondition) {
case 'Active':
return this.todos.filter(todo => !todo.isCompleted)
case 'Completed':
return this.todos.filter(todo => todo.isCompleted)
default:
return this.todos
}
}
clearCompletedTodos () {
this.todos = this.todos.filter(todo => !todo.isCompleted)
}
}
// store/index.js
import React from 'react'
import { createContext, useContext } from 'react'
import CounterStore from './CouterStore'
import TodoStore from './TodoStore'
class RootStore {
constructor () {
this.counterStore = new CounterStore()
this.todoStore = new TodoStore()
}
}
const rootStore = new RootStore()
const RootContext = createContext()
export const RootStoreProvider = ({ children }) => {
return (
{children}
)
}
export const useRootStore = () => {
return useContext(RootContext)
}
// components/Todos/index.js
import React from 'react'
import TodoApp from './TodoApp'
import TodoHeader from './Header'
import TodoFooter from './Footer'
import TodoMain from './Main'
export default function (props) {
return (
)
}
// components.Todos/TodoApp.js 样式组件
import styled from '@emotion/styled'
export default styled.section`
background: #fff;
margin: 130px auto 40px auto;
position: relative;
min-width: 230px;
max-width: 540px;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
& input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
& input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
& input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
& h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
& .new-todo,
& .edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
border: 0;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
& .new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}
& .main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
& .todo-list {
margin: 0;
padding: 0;
list-style: none;
}
& .todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
& .todo-list li:last-child {
border-bottom: none;
}
& .todo-list li.editing {
border-bottom: none;
padding: 0;
}
& .todo-list li.editing .edit {
display: block;
width: 506px;
padding: 12px 16px;
margin: 0 0 0 43px;
}
& .todo-list li.editing .view {
display: none;
}
& .todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
& .todo-list li .toggle:after {
content: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
display: block;
margin-top: 11px;
}
& .todo-list li .toggle:checked:after {
content: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
}
& .todo-list li label {
word-break: break-all;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
& .todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
& .todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
& .todo-list li .destroy:hover {
color: #af5b5e;
}
& .todo-list li .destroy:after {
content: '×';
}
& .todo-list li:hover .destroy {
display: block;
}
& .todo-list li .edit {
display: none;
}
& .todo-list li.editing:last-child {
margin-bottom: -1px;
}
& .footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
& .footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
& .todo-count {
float: left;
text-align: left;
}
& .todo-count strong {
font-weight: 300;
}
& .filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
& .filters li {
display: inline;
}
& .filters li button {
color: inherit;
padding: 0 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
& .filters li button:hover {
border-color: rgba(175, 47, 47, 0.1);
}
& .filters li button.selected {
border-color: rgba(175, 47, 47, 0.2);
}
& .clear-completed,
& html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
}
& .clear-completed:hover {
text-decoration: underline;
}
& .info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
& .info p {
line-height: 1;
}
& .info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
& .info a:hover {
text-decoration: underline;
}
& button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
`
// components/Todos/Main.js
import React from 'react'
import { observer } from 'mobx-react-lite'
import { useRootStore } from '../../store'
import Todo from './Todo'
function Main () {
const { todoStore } = useRootStore()
const { filterTodos } = todoStore
return (
{filterTodos.map(todo => (
))}
)
}
export default observer(Main)
// components/Todos/Todo.js
import React from 'react'
import { observer } from 'mobx-react-lite'
import TodoCompleted from './TodoCompleted'
import TodoEditing from './TodoEditing'
import TodoRemove from './TodoRemove'
import classname from 'classnames'
import Editing from './Editing'
function Todo ({ todo }) {
return (
)
}
export default observer(Todo)
// components/Todos/Editing.js
import React, { useEffect, useRef } from 'react'
function Editing ({ todo }) {
const ref = useRef(null)
const { modifyTodoIsEditing, modifyTodoTitle, title, isEditing } = todo
useEffect(() => {
if (isEditing) {
ref.current.focus()
}
}, [isEditing])
return (
modifyTodoTitle(ref.current.value)}
className='edit'
defaultValue={title}
/>
)
}
export default Editing