hooks 限制太多? 试试这个库吧| 8月更文挑战

352 阅读2分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

前言

熟悉 React hooks 的同学都知道,各种限制层出不穷,写着写着就不知道为什么页面怎么没更新了~ 而与之对应的,Vue 一直在追赶 React 的脚步,响应式变量让心智负担变得更轻了,React 的各种奇怪的页面表现也少了很多。那么有没有办法能够让 React 也用上响应式更新,不用再思考究竟哪个 useCallback 忘了添加变量依赖呢? 就是现在,试试 radioactive-state 吧~

使用方式

import useRS from 'radioactive-state';

const state = useRS({
  input: ''
})

<input
  value={state.input}
  onChange={(e) => state.input = e.target.value}
  type='text'
/>

可以看到,经过 useRS 包裹的对象会返回一个 state。通过 state 可以获取、设置响应式变量的值。 在 input 中使用了 state.input,并且当触发 onChange 事件时,将最新的值赋给 state.input

使用方式及其简单,和 Vue 中的响应式变量如出一辙。

下面我们通过源码来看看 useRS 有什么神奇之处。

useRS

const useRS = arg => {
    const [, forceUpdate] = useReducer(x => x + 1, 0)
    // ...
}

可以看到,这个通过 useReducer 创建了一个 reducer,并且第二个 set 函数取名为 forceUpdate。 这个函数用来对 React 进行强制刷新。

我们继续看。

const useRS = arg => {
    // ...
    const ref = useRef()
    
    if (!ref.current) {
        const initialState = unwrap(arg)
        checkInitialState(initialState)
        ref.current = reactify(checkInitialState, forceUpdate)
    }
    
    return ref.current
}

通过 useRef 创建了一个 ref, 并且执行了 unwrapcheckInitialStatereactify 这三个方法。

unwrap

const unwrap = (x) => typeof x === 'function' ? x() : x

如果传入 useRS 的参数是函数类型,则先执行函数后再进行响应式代理。

checkInitialState

const checkInitialState = initialState => {
    if (!isObject(initialState)) {
        throw new Error(`Invalid State "${initialState}" provided to useRS() hook.`)
    }
}

这个方法是对传入的参数进行校验,必须是 Array 或 Object 类型。

reactify

const reactify = (state, forceUpdate, path = []) => {
  const wrapper = Array.isArray(state) ? [] : {}
  Object.keys(state).forEach(key => {
    let slice = state[key]
    slice = unwrap(slice)
    wrapper[key] = isObject(slice) ? reactify(slice, forceUpdate, [...path, key]) : slice
  })

  // ...
}

传入 reactify 函数的参数有两个,分别是需要进行响应式绑定的数组或对象,以及强制更新的方法。

首先,创建了 wrapper 作为响应式绑定后最终返回的值。

然后先对传入的数组或对象进行遍历,再使用上文提过的 unwrap 处理,对函数直接取其返回值。

最后如果是数组或对象,则递归进行 reactify,并保存递归过程的 对象的 key。对于其他类型,则直接赋值。

const reactify = (state, forceUpdate, path = []) => {
  // ...
  
  return new Proxy(wrapper, {
    set(target, prop, value) {
    },
    
    deleteProperty(target, prop) {
    },
    
    get(target, prop) {
    },
  })
}

可以看到,这里才是 reactify 重点的部分,最终返回了对 wrapper 进行的代理值。

下面分三部分:setgetdeleteProperty 来分析。

proxy.set

set(target, prop, value) {
    const addingNonReactiveObject = isObject(value) && !value.__isRadioactive__
    const $value = addingNonReactiveObject ? reactify(value, forceUpdate, [...path, prop]) : value

    // if new value and old value are not same
    if (target[prop] !== $value) {
        schedule(forceUpdate)
        mutations++
    }

    return Reflect.set(target, prop, $value)
}

当对 useRS 返回的 state 进行赋值时:

  • 首先检查新值是否需要被 reactify,将代理后的值赋给 $value
  • 然后检查原值和新值是否相同,不同时,通过 schedule 对 React 进行 强制刷新
const schedule = (cb) => {
  if (!cb.scheduled) {
    cb.scheduled = true
    setTimeout(() => {
      cb()
      cb.scheduled = false
    }, 0)
  }
}

这里,schedule 是通过 setTimeout 进行批量更新。(保证同步任务中多次修改 state 但只执行一次 forceUpdate

  • 最后对 mutations++,并通过 Reflect.set 真正修改新值。

proxy.get

get(target, prop) {
  if (prop === '__target__') return unReactify(target)
  if (prop === '__isRadioactive__') return true
  // mutation count
  if (prop === '$') return mutations
  // inputBinding
  if (prop[0] === '$') return inputBinding(prop, target, forceUpdate)
  else return Reflect.get(target, prop)
}

当获取 useRS 返回 state 上的属性时,

  • 获取的是 __target__ 时,返回原值(未响应式绑定的值)
  • 获取的是 __isRadioactive__ 时,返回 true
  • 获取的是 $ 时,返回 变量被修改的次数(多次 set 可能只触发一次 forceUpdate,但 $ 始终自增)
  • 获取的是 $ 开头的属性时,例如 $age 时,返回 inputBinding 的返回值
const inputBinding = (prop, target, forceUpdate) => {
  const actualProp = prop.substring(1)
  if (target.hasOwnProperty(actualProp)) {

    let key = 'value'
    const propType = typeof target[actualProp]
    if (propType === 'boolean') {
      key = 'checked'
    }

    const binding =  {
      [key]: target[actualProp],
      onChange: e => {
        let value = e.target[key]
        if (propType === 'number') value = Number(value)
        Reflect.set(target, actualProp, value)
        forceUpdate()
      }
    }

    return binding
  }
}

解释:

  • 首先检查 $age 除去 $ 后的属性名是否存在,如果不存在则返回 undefined
  • 当属性名存在,判断该属性类型:
    • boolean 类型时,返回一个对象,包含 checkedonChange
    • 非 boolean 类型时,返回一个对象, valueonChange
  • 对于 checked 字段,即是该字段的 true | false
  • 对于 value 字段,对应该字段本身
  • 对于 onChange 事件,接收 { event: { value: xx } } 用于修改数据,并触发 forceUpdate
- 其他类型的值,则通过 `Reflect.get` 返回真正的值

proxy.deleteProperty

deleteProperty(target, prop) {
  schedule(forceUpdate)
  mutations++
  return Reflect.deleteProperty(target, prop)
}

当删除属性时,也会触发 forceUpdate,并且 mutations 进行加一。

最后通过 Reflect.deleteProperty 删除属性。

总结

radioactive-state 通过 Proxy 实现了数据的响应式处理。

另外,其中 inputBinding$ 的概念设计也非常巧妙。如果想在 React 中使用响应式变量,这个库也是一个不错的选择。