30行代码实现React双向绑定hook的示例代码
作者:CreditFE信用前端
Vue和MobX中的数据可响应给我们留下了深刻的印象,在React函数组件中我们也可以依赖hooks来实现一个简易好用的useReactive。
看一下我们的目标
const CountDemo = () => { const reactive = useReactive({ count: 0, }); return ( <div onClick={() => { reactive.count++; }} > {reactive.count} </div> ); };
简单来说就是我们不需要再手动触发setState的handler了,修改数据,组件中的数据就会直接更新。
在Vue中我们实现数据可响应概括来讲需要:
1.解析模板收集依赖
2.发布订阅实现更新
而React函数组件凭借函数的特性这个过程将更加简单,因为函数组件每一次render都会重新"执行"一遍,我们只需要改变数据之后再触发组件渲染就能达到我们的目的。
因此实现这个自定义hook的核心就是:
1.维护同一份数据
2.劫持对数据的操作
3.在劫持操作中触发组件更新 (setState)
使用Proxy代理数据
这个代理模式是实现响应式数据的核心。Vue2.0 中使用defineProperty来做数据劫持,现在则是被Proxy模式所替代了,一句话概括defineProperty和proxy的区别就是前者劫持的是属性访问器,而后者可以代理整个对象(Vue3.0,MobX)。
Proxy有多达13种拦截器,我们这次用到的有 get
, set
, delete
const observer = (initialState, cb) => { const proxy = new Proxy(initialState, { get(target, key, receiver) { const val = Reflect.get(target, key, receiver); return typeof val === "object" && val !== null ? observer(val, cb) : val; // 递归处理object类型 }, 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; }, }); return proxy; };
上面这个observer完成了对数据的基本操作代理。
这里补充一个知识点: 为什么Proxy代理的对象经常搭配Reflect而不是操作符访问?
Reflect
更加全面,功能更强大:
- 只要Proxy对象具有的代理方法,Reflect对象全部具有,以静态方法的形式存在。这些方法能够执行默认行为,无论 Proxy 怎么修改默认行为,总是可以通过 Reflect 对应的方法获取默认行为。
比如上文第4行这里 Reflect.get(target,key,receiver)
咋一看似乎可以和target[key]
等价,但实际上不是的看下面的例子,正是由于Reflect的静态方法的第三个参数receiver可以用来指定被调用时的this,所以使用 Reflect.get(target,key,receiver)
才能如我们预期返回正确结果。
let o = { getb() { return this.a; }, }; let o1 = Object.create( newProxy(o, { get(target, key, receiver) { return Reflect.get(target, key, receiver); }, }) ); o1.a = 42; o1.b; // 42 let o2 = Object.create( newProxy(o, { get(target, key) { return target[key]; }, }) ); o2.a = 42; o2.b; // undefined
- 修改某些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)让它们变成了函数行为。
const useReactive = (initState) => { return observer(initState); };
我们的基本结构大概如上面代码段所示,但是这里有两个问题 :
1.我们希望函数组件每次 执行
的时候它都引用同一个代理对象
2.在组件的生命周期里observer只需要代理一次
使用useRef创建同一份数据引用
看到维护同一份数据我们第一反应可能就是使用闭包来创建引用,但是如此就还需要我们手动维护组件的创建卸载和这份数据的关系,而React中天生就包含了ref
这样的api,所以我们不需要从自行管理数据的卸载和绑定,在函数组件中直接使用useRef就可以达到我们的目的。
const useReactive = (initState) => { const ref = useRef(initState); return observer(ref.current); };
这样子我们就使用useRef和Proxy实现了对initialState的代理
添加更新handler
我们发现还少了一个handler即数据更改后触发组件更新,其实到这一步就比较简单了只需要在操作ref的值之后setState一下就可以了。
因为是在函数组件内部所以我们可以直接借用useState引入一个“更新触发器”,并将这个触发器传入observer代理方法。
function useReactive<S extends object>(initialState: S): S { const [, setFlag] = useState({}); const ref = useRef < S > (initialState) return observer(ref.current, () => { setFlag({}); // {} !== {} 因此会触发组件更新 }); }
去除多次Proxy
在完成上面几个步骤之后,我们基本已经可以实现开头demo中的效果了,但是还有一个问题:
由于是函数组件在state更新之后useReactive
也会执行,因此observer
就会被多次执行,而我们的预期,这个代理行为应该只在组件创建之初执行一次就可以了,因此这里我们也需要进行一些改造,方法依然是依靠 ref在函数组件多次执行时返回同一份数据这个特点:
function useReactive(initialState) { const refState = useRef(initialState); const [, setUpdate] = useState({}); const refProxy = useRef({ data: null, initialized: false, }); // 在创建proxy的ref时我们加一个initialized标志位,这样当组件state更新执行时 // useReactive再次执行就可以根据这个标志位来决定是直接返回current上的data值还是重新执行proxy了 if (refProxy.current.initialized === false) { refProxy.current.data = observer(refState.current, () => { setUpdate({}); }); refProxy.current.initialized = true; return refProxy.current.data; } return refProxy.current.data; }
添加缓存完善代码
上面解决了函数组件更新方式所带来的重复执行问题,这里还需要解决外部操作导致的重复代理,即如果一个initialState已经被代理过了,那么我们是不希望它被二次代理的(用户可能使用了两次useReactive来代理同一个对象),我们可以使用 WeakMap
来进行缓存记录
const proxyMap = new WeakMap(); const observer = (initialState, cb) => { const existing = proxyMap.get(initialState); // 添加缓存 防止重新构建proxy if (existing) { return existing; } const proxy = new Proxy(initialState, { get(target, key, receiver) { const val = Reflect.get(target, key, receiver); return typeof val === "object" && val !== null ? observer(val, cb) : val; // 递归处理object类型 }, 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(initialState, proxy); return proxy; };
总结
至此我们的useReactive
就基本可用了,回顾一下全部代码:
const proxyMap = new WeakMap(); const observer = (initialState, cb) => { const existing = proxyMap.get(initialState); if (existing) return existing; const proxy = new Proxy(initialState, { get(target, key, receiver) { const val = Reflect.get(target, key, receiver); return typeof val === "object" && val !== null ? observer(val, cb) : val; // 递归处理object类型 }, 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; }, }); return proxyMap.set(initialState, proxy) && proxy; }; function useReactive(initialState) { const refState = useRef(initialState); const [, setUpdate] = useState({}); const refProxy = useRef({ data: null, initialized: false, }); if (refProxy.current.initialized === false) { refProxy.current.data = observer(refState.current, () => { setUpdate({}); }); refProxy.current.initialized = true; return refProxy.current.data; } return refProxy.current.data; }
Sandbox 示例
https://codesandbox.io/s/silly-haze-xfuxoy?file=/src/App.js
代码虽少但五脏俱全,上面这个useReactive实现方式几乎和ahooks中的useReactive一致,这个包里还包含了很多其他简单有用的hooks集合,感兴趣的朋友可以了解一下其他hooks的实现,辅助你业务开发的同时帮助你加深对 React 工作原理的理解。
到此这篇关于30行代码实现React双向绑定hook的示例代码的文章就介绍到这了,更多相关React双向绑定hook内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!