TodoMVC是一个示例项目,它使用目前流行的不一样JavaScript框架的来实现同一个Demo,来帮助你熟悉和选择最合适的前端框架。css
怎样快速学习和了解一门JavaScript框架或学习使用新框架的特性? 个人经验是作一个TodoMVC应用吧html
TodoMVC 应用功能点vue
TodoMVC
是一个功能和UI都十分完备的教学示例,实乃学习框架之必备🌰react
Redux
是遵循 Flux
模式的一种实现,是一个状态管理库,适用于 React
,Angular
,VueJs
等框架或库,而不是局限于某一特定UI库。git
Redux中文文档github
Redux 核心概念npm
+----------+
| | +-----------+ sliceState,action +------------+
| Action |dispacth(action)| +--------------------------------> |
| +----------------> store | | Reducers |
| Creators | | <--------------------------------+ |
| | +-----+-----+ (state, action) => state +------------+
+-----^----+ |
| |
| |subscribe
| |
| |
| |
| +--------+ |
+--------------+ View <---+
+--------+
复制代码
更新View的方式redux
dispatch
触发 action
action
携带方法名 type
, 和数据 payload
到 store
(期间可能进行异步数据处理)store
接收到 type
和 payload
交给相应的 reducer
reducer
找到对应的方法更新 state
Store、Action 与 Reducerbash
Store
是整个 Redux 应用的状态容器,是一个对象Action
也是一个对象,代表事件,须要有 type 字段Reducer
是一个函数,会根据不一样 Action 来决定返回不一样的数据新增Todo
删除Todo
complete
的数据修改Todo
查询Todo
all
active
complete
能够过滤list,而且高亮自动能切换active
和 complete
样式TodoMVC
模块npm install todomvc-common todomvc-app-css
import 'todomvc-common/base.css';
import 'todomvc-app-css/index.css';
复制代码
直接使用vue版TodoMVC模板,去掉vue相关代码,保留结构
使用 todomvc
UI, 能够免去样式和dom结构编写,让咱们专一于使用JS框架快速实现业务逻辑
页面基本结构
// App.tsx
const Input: SFC<any> = () => {...}
const TodoItem: SFC<ITodoItemProps> = {...}
const TodoList: SFC<any> = () => {...}
const Footer: SFC<any> = () => {...}
function TodoAPP() {
return (
<section className="todoapp"> <header className="header"> <h1>todos</h1> <Input /> </header> <TodoList /> <Footer /> </section>
);
}
复制代码
应用全局状态
const initialState: IAPPState = {
todos: todoStore.list,
newTodo: '',
editTodo: '',
visibility: ShowType.ALL,
};
复制代码
这里状态的划分为全局状态和局部状态,局部状态由组件内部管理,后面会说到,因此TodoMVC 的全局状态基本就是这4个就够了,在实际项目应用中,全局状态尽可能少,要作到每一个状态都是全局必须的,避免在全局存放过多的状态,可能会引发组件没必要要的更新。
事件方法
export enum ActionType {
// 新增Todo
CREATE = 'create',
// 更新Todo
UPDATE = 'update',
// 删除Todo
DELETE = 'delete',
// 删除状态是已完成的Tdo
REMOVE_COMPLETED = 'removeCompleted',
// 设置当前编辑的Todo
EDIT_SET = 'setEdit',
// 改变当前显示类型
CHANGE_SHOW_TYPE = 'changeShowType',
// 更新当前编辑的Todo
UPDATE_EDIT_TODO = 'updateEditTodo',
// 所有切换为 完成/未完成
TOGGLE_ALL = 'toggleAll',
}
复制代码
使用 createContext
建立 Context
组件
使用 useReducer 管理复杂的 state
useReducer
接收一个形如 (state, action) => newState
的 reducer
,并返回当前的 state
以及与其配套的 dispatch
方法
使用 <Context.Provider>
作为父组件的组件,将在 state
更新时,触发更新
// Store.tsx
const initialState: IAPPState = {
todos: todoStore.list,
newTodo: '',
editTodo: '',
visibility: ShowType.ALL,
};
const reducer = (state: IAPPState, action: IAction): IAPPState => {
const { type, payload } = action;
return methods[type] ? methods[type](state, payload) : state;
};
const Context = React.createContext({
state: initialState,
dispatch: (() => 0) as React.Dispatch<IAction>,
});
const Provider: SFC<any> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return <Context.Provider value={{ state, dispatch}}>{children}</Context.Provider>;
};
export { Provider, Context };
复制代码
上面的 methods
是具体更新 state
的方法对象
Provider
的子组件能够从 Context
中获取全局的 state
与 dispatch
// App.tsx
import { Provider, Context } from './Store'
function TodoAPP() {
return (
<Provider>
<section className="todoapp">
...
</section>
</Provider>
);
}
复制代码
使用 useContext 接收 Store
中的 Context
对象,并返回 state
和 dispatch
当组件上层最近的 <Context.Provider>
更新时,该 hook 会触发组件更新,并使用最新的 state
调用了 useContext
的组件总会在 context
值变化时从新渲染
使用 useState 建立组件内部状态 title
, 该状态在组件内部使用或销毁,并不影响全局state, 接收一个初始状态的初始值和更新状态的函数 changeTitle
使用 dispatch
触发一个 action
操做,建立一个 todo
const Input: SFC<any> = () => {
const { state, dispatch } = useContext(Context);
// 内部状态 title
const [title, changeTitle] = useState<string>('');
// 输入时更新 title
function onChange(e: ChangeEvent<HTMLInputElement>) {
changeTitle(e.target.value.trim());
}
// 回车新增一条Todo
function onKeyDown(e: KeyboardEvent<HTMLInputElement>) {
// 新增
dispatch({
type: ActionType.CREATE,
payload: { id: utils.uuid(), title, completed: false },
});
// 清空
changeTitle('');
}
return (
<input
className="new-todo"
autoFocus
autoComplete="off"
placeholder="What needs to be done?"
value={title}
onChange={onChange}
onKeyDown={onKeyDown}
/>
);
};
复制代码
editing
是属于每个 TodoItem
的内部状态
使用 useRef 来返回一个可变的 ref 引用对象,其 .current
属性被初始化为传入的参数,经常使用于访问 DOM对象
,这里用来使用编辑输入框获取焦点
useEffect 告诉组件须要在渲染后执行某些操做。React
会保存你传递的函数(咱们将它称之为 “effect”),而且在执行 DOM
更新以后调用它, 这里传入第二个参数 [editing]
作为依赖,以优化 effect
执行次数,只有当 editing
改变时,才执行
const TodoItem: SFC<ITodoItemProps> = ({ completed, title, index }) => {
const [editing, changeEditing] = useState<boolean>(false);
const iptRef = useRef(null);
const { state, dispatch } = useContext(Context);
const { editTodo } = state;
// DOM更新,获取焦点
useEffect(() => (editing && iptRef.current.focus()), [editing]);
// 双击编辑
function onDoubleClick() {...}
// 失焦,更新Todo
function onEditBlur() {...}
// 回车,更新Todo
function onEditEnter(e: KeyboardEvent<HTMLInputElement>) {...}
// 更新当前编辑的Todo
function onEditChange(e: ChangeEvent<HTMLInputElement>) {...}
// 切换完成状态
function onToggleComplete(e: ChangeEvent<HTMLInputElement>) {...}
// 删除
function onDestroy() {
return (
<li>
...
</li>
)
}
复制代码
使用 useMemo 获取相似Vue中的 计算属性,避免在每次渲染时都进行高开销的计算,传入 todos
, 当 todos
更新时才从新计算
const Footer: SFC<any> = () => {
const { state, dispatch } = useContext(Context);
const { todos, visibility } = state;
// 计算属性:正在进行中的数量、已完成的数量、提示文字
const { activedNum, completedNum, activeTodoWord } = useMemo(() => {...})
// 改变显示类型
function onChangeShowType(type: ShowType) {...}
// 清空已完成的Todo
function onClearCompleted() {...}
return (
<footer className="footer">
...
</footer>
)
}
复制代码
const TodoList: SFC<any> = () => {
const { state, dispatch } = useContext(Context);
const { todos, visibility } = state;
const activedNum = useMemo(() => utils.filterTodos(todos, ShowType.ACTIVE).length, [todos]);
const todoList = useMemo(() =>utils.filterTodos(todos, visibility), [todos, visibility]);
function onToggleAll(e: ChangeEvent<HTMLInputElement>) {...}
return (
<section className="main">
<input
id="toggle-all"
className="toggle-all"
type="checkbox"
onChange={onToggleAll}
checked={activedNum === 0}
/>
<label htmlFor="toggle-all">Mark all as complete</label>
<ul className="todo-list">
{todoList.map((v, i) => {
return <TodoItem key={v.id} completed={v.completed} title={v.title} index={i} />;
})}
</ul>
</section>
)
}
复制代码
为了防止文章过长,并无粘贴全部代码,主要说明 React
中的一些 hooks
在 TodoMVC
中的一些实际应用场景,以便于咱们更容易更快速的理解和掌握hooks的用法
在咱们公司的实际应用中,并无使用 redux
来管理应用状态,而是使用 mobx
,在实际开发中我更倾向局部状态与全局状态分而治之,在后台管理系统中,全局的状态并很少,无非就是一些用户信息、菜单状态和数据、消息等等,通常以页面划分 store
,复杂的组件有本身的 store
, store
中包含了全部状态和操做方法。
Redux 的缺点
actionTypes
、actions
、reducers
dispatch
须要引用 actions
或要记忆 Action Type
等问题优化 dispatch
在上面的 hooks 实现的伪 redux
中, dispatch
方法使用仍然比较繁琐,能够优化下面两点
import { ActionsTypes } from './types'
import { Context } from './store'
// 如今的调用方式
const {state, dispatch} = useContext(Context)
dispatch({
type: ActionTypes.TOGGLE_ALL,
payload: { completed },
});
// 优化后的调用方式
const {state, actions} = useContext(Context)
actions.toggleAll({ completed })
复制代码
这里的优化实现主要在 Provider
的实现中, 只要把 Actions
方法挂载到 Context
中就能够
const Provider: SFC<any> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
// 生成 Actions
type IActions = { [name in ActionType]?: (data?: Record<string, any>) => void };
let actions: IActions = {};
for (let i in ActionType) {
actions[ActionType[i] as ActionType] = data =>
dispatch({ type: ActionType[i] as ActionType, payload: data });
}
// Context 挂载 state、dispatch、actions
return <Context.Provider value={{ state, dispatch, actions }}>{children}</Context.Provider>;
};
复制代码
Vue
相较于 React
有一个优势就是 Vue
的包比 React
要小,在 React
中使用 setState
来管理应用状态,相比于 Vue
,代码写法上稍显繁琐,引用第三方的库如 Redux
或 mobx
无疑又会增长React的体积,hooks
的出现给了咱们以但愿,在一些相对简单的应用中彻底能够代替 mobx
或 redux
。可是一样也要当心使用,没有了生命周期的控制,使用不当也容易形成组件重复屡次渲染产生性能瓶颈,因此在使用 hooks
时,要多思考怎样使用更合理,怎样才不会产生性能问题。