使用 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 ()=>{ // 卸载时的逻辑 } },[]) 这里用[]作为依赖数组,是因为这样依赖就不会变动,也就是只在加载时执行一次加载逻辑,卸载时执行一次卸载逻辑。 不加依赖数组时,那么每次渲染都会执行一次加载和卸载。
查看详情
                    前言 学习React,生命周期很重要,我们了解完生命周期的各个组件,对写高性能组件会有很大的帮助. Ract生命周期 React 生命周期分为三种状态 1. 初始化 2.更新 3.销毁 初始化 1、getDefaultProps() 设置默认的props,也可以用dufaultProps设置组件的默认属性. 2、getInitialState() 在使用es6的class语法时是没有这个钩子函数的,可以直接在constructor中定义this.state。此时可以访问this.props 3、componentWillMount() 组件初始化时只调用,以后组件更新不调用,整个生命周期只调用一次,此时可以修改state。 4、 render() react最重要的步骤,创建虚拟dom,进行diff算法,更新dom树都在此进行。此时就不能更改state了。 5、componentDidMount() 组件渲染之后调用,只调用一次。 更新 6、componentWillReceiveProps(nextProps) 组件初始化时不调用,组件接受新的props时调用。 7、shouldComponentUpdate(nextProps, nextState) react性能优化非常重要的一环。组件接受新的state或者props时调用,我们可以设置在此对比前后两个props和state是否相同,如果相同则返回false阻止更新,因为相同的属性状态一定会生成相同的dom树,这样就不需要创造新的dom树和旧的dom树进行diff算法对比,节省大量性能,尤其是在dom结构复杂的时候 8、componentWillUpdata(nextProps, nextState) 组件初始化时不调用,只有在组件将要更新时才调用,此时可以修改state 9、render() 组件渲染 10、componentDidUpdate() 组件初始化时不调用,组件更新完成后调用,此时可以获取dom节点。 卸载 11、componentWillUnmount() 组件将要卸载时调用,一些事件监听和定时器需要在此时清除。 结束语 以上就是React 的生命周期,大家可以自行写下code测试一下,在这里我就不贴code 了。
查看详情
                    React 起源于 Facebook 内部项目,是一个用来构建用户界面的 Javascript 库,相当于MVC架构中的V层框架,与市面上其他框架不同的是,React 把每一个组件当成了一个状态机,组件内部通过 state 来维护组件状态的变化,当组件的状态发生变化时,React 通过虚拟DOM技术来增量并且高效的更新真实DOM。本文将对React 的这些特点进行简单的介绍。 Hello React  考虑到有的同学还不曾了解过 React,我们先来写一个简单的 React 组件,让大家一睹为快! ?12345678910111213141516// 创建一个HelloReact组件var HelloReact = React.createClass({    render:function(){        return (            <div>                 Hello React!            </div>        )    }}); // 使用HelloReact组件ReactDOM.render(    <HelloReact />,    document.querySelector('body')) 这样就定义了一个React组件,当然要运行这段代码是有条件的,需要引入React库,还需要引入JSX语法转换库,这里不多说了,这些基础的东西还需要各位亲自实践才好! 虚拟DOM(Virtual DOM) 在前端开发的过程中,我们经常会做的一件事就是将变化的数据实时更新到UI上,这时就需要对DOM进行更新和重新渲染,而频繁的DOM操作通常是性能瓶颈产生的原因之一,有时候我们会遇到这样一种尴尬的情况:比如有一个列表数据,当用户执行刷新操作时,Ajax会重新从后台请求数据,即使新请求的数据和上次完全相同,DOM也会被全部更新一遍并进行重新渲染,这样就产生了不必要的性能开销。 React为此引入了虚拟DOM(Virtual DOM)机制:对于每一个组件,React会在内存中构建一个相对应的DOM树,基于React开发时所有的DOM构造都是通过虚拟DOM进行,每当组件的状态发生变化时,React都会重新构建整个DOM数据,然后将当前的整个DOM树和上一次的DOM树进行对比,得出DOM结构变化的部分(Patchs),然后将这些Patchs 再更新到真实DOM中。整个过程都是在内存中进行,因此是非常高效的。借用一张图可以清晰的表示虚拟DOM的工作机制: React 生命周期 React 把每个组件都当作一个状态机来维护和管理,因此每个组件都拥有一套完整的生命周期,大致可以分为三个过程:初始化、更新和销毁。生命周期的每一个过程都明确的反映了组件的状态变化。对于开发来说就能很容易的把握组件的每个状态,不同的状态时期做对应的事情,互不干扰。以下是和组件生命周期相关的几个方法: getDefaultProps getInitialState componentWillMount componentDidMount componentWillReceiveProps shouldComponentUpdate componentWillUpdate componentDidUpdate componentWillUnmount 初始化 对于外部系统来说,组件是一个独立存在的封闭系统,内部的逻辑被隐藏,只对外暴露传递数据的接口,而React为我们提供了两种方式来向组件传递数据,即 props 和 state。 props 是在调用 ReactDOM.render() 时通过标签属性xxx传递,然后通过 this.props.xxx 来获取,getDefaultProps 允许你为组件设置一个默认的props值,在没有传递props的情况下显示默认值。 ?123456789101112131415161718192021222324// 创建HelloReact组件var HelloReact = React.createClass({    /**     * 当设置props的默认值 当没有传递时显示默认值     * @return {}     */    getDefaultProps:function(){       return {           data:"暂无数据"       }    },    render:function(){        return (            <div>               //显示data,当props发生变化时会自动更新               {this.props.data}            </div>        )    }});//传递props属性dataReactDOM.render(   <HelloReact data={"Hello React!"} />,   document.querySelector('body')) 和 props 不同的是,state不能通过外部传递,因此在使用state之前,需要在 getInitialState 中为state设置一个默认值,然后才能通过 this.state.xxx 来访问,当组件被挂载完成时,触发 componentDidMount 方法,我们可以在这里通过Ajax请求服务器数据,然后再通过 setState() 把state的值设置为真实数据。 ?12345678910111213141516171819202122232425262728293031323334353637383940414243// 创建HelloReact组件var HelloReact = React.createClass({    /**     * 设置组件的初始值     * @returns {{data: Array, msg: string}}     */    getInitialState:function(){        return {            data:"数据加载中..." //初始值为[]        }    },    /**     * 挂载后首次加载数据     */    componentDidMount:function(){        this.requestData();//请求数据    },    /**     * 请求后台数据     */    requestData:function(){        $.ajax({            url:'xxxx.ashx',            data:{},            success:function(data){                this.setState({                    data:data  //通过setState()更新服务器数据                })            }        }.bind(this))    },    render:function(){        return (            <div>               {this.state.data}            </div>        )    }});ReactDOM.render(    <HelloReact  />,    document.querySelector('body')) 更新 props属性是只读的,如果想要改变props的值,只能通过重新调用render()来传递新的props,但要注意的是,重新执行render()组件不会被重新挂载,而是通过虚拟DOM技术进行增量更新和渲染,这时还会触发 componentWillReceiveProps 方法,并将新的props作为参数传递,你可以在这里对新的props进行处理。 相比props,state天生就是用来反映组件状态的,因此它的值是可以被改变的,当state的值被改变时,通过setState就可以改变state的值,React同样也是采用虚拟DOM技术来计算需要被更新的部分,而不是牵一发动全身的更新和渲染。 当 props 和 state 的状态发生变化后,组件在即将更新之前还会触发一个叫 shouldConponentUpdate 的方法,如果 shouldConponentUpdate 返回的是 true,不管props和state 的值和上一次相比有没有变化,React 都会老老实实的进行对比。此时,如果你确定以及肯定两次数据没有变化,那就让 shouldConponentUpdate 返回 false,React就不会进行diff了,更不会重新渲染了。瞬间省去了 diff 的时间。 销毁 当组件从DOM中被移除时,React会销毁之。在销毁之前,细心的React还触发 componentWillUnmount 来通知你,看你最后有没有什么话想对这个即将销毁的组件说,当然你没什么事就不用了。 props 与 state  我们已经知道可以通过 props 和 state 两种方式向组件传递数据,props 是只读的不能被改变,而 state 是用来反映一个组件的状态,是可以改变的。因此,当组件所需要的数据在调用时是已经确定的,不频繁发生变化的,就可以使用props来传递,相反,当组件所需要的数据在调用时不能确定,需要等待异步回调时才能确定,比如ajax请求数据,input 的 onchange事件,这时就需要使用state 来记录和改变这些值得变化。
查看详情
                    简介 <script setup> 语法糖并不是新增的功能模块,它只是简化了以往的组合API(compositionApi)的必须返回(return)的写法,并且有更好的运行时性能。 在 setup 函数中:所有 ES 模块导出都被认为是暴露给上下文的值,并包含在 setup() 返回对象中。相对于之前的写法,使用后,语法也变得更简单。 你不必担心setup语法糖的学习成本,他是组合式API的简化,并没有新增的知识点。你只需要了解一些用法和细微的不同之处,甚至比之前写setup()还要顺手! 使用方式极其简单,仅需要在 script 标签加上 setup 关键字即可。示例: <script setup> </script> 注:因为setup语法糖是vue3.2正式确定下来的议案,所以vue3.2的版本是真正适合setup语法糖的版本。 1. 属性和方法无需返回,直接使用 以前使用响应式数据是: <template> {{msg}} </template> <script> import { ref } from 'vue' export default { setup () { const msg = ref('hello vue3'); return { msg } } } </script> 现在使用 setup 语法糖,不需要return返回 和 setup函数,只需要全部定义在<script setup>内即可: <template> {{msg}} </template> <script setup> import { ref } from 'vue' const msg = ref('hello vue3'); </script> reactive, computed, 也一样可以使用: <template> <div>{{msg}}</div> <div>{{obj.a}}</div> <div>{{sum}}</div> </template> <script setup> import { ref, reactive, computed } from 'vue' const msg = ref('hello vue3'); const obj = reactive({ a: 1, b: 2 }) const sum = computed(() => { return obj.a + 3; }); </script> 2. 组件自动注册 在 script setup 中,引入的组件可以直接使用,无需再通过components进行注册,并且无法指定当前组件的名字,它会自动以文件名为主,也就是不用再写name属性了。示例: <template> <Child /> </template> <script setup> import Child from '@/components/Child.vue' </script> 3. 组件数据传递(props和emits) props 和 emits 在语法糖中使用 defineEmits 和 defineProps 方法来使用: 子组件 Child.vue: <template> <div @click="toEmits">Child Components</div> </template> <script setup> // defineEmits,defineProps无需导入,直接使用 const emits = defineEmits(['getChild']); const props = defineProps({ title: { type: String, defaule: 'defaule title' } }); const toEmits = () => { emits('getChild', 'child value') // 向父组件传递数据 } // 获取父组件传递过来的数据 console.log(props.title); // parent value </script> 父组件 Home.vue: <template> <Child @getChild="getChild" :title="msg" /> </template> <script setup> import { ref } from 'vue' import Child from '@/components/Child.vue' const msg = ref('parent value') const getChild = (e) => { // 接收父组件传递过来的数据 console.log(e); // child value } </script> 4. 获取 slots 和 attrs 可以通过useContext从上下文中获取 slots 和 attrs。不过提案在正式通过后,废除了这个语法,被拆分成了useAttrs和useSlots。示例: // 旧 <script setup> import { useContext } from 'vue' const { slots, attrs } = useContext() </script> // 新 <script setup> import { useAttrs, useSlots } from 'vue' const slots = useSlots(); const attrs = useAttrs(); console.log(attrs.dataId); // 查看父组件传来的自定义属性 </script> 5. 对外暴露属性(defineExpose) <script setup> 的组件默认不会对外部暴露任何内部声明的属性。 如果有部分属性要暴露出去,可以使用 defineExpose 子组件 Child.vue: <template> {{msg}} </template> <script setup> import { ref } from 'vue' let msg = ref("Child Components"); let num = ref(123); // defineExpose无需导入,直接使用 defineExpose({ msg, num }); </script> 父组件 Home.vue: <template> <Child ref="child" /> </template> <script setup> import { ref, onMounted } from 'vue' import Child from '@/components/Child.vue' let child = ref(null); onMounted(() => { console.log(child.value.msg); // Child Components console.log(child.value.num); // 123 }) </script>
查看详情