30行代码实现React双向绑定hook的示例代码

目录
  • 使用Proxy代理数据
  • 使用useRef创建同一份数据引用
  • 添加更新handler
  • 去除多次Proxy
  • 添加缓存完善代码
  • 总结
  • Sandbox 示例

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

(0)

相关推荐

  • React Hooks常用场景的使用(小结)

    前言 React 在 v16.8 的版本中推出了 React Hooks 新特性.在我看来,使用 React Hooks 相比于从前的类组件有以下几点好处: 代码可读性更强,原本同一块功能的代码逻辑被拆分在了不同的生命周期函数中,容易使开发者不利于维护和迭代,通过 React Hooks 可以将功能代码聚合,方便阅读维护: 组件树层级变浅,在原本的代码中,我们经常使用 HOC/render props 等方式来复用组件的状态,增强功能等,无疑增加了组件树层数及渲染,而在 React Hooks

  • React Hooks的深入理解与使用

    你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗? --拥有了hooks,你再也不需要写Class了,你的所有组件都将是Function. 你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗? --拥有了Hooks,生命周期钩子函数可以先丢一边了. 你在还在为组件中的this指向而晕头转向吗? --既然Class都丢掉了,哪里还有this?你的人生第一次不再需要面对this. 这样看来,说React Hooks是今年最劲爆的新特性真的毫不夸张.如果你也对react

  • 详解如何使用React Hooks请求数据并渲染

    前言 在日常的开发中,从服务器端异步获取数据并渲染是相当高频的操作.在以往使用React Class组件的时候,这种操作我们已经很熟悉了,即在Class组件的componentDidMount中通过ajax来获取数据并setState,触发组件更新. 随着Hook的到来,我们可以在一些场景中使用Hook的写法来替代Class的写法.但是Hook中没有setState.componentDidMount等函数,又如何做到从服务器端异步获取数据并渲染呢?本文将会介绍如何使用React的新特性Hook

  • React hooks的优缺点详解

    前言 Hook 是 React 16.8 的新增特性.它是完全可选的,并且100%向后兼容.它可以让你使用函数组件的方式,运用类组件以及 react 其他的一些特性,比如管理状态.生命周期钩子等.从概念上讲,React 组件一直更像是函数.而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则. 优点: 1.代码可读性更强,原本同一块功能的代码逻辑被拆分在了不同的生命周期函数中,容易使开发者不利于维护和迭代,通过 React Hooks 可以将功能代码聚合,方便阅读维护.例如,每个生

  • 记录一次完整的react hooks实践

    写在前面 React在16.8版本正式发布了Hooks.关注了很久,最近正好有一个小需求,赶紧来试一下. 需求描述 需求很简单,部门内部的一个数据查询小工具.大致长成下面这样: 用户首次访问页面,会拉取数据展示.输入筛选条件,点击查询后,会再次拉取数据在前端展示. 需求实现 使用React Class Component的写法 如果使用以前的class写法,简单写一下,代码可能大概长成下面这样: import React from 'react'; import { Tabs, Input, R

  • 使用hooks写React组件需要注意的5个地方

    Hook是React16.8开始新增的特性.虽然React官方文档已经作出了针对React hooks的相关概念的讲解,但是光看官方文档是很难将hooks使用好的,在编写hooks的过程中很容易跳进陷阱和错误.本文总结了5个不好的地方. 01.不需要render的场景下使用useState 在函数组件中我们可以使用useState来管理状态,这使得对状态的管理变得很简单,但是也容易被滥用,我们通过下面的代码样例看下容易忽略的地方. 不推荐× function ClickButton(props)

  • 详解react hooks组件间的传值方式(使用ts)

    目录 父传子 子传父 跨级组件(父传后代) 父传子 通过props传值,使用useState来控制state的状态值 父组件 Father.tsx里: 子组件 Child.tsx里: 展示效果: 子传父 跟react的方式一样,像子组件传入回调函数,通过接收子组件的返回值,再去更新父组件的state 父组件,Father.tsx里: 子组件,Child.tsx里: 展示效果: 子传父优化版,使用useCallback存放处理事件的函数 父组件,Father.tsx里: 子组件,Child.tsx

  • React 使用Hooks简化受控组件的状态绑定

    开始之前 阅读本文需要对以下几项有一定了解 ECMAScript 6 文章中大量用到了 ES6 语法,比如解构赋值和函数参数默认值.剩余参数.展开语法.箭头函数等. Hooks React 在 16.8 版本中推出了 Hooks,它允许你在"函数组件"中使用"类组件"的一些特性. React 本身提供了一些 Hooks,比如 useState.useReducer 等.通过在一个以"use"作为命名起始的函数中调用这些 Hooks,就得到了一个

  • 30行代码实现React双向绑定hook的示例代码

    目录 使用Proxy代理数据 使用useRef创建同一份数据引用 添加更新handler 去除多次Proxy 添加缓存完善代码 总结 Sandbox 示例 Vue和MobX中的数据可响应给我们留下了深刻的印象,在React函数组件中我们也可以依赖hooks来实现一个简易好用的useReactive. 看一下我们的目标 const CountDemo = () => { const reactive = useReactive({ count: 0, }); return ( <div onCl

  • VUE-Table上绑定Input通过render实现双向绑定数据的示例

    效果 HTML的Table <Card> <div ref="print" > <Table width="100%" height="500" size="small" :columns="columns7" :stripe="true" :data="data" border ></Table> </div&g

  • React中前端路由的示例代码

    目录 一. url是什么 二. 使用步骤 一. url是什么 访问不同url, 展示不同的组件 二. 使用步骤 安装React路由:命令行中执行npm install react-router-dom --save(注意此处的版本为npm install react-router-dom@4.3.1 --save) 两个js文件,分别为list.js和newButton.js,要实现访问localhost:3000/button的时候就显示button.js:访问localhost:3000/l

  • 利用React实现虚拟列表的示例代码

    目录 列表项高度固定 代码实现 列表项高度动态 代码实现 思路说明 一些需要注意的问题 结尾 大家好,我是前端西瓜哥.这次我们来看看虚拟列表是什么玩意,并用 React 来实现两种虚拟列表组件. 虚拟列表,其实就是将一个原本需要全部列表项的渲染的长列表,改为只渲染可视区域内的列表项,但滚动效果还是要和渲染所有列表项的长列表一样. 虚拟列表解决的长列表渲染大量节点导致的性能问题: 一次性渲染大量节点,会占用大量 GPU 资源,导致卡顿: 即使渲染好了,大量的节点也持续占用内存.列表项下的节点越多,

  • React+EggJs实现断点续传的示例代码

    技术栈 前端用了React,后端则是EggJs,都用了TypeScript编写. 断点续传实现原理 断点续传就是在上传一个文件的时候可以暂停掉上传中的文件,然后恢复上传时不需要重新上传整个文件. 该功能实现流程是先把上传的文件进行切割,然后把切割之后的文件块发送到服务端,发送完毕之后通知服务端组合文件块. 其中暂停上传功能就是前端取消掉文件块的上传请求,恢复上传则是把未上传的文件块重新上传.需要前后端配合完成. 前端实现 前端主要分为:切割文件.获取文件MD5值.上传切割后的文件块.合并文件.暂

  • react实现Radio组件的示例代码

    本文旨在用最清楚的结构去实现一些组件的基本功能.希望和大家一起学习,共同进步 效果展示: 测试组件: class Test extends Component { constructor(props) { super(props) this.state = { active:1 } } onGroupChange(value) { this.setState({ active: value }) } render() { return ( <div> <RadioGroup onChan

  • vue自定义组件实现v-model双向绑定数据的实例代码

    项目中会遇到自定义公共组件供项目调用,正常情况可以使用 props定义参数接收父组件传的参数,然后通过子组件的$emits()方法回传数据给父组件. 类似如下: 父组件 <common-checkbox :checked="goodsSelected" class="left" :height="'16px'" :width="'16px'" @checkChange="checkChange">

  • vue使用Proxy实现双向绑定的方法示例

    前言:vue3.0要用Proxy来实现双向绑定,因此先来尝试一下实现方法. 1 Object.defineProperty 实现 原来vue2的实现使用Object.defineProperty,监听set,但对于数组直接下标给数组设置值监听不了. function observe(data) { if (!data || typeof data !== 'object') { return; } // 取出所有属性遍历 Object.keys(data).forEach(function(ke

  • “增强js程序代码的健壮性”之我见大量示例代码

    在书写和使用js程序的过程中,我们经常会遇到各种各样的错误,对于一个依赖于浏览器的语言,我们很难完全去控制其在任何时刻都正确无误的运行.但是我们仍需做出自己努力取增强我们书写的js程序代码的健壮性和安全性,尽可能减少错误的出现概率. 以下为本人在学习js过程总结的几点关于增强js程序的健壮性的心得,如果您觉得对你有一点的价值,那我就达到自己的目的了,如果你觉得没有什么意义,请您也不必扔砖头,谢谢. (1)对于必要的参数要判断是否被正确的传入. 代码示例: [Ctrl+A 全选 注:如需引入外部J

  • React Native 集成jpush-react-native的示例代码

    jpush-React-native是极光推送官方维护的一个插件,github地址:https://github.com/jpush/jpush-react-native 一.手动配置 1.集成插件到项目中 npm install jpush-react-native --save rnpm link jpush-react-native Android 使用 android Studio import 你的 react Native 应用(选择你的 React Native 应用所在目录下的

随机推荐