ahooks 源码学习 - useReactive

1,948 阅读4分钟

hooks 是什么?

随着 React16.8 的出现,hooks 也随之登场,而带来的也是一场巨大的生态变革,后续几乎所有的库都提供了 hooks 的版本,有的直接用 hooks 重写了。hooks 距今发布也有很长时间了,我们学习 React 也应该重点去学习它。

hooks 是 React 为函数组件提供的一种类似于类组件的能力,能够保存函数内部的状态,写法更加简介,状态逻辑复用能力更强了,也逐渐接近 React 的开发理念:函数式编程。

社区也有一批非常优秀的自定义 hooks,这里我们就学习了一下蚂蚁官方开源的 ahooks 库,学习优秀的开源库对我们理解 hooks 以及今后的开发都有很大的帮助,这里对其中的一些要点做了记录,首先看一下我们的 useReactive

useReactive 解决了什么问题?

在 React hooks 中,最常用的莫过于 useState,而 useState 我们都知道它会返回一个 state 和一个改变 state 的函数 setState。我们需要更新 state 时,调用 setState(newState) 去更新,这是 React hooks 的基本理念。

那用过 Vue 的同学应该也知道,在 Vue 中改变数据只需要 state.xxx = xxx 即可,因为 Vue 帮我们实现了数据代理,实现了响应式修改数据,使用起来非常方便。

那么把 Vue 的响应式数据理念结合 React hooks 就产生了 useReactive,让我们可以在 hooks 中获取数据响应式的操作体验,定义状态不再需要写 useState,直接修改属性就可以刷新视图。

示例

const state = useReactive({
  count: 0,
  obj: {
    value: '',
  },
})

return (
  <div>
    <p> state.count:{state.count}</p>
    <button style={{ marginRight: 8 }} onClick={() => state.count++}>
      state.count++
    </button>
    <button onClick={() => state.count--}>state.count--</button>

    <p style={{ marginTop: 20 }}> state.obj.value: {state.obj.value}</p>
    <input onChange={(e) => (state.obj.value = e.target.value)} />
  </div>
)

如何实现?

我们知道在 Vue 中实现数据响应式的核心就是是通过对象代理的方式,Vue2 中使用的 Object.defineProperty,Vue3 使用的是 Proxy。具体使用那种方式取决于你的项目是否需要兼容 IE,Proxy 的方式在性能和使用上都有明显的优势,所以 useReactive 也采用了 Proxy 来实现。

我们先实现一个代理对象的 observer 方法:

// k:v 原对象:代理过的对象
const proxyMap = new WeakMap();
// k:v 代理过的对象:原对象
const rawMap = new WeakMap();

function isObject(val) {
  return typeof val === 'object' && val !== null;
}

function observer(initialVal, cb) {
  const existingProxy = proxyMap.get(initialVal)

  // 添加缓存,防止重复构建 proxy
  if (existingProxy) {
    return existingProxy
  }

  // 防止重复代理已经代理过的对象
  if (rawMap.has(initialVal)) {
    return initialVal
  }

  // 构建 proxy
  const proxy = new Proxy(initialVal, {
    get(target, key, receiver) {
      // 查找 target 上的 key 属性
      // 这里使用 Reflect.get 而不是 target[key] 取值目的是如果 target 上存在 getter 函数
      // 可以把内部的 this 绑定到 receiver 上,这样 getter 内部取值的时候就可以再次走 get 方法
      const res = Reflect.get(target, key, receiver)
      // 如果值是对象,继续递归代理,否则返回属性值
      return isObject(res) ? observer(res, cb) : Reflect.get(target, key)
    },
    set(target, key, val) {
      const ret = Reflect.set(target, key, val)
      // 新的值设置结束触发回调函数
      cb()
      return ret
    },
    deleteProperty(target, key) {
      const ret = Reflect.deleteProperty(target, key);
      // 当我们删除一个属性的时候也需要触发回调函数
      cb();
      return ret;
    },
  })

  proxyMap.set(initialVal, proxy)
  rawMap.set(proxy, initialVal)

  return proxy
}

这样一个简单的 observer 方法就实现了,并且我们加了缓存,能够有效防止重复代理。这个是我们的核心方法,需要考它来实现数据响应式。

注意:为了防止重复代理以及代理已经代理过的对象observer 方法内部使用了两个 Map 结构,它们的键值对正好相反,可以相互查找,这样就能保证不会出现以上这两种情况。

其实 Vue 内部也有这种写法,不过最新的实现已经改了,但是目的都是一样的,感兴趣的可以去看一下

接下来就可以写 useReactive 的具体实现了:

function useReactive(initialState) {
  const [, setFlag] = useState({})
  // 将初始状态保存到 ref 上,可以保证对象不变
  const stateRef = useRef(initialState)

  const state = useCreation(() => {
    // 代理这个 stateRef 对象,回调函数就相当于类组件的 `forceUpdate`,能够实现刷新页面的效果
    // 也就是我们修改 state 值或者删除 state 的某个属性时会触发
    return observer(stateRef, () => {
      setFlag({})
    })
  }, [])
}

可以看到 useReactive 其实特别简单,核心就在于如何使用 Proxy 代理对象,另外可以注意到这里使用了 useCreation,它是 useMemouseRef 的替代品,具体可参考 useCreation 的用法讲解。

一些题外话

  • 使用 useReactive hooks 可以让我们获取数据响应式的操作体验,直接改属性就能够刷新页面,相比我们使用 setState 会更加方便一点
  • useReactive hooks 的核心就是对象代理,大家也可以去看一下 Vue3 的 reactivity 模块,里面会有更加精妙的设计与实现
  • useReactive 产生可操作的代理对象一直都是同一个引用,useEffect , useMemo ,useCallback , 子组件属性传递 等如果依赖的是这个代理对象是不会引起重新执行