BBYR Achieve
返回信息流
这是一条镜像帖。来源:北邮人论坛 / java-script / #4190同步于 2018/8/16
JavaScript机器人发帖

【讨论】【心得】Redux 基础 - react 全家桶学习笔记(一)

stevesasuke
2018/8/16镜像同步0 回复
注:这篇是16年10月的文章,搬运自本人 blog... https://github.com/BuptStEve/blog ## 零、环境搭建 **参考资料** - [英文原版文档](http://redux.js.org/) - [中文文档](http://cn.redux.js.org/) - [墙裂推荐作者出的教学视频 基础篇](https://egghead.io/courses/getting-started-with-redux) - [墙裂推荐作者出的教学视频 高级篇](https://egghead.io/courses/building-react-applications-with-idiomatic-redux) 首先要明确一点,虽然 redux 是由 [flux](http://facebook.github.io/flux/) 演变而来,但我们完全可以并且也应该抛开 react 进行学习,这样可以避免一开始就陷入各种细节之中。 所以推荐使用 [jsbin](https://jsbin.com/) 进行调试学习,或者使用 [create-react-app](https://github.com/facebookincubator/create-react-app) 作为项目脚手架。 ## 一、Redux 是什么? > Redux is a predictable state container for JavaScript apps. > Redux 是一个 JavaScript 状态容器,提供可预测化的状态管理。 http://buptsteve.github.io/blog/imgs/redux/overview.png **先不要在意那些细节** - 总的来说,redux 使用 store 保存并管理页面中的各种状态(state) - 当需要改变 state 时,使用 dispatch 调用 action creators 触发 action - 接着使用纯函数(pure function)reducer 来处理这些 action,它会根据当前 state 和 action 返回(注意这里不是修改)新的 state - view 层可以对于 state 进行订阅(subscribe),这样就可以得到新的 state,从而可以刷新界面(所以十分适合数据驱动的前端框架) > 纯函数:简单的说就是对于同样的输入总是返回同样的输出,并且没有副作用的函数。(推荐学习了解下函数式编程) ### 1.1. 为什么选择 redux? > - 随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。 > - 管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。 > - 如果这还不够糟糕,考虑一些来自前端开发领域的新需求,如更新调优、服务端渲染、路由跳转前请求数据等等。前端开发者正在经受前所未有的复杂性,难道就这么放弃了吗?当然不是。 > - 这里的复杂性很大程度上来自于:我们总是将两个难以厘清的概念混淆在一起:**变化**和**异步**。 我称它们为曼妥思和可乐。如果把二者分开,能做的很好,但混到一起,就变得一团糟。一些库如 React 试图在视图层禁止异步和直接操作 DOM 来解决这个问题。美中不足的是,React 依旧把处理 state 中数据的问题留给了你。Redux就是为了帮你解决这个问题。 > - 跟随 Flux、CQRS 和 Event Sourcing 的脚步,通过限制更新发生的时间和方式,Redux 试图让 state 的变化变得可预测。这些限制条件反映在 Redux 的 三大原则中。 **简单总结就是使用 Redux 我们就可以~~没有蛀牙(大雾)~~** - 拥有可预测(predictable)的应用状态,所以应用的行为也是可预测的 - 因为 reducer 是纯函数,所以方便对于状态迁移进行自动化测试 - 方便地记录日志,甚至实现时间旅行(time travel) ### 1.2. 三大原则(哲♂学) #### 1.2.1. 单一数据源(Single source of truth) 整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。 - 来自服务端的 state 可以在无需编写更多代码的情况下被序列化并注入到客户端中 - 便于调试,在开发时可以将状态保存在本地 - Undo/Redo 可以轻松实现,从而实现时间旅行 #### 1.2.2. State 是只读的(State is read-only) 惟一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。 因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,(dispatch 同步调用 reduce 函数)因此不用担心 race condition 的出现。 Action 就是普通对象而已,因此它们可以被日志打印、序列化、储存、后期调试或测试时回放出来。 #### 1.2.3. 使用纯函数来执行修改(Changes are made with pure functions) 为了描述 action 如何改变 state tree ,你需要编写 reducer。 Reducer 只是纯函数,它接收先前的 state 和 action,并返回新的 state。刚开始你可以只有一个 reducer,随着应用变大,你可以把它拆成多个小的 reducers,分别独立地操作 state tree 的不同部分。 ## 二、Redux 基础 ### 2.1. action Action 就是一个普通的 JavaScript Object。 redux 唯一限制的一点是必须有一个 type 属性用来表示执行哪种操作,值最好用字符串,而不是 Symbols,因为字符串是可被序列化的。 其他属性用来传递此次操作所需传递的数据,redux 对此不作限制,但是在设计时可以参照 [Flux 标准 Action](https://github.com/acdlite/flux-standard-action)。 **简单总结 Flux Standard action 就是** > 1. 一个 action 必须是一个 JavaScript Object,并且有一个 type 属性。 > 2. 一个 action 可以有 payload/error/meta 属性。 > 3. 一个 action 不能有其他属性。 ### 2.2. reducer Reducer 的工作就是接收旧的 state 和 action,返回新的 state。 > (previousState, action) => newState 之所以称作 reducer 是因为它将被传递给 `Array.prototype.reduce(reducer, ?initialValue)` 方法。保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作: - 修改传入参数; - 执行有副作用的操作,如 API 请求和路由跳转; - 调用非纯函数,如 Date.now() 或 Math.random()。 ### 2.3. store Store 就是用来维持应用所有的 state 树的一个对象。 在 redux 中只有一个 store(区别于 flux 的多个 store),在 store 中保存所有的 state,可以把它当成一个封装了 state 的类。而除了对其 dispatch 一个 action 以外无法改变内部的 state。 在实际操作中我们只需要把根部的 reducer 函数传递给 createStore 就可以得到一个 store。 ``` javascript import { createStore } from 'redux'; function reducer(state, action) { switch (action.type) { case 'SOME_ACTION': // 一些操作 return newState; // 返回新状态 default: return state; } } const store = createStore(reducer); ``` **redux 中提供了这几个 api 操作 store** #### 2.3.1. getState 返回当前的整个 state 树。 #### 2.3.2. dispatch(action) 分发 action 给对应的 reducer。 该函数会调用 getState() 和传入的 action 以【同步】的方式调用 store 的 reduce 函数,然后返回新的 state。从而 state 得到了更新,并且变化监听器(change listener)会被触发。(对于异步操作则将其放到了 action creator 这个步骤) #### 2.3.3. subscribe(listener) 为 store 添加一个变化监听器,每当 dispatch 的时候就会执行,你可以在 listener(回调函数)中使用 getState() 来得到当前的 state。 这个 api 设计的挺有意思,它会返回一个函数,而你执行这个函数后就可以取消订阅。 #### 2.3.4. replaceReducer(nextReducer) 替换 store 当前用来计算 state 的 reducer。 这是一个高级 API。只有在你需要实现代码分隔,而且需要立即加载一些 reducer 的时候才可能会用到它。在实现 Redux 热加载机制的时候也可能会用到。 ### 2.4. createStore 忽略各种类型判断,实现一个最简的 createStore 可以用以下代码。[参考资料](https://egghead.io/lessons/javascript-redux-implementing-store-from-scratch) ``` javascript const createStore = (reducer) => { let state; let listeners = []; const getState = () => state; const dispatch = (action) => { state = reducer(state, action); // 调用 reducer listeners.forEach(listener => listener()); // 调用所有变化监听器 }; const subscribe = (listener) => { listeners.push(listener); return () => { // 返回解除监听函数 listeners = listeners.filter(l => l !== listener); }; } dispatch({}); // 初始化 return { getState, dispatch, subscribe }; }; ``` ### 2.5. 计数器例子 - 纯 JavaScript 不涉及界面(可以在右侧 console 中尝试 store.dispatch) {% iframe http://jsbin.com/kejezih/edit?js,console 100% 600 %} - 增加界面 {% iframe http://jsbin.com/jihara/edit?html,js,output 100% 600 %} ## 三、与 React 进行结合 ### 3.1. 通过 script 标签导入 react 实现同样功能的 Counter {% iframe http://jsbin.com/qalevu/edit?html,js,output 100% 800 %} ### 3.2. 用 Redux 和 React 实现 TodoApp 在添加 react-redux 之前,为了体会下 react-redux 的作用,首先来实现一个比计数器更复杂一点儿的 TodoApp 栗子~ #### 3.2.1. 分析与设计 ##### 1. 容器组件 V.S. 展示组件 **组件一般分为** - 容器组件(Smart/Container Components) - 展示组件(Dumb/Presentational Components) | - | 容器组件 | 展示组件 | --- | --- | --- | Location | 最顶层,路由处理 | 中间和子组件 | Aware of Redux | 是 | 否 | 读取数据 | 从 Redux 获取 state | 从 props 获取数据 | 修改数据 | 向 Redux 派发 actions | 从 props 调用回调函数 最佳实践一般是由容器组件负责一些数据的获取,进行 dispatch 等操作。而展示组件组件不应该关心逻辑,所有数据都通过 props 传入。 这样才能达到展示组件可以在多处复用,在具体复用时就是通过容器组件将其包装,为其提供所需的各种数据。 ##### 2. 应用设计 - 一个 TodoApp 包含了三个部分: - 顶部的 AddTodo 输入部分 - 中间的 TodoList 展示部分 - 底部的 Footer 过滤部分 - State 应该包含: - filter:过滤 todos 的条件 - SHOW_ALL - SHOW_ACTIVE - SHOW_COMPLETED - todos:所有的 todo - todo:包含 id、text 和 completed - 然而传到应用中的 props 只需要: - visibleTodos:过滤后的 todos - filter:过滤条件 - Action 应该有三种: - ADD_TODO - TOGGLE_TODO - SET_VISIBILITY_FILTER #### 3.2.2. 编码实现 ##### 1. action 部分 ``` javascript // 暂且使用数字作为 id let nextTodoId = 0; /*-- action creators --*/ const addTodo = (text) => ( { type: 'ADD_TODO', id: nextTodoId++, text } ); const toggleTodo = (id) => ( { type: 'TOGGLE_TODO', id } ); const setVisibilityFilter = (filter) => ( { type: 'SET_VISIBILITY_FILTER', filter } ); ``` ##### 2. reducer 部分 ``` javascript // 默认初始状态 const initialState = { filter: 'SHOW_ALL', todos: [] }; function rootReducer(state = initialState, action) { switch (action.type) { case 'ADD_TODO': // 对象解构 const { id, text } = action; return { ...state, todos: { ...state.todos, { id, text, completed: false }, }, }; case 'TOGGLE_TODO': return { ...state, todos: state.todos.map(todo => { if (todo.id !== action.id) return todo; return { ...todo, completed: !todo.completed, }; }), }; case 'SET_VISIBILITY_FILTER': return { ...state, filter: action.filter, }; default: return state; } } ``` > 注意! > 1. 不要直接修改原有的 state,而是返回一个新的 state。可以使用 Object.assign() 新建一个新的 state。不能这样使用 Object.assign(state, { visibilityFilter: action.filter }),因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。你也可以开启对 ES7 提案对象展开运算符的支持, 从而使用 { ...state, ...newState } 达到相同的目的。 > 2. 在 default 的情况下返回旧的 state,用来兼容遇到未知的 action 这样的错误。 **拆分 reducer** 目前代码看着比较冗长,其实在逻辑上 todos 的处理和 filter 的处理应该分开,所以在 state 没有互相耦合时,可以将其拆分,从而让 reducer 精细地对于对应 state 的子树进行处理。 ``` javascript // 处理单个 todo const todoReducer = (state, action) => { switch (action.type) { case 'ADD_TODO': return { id: action.id, text: action.text, completed: false, }; case 'TOGGLE_TODO': if (state.id !== action.id) return state; return { ...state, completed: !state.completed, }; default: return state; } }; // 处理 todos const todosReducer = (state = [], action) => { switch (action.type) { case 'ADD_TODO': return [ ...state, todoReducer(undefined, action), ]; case 'TOGGLE_TODO': return state.map(t => todoReducer(t, action)); default: return state; }; }; // 处理 filter const filterReducer = (state = 'SHOW_ALL', action) => { switch (action.type) { case 'SET_VISIBILITY_FILTER': return action.filter; default: return state; }; }; const rootReducer = (state = initialState, action) => ({ todos: todosReducer(state.todos, action), filter: filterReducer(state.filter, action), }); ``` 注意观察最后的 rootReducer 函数,返回的是一个经过各种 reducer 处理过并合并后的新 state。 然鹅,注意这里 `todos: todos(state.todos, action),` 传入 state.todos,返回的一定也是 todos(因为都是 state 树上的节点)。 所以 redux 提供了很实用的 `combineReducers` api,用于简化 reducer 的合并。 ``` javascript import { combineReducers } from 'redux'; const rootReducer = combineReducers({ todos: todosReducer, filter: filterReducer, }); // initialState 可以作为第二个参数传入 const store = createStore(rootReducer, initialState); ``` 并且如果 reducer 与 state 节点同名的话(即 todosReducer -> todos)还能通过 es6 的语法更进一步地简化 ``` javascript import { combineReducers } from 'redux'; const rootReducer = combineReducers({ todos, filter }); // initialState 可以作为第二个参数传入 const store = createStore(rootReducer, initialState); ``` 随着应用的膨胀,我们还可以将拆分后的 reducer 放到不同的文件中, 以保持其独立性并用于专门处理不同的数据域。 ##### 3. view 部分 ###### 1. 只有根组件 首先只写一个根组件 `<TodoApp />`,store 通过 props 传入 TodoApp,并在生命周期的 componentDidMount 和 componentWillUnmount 时分别订阅与取消订阅。 ``` javascript import React, { Component } from 'react'; class TodoApp extends Component { // 订阅 store 的变化 componentDidMount() { const { store } = this.props; this.unsubscribe = store.subscribe( this.forceUpdate.bind(this) ); } // 取消订阅 componentWillUnmount() { this.unsubscribe(); } // 渲染单个 todo _renderTodo(todo) { const { store } = this.props; return ( <li key={todo.id} onClick={() => store.dispatch(toggleTodo(todo.id))} style={{ textDecoration: todo.completed ? 'line-through' : 'none', cursor: todo.completed ? 'default' : 'pointer', }} > {todo.text} </li> ); } // 根据当前 filter 是否匹配,返回字符串或是 a 链接 _renderFilter(renderFilter, name) { const { store } = this.props; const { filter } = store.getState(); if (renderFilter === filter) return name; return ( <a href='#' onClick={e => { e.preventDefault(); store.dispatch(setVisibilityFilter(renderFilter)) }}> {name} </a> ); } // 根据当前 filter 过滤需要渲染的 todos _getVisibleTodos(todos, filter) { switch (filter) { case 'SHOW_ALL': return todos; case 'SHOW_COMPLETED': return todos.filter(todo => todo.completed); case 'SHOW_ACTIVE': return todos.filter(todo => !todo.completed); default: return todos; } } render() { const { store } = this.props; const { todos, filter } = store.getState(); let input; return ( <div> {/* AddTodo */} <input type="text" ref={node => input = node} /> <button onClick={() => { if (!input.value) return; store.dispatch(addTodo(input.value)); input.value = ''; }}> addTodo </button> {/* TodoList */} <ul> {this._getVisibleTodos(todos, filter) .map(this._renderTodo.bind(this)) } </ul> {/* Footer */} <p> Show: {' '} {this._renderFilter('SHOW_ALL', 'all')} {', '} {this._renderFilter('SHOW_COMPLETED', 'completed')} {', '} {this._renderFilter('SHOW_ACTIVE', 'active')} </p> </div> ); } } ``` **TodoApp 只有根组件** {% iframe http://jsbin.com/bodise/edit?js,output 100% 800 %} ###### 2. 组件拆分 将所有界面内容全写在 TodoApp 中实在是太臃肿了,接下来根据之前的分析结果将其分为以下子组件(全是展示组件) - AddTodo - TodoList - Todo - Footer - FilterLink ``` javascript const AddTodo = ({ onAddClick }) => { let input; return ( <div> <input type="text" ref={node => input = node} /> <button onClick={() => { onAddClick(input.value); input.value = ''; }}> addTodo </button> </div> ); }; const Todo = ({ text, onClick, completed }) => ( <li onClick={onClick} style={{ textDecoration: completed ? 'line-through' : 'none', cursor: completed ? 'default' : 'pointer', }} > {text} </li> ); const TodoList = ({ todos, onTodoClick }) => ( <ul> {todos.map(todo => <Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)} /> )} </ul> ); const FilterLink = ({ filter, onClick, renderFilter, children }) => { if (renderFilter === filter) return (<span>{children}</span>); return ( <a href='#' onClick={e => { e.preventDefault(); onClick(renderFilter); }}> {children} </a> ); }; const Footer = ({ filter, onFilterClick }) => ( <p> Show: {' '} <FilterLink filter={filter} renderFilter="SHOW_ALL" onClick={onFilterClick} > all </FilterLink> {', '} <FilterLink filter={filter} renderFilter="SHOW_COMPLETED" onClick={onFilterClick} > completed </FilterLink> {', '} <FilterLink filter={filter} renderFilter="SHOW_ACTIVE" onClick={onFilterClick} > active </FilterLink> </p> ); ``` 所以 TodoApp 精简后是这样~ ``` javascript class TodoApp extends Component { // ... render() { const { store } = this.props; const { todos, filter } = store.getState(); return ( <div> <AddTodo onAddClick={text => { if (!text) return; store.dispatch(addTodo(text)); }} /> <TodoList todos={this._getVisibleTodos(todos, filter)} onTodoClick={id => store.dispatch(toggleTodo(id))} /> <Footer filter={filter} onFilterClick={filter => { store.dispatch(setVisibilityFilter(filter)); }} /> </div> ); } } ``` ###### 3. 增加容器组件 现在我们仍然是以 TodoApp 作为容器组件,其中各个子组件都是展示组件。 但是这样做的话一旦子组件需要某个属性,就需要从根组件层层传递下来,比如 FilterLink 中的 filter 属性。 所以下面我们增加容器组件,让展示组件通过容器组件获得所需属性。 - AddTodo(container) - VisibleTodoList(container) - TodoList - Todo - Footer - FilterLink(container) - Link ``` javascript // store.dispatch 又被放回来了, // 因为暂时我们只在 AddTodo 组件中使用 addTodo 这个 action // 以后增加了新的 form 之后可以考虑再将 store.dispatch 移出去 const AddTodo = ({ store }) => { let input; return ( <div> <input type="text" ref={node => input = node} /> <button onClick={() => { if (!input.value) return; store.dispatch(addTodo(input.value)); input.value = ''; }}> addTodo </button> </div> ); }; const Todo = ({ text, onClick, completed }) => ( <li onClick={onClick} style={{ textDecoration: completed ? 'line-through' : 'none', cursor: completed ? 'default' : 'pointer', }} > {text} </li> ); const TodoList = ({ todos, onTodoClick }) => ( <ul> {todos.map(todo => <Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)} /> )} </ul> ); // 容器组件 class VisibleTodoList extends Component { // 订阅 store 的变化 componentDidMount() { const { store } = this.props; this.unsubscribe = store.subscribe( this.forceUpdate.bind(this) ); } // 取消订阅 componentWillUnmount() { this.unsubscribe(); } // 根据当前 filter 过滤需要渲染的 todos _getVisibleTodos(todos, filter) { switch (filter) { case 'SHOW_ALL': return todos; case 'SHOW_COMPLETED': return todos.filter(todo => todo.completed); case 'SHOW_ACTIVE': return todos.filter(todo => !todo.completed); default: return todos; } } render() { const { store } = this.props; const { todos, filter } = store.getState(); return ( <TodoList todos={this._getVisibleTodos(todos, filter)} onTodoClick={id => { store.dispatch(toggleTodo(id)) }} /> ); } } // 原本的 FilterLink 改成 Link,去掉 filter 和 renderFilter 属性,改为传入 active const Link = ({ active, onClick, children }) => { if (active) return (<span>{children}</span>); return ( <a href='#' onClick={e => { e.preventDefault(); onClick(); }}> {children} </a> ); }; // 容器组件 class FilterLink extends Component { // 订阅 store 的变化 componentDidMount() { const { store } = this.props; this.unsubscribe = store.subscribe( this.forceUpdate.bind(this) ); } // 取消订阅 componentWillUnmount() { this.unsubscribe(); } render() { const { store, renderFilter, children } = this.props; const { filter } = store.getState(); return ( <Link active={filter === renderFilter} onClick={() => store.dispatch( setVisibilityFilter(renderFilter) )} > {children} </Link> ); } } // 展示组件 const Footer = ({ store }) => ( <p> Show: {' '} <FilterLink store={store} renderFilter="SHOW_ALL" > all </FilterLink> {', '} <FilterLink store={store} renderFilter="SHOW_COMPLETED" > completed </FilterLink> {', '} <FilterLink store={store} renderFilter="SHOW_ACTIVE" > active </FilterLink> </p> ); // 在不使用全局变量 store 的情况下, // 暂时只能通过 props 传递进来, // Don't worry~很快就不会这么麻烦了~ const TodoApp = ({ store }) => ( <div> <AddTodo store={store} /> <VisibleTodoList store={store} /> <Footer store={store} /> </div> ); ``` **通过观察重构后的代码可以发现有三点麻烦的地方** 1. 根组件需要通过 props 将 store 传给各个子组件 2. 容器组件都要定义 componentDidMount 进行订阅和 componentWillUnmount 取消订阅 3. 应用其实并不需要渲染所有的 todos,所以内部很麻烦地定义了 `_getVisibleTodos` 函数 ###### 4. Provider 让我们先来解决第一个麻烦~,利用 React 提供的 [context 特性](http://facebook.github.io/react/docs/context.html) ``` javascript class Provider extends Component { // 通过该方法向 children 的 context 注入 store getChildContext() { return { store: this.props.store }; } render() { return this.props.children; } } // 必须要声明传入 context 的 store 的类型 Provider.childContextTypes = { store: React.PropTypes.object, }; ``` **自顶向下地看一下如何使用到 TodoApp 中** ``` javascript // 1. 使用 Provider 包裹 TodoApp,并将 store 作为 props 传入 ReactDOM.render( <Provider store={createStore(rootReducer, initialState)}> <TodoApp /> </Provider>, document.getElementById('container'), ); // 2. 根组件 TodoApp: 和 store say goodbye~, // 因为 TodoApp 并不是容器组件~ const TodoApp = () => ( <div> <AddTodo /> <VisibleTodoList /> <Footer /> </div> ); // 3. AddTodo: 由于 props 固定作为第一个传入子组件的参数, // 所以 { store } 要声明在第二位,然鹅需要声明 contextTypes... const AddTodo = (props, { store }) => { // ... }; // 必须声明 AddTodo.contextTypes = { store: React.PropTypes.object, }; // 4. VisibleTodoList: 从 props 改成从 context 中获取 store, // 同样声明 contextTypes... class VisibleTodoList extends Component { // 订阅 store 的变化 componentDidMount() { const { store } = this.context; // props -> context // ... } // ... render() { const { store } = this.context; // props -> context const { todos, filter } = store.getState(); // ... } } // 必须声明 VisibleTodoList.contextTypes = { store: React.PropTypes.object, }; // -- TodoList 和 Todo 不变 -- // 5. Footer:和 store say goodbye... const Footer = () => ( <p> Show: {' '} <FilterLink renderFilter="SHOW_ALL"> all </FilterLink> {', '} <FilterLink renderFilter="SHOW_COMPLETED"> completed </FilterLink> {', '} <FilterLink renderFilter="SHOW_ACTIVE"> active </FilterLink> </p> ); // 6. FilterLink: 同 VisibleTodoList(props + contextTypes...) class FilterLink extends Component { // 订阅 store 的变化 componentDidMount() { const { store } = this.context; // props -> context // ... } // ... render() { const { renderFilter, children } = this.props; const { store } = this.context; // props -> context const { filter } = store.getState(); // ... } } // 必须声明 FilterLink.contextTypes = { store: React.PropTypes.object, }; // -- Link 不变 -- ``` **现在中间的非容器组件完全不用为了自己的孩子而费劲地传递 store={store}** 所以以上我们就实现了简化版的由 react-redux 提供的第一个组件 `<Provider />`。 然鹅,有木有觉得老写 contextTypes 好烦啊,而且 context 特性并不稳定,所以 context 并不应该直接写在我们的应用代码里。 > 计将安出? ###### 5. connect - OOP思维:这还不简单?写个函数把容器组件传进去作为父类,然后返回写好了 componentDidMount,componentWillUnmount 和 contextTypes 的子类不就好啦~ 恭喜你~面向对象的思想学的很不错~ 虽然 JavaScript 底层各种东西都是面向对象,然而在前端一旦与界面相关,照搬面向对象的方法实现起来会很麻烦... - React 早期用户:这还不简单?写个 mixin 岂不美哉~~? 作为 react 亲生的 mixin 确实在多组件间共享方法提供了一些便利,然而使用 mixin 的组件需要了解细节,从而避免状态污染,所以一旦 mixin 数量多了之后会越来越难维护。 > Unfortunately, we will not launch any mixin support for ES6 classes in React. That would defeat the purpose of only using idiomatic JavaScript concepts. 所以官方也放弃了在 ES6 class 中对 mixin 的支持。 - 函数式(FP):高阶组件 High Order Component(下称 hoc)才是终极解决方案~~ > hocFactory:: W: React.Component => E: React.Component 如上所示 hoc 的构造函数接收一个 W(代表 WrappedComponent)返回一个 E(代表 Enhanced Component),而 E 就是这个高阶组件。 假设我们有一个旧组件 Comp,然鹅现在接收参数有些变动。 当然你可以复制粘贴再修改旧组件的代码...(大侠受窝一拜) 也可以这么写,返回一个新组件来包裹旧组件。 ``` javascript class NewComp extends Component { mapProps(props) { return {/* new props */}; } render() { return (<Comp {...this.mapProps(this.props)} />); } } ``` 然鹅,如果有同样逻辑的更多的组件需要适配呢???总不能有几个抄几遍吧... **所以骚年你听说过高阶组件么~?** ``` javascript // 先返回一个函数,而那个函数再返回新组件 const mapProps = mapFn => Comp => { return class extends Component { render() { return (<Comp {...this.mapProps(this.props)} />); } }; }; const NewComp = mapProps(mapFn)(Comp); // 注意调用了两次 ``` 可以看到借助高阶组件我们将 mapFn 和 Comp 解耦合,这样就算需要再嵌套多少修改逻辑都没问题~~~天黑都不怕~~~ **ok,扯了这么多的淡,终于要说到 connect 了** 是哒,你木有猜错,react-redux 提供的第二个也是最后一个 api —— connect 返回的就是一个高阶组件。 使用的时候只需要 `connect()(WrappedComponent)` 返回的 component 自动就完成了在 componentDidMount 中订阅 store,在 componentWillUnmount 中取消订阅和声明 contextTypes。 **这样就只剩下最后一个麻烦** > 3.应用其实并不需要渲染所有的 todos,所以内部很麻烦地定义了 `_getVisibleTodos` 函数 其实 connect 函数的第一个参数叫做 mapStateToProps,作用就是将 store 中的数据提前处理或过滤后作为 props 传入内部组件,以便内部组件高效地直接调用。这样最后一个麻烦也解决了~ **然鹅,我们问自己这样就够了么?并没有...** 还有最后一个细节,以 FilterLink 为例。 ``` javascript class FilterLink extends Component { // ... render() { const { store, renderFilter, children } = this.props; const { filter } = store.getState(); return ( <Link active={filter === renderFilter} onClick={() => store.dispatch( setVisibilityFilter(renderFilter) )} > {children} </Link> ); } } ``` 除了从 store 中获取数据(filter),我们还从中获取了 dispatch,以便触发 action。如果将回调函数 onClick 的内容也加到 props 中,那么借助 connect 整个 FilterLink 的逻辑岂不是都被我们抽象完了? 是哒,connect 的第二个参数叫做 mapDispatchToProps,作用就是将各个调用到 dispatch 的地方都抽象成函数加到 props 中的传给内部组件。这样最后一个麻烦终于真的被解决了~ ``` javascript const mapStateToLinkProps = (state, ownProps) => ({ // ownProps 是原组件的 props, // 这里为了和高阶组件的 props 区分 active: ownProps.renderFilter === state.filter, }); const mapDispatchToLinkProps = (dispatch, ownProps) => ({ onClick: () => { dispatch( setVisibilityFilter(ownProps.renderFilter) ); }, }); // 注意原 FilterLink 整个都被我们删了 const FilterLink = connect( mapStateToLinkProps, mapDispatchToLinkProps )(Link); ``` **TodoApp 使用 react-redux** {% iframe http://jsbin.com/fumihi/edit?js,output 100% 800 %}
订阅后,新回复会通过你的通知中心匿名送达。
0 条回复
暂无回复 · 你可以订阅本帖等待新回复。