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内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

30行代码实现React双向绑定hook的示例代码的更多相关文章

  1. ios – React native链接到另一个应用程序

    如果是错误的,有人知道如何调用正确的吗?

  2. ios – React Native – 在异步操作后导航

    我正在使用ReactNative和Redux开发移动应用程序,我正面临着软件设计问题.我想调用RESTAPI进行登录,如果该操作成功,则导航到主视图.我正在使用redux和thunk所以我已经实现了异步操作,所以我的主要疑问是:我应该把逻辑导航到主视图?我可以直接从动作访问导航器对象并在那里执行导航吗?.我对组件中的逻辑没有信心.似乎不是一个好习惯.有没有其他方法可以做到这一点?

  3. 在ios中使用带有React Native(0.43.4)的cocoapods的正确方法是什么?

    我已经挖掘了很多帖子试图使用cocoapods为本地ios库设置一个反应原生项目,但我不可避免地在#import中找到了丢失文件的错误.我的AppDelegate.m文件中的语句.什么是使用反应原生的可可豆荚的正确方法?在这篇文章发表时,我目前的RN版本是0.43.4,而我正在使用Xcode8.2.1.这是我的过程,好奇我可能会出错:1)

  4. ios – React Native WebView滚动行为无法按预期工作

    如何确保滚动事件的行为与ReactNative应用程序中的浏览器相同?

  5. ios – React Native – BVLinearGradient – 找不到’React/RCTViewManager.h’文件

    谢谢.解决方法几天前我遇到了完全相同的问题.问题是在构建应用程序时React尚未链接.试试这个:转到Product=>Scheme=>管理方案…=>点击你的应用程序Scheme,然后点击Edit=>转到Build选项卡=>取消选中ParallelizeBuild然后点击标志添加目标=>搜索React,选择第一个名为React的目标,然后单击Add然后在目标列表中选择React并将其向上拖动到该列表中的第一个.然后转到Product=>再次清理并构建项目.这应该有所帮助.

  6. ios – React Native – NSNumber无法转换为NSString

    解决方法在你的fontWeight()函数中也许变成:

  7. ios – React native error – react-native-xcode.sh:line 45:react-native:command not found命令/ bin/sh失败,退出代码127

    尝试构建任何(新的或旧的)项目时出现此错误.我的节点是版本4.2.1,react-native是版本0.1.7.我看过其他有相同问题的人,所以我已经更新了本机的最新版本,但是我仍然无法通过xcode构建任何项目.解决方法要解决此问题,请使用以下步骤:>使用节点版本v4.2.1>cd进入[你的应用]/node_modules/react-native/packager>$sh./packager.s

  8. 反应原生 – 如何通过Xcode构建React Native iOS应用程序到设备?

    我试图将AwesomeProject应用程序构建到设备上.构建成功并启动屏幕显示,但后来我看到一个红色的“无法连接到开发服务器”屏幕.它表示“确保节点服务器正在运行–从Reactroot运行”npmstart“.看起来节点服务器已经运行,因为当我做npm启动时,我收到一个EADDRINUSE消息,表示该端口已经在使用.解决方法从设备访问开发服务器您可以使用开发服务器快速迭代设备.要做到这一点,你的

  9. 静音iOS推送通知与React Native应用程序在后台

    我有一个ReactNative应用程序,我试图获得一个发送到JavaScript处理程序的静默iOS推送通知.我看到的行为是AppDelegate中的didReceiveRemoteNotification函数被调用,但是我的JavaScript中的处理程序不会被调用,除非应用程序在前台,或者最近才被关闭.我很困惑的事情显然是应用程序正在被唤醒,并且它的didReceiveRemoteNotifi

  10. 如何为iOS的React Native设置分析

    所以我已经完成了一个针对iOS的ReactNative项目,但是我想在其中分析.我尝试了react-native-google-analytics软件包,但是问题阻止了它的正常工作.此外,react-native-cordova-plugin软件包只适用于Android,因此插入Cordova插件进行分析的能力现在已成为问题.我也没有Swift/ObjectiveC的经验,所以将完全失去GA的插入.有没有人有任何建议如何连接GoogleAnalytics的ReactNativeforiOS?

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部