广东小蓝科创有限公司
                    
                    
                    
    
                    使用 create-vite 脚手架生成基础模板 运行命令安装脚手架 yarn create vite 我在安装时提供的命令行选项那里,选择了 React + TypeScript。 使用下面的命令启动项目 yarn dev 此时的项目已经默认集成了 @vitejs/plugin-react 这个插件。 到这一步其实就已经基本结束了,自动集成 HMR,jsx,ts,css module,资源打包等一系列功能。 相比于 webpack,简直不要太友好。 eslint 先安装 eslint: yarn add eslint -D 然后初始化eslint配置: yarn eslint --init 选择选项后,我自己安装的库大致是: eslint-plugin-react@latest eslint-config-standard-with-typescript@latest @typescript-eslint/eslint-plugin@^5.0.0 eslint@^8.0.1 eslint-plugin-import@^2.25.2 eslint-plugin-n@^15.0.0 eslint-plugin-promise@^6.0.0 typescript@* // 这个可以移除 之后有两个方案: 方案一,使用vite-plugin-eslint,这个会在报错时在页面上显示报错信息。 方案二,使用 VSCode 的 ESlint 插件去使用 ESLint,这个是在代码上出现红线报错。(个人更喜欢这种) 方案二直接用插件即可,方案一需要安装一下库: yarn add vite-plugin-eslint -D 安装完毕后,在vite.config.ts中配置: //... import eslint from "vite-plugin-eslint"; export default defineConfig({ plugins: [react(), eslint()], //... }); 无论方案一还是方案二,此时仍会报错,因为 ESLint 无法解析 ts 代码,所以还需要安装 @typescript-eslint/parser yarn add @typescript-eslint/parser -D 最后你还需要在.eslintrc.json 加上这行配置: "parserOptions": { //... "project": "tsconfig.json" }, 基本完毕。 为什么说是基本?因为 eslint 配置还是更多跟代码习惯有关,比如单双引号的使用之类的,所以刚配置完一般都一堆报错,还需要自己去慢慢调整。 而且还涉及到与 prettier 的配置相冲突的问题,eslint 和 prettier 的配置分别是代码校验和代码格式化时的规则,所以也是要保证规则一致的。 手动调整规则太繁琐了,一般使用eslint-config-prettier禁用掉 ESLint 中和 Prettier 配置有冲突的规则,然后用eslint-plugin-prettier保证 eslint 用 prettier 的风格校验。 yarn add eslint-config-prettier eslint-plugin-prettier -D 然后在.eslintrc.json 中加上配置: { "extends": [ //... "plugin:prettier/recommended" ], } 另外根据需要一般常用的配置列一下: { "rules": { "react/react-in-jsx-scope":"off", // 使用 jsx 时不需要引用 React "@typescript-eslint/strict-boolean-expressions":"off" // 表达式中的布尔值必须严格是布尔类型 } } 这里也可能涉及到对tsconfig.json的修改: { "compilerOptions": { "noImplicitAny":false, // 未声明类型的变量自动默认为any类型 } } 这里的配置较多,并且也因人而异,就不一一赘述了。 只要保持团队内部代码风格统一,就算是不符合标准规范的代码,其实也是好代码。 prettier 安装 yarn add prettier -D 根目录下新建.prettierrc 配置文件,然后给个我自己用的配置方案: { printWidth: 100, tabWidth: 4, useTabs: false, singleQuote: true, jsxSingleQuote: false, endOfLine: 'lf' } 一般这个配合 VSCode 的 Prettier 插件和保存时格式化即可。 另外,对于 lf 和 crlf 的处理,保存时没法切换,需要在 VSCode 中设置。 react-router 安装: yarn add react-router-dom 然后修改 main.tsx 中的代码吧: //... import {RouterProvider} from "react-router-dom"; import router from './router'; //... ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( <React.StrictMode> <RouterProvider router={router} /> </React.StrictMode> ); 这里我将路由相关代码放在了单独的路由文件 router.tsx 中: import { createBrowserRouter } from 'react-router-dom'; import Framework from './Framework'; import Error from './Error'; import Home from '@/pages/home'; import About from '@/pages/about'; const router = createBrowserRouter([ { path: '/', element: <Framework />, errorElement: <Error />, children: [ { path: 'home', element: <Home />, }, { path: 'about', element: <About />, }, ], }, ]); export default router; antd 安装命令: yarn add antd 然后在主 less 文件中加上代码: @import 'antd/es/style/themes/default.less'; @import 'antd/dist/antd.less'; @primary-color: #4294ff; // 更换全局主色 然后还需要更改 vite.config.ts: //... export default defineConfig({ //... css: { preprocessorOptions: { less: { javascriptEnabled: true, }, }, }, }); 别名 通常我们会使用下面的方式来使用别名: import reactLogo from "@/assets/react.svg"; 默认情况下,会直接报错,所以我们需要在vite.config.ts进行如下配置: //... import path from "path"; export default defineConfig({ //... resolve: { alias: { "@": path.resolve(__dirname, "src"), }, }, }); 这里因为没有 path 这个依赖库,所以还要运行命令安装: yarn add path -D 此时别名功能已经可以正常使用,但是__dirname会报红,需要安装@types/node yarn add @types/node -D 这时别名时没有智能提示的,所以还需要在tsconfig.json中,配置: { "compilerOptions": { //... "baseUrl": "./", "paths": { "@/*": ["src/*"] } }, } 完毕。 Less 与 CSS Module Vite 已集成了 CSS Module 功能,但是想要使用 Less 还需要安装 less 这个库。 yarn add less 最后使用的方式如下: import styles from "./App.module.less"; 我们可能会用到一些Less全局变量来作为主题之类的,它可能是这样的theme.less: @primaryColor: #4294ff; // 全局主色 然后修改vite.config.ts为: export default defineConfig({ // ... css: { preprocessorOptions: { less: { javascriptEnabled: true, additionalData: `@import "${path.resolve(__dirname, 'src/theme.less')}";`, }, }, }, }); 这个可以默认给每个less文件都引入这个theme.less。 转换svg 将引入的svg文件转为react组件。 安装库: yarn add vite-plugin-svgr -D 然后再vite.config.js中配置一下: import svgr from 'vite-plugin-svgr' export default { // ... plugins: [svgr()], } 因为使用了typescript,还需要在vite-env.d.ts中配置一下: /// <reference types="vite-plugin-svgr/client" /> 最后的使用方法: import { ReactComponent as Logo } from './logo.svg'
查看详情
                    最常见的就是父子组件之间传递参数 父组件往子组件传值,直接用this.props就可以实现 在父组件中,给需要传递数据的子组件添加一个自定义属性,在子组件中通过this.props就可以获取到父组件传递过去的数据 // 父组件 render() { return ( // 使用自定义属性传递需要传递的方法或者参数 <ShopUi toson={this.state}></ShopUi> ) } //子组件 //通过this.props.toson就可以获取到父组件传递过来的数据 、、如果还需要往孙组件传递那么在子组件通过自定义属性继续传递就行了 tograndson={this.props.toson} 、、孙组件通过this.props.tograndson获取到数据 子组件给父组件传值的话,需要在父组件设置接收函数和state,同时将函数名通过props传递给子组件 也就是给子组件传入父组件的方法,在子组件进行调用 //孙子组件 export default class Grandson extends Component{ render(){ return ( <div style={{border: "1px solid red",margin: "10px"}}> {this.props.name}: <select onChange={this.props.handleSelect}> <option value="男">男</option> <option value="女">女</option> </select> </div> ) } }; //子组件 export default class Child extends Component{ render(){ return ( <div style={{border: "1px solid green",margin: "10px"}}> {this.props.name}:<input onChange={this.props.handleVal}/> <Grandson name="性别" handleSelect={this.props.handleSelect}/> </div> ) } }; //父组件 export default class Parent extends Component{ constructor(props){ super(props) this.state={ username: '', sex: '' } }, handleVal(event){ this.setState({username: event.target.value}); }, handleSelect(value) { this.setState({sex: event.target.value}); }, render(){ return ( <div style={{border: "1px solid #000",padding: "10px"}}> <div>用户姓名:{this.state.username}</div> <div>用户性别:{this.state.sex}</div> <Child name="姓名" handleVal={this.handleVal} handleSelect={this.handleSelect}/> </div> ) } } 前一段时间有人问过我这样一个问题,constructor里面的super()是干嘛用的? 总结一下: 如果要在子类的constructor里使用this,必须调用父类constructor,否则就拿不到this 那么问题就来了,如何调用父类的constructor呢? 通过super() 如果要在constructor里使用父组件传递过来的参数,必须在调用父组件super时,传递参数给父组件的constructor 如果不在constructor里面使用this,或者参数,就不需要super ; 因为React以及帮你做了this,props的绑定   路由传参 安装 npm install react-router-dom --save-dev 定义路由(一般会放在外面) <HashRouter> <Switch> <Route exact path="/" component={Home}/> <Route exact path="/detail" component={Detail}/> </Switch> </HashRouter> 当页面跳转时 <li onClick={el => this.props.history.push({ pathname:'/detail', state:{id:3}})} > </li>   接收    通过this.props.history.location可以获取到传递过来的数据 路由传参可能会有这个问题,就是只有在路由定义时挂载的组件中才会有props里面的location history match  路由上挂载的那个组件一般都是Container.js,一般我们会往下分出UI.js组件,在这里面进行点击跳转,UI组件props里没有location history match  需要用到高阶组件withRouter    状态提升 将多个组件需要共享的状态提升到离他们最近的那个公共父组件上,然后父组件通过props分发给子组件   context 当某个组件在自己的context中保存了某个状态,那个该组件下的所有子孙组件都可以访问到这个状态,不需要中间组件的传递,而这个组件的父组件是没办法访问的  class Index extends Component { static childContextTypes = { themeColor: PropTypes.string } constructor () { super() this.state = { themeColor: 'red' } } getChildContext () { return { themeColor: this.state.themeColor } } render () { return ( <div> <Header /> <Main /> </div> ) } }通过getChildContext()将属性传递给所有的子孙组件提供 context 的组件必须提供 childContextTypes 作为 context 的声明和验证。   class Title extends Component { static contextTypes = { themeColor: PropTypes.string } render () { return ( <h1 style={{ color: this.context.themeColor }}>标题</h1> ) } }子组件要获取 context 里面的内容的话,就必须写 contextTypes 来声明和验证你需要获取的状态的类型,它也是必写的,如果你不写就无法获取 context 里面的状态。Title 想获取 themeColor,它是一个字符串,我们就在 contextTypes 里面进行声明。   引入redux redux为React提供可预测化的状态管理机制 redux将整个应用状态存储到store,store里保存着一个state状态树 组件可以派发(dispatch)  行为 (action)  给store , 而不是直接通知其它组件 其它组件可以通过订阅store中的状态state来刷新自己的视图  
查看详情
                    按图索骥找到Hook相关源码(可以直接跳) 首先我们从Github上得到react的源码,然后可以在packages中找到react文件夹,其中的index.js就是我们的入口。 代码很简单,就两行: const React = require('./src/React'); module.exports = React.default || React; 所以接下来我们去看看 'react/src/React',代码有点多,我们简化一下: import ReactVersion from 'shared/ReactVersion'; // ... import { useEffect, } from './ReactHooks'; const React = { useEffect }; //... export default React; 很好,现在我们至少知道为什么Hooks的引用方式是: import {useEffect} from 'react' 接下来我们继续看看 'react/src/ReactHooks'。 ReactHooks文件(可以直接跳) 之前说好了只看useEffect,所以同样需要简化一下代码。 并且考虑到有人对TypeScript语法不熟悉,还去掉了TypeScript语法,之后的简化代码也会如此。 现在我们看下简化后的代码: import invariant from 'shared/invariant'; import ReactCurrentDispatcher from './ReactCurrentDispatcher'; function resolveDispatcher() { const dispatcher = ReactCurrentDispatcher.current; // React版本不对或者Hook使用有误什么的就报错 // ... return dispatcher; } export function useEffect(create,inputs) { const dispatcher = resolveDispatcher(); return dispatcher.useEffect(create, inputs); } 这里可以看到,我们的useEffect实际上是ReactCurrentDispatcher.current.useEffect。 ReactCurrentDispatcher文件(可以直接跳) 看一下ReactCurrentDispatcher文件,这里没有简化: import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks'; const ReactCurrentDispatcher = { current: (null: null | Dispatcher), }; export default ReactCurrentDispatcher; 发现他的current的类型是null或者Dispatcher,所以这里我们很简单就能猜到,这个东西的源码在 'react-reconciler/src/ReactFiberHooks'。 ReactFiberHooks文件 几千行代码,头大。但是莫慌,咱们又不是来写react的,看看原理而已。 我们之前已经知道useEffect实际上是ReactCurrentDispatcher.current.useEffect。 很明显ReactCurrentDispatcher.current不管是什么东西单独列出来,我们只需要知道谁赋值给他就行了。 精简代码,去掉用__DEV__区分的开发代码之后,我们发现整个文件给ReactCurrentDispatcher.current赋值的没几个。 而唯一一个与异常判断无关的是renderWithHooks函数中的这一块代码: export function renderWithHooks( current, workInProgress, Component, props, secondArg, nextRenderExpirationTime ){ ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; let children = Component(props, secondArg); return children; } 我们不知道这段代码是干嘛的,但是他肯定是渲染组件时用的。 而这里很显然ReactCurrentDispatcher.current的值就只能是HooksDispatcherOnMount和HooksDispatcherOnUpdate。 很明显这两个一个用于加载时,一个用于更新时。 然后我们们搜一下相关代码: const HooksDispatcherOnMount = { useEffect: mountEffect }; const HooksDispatcherOnUpdate = { useEffect: updateEffect }; 也就是说,组件加载时,useEffect会调用mountEffect,组件更新时会调用updateEffect。 让我们继续看看这两个函数: function mountEffect(create, deps) { return mountEffectImpl( UpdateEffect | PassiveEffect, UnmountPassive | MountPassive, create, deps, ); } function updateEffect(create, deps) { return updateEffectImpl( UpdateEffect | PassiveEffect, UnmountPassive | MountPassive, create, deps, ); } 这里的UpdateEffect和PassiveEffect是二进制常数,用位运算的方式操作。 先不用知道具体意义,知道是个常量即可。 接下来我们看看具体的mountEffectImpl: function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps){ const hook = mountWorkInProgressHook(); // useEffect不传依赖,那么就为null const nextDeps = deps === undefined ? null : deps; currentlyRenderingFiber.effectTag |= fiberEffectTag; // 链表尾部hook对象的memoizedState为pushEffect的返回值 hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps); } 我们看到第一行代码调用mountWorkInProgressHook新建了一个hook对象,让我们看看mountWorkInProgressHook: function mountWorkInProgressHook() { const hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null, }; if (workInProgressHook === null) { // This is the first hook in the list currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else { // Append to the end of the list workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; } 很明显这里有个链表结构workInProgressHook,如果workInProgressHook链表为null就将新建的hook对象赋值给它,如果不为null,那么就加在链表尾部。 这里有必要讲解一下: Hooks作为一个链表存储在fiber的memoizedState中。 currentHook 是当前fiber的链表。 workInProgressHook 是即将被加入到 work-in-progress fiber的链表。 然后我们再看看pushEffect: function pushEffect(tag, create, destroy, deps) { // 新建一个effect,很明显又是个链表结构 const effect = { tag, create, destroy, deps, // Circular next: null, }; // 从currentlyRenderingFiber.updateQueue获取组件更新队列 let componentUpdateQueue= currentlyRenderingFiber.updateQueue; // 判断组件更新队列是否为空,每次在调用renderWithHooks都会将这个componentUpdateQueue置为null // 这样的话每次update这个组件时,就会创建一个新的effect链表 if (componentUpdateQueue === null) { // 为空就创建一个组件更新队列 componentUpdateQueue = createFunctionComponentUpdateQueue(); // 并赋值给currentlyRenderingFiber.updateQueue currentlyRenderingFiber.updateQueue = componentUpdateQueue; // 组件更新队列最新的effect为我们新建的effect componentUpdateQueue.lastEffect = effect.next = effect; } else { // 如果组件更新队列已经存在,获取它最新的Effect const lastEffect = componentUpdateQueue.lastEffect; if (lastEffect === null) { // 如果最新的Effect为null,那么组件更新队列最新的Effect为我们新建的effect componentUpdateQueue.lastEffect = effect.next = effect; } else { // 否则将我们的effect加入到链表结构中最末尾,然后他的next为链表结构的第一个effect // 这里的effect链表是个闭环 const firstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; } } return effect; } 我们再看看更新时调用的updateEffectImpl: function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) { // 这里 updateWorkInProgressHook // workInProgressHook = workInProgressHook.next; // currentHook = currentHook.next; const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; let destroy = undefined; if (currentHook !== null) { const prevEffect = currentHook.memoizedState; destroy = prevEffect.destroy; if (nextDeps !== null) { const prevDeps = prevEffect.deps; // 对比两个依赖数组的各个值之间是否有变动,如果没变动,那么就设置标志位为NoHookEffect if (areHookInputsEqual(nextDeps, prevDeps)) { pushEffect(NoHookEffect, create, destroy, nextDeps); return; } } } currentlyRenderingFiber.effectTag |= fiberEffectTag; hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps); } 我们可以看到updateEffectImpl和mountEffectImpl很像,最重要的是我们得两个函数串起来,看看他们到底实现了一个什么。 Hook相关数据结构简图 这里我自己画了一张图,利于理解: 这张图的结构是一个组件某一时刻的结构。 图中黄色为Fiber节点,绿色为Hook节点,蓝色为Effect节点。 Fiber节点,其实就是我们的虚DOM节点,react会生成一个Fiber节点树,每个组件在Fiber树上都有对应的Fiber节点。 其中currentlyRenderingFiber表示我们正在进行渲染的节点,它来自于workInProgress,current表示已经渲染的节点。 组件加载时,会执行各个useEffect,然后就会建立一个Hook链表,而workInProgress的memoizedState字段就指向了Hook链表的尾部Hook节点。 而构建每个Hook节点时,会同时构造一个Effect节点,同样,Hook节点的memoizedState字段就指向了对应的Effect节点。 而每个Effect节点又会连接起来形成一个链表,然后workInProgress的updateQueue字段指向了Effect链表的尾部Effect节点。 组件更新时,会依次对比currentHook指向的Effect的依赖数组与新的依赖数组的不同,如果一样,就设置Effect节点的effectTag为NoHookEffect。 但是无论依赖数组中的值是否改变,都会新构造一个Effect节点,作为Hook节点的memoizedState字段的值。 然后在准备渲染时,会去直接找到Fiber节点的updateQueue的lastEffect,也就是直接指向Effect链表的尾部Effect节点。 因为effect链表是闭环的,这里通过lastEffect的next找到第一个Effect。 然后循环遍历effect链表,当effectTag为NoHookEffect则不做操作,否则会去先执行effect的destroy操作,然后再执行create操作。 对,你没看错,总结起来就是每次更新后,只要依赖项改变,那么就会执行useEffect的卸载函数,再执行第一个参数create函数。 这一部分代码比较远: function commitHookEffectList( unmountTag, mountTag, finishedWork, ) { const updateQueue = finishedWork.updateQueue; let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { if ((effect.tag & unmountTag) !== NoHookEffect) { // Unmount const destroy = effect.destroy; effect.destroy = undefined; if (destroy !== undefined) { destroy(); } } if ((effect.tag & mountTag) !== NoHookEffect) { // Mount const create = effect.create; effect.destroy = create(); } effect = effect.next; } while (effect !== firstEffect); } } 这里的位运算大家可能有点看不懂,因为NoHookEffect的值是0,所以只要effect.tag被设置为NoHookEffect,那么 effect.tag & unmountTag 就必然为NoHookEffect。 我们还记得,我们之前的玩法,依赖数组各个值不变时,就设置Effect节点的effectTag为NoHookEffect。 此时是绝对不会执行先destroy Effect节点,再执行Effect函数create的操作。 而如果effect.tag的值不为NoHookEffect,那也得需要effect.tag与unmountTag至少有一个位相同才能执行destroy。 让我们看看之前无论是mountEffectImpl还是updateEffectImpl都默认传的是:UnmountPassive | MountPassive,也就是说effect.tag为UnmountPassive | MountPassive。 而很明显这个设计的目的在于,当mountTag为MountPassive时执行create函数,而unmountTag为UnmountPassive时创建执行destroy函数。 而只有下面这个地方会做这个Passive操作: export function commitPassiveHookEffects(finishedWork: Fiber): void { if ((finishedWork.effectTag & Passive) !== NoEffect) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Chunk: { commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork); commitHookEffectList(NoHookEffect, MountPassive, finishedWork); break; } default: break; } } } 这里的意思很明显,先遍历一遍effect链表,每个依赖项变了的hook都destroy一下,然后再遍历一遍effect链表,每个依赖项变了的,都执行create函数一下。 也就是说每次都会按照useEffect的调用顺序,先执行所有useEffect的卸载函数,再执行所有useEffect的create函数。 而commitPassiveHookEffects又是只有flushPassiveEffects这个函数最终能调用到。 而每次 React 在检测到数据变化时,flushPassiveEffects就会执行。 不论是props还是state的变化都会如此。 所以如果您真的有需要去模拟一个像之前的componentDidMount和componentWillUnmount的生命周期,那么最好用上一个单独的Effect: useEffect(()=>{ // 加载时的逻辑 return ()=>{ // 卸载时的逻辑 } },[]) 这里用[]作为依赖数组,是因为这样依赖就不会变动,也就是只在加载时执行一次加载逻辑,卸载时执行一次卸载逻辑。 不加依赖数组时,那么每次渲染都会执行一次加载和卸载。
查看详情