这是我参与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, 并且执行了 unwrap、checkInitialState、reactify 这三个方法。
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 进行的代理值。
下面分三部分:set、get、deleteProperty 来分析。
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 类型时,返回一个对象,包含
checked、onChange- 非 boolean 类型时,返回一个对象,
value、onChange- 对于 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 中使用响应式变量,这个库也是一个不错的选择。