提起 Redux 咱们想到最多的应该就是 React-redux 这个库,可是实际上 Redux 和 React-redux 并不是同一个货色, Redux 是一种架构模式,源于 Flux。React-redux 是 Redux 思维与 React 联合的一种具体实现。
在咱们应用 React 的时候,经常会遇到组件深层次嵌套且须要值传递的状况,如果应用 props 进行值的传递,显然是十分苦楚的。为了解决这个问题,React 为咱们提供了原生的 context API,但咱们用的最多的解决方案却是应用 React-redux 这个基于 context API 封装的库。
本文并不介绍 React-redux 的具体用法,而是通过一个小例子,来理解下什么是 redux。
好了,当初咱们言归正传,来实现咱们本人的 redux。
一、最后
首先,咱们用 creat-react-app 来创立一个我的项目,删除 src 下冗余局部,只保留 index.js,并批改 index.html 的 DOM 构造:
# index.html
<div id="root">
<div id="head"></div>
<div id="body"></div>
</div>
咱们在 index.js 中创立一个对象,用它来贮存、治理咱们整个利用的数据状态,并用渲染函数把数据渲染在页面:
const appState = {
head: {
text: '我是头部',
color: 'red'
},
body: {
text: '我是 body',
color: 'green'
}
}
function renderHead (state){const head = document.getElementById('head')
head.innerText = state.head.text;
head.style.color = state.head.color;
}
function renderBody (state){const body = document.getElementById('body')
body.innerText = state.body.text;
body.style.color = state.body.color;
}
function renderApp (state){renderHead(state);
renderBody(state);
}
renderApp(appState);
此时运行代码,关上页面,咱们能够看到,在 head 中曾经呈现了红色字体的‘我是头部’,在 body 中呈现了绿色字体的‘我是 body’。
如果咱们把 head 和 body 看作是 root 中的两个组件,那么咱们曾经实现了一个全局惟一的 state。这个 state 是全局共享的,随处可调用的。
咱们能够批改 head 的渲染函数,来看下成果:
function renderHead (state){const head = document.getElementById('head')
head.innerText = state.head.text + '--' + state.body.text;
head.style.color = state.head.color;
state.body.text = '我是通过 head 批改后的 body';
}
咱们看到,在 head 渲染函数中,咱们不仅能够取用 body 属性的值,还能够扭转他的值。这样就存在一个重大的问题,因为 state 是全局共用的,一旦在一个中央扭转了 state 的值,那么,所有用到这个值的组件都将受到影响,而且这个扭转是不可预期的,显然给咱们的代码调试减少了难度系数,这样的后果是咱们不违心看到的!
二、dispatch
当初看来,在咱们背后呈现了一个矛盾:咱们须要数据共享,但共享数据被任意的批改又会造成不可预期的问题!
为了解决这个矛盾,咱们须要一个管家,专门来治理共享数据的状态,任何对共享数据的操作都要通过他来实现,这样,就防止了随便批改共享数据带来的不可预期的危害!
咱们从新定义一个函数,用这个函数充当咱们的管家,来对咱们的共享数据进行治理:
function dispatch(state, action) {switch (action.type) {
case 'HEAD_COLOR':
state.head.color = action.color
break
case 'BODY_TEXT':
state.body.text = action.text
break
default:
break
}
}
咱们来从新批改 head 的渲染函数:
function renderHead (state){const head = document.getElementById('head')
head.innerText = state.head.text + '--' + state.body.text;
head.style.color = state.head.color;
dispatch(state, { type: 'BODY_TEXT', text: '我是 head 通过调用 dispatch 批改后的 body'})
}
dispatch 函数接管两个参数,一个是须要批改的 state,另一个是批改的值。这时,尽管咱们仍旧批改了 state,然而通过 dispatch 函数,咱们使这种扭转变得可控,因为任何扭转 state 的行为,咱们都能够在 dispatch 中找到扭转的源头。
这样,咱们仿佛曾经解决了之前的矛盾,咱们创立了一个全局的共享数据,而且严格的把控了任何扭转这个数据的行为。
然而,在一个文件中,咱们既要保留 state,还要保护管家函数 dispatch,随着利用的越来越简单,这个文件势必会变得简短繁冗,难以保护。
当初,咱们把 state 和 dispatch 独自抽离进去:
- 用一个文件独自保留 state
- 用另一个文件独自保留 dispatch 中批改 state 的对照关系 changeState
- 最初再用一个文件,把他们联合起来,生成全局惟一的 store
这样,不仅使单个文件变得更加精简,而且在其余的利用中,咱们也能够很不便的复用咱们这套办法,只须要传入不同利用的 state 和批改 state 的对应逻辑 stateChange,就能够释怀的通过调用 dispatch 办法,对数据进行各种操作了:参考 前端手写面试题具体解答
# 扭转咱们的目录构造,新增 redux 文件夹
+ src
++ redux
--- state.js // 贮存利用数据状态
--- storeChange.js // 保护一套批改 store 的逻辑, 只负责计算,返回新的 store
--- createStore.js // 联合 state 和 stateChange , 创立 store , 不便任何利用援用
--index.js
## 批改后的各个文件
# state.js -- 全局状态
export const state = {
head: {
text: '我是头部',
color: 'red'
},
body: {
text: '我是 body',
color: 'green'
}
}
# storeChange.js -- 只负责计算,批改 store
export const storeChange = (store, action) => {switch (action.type) {
case 'HEAD_COLOR':
store.head.color = action.color
break
case 'BODY_TEXT':
store.body.text = action.text
break
default:
break
}
}
# createStore.js -- 创立全局 store
export const createStore = (state, storeChange) => {const store = state || {};
const dispatch = (action) => storeChange(store, action);
return {store, dispatch}
}
# index.js
import {state} from './redux/state.js';
import {storeChange} from './redux/storeChange.js';
import {createStore} from './redux/createStore.js';
const {store, dispatch} = createStore(state, storeChange)
function renderHead (state){const head = document.getElementById('head')
head.innerText = state.text;
head.style.color = state.color;
}
function renderBody (state){const body = document.getElementById('body')
body.innerText = state.text;
body.style.color = state.color;
}
function renderApp (store){renderHead(store.head);
renderBody(store.body);
}
// 首次渲染
renderApp(store);
通过以上的文件拆分,咱们看到,不仅使单个文件更加精简,文件的职能也更加明确:
- 在 state 中,咱们只保留咱们的共享数据
- 在 storeChange 中,咱们来保护扭转 store 的对应逻辑,计算出新的 store
- 在 createStore 中,咱们创立 store
- 在 index.js 中,咱们只须要关怀相应的业务逻辑
三、subscribe
所有仿佛都那么美妙,可是当咱们在首次渲染后调用 dispatch 批改
store 时,咱们发现,尽管数据被扭转了,可是页面并没有刷新,只有在 dispatch 扭转数据后, 从新调用 renderApp() 能力实现页面的刷新。
// 首次渲染
renderApp(store);
dispatch({type: 'BODY_TEXT', text: '我是调用 dispatch 批改的 body'}) // 批改数据后,页面并没有主动刷新
renderApp(store); // 从新调用 renderApp 页面刷新
这样,显然并不能达到咱们的预期,咱们并不想在每次扭转数据后手动的刷新页面,如果能在扭转数据后,主动进行页面的刷新,当然再好不过了!
如果间接把 renderApp 写在 dispatch 里,显然是不太适合的,这样咱们的 createStore 就失去了通用性。
咱们能够在 createStore 中新增一个收集数组,把 dispatch 调用后须要执行的办法对立收集起来,而后再循环执行,这样,就保障了 createStore 的通用性:
# createStore
export const createStore = (state, storeChange) => {const listeners = [];
const store = state || {};
const subscribe = (listen) => listeners.push(listen);
const dispatch = (action) => {storeChange(store, action);
listeners.forEach(item => {item(store);
})
};
return {store, dispatch, subscribe}
}
# index.js
···
const {store, dispatch, subscribe} = createStore(state, storeChange)
···
···
// 增加 listeners
subscribe((store) => renderApp(store));
renderApp(store);
dispatch({type: 'BODY_TEXT', text: '我是调用 dispatch 批改的 body'});
这样,咱们每次调用 dispatch 时,页面就会从新刷新。如果咱们不想刷新页面,只想 alert 一句话,只须要更改增加的 listeners 就好了:
subscribe((store) => alert('页面刷新了'));
renderApp(store);
dispatch({type: 'BODY_TEXT', text: '我是调用 dispatch 批改的 body'});
这样咱们就保障了 createStore 的通用性。
四、优化
到这里,咱们仿佛曾经实现了之前想达到的成果:咱们实现了一个全局专用的 store,而且这个 store 的批改是通过严格把控的,并且每次通过 dispatch 批改 store 后,都能够实现页面的主动刷新。
可是,显然这样并不足够,以上的代码仍有些简陋,存在重大的性能问题,
尽管咱们只是批改了 body 的文案,可是,在页面从新渲染时,head 也被再次渲染。那么,咱们是不是能够在页面渲染的时候,来比照新旧两个 store 来感知哪些局部须要从新渲染,哪些局部不用再次渲染呢?
依据下面的想法,咱们再次来批改咱们的代码:
# storeChange.js
export const storeChange = (store, action) => {switch (action.type) {
case 'HEAD_COLOR':
return {
...store,
head: {
...store.head,
color: action.color
}
}
case 'BODY_TEXT':
return {
...store,
body: {
...store.body,
text: action.text
}
}
default:
return {...store}
}
}
# createStore.js
export const createStore = (state, storeChange) => {const listeners = [];
let store = state || {};
const subscribe = (listen) => listeners.push(listen);
const dispatch = (action) => {const newStore = storeChange(store, action);
listeners.forEach(item => {item(newStore, store);
})
store = newStore;
};
return {store, dispatch, subscribe}
}
# index.js
import {state} from './redux/state.js';
import {storeChange} from './redux/storeChange.js';
import {createStore} from './redux/createStore.js';
const {store, dispatch, subscribe} = createStore(state, storeChange);
function renderHead (state){console.log('render head');
const head = document.getElementById('head')
head.innerText = state.text;
head.style.color = state.color;
}
function renderBody (state){console.log('render body');
const body = document.getElementById('body')
body.innerText = state.text;
body.style.color = state.color;
}
function renderApp (store, oldStore={}){if(store === oldStore) return;
store.head !== oldStore.head && renderHead(store.head);
store.body !== oldStore.body && renderBody(store.body);
console.log('render app',store, oldStore);
}
// 首次渲染
subscribe((store, oldStore) => renderApp(store, oldStore));
renderApp(store);
dispatch({type: 'BODY_TEXT', text: '我是调用 dispatch 批改的 body'});
以上,咱们批改了 storeChange,让他不再间接批改原来的 store,而是通过计算,返回一个新的 store。咱们又批改了 cearteStore 让他接管 storeChange 返回的新 store,在 dispatch 批改数据并且页面刷新后,把新 store 赋值给之前的 store。而在页面刷新时,咱们来通过比拟 newStore 和 oldStore,感知须要从新渲染的局部,实现一些性能上的优化。
最初
咱们通过简略的代码例子,简略理解下 redux,尽管代码仍有些简陋,可是咱们曾经实现了 redux 的几个核心理念:
- 利用中的所有 state 都以一个 object tree 的模式存储在一个繁多的 store 中。
- 惟一能改 store 的办法是触发 action,action 是动作行为的形象。