响应式hook: 写一个useReactive

616 阅读3分钟

Vue和MobX中的数据可响应给我们留下了深刻地印象,在React函数组件中我们也可以依赖hooks来实现一个简易好用的useReactive。

看一下我们的目标

const CountDemo = () => {
	const { count } = useReactive({
  	count:0
  })
  return <div onClick={()=>{
  	count++
  }} >{count}</div>
}

简单来说就是我们不需要再手动触发setState的handler了,修改数据,组件中的数据就会直接更新。

在Vue中我们实现数据可响应概括来讲需要

1.解析模板收集依赖 2.发布订阅实现更新

而React函数组件凭借函数的特性这个过程将更加简单,因为函数组件每一次render都会重新"执行"一遍,我们只需要改变数据之后再触发组件渲染就能达到我们的目的。

因此实现这个自定义hook的核心就是:1.维护同一份数据 2.劫持对数据的操作

useRef

使用Proxy代理数据

这个代理模式是实现响应式数据的核心。Vue2.0 中使用defineProperty来做数据劫持,现在则是被Proxy模式所替代了,一句话概括defineProperty和proxy的区别就是前者劫持的是属性访问器,而后者可以代理整个对象(Vue3.0,MobX)。

Proxy有多达13种拦截器,我们这次用到的有 get, set, delete

const observer = (initialState) => {
	const proxy = new Proxy(initialState,{
  	get(target,key,receiver) {
      const val = Reflect.get(target,key,receiver)
    	return typeof val === 'object' && val !== null ? observer(val):val
    },
    set(target,key,val) {
      return Reflect.set(target,key,val) // Reflect set deleteProperty的返回值是是否设置成功的布尔值
    },
    deleteProperty(target,key) {
    	return Reflect.deleteProperty(target,key)
    }
  })
}

上面这个observer完成了对数据的基本操作代理。补充一个知识点为什么Proxy代理的对象一定要用Reflect而不是操作符访问?

比如上文第4行这里 Reflect.get(target,key,receiver)似乎可以和target[key]等价

  • 只要Proxy对象具有的代理方法,Reflect对象全部具有,以静态方法的形式存在。这些方法能够执行默认行为,无论Proxy怎么修改默认行为,总是可以通过Reflect对应的方法获取默认行为。
  • 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
  • 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。

上面这个observer完成了对数据的基本操作代理。

const useReactive = ( initState ) => {
	return observer(initState)
}

我们的基本结构大概如上面代码段所示,但是这里有两个问题

1.我们希望函数组件每次 执行的时候它都返回同一个对象

2.在组件的生命周期里observer只需要代理一次

回过头去看文章一开始介绍的useRef 是不是刚好可以满足我们的诉求?

const useReactive = ( initState ) => {
  const { current } = useRef({
  	observer:initialState,
    initialize:false
  })
  if(!current.initialize) {
  	current.observer = observer(current.observer)
    current.initialize = true
  }
	return observer(initState)
}

这样子我们就使用useRef和Proxy实现了对initialState的代理

添加更新handler

我们发现还少了一个handler即数据更改后触发组件更新,其实到这一步就比较简单了只需要在操作ref的值之后setState一下就可以了。

因为是在函数组件内部所以我们可以直接借用useState引入一个“更新触发器”,并将这个触发器传入observer代理方法。

function useReactive<S extends object>(initialState: S): S {
  const [, setFlag] = useState({});
  const { current } = useRef<{observer: S,initialize: boolean}>({
    observer:initialState,
    initialize: false
  });
  
  if(!current.initialize) {
    current.observer = observer(current.observer, () => {
      setFlag({}); // {} !== {} 因此会触发组件更新
    });
    current.initialize = true
  }
  return current.observer;
}

优化完善

添加Proxy缓存

使用WeakMap

总结

代码虽少但五脏俱全,上面这个useReactive实现方式几乎和ahooks中的useReactive一致,这个包里还包含了很多其他简单有用的hooks集合,感兴趣的朋友可以了解一下其他hooks的实现,辅助你业务开发的同时帮助你加深对React 工作原理的理解。