关于前端:setup-vs-5-react-hooks助你避开沟中陷阱

2次阅读

共计 6713 个字符,预计需要花费 17 分钟才能阅读完成。

序言

本文主题围绕 setup 来开展,既然提到了 setup 就离不开 composition api 这个关键词,精确的说 setup 是由 composition api 带进去的概览,而 composition api(组合 api)和 optional api(可选 api)两种组织代码的形式,置信大家在vue3 各种相干的介绍文里曾经理解到不少了,它们能够同时存在,并非强制你只能应用哪一种,但组合 api 两大劣势确实让开发者们更偏向于应用它来代替可选 api。

  • 以函数为根底单位来打包可复用逻辑,并注入到任意组件,让视图和业务解耦更优雅
  • 让雷同性能的业务更加严密的搁置到一起,不被割裂开,进步开发与保护体验

以上两点在 react 里均被 hook 优雅的解决了,那么相比 hook,组合 api 还具备其余什么劣势呢?置信已有小伙伴在尤大介绍组合 api 时曾经晓得,组合 api 是动态定义的,解决了hook 必须每次渲染都从新生成长期闭包函数的性能问题,也没有了 hook 里闭包旧值陷阱,人工检测依赖等编码体验问题。

然而,react 是 all in js 的编码方式,所以只有咱们敢想、敢做,所有优良的编程模型都能够吸纳进来,接下来咱们用原生 hook 和 concent 的 setup 并通过实例和解说,来彻底解决尤大提到的这个对于 hook 的痛点吧 ^\_^

react hook

咱们在此先设计一个传统的计数器,要求如下

  • 有一个小数,一个大数
  • 有两组加、减按钮,别离对小数大数做操作,小数按钮加减 1,大数按钮加减 100
  • 计数器首次挂载时拉取欢送问候语
  • 当小数达到 100 时,按钮变为红色,否则变为绿色
  • 当大数达到 1000 时,按钮变为紫色,否则变为绿色
  • 当大数达到 10000 时,上报大数的数字
  • 计算器卸载时,上报以后的数字

为了实现此需要,咱们须要用到以下 5 把钩子

useState

过完需要,咱们须要用到第一把钩子 useState 来做组件首次渲染的状态初始化

function Counter() {const [num, setNum] = useState(6);
  const [bigNum, setBigNum] = useState(120);
}

useCallback

如需应用缓存函数,则要用到第二把钩子useCallback,此处咱们应用这把钩子来定义加减函数

  const addNum = useCallback(() => setNum(num + 1), [num]);
  const addNumBig = useCallback(() => setBigNum(bigNum + 100), [bigNum]);

useMemo

如需用到缓存的计算结果,则要用到第三把钩子useMemo,此处咱们应用这把钩子来计算按钮色彩

 const numBtnColor = useMemo(() => {return num > 100 ? 'red' : 'green';}, [num]);
  const bigNumBtnColor = useMemo(() => {return bigNum > 1000 ? 'purple' : 'green';}, [bigNum]);

useEffect

处理函数的副作用则需用到第四把钩子useEffect,此处咱们用来解决一下两个需要

  • 当大数达到 10000 时,上报大数的数字
  • 计算器卸载时,上报以后的数字
  useEffect(() => {if (bigNum > 10000) api.report('reach 10000')
  }, [bigNum]);
  useEffect(() => {return ()=>{api.reportStat(num, bigNum)
    }
  }, []);

嘿嘿,写到这里,react 老手曾经被带到陷阱里了,即 闭包旧值陷阱 ,卸载那一刻提交的是最后的值,同时这里的清理函数的useEffect 写法在 IDE 是也会被正告,因为外部应用了 num, bigNum 变量,所以要求咱们申明依赖。

useRef

可如果为了防止 IDE 正告,咱们改为如下形式显然也不是咱们表白的本意,咱们只是想组件卸载时报告一下数字,而不是每一轮渲染都触发清理函数

  useEffect(() => {return ()=>{api.reportStat(num, bigNum)
    }
  }, [num, bigNum])

这个时候咱们须要第 5 把钩子useRef,来帮忙咱们固定依赖了,所以正确的写法是

  const ref = useRef(); // ref 是一个固定的变量,每一轮渲染都指向同一个值
  ref.current = {num, bigNum}; // 帮咱们记住最新的值
  useEffect(() => {return () => {const {num, bigNum} = ref.current;
      reportStat(num, bigNum);
    };
  }, [ref]);

残缺的计数器

使完 5 把钩子,咱们残缺的组件如下

function Counter() {const [num, setNum] = useState(88);
  const [bigNum, setBigNum] = useState(120);
  const addNum = useCallback(() => setNum(num + 1), [num]);
  const addNumBig = useCallback(() => setBigNum(bigNum + 100), [bigNum]);
  const numBtnColor = useMemo(() => {return num > 100 ? "red" : "green";}, [num]);
  const bigNumBtnColor = useMemo(() => {return bigNum > 1000 ? "purple" : "green";}, [bigNum]);
  useEffect(() => {if (bigNum > 10000) report("reach 10000");
  }, [bigNum]);

  const ref = useRef();
  ref.current = {num, bigNum};
  useEffect(() => {return () => {const {num, bigNum} = ref.current;
      reportStat(num, bigNum);
    };
  }, [ref]);

  // render ui ...
}

当然咱们能够基于 hook 可定制的个性,将这段代码独自形象为一个钩子,这样的话只需将数据和办法导出,以便让多种 ui 表白的 Counter 组件能够复用,同时也做到 ui 与业务隔离,利于保护。

function useMyCounter(){
  // .... 略
  return {num, bigNum. addNum, addNumBig, numBtnColor, bigNumBtnColor}
}

concent setup

hook函数在每一轮渲染期间肯定是须要全副从新执行一遍的,所以不可避免的在每一轮渲染期间都会产生大量的长期闭包函数,如果咱们能省掉他们,确实能帮 gc 加重一些回收压力的,当初咱们来看看应用 setup 革新结束后的 Count

应用 concent 非常简单,只须要在根组件之前,先应用runapi 启动即可,因而处咱们没有模块定义,间接调用就能够了。

import {run} from 'concent';

run();// 先启动,在 render
ReactDOM.render(<App />, rootEl)

接着咱们将以上逻辑稍加革新,全副包裹到 setup 外部,setup 函数外部的逻辑只会被执行一次,须要用到的由渲染上下文 ctx 提供的 api 有 initStatecomputedeffectsetState,同时配合setState 调用时还须要读取的状态 state,也由ctx 取得。

function setup(ctx) {// 渲染上下文
  const {initState, computed, effect, state, setState} = ctx;
  // setup 仅在组件首次渲染之前执行一次,咱们可在外部书写相干业务逻辑
}

initState

initState用于初始化状态,代替了useState,当咱们的组件状态较大时仍然能够不必思考如何切分状态粒度。

initState({num: 6, bigNum: 120});

此处也反对函数式写法初始化状态

initState(()=>({ num: 6, bigNum: 120}));

computed

computed用于定义计算函数,从参数列表里解构时就确定了计算的输出依赖,相比useMemo,更间接与优雅。

// 仅当 num 发生变化时,才触发此计算函数
computed('numBtnColor', ({ num}) => (num > 100 ? 'red' : 'green'));

此处咱们须要定义两个计算函数,能够用计算对象形容体来配置计算函数,这样只需调用一次 computed 即可

computed({numBtnColor: ({ num}) => num > 100 ? 'red' : 'green',
  bigNumBtnColor: ({bigNum}) => bigNum > 1000 ? 'purple' : 'green',
});

effect

effect的用法和 useEffect 是截然不同的,区别仅仅是依赖数组仅传入 key 名称即可,同时 effect 外部将函数组件和类组件的生命周期进行了对立封装,用户能够将业务不做任何批改便迁徙到类组件身上

effect(() => {if (state.bigNum > 10000) api.report('reach 10000')
}, ['bigNum'])
effect(() => {
  // 这里能够书写首次渲染结束时须要做的事件
  return () => {
      // 卸载时触发的清理函数
    api.reportStat(state.num, state.bigNum)
  }
}, []);

setState

用于批改状态,咱们在 setup 外部基于 setState 定义完办法后,而后返回即可,接着咱们能够在任意应用此 setup 的组件里,通过 ctx.settings 拿到这些办法句柄便可调用

function setup(ctx) {// 渲染上下文
  const {state, setState} = ctx;
  return {// 导出办法
    addNum: () => setState({ num: state.num + 1}),
    addNumBig: () => setState({ bigNum: state.bigNum + 100}),
  }
}

残缺的 Setup Counter

基于上述几个 api,咱们最终的 Counter 的逻辑代码如下

function setup(ctx) {// 渲染上下文
  const {initState, computed, effect, state, setState} = ctx;
  // 初始化数据
  initState({num: 6, bigNum: 120});
  // 定义计算函数
  computed({
    // 参数列表解构时就确定了计算的输出依赖
    numBtnColor: ({num}) => num > 100 ? 'red' : 'green',
    bigNumBtnColor: ({bigNum}) => bigNum > 1000 ? 'purple' : 'green',
  });
  // 定义副作用
  effect(() => {if (state.bigNum > 10000) api.report('reach 10000')
  }, ['bigNum'])
  effect(() => {return () => {api.reportStat(state.num, state.bigNum)
    }
  }, []);

  return {// 导出办法
    addNum: () => setState({ num: state.num + 1}),
    addNumBig: () => setState({ bigNum: state.bigNum + 100}),
  }
}

定义完外围的业务逻辑,紧接着,咱们可在任意函数组件外部应用 useConcent 拆卸咱们定义好的 setup 来应用它了,useConcent会返回一个渲染上下文(和 setup 函数参数列表里指的是同一个对象援用,有时咱们也称实例上下文),咱们可按需获从 ctx 上取出指标数据和办法,针对此示例,咱们能够导出

state(数据),settings(setup 打包返回的法法),refComputed(实例的计算函数后果容器)这 3 个 key 来应用即可。

import {useConcent} from 'concent';

function NewCounter() {const { state, settings, refComputed} = useConcent(setup);
  // const {num, bigNum} = state;
  // const {addNum, addNumBig} = settings;
  // const {numBtnColor, bigNumBtnColor} = refComputed;
}

咱们下面提到 setup 同样能够拆卸给类组件,应用 register 即可,须要留神的是拆卸后的类组件,能够从 this.ctx 上间接获取 concent 为其生成的渲染上下文,同时呢 this.statethis.ctx.state是等效的,this.setStatethis.ctx.setState 也是等效的,不便用户代码 0 改变即可接入 concent 应用。

import {register} from 'concent';

@register(setup)
class NewClsCounter extends Component{render(){const { state, settings, refComputed} = this.ctx;
  }
}

结语

比照原生 hook,setup将业务逻辑固定在只会被执行一次的函数外部,提供了更敌对的 api,且同时完满兼容类组件与函数组件,让用户能够逃离 hook 的应用规定懊恼(想想看 useEffect 配合 useRef,是不是都有不小的认知老本?), 而不是将这些束缚学习阻碍转嫁给用户,同时对 gc 也更加敌对了,置信大家都已默认了 hookreact的一个重要创造,然而其实它不是针对用户的,而是针对框架的,用户其实是不须要理解那些烧脑的细节与规定的,而对于 concent 用户来说,其实只需一个钩子开启一个传送门,即可在另一个空间外部实现所有业务逻辑,而且这些逻辑同样能够复用到类组件上。

眼过百遍不如手过一遍,以下是两种写法的链接,尝试尝试肯定有所心得

  • 原始 hook Counter
  • setup Counter

上诉两个 hook Counter 如果想做状态共享,咱们须要革新代码接入 redux 或者自建 Context,然而在concent 的开发模式下,setup无需任何革新,仅仅只须要提前申明一个模块,而后注册组件内属于该模块即可,这种丝滑般的迁徙过程能够让用户灵便应答各种简单场景。

import {run} from 'concent';

run({
  counter:{state: { num:88, bigNum: 120},
  },
  //reducer: {...}, // 如操作数据流程简单,可再将业务晋升到此处
})

// 对于函数组件
useConcent({setup});
//  ---> 改为
useConcent({setup, module:'counter'})

// 对于函数组件
@register({setup});
//  ---> 改为
@register({setup, module:'counter'});
  • shared Counter

One more thing

如果对 concent 搭建 admin 站点感兴趣,咱们也提供一个示例站点 tntweb-admin 供你参考,得益于 wp2vite 的反对,实现了本地既可 vite 启动也可 webpack 启动的双引擎驱动能力,你只需 3 步即可:

git clone git@github.com:tnfe/tntweb-admin.git
npm i
npm run vite

当然了如果心愿用 webpack 启动,应用 npm run start 接能够了,不过举荐上线还是用 webpack 构建来上线,即npm run build

除了双擎驱动,tntweb-admin还内置了超多个性,如 实时的主题换肤 页签 27 种动静排版 等性能,欢送关注,同时他自身也是内置了微前端架构模式的站点,这一块还在开发中,后续咱们的更多模板页面公布结束,以及微前端部署模式文档 ready 后,会第一工夫凋谢给宽广开发者共享。

团队

TNTWeb – 腾讯新闻前端团队,TNTWeb 致力于行业前沿技术摸索和团队成员集体能力晋升。为前端开发人员整顿出了小程序以及 web 前端技术畛域的最新优质内容,每周更新✨,欢送 star,github 地址:https://github.com/tnfe/TNT-Weekly

正文完
 0