共计 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
非常简单,只须要在根组件之前,先应用run
api 启动即可,因而处咱们没有模块定义,间接调用就能够了。
import {run} from 'concent';
run();// 先启动,在 render
ReactDOM.render(<App />, rootEl)
接着咱们将以上逻辑稍加革新,全副包裹到 setup
外部,setup 函数外部的逻辑只会被执行一次,须要用到的由渲染上下文 ctx
提供的 api 有 initState
、computed
、effect
、setState
,同时配合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.state
和this.ctx.state
是等效的,this.setState
和 this.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 也更加敌对了,置信大家都已默认了 hook
是react
的一个重要创造,然而其实它不是针对用户的,而是针对框架的,用户其实是不须要理解那些烧脑的细节与规定的,而对于 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