颤抖吧!一起手写一个redux框架!

redux是一个前端架构,经常和react一起使用。你要用react.js基本上都要用到redux和react-redux,但这两者并不是一个东西!

  • redux是一个前端框架,你可以把它用到react、vue,设置jquery。
  • react-redux是把redux这个前端架构结合到react形成的库,就是redux架构在react中的体现。

话不多说,我们来从头手写一个redux框架。

create-react-app新建一个项目myRedux,修改public/index.html里的body结构为:

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

src/index.js里的代码替换为如下代码,代表我们的应用状态:

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

我们新增几个渲染函数,它会把上面的状态渲染到页面上:

function renderApp (appState) {
  renderTitle(appState.title)
  renderContent(appState.content)
}

function renderTitle (title) {
  const titleDOM = document.getElementById('title')
  titleDOM.innerHTML = title.text
  titleDOM.style.color = title.color
}

function renderContent (content) {
  const contentDOM = document.getElementById('content')
  contentDOM.innerHTML = content.text
  contentDOM.style.color = content.color
}

很简单,renderApp方法会调用renderTitlerenderContent,而这两个方法会把appState的数据渲染到页面上。


运行截图贴在这里


一. 手写dispatch

这是一个很简单的页面,但是存在着严重的隐患:我们渲染页面时,用到了一个共享数据appState,每个人都在哪里都可以修改它。如果在渲染之前做了一系列其他操作:

loadDataFromServer()
doSomethingUnexpected()
doSomthingMore()
// ...
renderApp(appState)

你根本无法知道这些方法会对renderApp做什么修改。这种任何地方都可以对共享数据进行修改的写法,会给debug/开发造成极大的难度。
有过一定开发经验的朋友看到这里,一定会忍不住对修改共享数据的操作做一个收口,让所有对共享数据修改的操作统一收口在一起。
原来各个模块可以直接修改appState,如下图:
在这里插入图片描述
而现在要写更改appState,必须通过dispatch,如下图:
在这里插入图片描述
我们定义一个收口函数dispatch,让它专门负责共享数据的修改:

function dispatch (action) {
  switch (action.type) {
    case 'UPDATE_TITLE_TEXT':
      appState.title.text = action.text
      break
    case 'UPDATE_TITLE_COLOR':
      appState.title.color = action.color
      break
    default:
      break
  }
}

所有对共享数据的修改,必须通过dispatch函数。它接受一个参数action,这个action是一个普通的JavaScript对象,里面必须包含一个type指明你想干什么。dispatchswitch里会去识别这个type字段,然后对appState进行相应的修改。
上面的dispatch只能识别两种操作:一种是'UPDATE_TITLE_TEXT'会用actiontext字段去更新appState.title.text;一种是'UPDATE_TITLE_COLOR'会用actioncolor字段去更新appState.title.color。可以看到,action里除了type字段,其他都是可以自定义的。

二. 手写store

上面我们有了appStatedispatch
现在我们进一步整合,把它们都集中在一个地方,并给这个地方起名叫store,然后创建一个createStore方法来构建store

function createStore (state, stateChanger) {
  const getState = () => state
  const dispatch = (action) => stateChanger(state, action)
  return { getState, dispatch }
}

createStore接受两个参数,一个表示app的状态state,一个用来修改state。
现在,我们来修改原来的代码:

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

function stateChanger (state, action) {
  switch (action.type) {
    case 'UPDATE_TITLE_TEXT':
      state.title.text = action.text
      break
    case 'UPDATE_TITLE_COLOR':
      state.title.color = action.color
      break
    default:
      break
  }
}

const store = createStore(appState, stateChanger)

renderApp(store.getState()) // 首次渲染页面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 小书》' }) // 修改标题文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改标题颜色
renderApp(store.getState()) // 把新的数据渲染到页面上

针对不同的app,我们给createStore传入初始状态appState和用于描述appState变化的stateChanger。需要修改的数据的时候通过store.dispatch,需要获取数据的时候通过store.getState

监控数据的变化

上面的代码有一个问题,在dispatch修改数据的时候,其实只是数据发生了变化,并没有调用renderApp方法,页面上的内容是不会变化的。然而,我们又不能每次dispatch的时候又renderApp,我们希望用一种通用的监听数据变化的方式,然后重新渲染页面。

function createStore (state, stateChanger) {
  const listeners = []
  const subscribe = (listener) => listeners.push(listener)
  const getState = () => state
  const dispatch = (action) => {
    stateChanger(state, action)
    listeners.forEach((listener) => listener())
  }
  return { getState, dispatch, subscribe }
}

我们在createStore中定义一个数组listeners,并对外提供一个subscribe方法,可以用该方法给数组push一个渲染函数,每当dispatch的时候,listeners里的渲染函数都会被调用。这样我们就可以在数据变化的时候重新渲染页面:

const store = createStore(appState, stateChanger)
store.subscribe(() => renderApp(store.getState()))

renderApp(store.getState()) // 首次渲染页面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 小书》' }) // 修改标题文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改标题颜色

三. 手写reducer

细心的朋友会发现,前面的代码有着严重的性能问题。我们在每个渲染函数前打下log:

function renderApp (appState) {
  console.log('render app...')
  renderTitle(appState.title)
  renderContent(appState.content)
}

function renderTitle (title) {
  console.log('render title...')
  const titleDOM = document.getElementById('title')
  titleDOM.innerHTML = title.text
  titleDOM.style.color = title.color
}

function renderContent (content) {
  console.log('render content...')
  const contentDOM = document.getElementById('content')
  contentDOM.innerHTML = content.text
  contentDOM.style.color = content.color
}

我们接下来dispatch两个action,来修改title的color和text:

const store = createStore(appState, stateChanger)
store.subscribe(() => renderApp(store.getState())) // 监听数据变化

renderApp(store.getState()) // 首次渲染页面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 小书》' }) // 修改标题文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改标题颜色

可以在控制台看到:
在这里插入图片描述
整个页面都刷新了三遍!第一次是页面初始化,后面两次修改title的状态的操作,把整个页面都刷新了!
但其实,我们只希望让跟修改的数据(title)有关系的组件刷新。
这里给出一个解决方案:
每次渲染之前,先对新数据和旧数据做一下比较,把不一样的部分进行渲染。

function renderApp (newAppState, oldAppState = {}) { // 防止 oldAppState 没有传入,所以加了默认参数 oldAppState = {}
  if (newAppState === oldAppState) return // 数据没有变化就不渲染了
  console.log('render app...')
  renderTitle(newAppState.title, oldAppState.title)
  renderContent(newAppState.content, oldAppState.content)
}

function renderTitle (newTitle, oldTitle = {}) {
  if (newTitle === oldTitle) return // 数据没有变化就不渲染了
  console.log('render title...')
  const titleDOM = document.getElementById('title')
  titleDOM.innerHTML = newTitle.text
  titleDOM.style.color = newTitle.color
}

function renderContent (newContent, oldContent = {}) {
  if (newContent === oldContent) return // 数据没有变化就不渲染了
  console.log('render content...')
  const contentDOM = document.getElementById('content')
  contentDOM.innerHTML = newContent.text
  contentDOM.style.color = newContent.color
}

我们修改下stateChanger,让它接收到action后不是直接去修改state,而是重新生成一个对象。

function stateChanger (state, action) {
  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 // 没有修改,返回原来的对象
  }
}

因为我们改了stateChanger,我们来修改下createStore

function createStore (state, stateChanger) {
  const listeners = []
  const subscribe = (listener) => listeners.push(listener)
  const getState = () => state
  const dispatch = (action) => {
    state = stateChanger(state, action) // 覆盖原对象
    listeners.forEach((listener) => listener())
  }
  return { getState, dispatch, subscribe }
}

此时,我们再去执行刚才的代码:
在这里插入图片描述
我们就这样成功优化了页面性能,每次只刷新了修改过的数据对应的组件。
此时,我们的stateChanger还有没有优化的空间了?
其实,可以把appState和stateChanger合并到一起:

function stateChanger (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
  }
}

这样createStore就只有了一个参数:

function createStore (stateChanger) {
  let state = null
  const listeners = []
  const subscribe = (listener) => listeners.push(listener)
  const getState = () => state
  const dispatch = (action) => {
    state = stateChanger(state, action)
    listeners.forEach((listener) => listener())
  }
  dispatch({}) // 初始化 state
  return { getState, dispatch, subscribe }
}

此时,这个stateChanger,就是reduxreducer了!

四. 总结

此时,我们已经完整地手写了一个redux框架。
redux的核心就是发明了store,通过dispatch一个action来更改store里的值。

五. 参考

https://imweb.io/topic/59f5a7fdb72024f03c7f49bc
http://huziketang.mangojuice.top/books/react/lesson35

猜你喜欢

转载自blog.csdn.net/colinandroid/article/details/88204523