用@vue/reactivity 拯救我react项目的全局数据共享

1,414 阅读3分钟

终究还是vue拯救了我

遇到的麻烦

最近在公司React项目里写了一个库,全局modal api,使用方式如下:

import modals from "/@/src/service/global"
import { Settings } from "/@/components/modals"// 任意位置
// 当只是简单的确认框时
// 等价于 modals.show("Confirm", {
//   title: "提示",
//   description: "描述内容" 
// })
​
modals.Confirm("提示", "描述内容", options).then(res => res ? "confirm" : "cancel")
​
// 当需要自定义Content时,Content可以是任何复杂的大型组件
// Content 在components/modals里, 使用 Content: lazy(() => import("ComplexContent")) 懒加载
// 下面这个api就会复用Confirm的title和actions,将Content替换为Settings
modals.show("Confirm", {
  title: "设置",
  Content: Settings
})
​
// 如果是electron单机版,show方法会有分支语句 打开新的BrowserWindow,通过特殊的url参数仅显示这个Content
modals.show("Settings", {
  asWindow: true // 实际里是在设置里做了本地化,在show方法里主动进行了处理
})
// 此时的show实际上会开一个新窗口 url 为 host/modal?name=Settings
if (route === "modal") {
  ReactDOM.render( // modal context
    //...
      <Component render={query.name} />
    //...
    ,
   dom
  )
} else {
  ReactDOM.render( // app context
    // App
  )
}

这样保证了一个入口和host能处理多窗口不同的各种显示需求,代码通过split chunk 和懒加载 处理当入口唯一而显示复杂的需求。

但同样,modal作为全局任意地方可调用的api,用Provider控制就必须写在这一条context里,那么多窗口里写无数个context就会很恶心。同时,弹窗何其多!那干脆做得绝一点,另外起一个react实例的context。原理就和ant-design的message、notification组件一样(保存了一份另一个context的react实例,利用这个实例和ref调用内部组件的方法修改state,使得你可以在不同的context也能弹窗)。这个都不是重点就不细说有兴趣的同学可以去看ant-design的实现。

既然不同context,那么react就麻烦了,provider和reducer都是基于context的实现。跨context更新个毛啊!

试了redux/rxjs等各种方案后,还是vue香啊,直接上手@vue/reactivity,provider/context 或者 reducer 贼麻烦,而且还不能跨context,reactivity完全轻量级嘛。

用@vue/reactivity 拯救我react项目的全局数据共享

核心思想,vue3的reactivity是和vue主框架分离的,也就是响应式系统是纯原生js,同时可独立用于web或者node。gzip才11kb左右。

所以,我们只需要写一个globalState监听state的变化,每次变化时设置react的state就OK了。

在此之前,@vue/reactivity时没有watch的,因为watch api 在compilar-dom里深度绑定了生命周期。所以我们要稍微的实现一个简易版的watch:

// 这里面一个比较重要的api 就是effect
// 第一个参数getter返回一个响应式对象(监听目标)的函数,Ref或者Reactive都可以
// 第二参数配置项里的scheduler是一个方法,可以理解为监听目标change之后的副作用函数。
// effect返回一个runner,可以通过stop(runner)(reactivity提供的api)停止监听。
function traverse(value, seen = new Set()) { /* 遍历收集响应式 */ }
​
const watch = (source, fn, { deep = true, lazy } = {}) => {
  let getter = isRef(source)
    ? () => source.value
    : isReactive(source)
      ? () => source
      : source
​
  if (deep) {
    let _source = getter()
    getter = () => traverse(_source)
  }
​
  const runner = effect(getter, {
    lazy,
    scheduler: () => {
      fn()
      stop(runner)
    }
  })
}

参考:antfu watch with @vue/reactivity

// somepaht/store.js
const globalState = reactive({
  count: 0
})
​
export function useCount(num) {
  const [count, setCount] = useState(num)
​
  const observe = (v) => {
    globalState.count = v
  }
​
  useEffect(() => {
    watch(globalState, () => {
      setCount(globalState.count) // 副作用去修改react组件state
    })
  })
​
  return [
    count,
    observe
  ]
}
// some component.jsx
import { useCount } from "somepath/store";
​
export default function TheCount() {
  const [count, setCount] = useCount(2)
  console.log("Info count", count)
  return <p style={{textAlign: "center"}}>
    <button type="button" onClick={() => {
      setCount(count + 1)
    }}>
      count in Info context is: {count}
    </button>
  </p>
}

就这么简单,实现了一个context无关,全局响应式,轻量级,扩展方便的全局数据管理。再也不用为store、主题同步(之前的modals主题同步太麻烦了,用provider还需要通知update另一个context)烦恼了。

另外,我写了一个demo 对比reducer和@vue/reactivity CodeSandbox

如果你感兴趣,可以支持一下 github antfu/reactivue