基本参考来自 《React.js 小书》

一、目的

脱离 React ,加深 Redux

自己实现一个 redux 模式的几个关键函数,但是没有实现所有的 中间件

主要实现的几个功能点是:

  • 观察者模式下的数据自动刷新
  • createStore
  • 纯函数 reducer

二、关键部分

关键原理解释都在代码中。

1、DOM 结构 和 数据模型

  <body>
    <h1 id='title'></h1>
    <div id="content"></div>
  </body>

用到两个数据模型:

一个负责 title 的文字和颜色,一个负责内容的文字和颜色

          title: {
            text: 'React.js 小书',
            color: 'red',
          },
          content: {
            text: 'React.js 小书内容',
            color: 'blue'
          }

2、三个基本渲染方法

三个基本渲染方法主要是为了将内容挂载到 DOM 上,不过在这个过程中,直接考虑从了 DOM 操作的性能问题:

如果只修改 title 或者是 content,或者是都不修改,则就不重新渲染某些 DOM ,因此是进行了 新旧数据比较 的。

整体渲染方法:

该方法调用 renderTilerenderContent, 用来将 title 和 content 挂载到 dom 节点上,并且设置 style。

/**
 * APP 内容渲染
 * - 主要调用 renderTile 和 renderContent 两个方法进行内容的渲染
 * - 其中  newAppState 和 oldAppState 
 * @param {*} newAppState 
 * @param {*} oldAppState 
 */
function renderApp(newAppState, oldAppState = {}) {
    if (newAppState === oldAppState) // 进行数据变化比较 
        return; // 数据无变化不进行更新
    console.log('app...'); // 性能验证输出
    renderTitle(newAppState.title, oldAppState.title); // 渲染 title
    renderContent(newAppState.content, oldAppState.content); // 渲染 content
}

title 渲染方法:

/**
 * 标题渲染
 * @param {*} title 
 * @param {*} oldTile 
 */
function renderTitle(title, oldTile = {}) {
    if (title === oldTile) // 进行数据变化比较 
        return; // 数据无变化则不进行更新
    console.log('title...')// 性能验证输出
    const titleDOM = document.getElementById('title');
    titleDOM.innerHTML = title.text;
    titleDOM.style.color = title.color;
}

content 渲染方法:

/**
 * 内容渲染
 * @param {*} content 
 * @param {*} oldContent 
 */
function renderContent(content, oldContent = {}) {
    if (content === oldContent) // 进行数据变化比较
        return; // 比较变化
    console.log('content...');
    const contentDOM = document.getElementById('content');
    contentDOM.innerHTML = content.text;
    contentDOM.style.color = content.color;
}

3、观察者模式的 createStore

要生成一个 store,基本的功能主要如下:

  • 需要通过观察者模式,对所有该 store 下的观察者自动进行一次数据刷新 —— subscribe
  • 需要通过 reducer 对 state 进行新旧数据产出 —— dispatch
  • 需要能够触发 reducer 的 action —— dispatch
  • 需要能够返回 state —— getState
/**
 * createStore
 * - 用来创建 store
 * - 实现了观察者模式
 * - 对 reducer 进行了处理
 * - 默认会首先 dispatch 一次空对象,也就是通过 reducer 生成初始化的数据
 * @param {*} reducer
 */
function createStore(reducer) {
    let state = null; // 初始化 state 是 null
    const listeners = []; // 监听者是空
    const subscribe = (listener) => listeners.push(listener); // 如果有监听者,则将其 push 进数组
    const getState = () => state; // 返回 state
    const dispatch = (action) => {
        // 触发 action 
        state = reducer(state, action); // 使用 reducer 覆盖原来的 state
        listeners.forEach((listener) => listener());// 对所有监听者进行更新操作
    };
    dispatch({});// 默认进行一次初始化state
    return {getState, dispatch, subscribe}; // 返回 store 对象
}

4、reducer

一个能够返回初始值的 reducer

/**
 * reducer
 * - 能够产出默认数据
 * - 能够对 action 进行处理 action 主要有 type 和 [data] 两部分组成
 * @param {*} state 
 * @param {*} action 
 */
function reducer(state, action) {
    if (!state) {
        return {
          title: {
            text: 'React.js 小书',
            color: 'red',
          },
          content: {
            text: 'React.js 小书内容',
            color: 'blue'
          }
        }
      }
    switch (action.type) {
        case 'UPDATE_TITLE_TEXT':
        
            return { // 构建新的对象并且返回
                ...state,
                title: {
                    ...state.title,
                    text: action.text
                }
            }
        case 'UPDATE_TITLE_COLOR':
            return { // 构建新的对象并且返回
                ...state,
                title: {
                    ...state.title,
                    color: action.color
                }
            }
        default:
            return state // 没有修改,返回原来的对象
    }
}

三、使用方法

上面其实主要的代码都完成了, store 能够创建了,reducer 有了,现在就是进行应用即可:

根据 redux 思想的基本流程是:

  • 通过一个 reducer 得到一个 store
  • 在某个位置有一个观察者加入(通过 store.subscribe(()=>{})
  • 通过 store 得到 state
  • 进行第一次渲染
  • 之后的渲染通过 dispatch 就可以。
// 通过上面的 reducer 创建一个 srore
const store = createStore(reducer);
// 得到旧数据(初次得到的是初始化数据),这些数据将用来进行 新旧数据对比
let oldState = store.getState();
// 添加一个观察者
// 观察者主要的作用是:
// - 获取新数据
// - 进行新数据的渲染
// - 渲染完成后,将 oldState 更换为 新的 newState,用来进行下一次数据比对
store.subscribe(() => {
    const newState = store.getState(); // 获取新的 state
    renderApp(newState, oldState); // 进行渲染
    oldState = newState; // 渲染完成后,将 oldState 更新成 newState
});

// 首次渲染
renderApp(store.getState());

// 进行触发
store.dispatch({type: 'UPDATE_TITLE_TEXT', text: 'React.js 小书 新标题'});
store.dispatch({type: 'UPDATE_TITLE_COLOR', color: '#0099CC'});

四、注意部分

1、新旧数据比对

新旧数据比对主要是为了性能考虑,如果每次都将 title 和 content 渲染一遍,这显然是没必要的。

而数据对比使用的是 对象,因此借助了 ES6 中的 对象扩展运算符以及 Object.assign() 浅拷贝都可以做到。

具体为什么可以使用 === 来比较对象,可以查看 ES6 对象这部分的内容:

五、带新旧数据比对的效果

可以看到,在初始渲染中,title 和 content 都进行更新了

而在 dispatch 两次中,只有 title 更新,content 并没有进行 DOM 的更新。

1.jpg