-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.json
1 lines (1 loc) · 229 KB
/
content.json
1
[{"title":"什么是ssr","date":"2024-03-12T13:30:03.000Z","path":"2024/03/12/ 什么是ssr/","text":"概念服务器端渲染(Server-Side Rendering,SSR)指的是在服务器上完成网页渲染并将将其发送给客户端的过程。 为什么需要SSR?SSR 发送给客户端的是包含了完整内容的网页,这样用户可以先看到网页的内容,而不需要等待“网页加载>执行JS>加载数据>渲染到网页”,从而提升用户体验。另一方面,因为网页内已经包含了具体内容,对搜索引擎也更加友好。 SSR的优势SSR 能改善用户体验。与传统服务器语言的对比很久之前,我们用 PHP 之类的语言输出 HTML,但是我们并不称其为”服务器端渲染”,因为现在的SSR 有更多的优化: 语言同构化:开发难度降低。 数据传递与状态管理:虽然还是 JSON,但框架尽量帮我们做好了。 渲染函数由边缘计算负责:更快的速度、 页面切换时不会重新加载- 与传统服务器语言的对比很久之前,我们用PHP之类的语言输出HTML,但是我们并不称其为”服务器端渲染”,因为现在的服务器端渲染有更多的优: 语言同构化:开发难度降低。 数据传递与状态管理:虽然还是 JSON,但框架尽量帮我们做好了。 渲染函数由边缘计算负责:更快的速度、 页面切换时不会重新加载。 SSR的一般构成当谈论服务器端渲染(SSR)时,一般构成包掊以下几个关键部分: 服务器端应用程序:这是运行在服务器上的应用程序,负责接收客户端请求,执行渲染过程,并返回渲染后的页面给客户端。 路由:服务器端应用程序需要能够根据客户端请求的不同路径,调用对应的渲染逻辑和数据获取方法。 模板引擎:用于将页面模板和数据结合,生成最终的 HTML 内容。我们目前会使用的模版引擎都基于某款 MVVM 框架。 数据获取:在渲染页面之前,服务器端应用程序通常需要获取页面所需的数据,这可能涉及到从数据库、API或其他来源获取数据。 状态管理:在 SSR 应用中,需要考虑如何管理客户端和服务器端的状态同步,以避免出现不一致的情况。 客户端交互:尽管整个页面的初始渲染是在服务器端完成的,但在客户端加载后仍可能需要进行交互,如使用 JavaScript 添加动态内容或处理用户操作。 Nuxt3 的SSR 组件对于大部分应用而言,服务器端渲染=加载数据+渲染HTML。所以理解 Nuxt3 里复杂加载数据的组件非常重要。 与异步组件 和useAsyncData useLazyAsyncData useFetch和useLazyFetchi 用 process.client 和 <client-only>来处理仅限浏览器内部使用的功能 用 process.server 来处理仅限服务器使用的功能5 https:/nuxt.com/docs/getting-started/data-fetching#serializing-data-from-api-routes Nuxt3 的渲染规则与缓存处理Nuxt3 提供三种不同的渲染模式: SSR:默认。即在服务器端渲染之后再发给客户端 ISR:部署后,渲染之后即保留缓存至下次渲染(渐进式渲染) SWR:保留缓存,并在指定时间后校验缓存 prerender:部署时生成静态页面https://nuxt.com/docs/guide/concepts/rendering! 如何鉴别用户身份在 vue,或者说传统 SPA 里,所有请求都是后请求,即完成网页加载、JS 执行完毕后,再发起请求。这些请求,可以认为完全由开发者控制,即你知道什么时候该请求,然后发起请求。一般来说所有的请求都是数据交互类。 在 Nuxt 里,因为 SSR 的存在,所以请求至少可以分成两类:页面渲染类,数据交互类。前者会影响到 HTML 的内容。在网络环境里,存在大量缓存节点,假如跟用户相关的敏感数据渲染成HTML,缓存到 CDN 当中,会是非常大的安全问题。所以 Nuxt 在 SSR 及其内部发起请求时,不会携带 cookie;在用户主动发起的请求里,才会携带 cookie。官方文档 useRequestHeaders 如果要使用 localstorage 保存用户数据,则势必会存在两次渲染,请处理好loading 状态。","tags":[{"name":"SSR","slug":"SSR","permalink":"https://ayozoo.github.io/tags/SSR/"}]},{"title":"nuxt-尝鲜","date":"2024-03-12T13:15:33.000Z","path":"2024/03/12/ nuxt-尝鲜/","text":"runtimeConfig与app.confg回看app.config.ts与nuxt.config.ts,俩者都是向应用程序的其余部分公开变量 runtimeConfig:使用环境变量构建后需要指定的私有或公共令牌。 app.config:在构建时确定的公共令牌、网站配置(如主题变体、标题和任何不敏感的项目配置)。 以下有使用准则,请参考以下标准: 特征比较 特征 runtimeConfig app.config 客户端 水合 捆绑 环境变量 ✅是的 不 反应性 ✅是的 ✅是的 类型支持 ✅部分 ✅是的 每个请求的配置 ✅不 ✅是的 热模块更换 ✅不 ✅是的 非原始 JS 类型 ✅不 ✅是的 样式本地样式表 本地样式表存在assets/目录 组件内导入 通过@import导入 1234567891011<script>// Use a static import for server-side compatibilityimport '~/assets/css/first.css'// Caution: Dynamic imports are not server-side compatibleimport('~/assets/css/first.css')</script><style>@import url(\"~/assets/css/second.css\");</style> 通过npm分发下载终端下载 1npm install animate.css 页面、组件、布局引用 1234567<script>import 'animate.css'</script><style>@import url(\"animate.css\");</style> 下载的包也可以在Nuxt配置中CSS属性中作为字符串引用 123export default defineNuxtConfig({ css: ['animate.css']}) 外部样式表可以通过Nuxt.config.ts文件的head添加link元素,实例如下: 1234567export default defineNuxtConfig({ app: { head: { link: [{ rel: 'stylesheet', href: 'https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css' }] }}}) 动态添加样式表使用useHead可组合项进行动态设置head的值 1234useHead({ link: [{ rel: 'stylesheet', href: 'https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css' }]}) 使用Nitro插件修改渲染的头部这个属于更高级别的控制,可以使用钩子拦截渲染的html,以编程的方式修改头部。创建插件如下: 123456export default defineNitroPlugin((nitro) => { nitro.hooks.hook('render:html', (html) => { html.head.push('<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css\">') })}) 外部样式表是阻止呈现的资源:必须在浏览器呈现页面之前加载和处理它们。包含不必要的大样式的网页需要更长的时间来呈现。 使用预处理器 首先确保安装sass或者less 知悉编写样式表的目录,通过预处理器的语法将原文件导入需要使用的地方123<style lang=\"scss\"> @use \"~/assets/scss/main.scss\";</style> 或者在nuxt.config.ts中配置预处理器123export default defineNuxtConfig({css: ['~/assets/scss/main.scss']}) 单文件组件动态样式 通过ref或者reactive 变量动态依赖设置 通过computed计算属性动态设置 通过三目运算以数组的形式设置 通过动态字符串 动态v-bindv-bind函数在样式块中可以引用JavaScript变量和表达式。这种绑定是动态的,当依赖值变化时,样式也会更新。 1234567891011121314<script setup lang=\"ts\">const color = ref(\"red\")</script><template> <div class=\"text\">hello</div></template><style>.text { color: v-bind(color);}</style> 作用域组件通过在style标签声明scoped属性,声明后的样式将仅应用于当前组件。 css模块可以使用css模块替换为module属性。使用注入的变量访问。 123456789<template> <p :class=\"$style.red\">This should be red</p></template><style module>.red { color: red;}</style> 预处理器SFC样式快支持预处理器语法,Vite内置了对 .scss、.sass、.less、.styl 和 .stylus 文件的支持,无需配置。通过下载他们,直接在SFC中使用lang属性提供 1<style lang=\"scss||sass||less||stylus||stylus\"> 路由Nuxt文件路由为pages/目录下的文件提供路由。Nuxt核心功能是将页面路由映射到文件。每个Vue文件都位于pages/目录中,文件名将映射到URL。 页面Nuxt路由基于vue-router,从pages目录,基于文件名。文件系统通过使用命名约定来创建动态路由和嵌套路由,如:文件目录为| pages/—| about.vue—| index.vue—| posts.vue—-| [id].vue映射出来路由文件为: 12345678910111213141516{ \"routes\": [ { \"path\": \"/about\", \"component\": \"pages/about.vue\" }, { \"path\": \"/\", \"component\": \"pages/index.vue\" }, { \"path\": \"/posts/:id\", \"component\": \"pages/posts/[id].vue\" } ]} 导航**<NuxtLink>**组件,类似于vue-router中,通过该组件链接之间页面,他呈现的是一个标签,属性设置为页面路由。<NuxtLink>进入客户端的视口,Nuxt会自动提前加载链接页面组件和页面,从而快速地呈现页面。 1234567891011<template> <header> <nav> <ul> <li><NuxtLink to=\"/about\">About</NuxtLink></li> <li><NuxtLink to=\"/posts/1\">Post 1</NuxtLink></li> <li><NuxtLink to=\"/posts/2\">Post 2</NuxtLink></li> </ul> </nav> </header></template> 路由参数通过useRoute()api在Vue组件的块或者方法中使用,以便于访问当前路由的详细信息。 1234567<script setup lang=\"ts\">const route = useRoute()// When accessing /posts/1, route.params.id will be 1console.log(route.params.id)</script> 路由中间件Nuxt提供了可以自定义的路由中间件,用于导航到特定路由之前提取要运行的代码。 可以使用middleware属性在nuxt.config.ts中配置,或者在pages/目录下创建中间件文件,文件名必须为middleware.js或者middleware.ts,文件中必须导出一个函数,函数参数为context,返回一个Promise,当函数执行完成时,返回的Promise会作为路由执行结果。 路由中间件有三种: 匿名(或内联)路由中间件,直接在使用它们的页面中定义。 命名路由中间件,放置在middleware/目录,当在页面上使用时,将通过异步导入自动加载。(注意:路由中间件名称被规范化为 kebab-case,因此变为 。someMiddlewaresome-middleware 全局路由中间件,放置在middleware/目录(带后缀),并将在每次路线更改时自动运行。.global 路由验证Nuxt通过validate属性在路由中配置,**通过**definePageMeta**()**。当路由匹配时,验证函数将被调用,如果返回false,路由将不会被激活,路由将被重定向到其他页面。 12345678<script setup lang=\"ts\">definePageMeta({ validate: async (route) => { // Check if the id is made up of digits return typeof route.params.id === 'string' && /^\\d+$/.test(route.params.id) }})</script> SEO和Meta默认设置使用强大的头部配置、可组合项和组件可以改善Nuxt应用的SEO。Nuxt提供了一些默认的SEO配置,可以在nuxt.config.ts中配置。 12345678export default defineNuxtConfig({ app: { head: { charset: 'utf-8', viewport: 'width=device-width, initial-scale=1', } }}) app.head可以通过nuxt.config.ts配置文件中配置,允许我们为整个应用程序自定义头部。 useHeaduseHead()可组合功能可以在组件中使用,我们可以方便的通过编程和响应式方法管理应用头部标签。跟其他可组合项一样,只能与组件和生命周期钩子一起使用。 12345678910111213<script setup lang=\"ts\">useHead({ title: 'My App', meta: [ { name: 'description', content: 'My amazing site.' } ], bodyAttrs: { class: 'test' }, script: [ { innerHTML: 'console.log(\\'Hello world\\')' } ]})</script> useSeoMetauseSeoMeta()是useHead()的别名,它允许我们使用与useHead()相同的配置,但是它将自动处理title和meta属性,这使得配置SEO更简单。它的主要作用是可以将站点的SEO元标记标为具备ts支持的平面对象,便于更好的配置。 1234567891011<script setup lang=\"ts\">useSeoMeta({ title: 'My Amazing Site', ogTitle: 'My Amazing Site', description: 'This is my amazing site, let me tell you all about it.', ogDescription: 'This is my amazing site, let me tell you all about it.', ogImage: 'https://example.com/image.png', twitterCard: 'summary_large_image',})</script> 组件中Nuxt提供 <Title>, <Base>, <NoScript>, <Style>, <Meta>, <Link>, <Body>, <Html> 和<Head>,便于直接于组件间模板元数据交互。 注意,这些组件名称在模板中必须将他们大写。。 并且可以接受嵌套的元标记,但这不会影响哪里嵌套的元标记在最终的 HTML 中呈现。 1234567891011121314151617181920212223242526272829303132<script setup lang=\"ts\">const title = ref('Hello World')</script><template> <div> <Head> <Title>{{ title }}</Title> <Meta name=\"description\" :content=\"title\" /> <Style type=\"text/css\" children=\"body { background-color: green; }\" ></Style> </Head> <h1>{{ title }}</h1> </div></template>```## 用于`useHead()`,`app.head`和组件传值类型```tsinterface MetaObject { title?: string titleTemplate?: string | ((title?: string) => string) templateParams?: Record<string, string | Record<string, string>> base?: Base link?: Link[] meta?: Meta[] style?: Style[] script?: Script[] noscript?: Noscript[]; htmlAttrs?: HtmlAttributes; bodyAttrs?: BodyAttributes;} 特征反应可以通过计算值或响应式变量或对象来动态更新元标记,所有的属性都支持反应式。建议使用getter()而不是computed(),因为computed()将创建一个新对象,这将导致不必要的重新渲染。以下是三种更改元标记的三种写法。 使用useHead()123456789101112131415161718192021222324252627282930313233343536373839<script setup lang=\"ts\">const description = ref('My amazing site.')useHead({meta: [{ name: 'description', content: description }], })</script> ```+ 使用SeoMeta ```vue<script setup lang=\"ts\">const description = ref('My amazing site.')useSeoMeta({ description})</script> ```+ 组件 ```vue<script setup lang=\"ts\">const description = ref('My amazing site.')</script><template> <div> <Meta name=\"description\" :content=\"description\" /> </div></template>```### 标题模板可以通过`titleTemplate`~属性自定义动态标题模板,该属性可以是字符串或函数。这样可以将网站的名称添加到每个页面的标题中。如果需要使用某个函数完全控制标题,应该在`nuxt.config.ts`中进行设置。```vue<script setup lang=\"ts\">useHead({ title:'sdf' titleTemplate: (titleChunk) => { return titleChunk ? `${titleChunk} - Site Title` : 'Site Title'; }})</script> Body标签可以使用标签的选项将他们附加到标签的末尾 123456789101112<script setup lang=\"ts\">useHead({ script: [ { src: 'https://third-party-script.com', // valid options are: 'head' | 'bodyClose' | 'bodyOpen' tagPosition: 'bodyClose' // 标签闭合的末尾 } ]})</script> 动态标题以下示例,设置带有占位符的字符串,这样就可以灵活的为Nuxt应用程序设置每个路由动态设置页面标题:titleTemplate %s || function 123456789101112131415161718192021 <!-- 字符串匹配 --><script setup lang=\"ts\">useHead({ // as a string, // where `%s` is replaced with the title titleTemplate: '%s - Site Title',})</script><!-- 函数匹配 --><script setup lang=\"ts\">useHead({ // or as a function titleTemplate: (productCategory) => { return productCategory ? `${productCategory} - Site Title` : 'Site Title' }})</script> nuxt.config也用于设置页面标题的替代方法,但是不允许页面标题是是动态的。因此,一般建议在文件中使用添加一个动态标题,然后将其应用月Nuxt应用程序的所有路由。 外部CSS以下是可以使用useHead和link可组合或使用组件 12345678910111213141516171819202122232425<!-- 使用头 --><script setup lang=\"ts\">useHead({ link: [ { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Roboto&display=swap', crossorigin: '' } ]})</script><!-- 在组件中 --><template> <div> <Link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" /> <Link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Roboto&display=swap\" crossorigin=\"\" /> </div></template> 过渡页面过渡 全部生效可以在Nuxt.config.ts中配置页面过渡,这个设置会将过渡应用于所有页面。如下所示:12345export default defineNuxtConfig({ app: { pageTransition: { name: 'page', mode: 'out-in' } },}) 页面之间只需要在页面之间添加过渡,则在app.vue设置过渡效果 页面设置不同的将需要设置的页面通过definePageMeta键入pageTransition属性,例如:12345678910111213141516<script setup lang=\"ts\">definePageMeta({ pageTransition: { name: 'rotate' }})</script>```## 布局过渡通过`defingNuxtConfig`配置启用`layout`布局过渡```jsexport default defineNuxtConfig({ app: { layoutTransition: { name: 'layout', mode: 'out-in' } },}) 也可以通过definePageMeta属性在页面中设置layoutTransition布局过渡123456789<script setup lang=\"ts\">definePageMeta({ layout: 'orange', layoutTransition: { name: 'slide-in' }})</script> 全局设置可以使用nuxt.confg全局自定义默认这些过渡名称密钥都接受pageTransition、layoutTransition和transitionProps可作为序列化值,其中都可以传递自定义CSS转换的、和其他有效的过渡道具。 12345678910111213export default defineNuxtConfig({ app: { pageTransition: { name: 'fade', mode: 'out-in' // default }, layoutTransition: { name: 'slide', mode: 'out-in' // default } }}) 覆盖全局过渡属性,为单个nuxt页面设置不同的过渡属性,可以使用definePageMeta 123456789<script setup lang=\"ts\">definePageMeta({ pageTransition: { name: 'bounce', mode: 'out-in' // default }})</script> 禁用过渡pageTransition和layoutTransition属性可以设置为false,以禁用页面和布局过渡。单个页面中 123456<script setup lang=\"ts\">definePageMeta({ pageTransition: false, layoutTransition: false})</script> 全局设置 1234567export default defineNuxtConfig({ app: { pageTransition: false, layoutTransition: false }}) 通过JavaScript钩子精准控制过渡1234567891011121314<script setup lang=\"ts\">definePageMeta({ pageTransition: { name: 'custom-flip', mode: 'out-in', onBeforeEnter: (el) => { console.log('Before enter...') }, onEnter: (el, done) => {}, onAfterEnter: (el) => {} }})</script> 动态过渡如果需要通过条件控制过渡,可以使用内联中间件分配不同的过渡名。to.meta.pageTransition 12345678910111213141516171819202122232425262728293031323334353637383940414243<script setup lang=\"ts\">definePageMeta({ pageTransition: { name: 'slide-right', mode: 'out-in' },// 中间件 middleware (to, from) { if (to.meta.pageTransition && typeof to.meta.pageTransition !== 'boolean') to.meta.pageTransition.name = +to.params.id > +from.params.id ? 'slide-left' : 'slide-right' }})</script><template> <h1>#{{ $route.params.id }}</h1></template><style>.slide-left-enter-active,.slide-left-leave-active,.slide-right-enter-active,.slide-right-leave-active { transition: all 0.2s;}.slide-left-enter-from { opacity: 0; transform: translate(50px, 0);}.slide-left-leave-to { opacity: 0; transform: translate(-50px, 0);}.slide-right-enter-from { opacity: 0; transform: translate(-50px, 0);}.slide-right-leave-to { opacity: 0; transform: translate(50px, 0);}</style> 使用NuxtPage过渡在app.vue中使用时,transitionProps可以作为组件props传递以激活全局转换 1234567891011<template> <div> <NuxtLayout> <NuxtPage :transition=\"{ name: 'bounce', mode: 'out-in' }\" /> </NuxtLayout> </div></template> 数据获取Nuxt提供了可组合项来处理应用程序中的数据获取。Nuxt提供俩个组合项: useFetch:组件设置函数中处理数据获取的最直接方法。 useAsyncData:结合使用,提供了更细颗粒度的控制Nuxt提供内置库: $fetch:非常适合根据用户交互发出网络请求。 useFetchuseFetch该组合项是执行数据提取的最直接方法。 12345678<script setup lang=\"ts\">const { data: count } = await useFetch('/api/count')</script><template> <p>Page visits: {{ count }}</p></template> $fetchNuxt内置了$fetch包含该库,用于页面数据异步加载时候获取数据。 1234567891011<script setup lang=\"ts\">async function addTodo() { const todo = await $fetch('/api/todos', { method: 'POST', body: { // My todo data } })}</script> useAsyncDatauseAsyncData是useFetch的更细颗粒度的控制。负责包装异步逻辑,并解析后返回结果。 当CMS或第三方提供自己的API时,可以使用useAsyncData。例如: 1234567<script setup lang=\"ts\">const { data, error } = await useAsyncData('users', () => myGetFunction('users'))// This is also possible:const { data, error } = await useAsyncData(() => myGetFunction('users'))</script> 第一个参数useAsyncData是key,用于缓存数据。这个key可以通过直接传递查询函数来忽略,key会自动生成。因为自动生成键只考虑调用的文件和行,为了避免产生不必要的行为因此,可以传递一个自定义的key。 12345678<script setup lang=\"ts\">const { id } = useRoute().paramsconst { data, error } = await useAsyncData(`user:${id}`, () => { return myGetFunction('users', { id })})</script> 可组合项是个可以等待多个完成,然后检索每个结果的好方法。 12345678910111213<script setup lang=\"ts\">const { data: discounts, pending } = await useAsyncData('cart-discount', async () => { const [coupons, offers] = await Promise.all([ $fetch('/cart/coupons'), $fetch('/cart/offers') ]) return { coupons, offers }})// discounts.value.coupons// discounts.value.offers</script> 返回值useFetch具有下面列出相同的返回值。 data:传入的异步函数结果 peding:布尔值,指示这个异步函数是否执行完毕 refresh/execute:可用于刷新函数返回的数据的函数 error:数据获取失败返回的错误信息 status:返回数据请求状态的字符串","tags":[{"name":"SSR","slug":"SSR","permalink":"https://ayozoo.github.io/tags/SSR/"}]},{"title":"React-状态管理","date":"2023-11-20T15:03:43.000Z","path":"2023/11/20/ React-状态管理/","text":"react之状态管理随着应用的不断加大,我们应该更有意识的的去关注我们的应用状态如何去组织,以及数据如何在组件中流动。我们应该去避免冗余或重复的状态,这样可以避免一些缺陷的产生。这时候,如何组织好状态,如何保持状态更新逻辑的可维护性,以及如何跨组件共享状态就变得尤为重要。 使用状态响应输入在使用React时,我们不需要直接代码层面去修改UI。我们可以直接通过组件的不同状态去展现的UI,然后根据用户输入触发状态修改。 声明式UI与命令式UI的比较当设计UI交互时,会思考UI如何根据用户的操作而响应变化。想象一个允许用户提交一个答案的表单: 当表单输入的时候,“提交”按钮会变成可用状态 点击“提交”后,表单和提交按钮都会随之变成不可用状态 如果网络请求成功,表单会随之隐藏,同时会出现“提交成功”提示 如果网络请求失败,会出现错误提示信息,表单又变为可用状态 命令式编程中,我们需要根据具体的场景去设计如何实现交互。我们必须根据可能会发生的事情去写一些明确的命令去操作UI。也就是我们需要“命令”每个元素(操作dom),告诉计算机应该如何去更新UI的编程方式被称为命令式编程。 对于独立系统来说,命令式控制用户界面的效果也不错,但是如果要实现更为复杂的系统时,代码的组织就会指数级难度增长。 而React就是为了解决这样的问题而诞生。 在React中,不比直接去操作UI(不需要去直接操作dom)。相反,我们只需要声明我们想要显示的内容。React就会通过计算如何去更新UI。 声明式考虑UI通过上面的思考方式,我们来看看React时如何去实现这个UI。 定位组件中不同的视图状态 确定是什么触发这些state的改变 表示内存中的state(需要使用useState) 删除任何不必要的state变量 连接事件处理函数去设置state 步骤1:定位组件中不同的视图状态在React中,不同的可视化UI界面中用户所有看到的都是不同的“状态”。 无数据:表单有一个不可用状态的“提交”按钮。 输入中:表单有一个可用状态的“提交”按钮。 提交中:表单完全处于不可用状态,加载动画出现。 成功时:显示“成功”的消息而非表单。 错误时:与输入状态类似,但会多错误的消息。 步骤2:确定是什么触发了这些状态的改变触发state的更新来响应俩种输入: 人为输入。比如点击按钮、在表单中输入内容,或导航到链接。 计算机输入。比如网络请求得到反馈、定时器被触发,或加载一张图片。 以上两种情况中,你必须设置 state 变量 去更新 UI。对于正在开发中的表单来说,你需要改变 state 以响应几个不同的输入: 改变输入框中的文本时(人为)应该根据输入框的内容是否是空值,从而决定将表单的状态从空值状态切换到输入中或切换回原状态。 点击提交按钮时(人为)应该将表单的状态切换到提交中的状态。 网络请求成功后(计算机)应该将表单的状态切换到成功的状态。 网络请求失败后(计算机)应该将表单的状态切换到失败的状态,与此同时,显示错误信息。 步骤3:通过useState表示内存中的state接下来,我们会需要在内存中通过 useState 表示组件中的视图状态。state的每个部分都是“处在变化中的”,并且需要让“变化大的部分”尽可能的少。 先从绝对必须存在的状态开始。 接下来,创建一个状态变量去代表想要显示的可时状态 在很难想出最好的办法时,就从添加足够多的state开始,确保所有可能的视图状态都包含。 最初的想法或许不是最好的,必要时,**重构state**也是步骤中的一部分。 步骤4:删除任何不必要的state变量我们想要避免state内容中的重复,从而只需要关注必要的部分。这就需要我们花费时间去重构我们的state结构,这样会让我们的组件更容易被理解,从而减少重复避免歧义。主要的目的是防止出现在内存中的state不代表任何我们让用户看到的有效UI的情况。 在创建state变量的时候,我们应该反问自己以下这些问题: 这个state是否会导致矛盾?例如,isTyping 与 isSubmitting 的状态不能同时为 true。矛盾的产生通常说明了这个 state 没有足够的约束条件。两个布尔值有四种可能的组合,但是只有三种对应有效的状态。为了将“不可能”的状态移除,可以将 'typing'、'submitting' 以及 'success' 这三个中的其中一个与 status 结合。 相同的信息是否已经在另一个 state 变量中存在?另一个矛盾:isEmpty 和 isTyping 不能同时为 true。通过使它们成为独立的 state 变量,可能会导致它们不同步并导致 bug。幸运的是,可以移除 isEmpty 转而用 message.length === 0。 是否可以通过另一个 state 变量的相反值得到相同的信息?isError 是多余的,因为你可以检查 error !== null。 正是因为在不破坏功能的情况下删除其中任何一个状态变量,才可以确定这些都是必要的。 步骤5:连接事件处理函数以设置state最后,创建事件处理函数去设置state变量。 选择状态结构良好的状态组织,可以区分易于修改和调试的组件和频繁出问题的组件。最重要的原则是,状态不应该包含冗余或重复的信息。 构建state的原则当编写一个存有state的组件时,需要我们去选择使用多少个state变量以及他们都是什么数据格式。以下是构建state的原则来指导哦们做出更好的决策: **合并关联的state**。如果存在同时更新俩个或者多个的state变量,就该考虑将他们合并为一个单独的state变量。 **避免互相矛盾的state**。当state结构存在多个互相矛盾或者“不一致”的state时,就应该避免这个情况。 **避免冗余的state**。如果在渲染期间从组件的props或现有的state变量中计算一些信息,那么这些信息不因该放在该组件的state中。 **避免重复的state**。当统一数据在多个state变量之间或在多个嵌套对象总重复时,会很难保持同步。因此应尽量减少重复。 **避免深层嵌套的state**。深度分层的state更新起来会不方便,最好时构建扁平化的state。 这些原则背后的目标是使state易于更新而不引入错误。从state中删除冗余和重复数据有助于所有部分保持同步。这类似于数据库工程师想要 “规范化”数据库结构,以减少出现错误的机会。用爱因斯坦的话说,“让你的状态尽可能简单,但不要过于简单。” 合并关联的state有时候我们可能不确定使用单个state变量还是多个state变量。 例如下面的存在x、y 是这样做? 12const [x, setX] = useState(0);const [y, setY] = useState(0); 还是这样做? 1const [position, setPosition] = useState({ x: 0, y: 0 }); 从技术实现上来说,可以使用任何一种方法。但是,如果俩个state变量总是一起变化,则将它们统一成state变量可能会更好。这样我们就不会忘记让它们始终保持同步。 另一种情况是,我们将数据整合到一个对象或一个数组中时,不知道需要多少个state片段。例如,用户可以自定义字段的表单,这样就很有帮助。 注意:如果 state 变量是一个对象时,请记住,不能只更新其中的一个字段 而不显式复制其他字段。例如,在上面的例子中,不能写成 setPosition({ x: 100 }),因为它根本就没有 y 属性! 相反,如果你想要仅设置 x,则可执行 setPosition({ ...position, x: 100 }),或将它们分成两个 state 变量,并执行 setX(100)。 避免矛盾的state看以下例子代码: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647import { useState } from 'react';export default function FeedbackForm() { const [text, setText] = useState(''); // 俩个状态去控制 很容易去忘记更新 const [isSending, setIsSending] = useState(false); const [isSent, setIsSent] = useState(false); async function handleSubmit(e) { e.preventDefault(); setIsSending(true); await sendMessage(text); setIsSending(false); setIsSent(true); } if (isSent) { return <h1>Thanks for feedback!</h1> } return ( <form onSubmit={handleSubmit}> <p>How was your stay at The Prancing Pony?</p> <textarea disabled={isSending} value={text} onChange={e => setText(e.target.value)} /> <br /> <button disabled={isSending} type=\"submit\" > Send </button> {isSending && <p>Sending...</p>} </form> );}// 假装发送一条消息。function sendMessage(text) { return new Promise(resolve => { setTimeout(resolve, 2000); });} 观察这段代码,其实是有效的,但是会存在一些state“极难处理”。如果忘记 setIsSent 和 setIsSending,则可能会出现 isSending 和 isSent 同时为 true 的情况。组件越复杂,就很难理解发生了什么。 因为 isSending 和 isSent 不应同时为 true,所以最好用一个 status 变量来代替它们,这个 state 变量可以采取三种有效状态其中之一:'typing' (初始), 'sending', 和 'sent': 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849import { useState } from 'react';export default function FeedbackForm() { const [text, setText] = useState(''); // 将state合并为三个状态 动态修改去做条件判断 const [status, setStatus] = useState('typing'); async function handleSubmit(e) { e.preventDefault(); setStatus('sending'); await sendMessage(text); setStatus('sent'); } // 它们不是 state 变量,所以不必担心它们彼此失去同步。 const isSending = status === 'sending'; const isSent = status === 'sent'; if (isSent) { return <h1>Thanks for feedback!</h1> } return ( <form onSubmit={handleSubmit}> <p>How was your stay at The Prancing Pony?</p> <textarea disabled={isSending} value={text} onChange={e => setText(e.target.value)} /> <br /> <button disabled={isSending} type=\"submit\" > Send </button> {isSending && <p>Sending...</p>} </form> );}// 假装发送一条消息。function sendMessage(text) { return new Promise(resolve => { setTimeout(resolve, 2000); });} 避免冗余的state如果我们在渲染期间从组件的props或现有的state变量中计算出一些信息,则不应该把这些信息放到该组件的state中。 仔细看看以下代码。他可以允许,仔细观察就能发现其中的冗余之处。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546import { useState } from 'react';export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); // 没错就是这里 这里fullName其实可以通过firstName 和 lastName计算得出 // const [fullName, setFullName] = useState(''); // 就可以替换成以下这段代码 // 这样就不需要每次在依赖的state中去重新修改状态 const fullName = firstName + ''+lastName function handleFirstNameChange(e) { setFirstName(e.target.value); // setFullName(e.target.value + ' ' + lastName); } function handleLastNameChange(e) { setLastName(e.target.value); //setFullName(firstName + ' ' + e.target.value); } return ( <> <h2>Let’s check you in</h2> <label> First name:{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Last name:{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Your ticket will be issued to: <b>{fullName}</b> </p> </> );} 为什么能用常量去做呢?这是因为事件处理程序不需要任何操作去更新它,当我们调用setFirstName 或 setLastName 时,你会触发一次重新渲染,然后下一个 fullName 将从新数据中计算出来。 深入探讨:不要在 state 中镜像 props看看以下代码: 12function Message({ messageColor }) { const [color, setColor] = useState(messageColor); 这里,一个 color state 变量被初始化为 messageColor 的 prop 值。这段代码的问题在于,如果父组件稍后传递不同的 messageColor 值(例如,将其从 'blue' 更改为 'red'),则 color state 变量将不会更新! state 仅在第一次渲染期间初始化。 这就是为什么在 state 变量中,“镜像”一些 prop 属性会导致混淆的原因。相反,你要在代码中直接使用 messageColor 属性。如果想给它起一个更短的名称,请使用常量: 123function Message({ messageColor }) { const color = messageColor; 这种写法就不会与从父组件传递的属性失去同步。 只有当你 想要 忽略特定 props 属性的所有更新时,将 props “镜像”到 state 才有意义。按照惯例,prop 名称以 initial 或 default 开头,以阐明该 prop 的新值将被忽略: 1234function Message({ initialColor }) { // 这个 `color` state 变量用于保存 `initialColor` 的 **初始值**。 // 对于 `initialColor` 属性的进一步更改将被忽略。 const [color, setColor] = useState(initialColor); 避免重复的state下面这是一个可以选择的菜单列表: 123456789101112131415161718192021222324252627282930313233import { useState } from 'react';const initialItems = [ { title: 'pretzels', id: 0 }, { title: 'crispy seaweed', id: 1 }, { title: 'granola bar', id: 2 },];export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedItem, setSelectedItem] = useState( items[0] ); return ( <> <h2>What's your travel snack?</h2> <ul> {items.map(item => ( <li key={item.id}> {item.title} {' '} <button onClick={() => { setSelectedItem(item); }}>Choose</button> </li> ))} </ul> <p>You picked {selectedItem.title}.</p> </> );} 这里的所选元素作为对象存储在selectedItem state变量中。然而,**selecteItem的内容与items列表中的某一项是同一个对象**,这意味着关于该项本身的信息在俩个地方产生了重复。 我们在将我们的组件进行修改,让每个项目都可以编辑,看看会出现什么问题? 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051import { useState } from 'react';const initialItems = [ { title: 'pretzels', id: 0 }, { title: 'crispy seaweed', id: 1 }, { title: 'granola bar', id: 2 },];export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedItem, setSelectedItem] = useState( items[0] ); function handleItemChange(id, e) { setItems(items.map(item => { if (item.id === id) { return { ...item, title: e.target.value, }; } else { return item; } })); } return ( <> <h2>What's your travel snack?</h2> <ul> {items.map((item, index) => ( <li key={item.id}> <input value={item.title} onChange={e => { handleItemChange(item.id, e) }} /> {' '} <button onClick={() => { setSelectedItem(item); }}>Choose</button> </li> ))} </ul> <p>You picked {selectedItem.title}.</p> </> );} 注意,当我们选择“choose”按钮时,然后编辑它,输入会更新,但是底部的标签不会反应编辑的内容。这是因为我们存在了重复的state,并且忘记去更新了selectedItem。 我们也可以选择去更新selectedItem,但更简单的办法是去消除重复项。在下面对代码中,我们就可以将selectedId保存在state中,而不是在selectedItem对象中(它创建了一个与items内重复的对象),然后通过items数组去过滤出具有该ID的项,以此来获取selectedItem: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253import { useState } from 'react';const initialItems = [ { title: 'pretzels', id: 0 }, { title: 'crispy seaweed', id: 1 }, { title: 'granola bar', id: 2 },];export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedId, setSelectedId] = useState(0); const selectedItem = items.find(item => item.id === selectedId ); function handleItemChange(id, e) { setItems(items.map(item => { if (item.id === id) { return { ...item, title: e.target.value, }; } else { return item; } })); } return ( <> <h2>What's your travel snack?</h2> <ul> {items.map((item, index) => ( <li key={item.id}> <input value={item.title} onChange={e => { handleItemChange(item.id, e) }} /> {' '} <button onClick={() => { setSelectedId(item.id); }}>Choose</button> </li> ))} </ul> <p>You picked {selectedItem.title}.</p> </> );} 在或者,我们可以将所选择的索引保存在state中。 state 过去常常是这样赋值的: items = [{ id: 0, title: 'pretzels'}, ...] selectedItem = {id: 0, title: 'pretzels'} 改了之后是这样的: items = [{ id: 0, title: 'pretzels'}, ...] selectedId = 0 这样,就不存在重复的state,只保留了必要的state。这样的话,当我们编辑selected元素,下面的标签就会立即更新。这是因为setItem会触发重新渲染,而item.find(...)会找到带有更新文本的元素,我们不需要在state中保存选定的元素,因为只有选定的ID是必要的。其余的都可以在渲染期间计算得出。 避免深度的嵌套state想象一下,一个由行星、大陆和国家组成的旅行计划。你可能会尝试使用嵌套对象和数组来构建它的 state,就像下面这个例子: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234// app.jsimport { useState } from 'react';import { initialTravelPlan } from './places.js';// 递归展示function PlaceTree({ place }) { const childPlaces = place.childPlaces; return ( <li> {place.title} {childPlaces.length > 0 && ( <ol> {childPlaces.map(place => ( <PlaceTree key={place.id} place={place} /> ))} </ol> )} </li> );}export default function TravelPlan() { const [plan, setPlan] = useState(initialTravelPlan); const planets = plan.childPlaces; return ( <> <h2>Places to visit</h2> <ol> {planets.map(place => ( <PlaceTree key={place.id} place={place} /> ))} </ol> </> );}// place.jsexport const initialTravelPlan = { id: 0, title: '(Root)', childPlaces: [{ id: 1, title: 'Earth', childPlaces: [{ id: 2, title: 'Africa', childPlaces: [{ id: 3, title: 'Botswana', childPlaces: [] }, { id: 4, title: 'Egypt', childPlaces: [] }, { id: 5, title: 'Kenya', childPlaces: [] }, { id: 6, title: 'Madagascar', childPlaces: [] }, { id: 7, title: 'Morocco', childPlaces: [] }, { id: 8, title: 'Nigeria', childPlaces: [] }, { id: 9, title: 'South Africa', childPlaces: [] }] }, { id: 10, title: 'Americas', childPlaces: [{ id: 11, title: 'Argentina', childPlaces: [] }, { id: 12, title: 'Brazil', childPlaces: [] }, { id: 13, title: 'Barbados', childPlaces: [] }, { id: 14, title: 'Canada', childPlaces: [] }, { id: 15, title: 'Jamaica', childPlaces: [] }, { id: 16, title: 'Mexico', childPlaces: [] }, { id: 17, title: 'Trinidad and Tobago', childPlaces: [] }, { id: 18, title: 'Venezuela', childPlaces: [] }] }, { id: 19, title: 'Asia', childPlaces: [{ id: 20, title: 'China', childPlaces: [] }, { id: 21, title: 'India', childPlaces: [] }, { id: 22, title: 'Singapore', childPlaces: [] }, { id: 23, title: 'South Korea', childPlaces: [] }, { id: 24, title: 'Thailand', childPlaces: [] }, { id: 25, title: 'Vietnam', childPlaces: [] }] }, { id: 26, title: 'Europe', childPlaces: [{ id: 27, title: 'Croatia', childPlaces: [], }, { id: 28, title: 'France', childPlaces: [], }, { id: 29, title: 'Germany', childPlaces: [], }, { id: 30, title: 'Italy', childPlaces: [], }, { id: 31, title: 'Portugal', childPlaces: [], }, { id: 32, title: 'Spain', childPlaces: [], }, { id: 33, title: 'Turkey', childPlaces: [], }] }, { id: 34, title: 'Oceania', childPlaces: [{ id: 35, title: 'Australia', childPlaces: [], }, { id: 36, title: 'Bora Bora (French Polynesia)', childPlaces: [], }, { id: 37, title: 'Easter Island (Chile)', childPlaces: [], }, { id: 38, title: 'Fiji', childPlaces: [], }, { id: 39, title: 'Hawaii (the USA)', childPlaces: [], }, { id: 40, title: 'New Zealand', childPlaces: [], }, { id: 41, title: 'Vanuatu', childPlaces: [], }] }] }, { id: 42, title: 'Moon', childPlaces: [{ id: 43, title: 'Rheita', childPlaces: [] }, { id: 44, title: 'Piccolomini', childPlaces: [] }, { id: 45, title: 'Tycho', childPlaces: [] }] }, { id: 46, title: 'Mars', childPlaces: [{ id: 47, title: 'Corn Town', childPlaces: [] }, { id: 48, title: 'Green Hill', childPlaces: [] }] }]}; 这时候如果我们想添加一个按钮来删除已经去过的地方。该如何做呢?更新嵌套的 state 需要从更改部分一直向上复制对象。删除一个深度嵌套的地点将涉及复制其整个父级地点链。这样的代码可能非常冗长。 如果 state 嵌套太深,难以轻松更新,可以考虑将其“扁平化”。 这里可以通过一个方法来重构上面这个数据:不同树状结构,每个节点的place都是一个包含其子节点的数组,我们可以让每个节点的place作为数组保存其子节点的ID。然后存储一个节点ID与相应节点的映射关系。 看看下面重组的数据: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294// app.jsimport { useState } from 'react';import { initialTravelPlan } from './places.js';// 先找到该节点的 孩子id 再去递归id去寻找function PlaceTree({ id, placesById }) { const place = placesById[id]; const childIds = place.childIds; return ( <li> {place.title} {childIds.length > 0 && ( <ol> {childIds.map(childId => ( <PlaceTree key={childId} id={childId} placesById={placesById} /> ))} </ol> )} </li> );}export default function TravelPlan() { const [plan, setPlan] = useState(initialTravelPlan); const root = plan[0]; const planetIds = root.childIds; return ( <> <h2>Places to visit</h2> <ol> {planetIds.map(id => ( <PlaceTree key={id} id={id} placesById={plan} /> ))} </ol> </> );}// places.jsexport const initialTravelPlan = { 0: { id: 0, title: '(Root)', childIds: [1, 42, 46], }, 1: { id: 1, title: 'Earth', childIds: [2, 10, 19, 26, 34] }, 2: { id: 2, title: 'Africa', childIds: [3, 4, 5, 6 , 7, 8, 9] }, 3: { id: 3, title: 'Botswana', childIds: [] }, 4: { id: 4, title: 'Egypt', childIds: [] }, 5: { id: 5, title: 'Kenya', childIds: [] }, 6: { id: 6, title: 'Madagascar', childIds: [] }, 7: { id: 7, title: 'Morocco', childIds: [] }, 8: { id: 8, title: 'Nigeria', childIds: [] }, 9: { id: 9, title: 'South Africa', childIds: [] }, 10: { id: 10, title: 'Americas', childIds: [11, 12, 13, 14, 15, 16, 17, 18], }, 11: { id: 11, title: 'Argentina', childIds: [] }, 12: { id: 12, title: 'Brazil', childIds: [] }, 13: { id: 13, title: 'Barbados', childIds: [] }, 14: { id: 14, title: 'Canada', childIds: [] }, 15: { id: 15, title: 'Jamaica', childIds: [] }, 16: { id: 16, title: 'Mexico', childIds: [] }, 17: { id: 17, title: 'Trinidad and Tobago', childIds: [] }, 18: { id: 18, title: 'Venezuela', childIds: [] }, 19: { id: 19, title: 'Asia', childIds: [20, 21, 22, 23, 24, 25], }, 20: { id: 20, title: 'China', childIds: [] }, 21: { id: 21, title: 'India', childIds: [] }, 22: { id: 22, title: 'Singapore', childIds: [] }, 23: { id: 23, title: 'South Korea', childIds: [] }, 24: { id: 24, title: 'Thailand', childIds: [] }, 25: { id: 25, title: 'Vietnam', childIds: [] }, 26: { id: 26, title: 'Europe', childIds: [27, 28, 29, 30, 31, 32, 33], }, 27: { id: 27, title: 'Croatia', childIds: [] }, 28: { id: 28, title: 'France', childIds: [] }, 29: { id: 29, title: 'Germany', childIds: [] }, 30: { id: 30, title: 'Italy', childIds: [] }, 31: { id: 31, title: 'Portugal', childIds: [] }, 32: { id: 32, title: 'Spain', childIds: [] }, 33: { id: 33, title: 'Turkey', childIds: [] }, 34: { id: 34, title: 'Oceania', childIds: [35, 36, 37, 38, 39, 40, 41], }, 35: { id: 35, title: 'Australia', childIds: [] }, 36: { id: 36, title: 'Bora Bora (French Polynesia)', childIds: [] }, 37: { id: 37, title: 'Easter Island (Chile)', childIds: [] }, 38: { id: 38, title: 'Fiji', childIds: [] }, 39: { id: 40, title: 'Hawaii (the USA)', childIds: [] }, 40: { id: 40, title: 'New Zealand', childIds: [] }, 41: { id: 41, title: 'Vanuatu', childIds: [] }, 42: { id: 42, title: 'Moon', childIds: [43, 44, 45] }, 43: { id: 43, title: 'Rheita', childIds: [] }, 44: { id: 44, title: 'Piccolomini', childIds: [] }, 45: { id: 45, title: 'Tycho', childIds: [] }, 46: { id: 46, title: 'Mars', childIds: [47, 48] }, 47: { id: 47, title: 'Corn Town', childIds: [] }, 48: { id: 48, title: 'Green Hill', childIds: [] }}; 此刻,state 已经“扁平化”(也称为“规范化”),更新嵌套项会变得更加容易。 现在删除一个地点的话,只需要更新两个 state 级别: 其 父级 地点的更新版本应该从其 childIds 数组中排除已删除的 ID。 其根级“表”对象的更新版本应包括父级地点的更新版本。 看以下代码处理的实例: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320// app.jsimport { useState } from 'react';import { initialTravelPlan } from './places.js';export default function TravelPlan() { const [plan, setPlan] = useState(initialTravelPlan); function handleComplete(parentId, childId) { const parent = plan[parentId]; // 创建一个其父级地点的新版本 // 但不包括子级 ID。 const nextParent = { ...parent, childIds: parent.childIds .filter(id => id !== childId) }; // 更新根 state 对象... setPlan({ ...plan, // ...以便它拥有更新的父级。 [parentId]: nextParent }); } const root = plan[0]; const planetIds = root.childIds; return ( <> <h2>Places to visit</h2> <ol> {planetIds.map(id => ( <PlaceTree key={id} id={id} parentId={0} placesById={plan} onComplete={handleComplete} /> ))} </ol> </> );}function PlaceTree({ id, parentId, placesById, onComplete }) { const place = placesById[id]; const childIds = place.childIds; return ( <li> {place.title} <button onClick={() => { onComplete(parentId, id); }}> Complete </button> {childIds.length > 0 && <ol> {childIds.map(childId => ( <PlaceTree key={childId} id={childId} parentId={id} placesById={placesById} onComplete={onComplete} /> ))} </ol> } </li> );}// place.jsexport const initialTravelPlan = { 0: { id: 0, title: '(Root)', childIds: [1, 42, 46], }, 1: { id: 1, title: 'Earth', childIds: [2, 10, 19, 26, 34] }, 2: { id: 2, title: 'Africa', childIds: [3, 4, 5, 6 , 7, 8, 9] }, 3: { id: 3, title: 'Botswana', childIds: [] }, 4: { id: 4, title: 'Egypt', childIds: [] }, 5: { id: 5, title: 'Kenya', childIds: [] }, 6: { id: 6, title: 'Madagascar', childIds: [] }, 7: { id: 7, title: 'Morocco', childIds: [] }, 8: { id: 8, title: 'Nigeria', childIds: [] }, 9: { id: 9, title: 'South Africa', childIds: [] }, 10: { id: 10, title: 'Americas', childIds: [11, 12, 13, 14, 15, 16, 17, 18], }, 11: { id: 11, title: 'Argentina', childIds: [] }, 12: { id: 12, title: 'Brazil', childIds: [] }, 13: { id: 13, title: 'Barbados', childIds: [] }, 14: { id: 14, title: 'Canada', childIds: [] }, 15: { id: 15, title: 'Jamaica', childIds: [] }, 16: { id: 16, title: 'Mexico', childIds: [] }, 17: { id: 17, title: 'Trinidad and Tobago', childIds: [] }, 18: { id: 18, title: 'Venezuela', childIds: [] }, 19: { id: 19, title: 'Asia', childIds: [20, 21, 22, 23, 24, 25], }, 20: { id: 20, title: 'China', childIds: [] }, 21: { id: 21, title: 'India', childIds: [] }, 22: { id: 22, title: 'Singapore', childIds: [] }, 23: { id: 23, title: 'South Korea', childIds: [] }, 24: { id: 24, title: 'Thailand', childIds: [] }, 25: { id: 25, title: 'Vietnam', childIds: [] }, 26: { id: 26, title: 'Europe', childIds: [27, 28, 29, 30, 31, 32, 33], }, 27: { id: 27, title: 'Croatia', childIds: [] }, 28: { id: 28, title: 'France', childIds: [] }, 29: { id: 29, title: 'Germany', childIds: [] }, 30: { id: 30, title: 'Italy', childIds: [] }, 31: { id: 31, title: 'Portugal', childIds: [] }, 32: { id: 32, title: 'Spain', childIds: [] }, 33: { id: 33, title: 'Turkey', childIds: [] }, 34: { id: 34, title: 'Oceania', childIds: [35, 36, 37, 38, 39, 40, 41], }, 35: { id: 35, title: 'Australia', childIds: [] }, 36: { id: 36, title: 'Bora Bora (French Polynesia)', childIds: [] }, 37: { id: 37, title: 'Easter Island (Chile)', childIds: [] }, 38: { id: 38, title: 'Fiji', childIds: [] }, 39: { id: 39, title: 'Hawaii (the USA)', childIds: [] }, 40: { id: 40, title: 'New Zealand', childIds: [] }, 41: { id: 41, title: 'Vanuatu', childIds: [] }, 42: { id: 42, title: 'Moon', childIds: [43, 44, 45] }, 43: { id: 43, title: 'Rheita', childIds: [] }, 44: { id: 44, title: 'Piccolomini', childIds: [] }, 45: { id: 45, title: 'Tycho', childIds: [] }, 46: { id: 46, title: 'Mars', childIds: [47, 48] }, 47: { id: 47, title: 'Corn Town', childIds: [] }, 48: { id: 48, title: 'Green Hill', childIds: [] }}; 这样就可以随心所欲嵌套state,“扁平化”可以解决很多问题。这使得state更容易更新,确保嵌套对象的不同部分中没有重复。 在组件间共享状态如果我们需要俩个组件的状态始终同步修改。我们可以将相关状态从这俩个组件上移除,将这些状态移到最近的父级组件,然后通过props将这些状态传递给这俩个组件。这被称为“状态提升”。 对state进行保留和重置状态当我们重新渲染一个组件时,React需要决定组件树哪些部分要保留和更新,以及丢弃或重新创建。在大多数情况下,React的自动处理机制已经做了大部分工作。默认情况下,React会保留树中与先前渲染组件树“匹配”的部分。 React允许覆盖默认行为,这时候可以通过向组件传递一个唯一key来强制重置其状态。这将告诉React,组件需要重新渲染。 提取状态到reducer中对于需要更新多个状态的组件来说,会存在过于分散的事件处理程序。对于这种情况,我们可以在组件外部将所有的状态更新逻辑合并到一个称为“reducer”的函数中。这样,事件处理程序就会变的简洁,只需要我们指定对应的“action”。同时定义,reducer函数指定状态因该如何更新去响应每个action! 使用Context进行深层数据传递通常,我们会存在需要通过props将信息从父组件传递给子组件。如果需要在组件树中深入传递prop,或者树中许多组件都需要使用相同的prop,那么传递prop可能会变得麻烦。Context允许父组件将一些信息提供给它下层的任何组件,不管组件多深层也无需通过props逐层透传。 使用Reducer和Context进行状态扩展Reducer可以帮助我们合并组件的状态更新逻辑。Context可以帮助我们将信息深处传递给其他组件。可以将二者结合使用,以管理复杂应用的状态。","tags":[{"name":"react","slug":"react","permalink":"https://ayozoo.github.io/tags/react/"}]},{"title":"React之Hook篇","date":"2023-11-08T15:21:48.000Z","path":"2023/11/08/ React之hook篇/","text":"React之Hook篇useState什么是state?在react中,数据不称为data,而称为state data = > state(状态) React,其实是一个view library (view库—只关注视图) view => update => 视图的具体状态 state <=> view state和视图是相关联的。 视图是某一个状态发生了变化,所以视图要进行相应的更新。 useState()=> state setState 这个可以解释为,视图需要state状态。通过useState()创建了一个状态和设置状态的方法。 react设计理念 react设计理念:一切操作函数化。 在react中,贯彻js—–“函数是一等公民”的理念。 react提供的东西都是朴素的,简单的。 react大部分都是运行时;vue都是编译时的行为。 使用姿势根据状态变更,有以下俩种: setXxx(xxx) 简单情况 setXxx((x)=>{ return 表达式}) 复杂情况 常用写法一:setXxx(xxx) 1234567891011121314151617181920212223242526272829303132import { useState } from \"react\";let initialState = 0;export default function useStateHook() { const [count, setCount] = useState(initialState); return ( <div> <h1>{count}</h1> <button onClick={() => setCount(count + 1)}>+ </button> </div> );}// 变式import { useState } from \"react\";let initialState = 0;export default function useStateHook() { const [count, setCount] = useState(initialState); function handleClick() { return setCount(count + 1); } return ( <div> <h1>{count}</h1> <button onClick={handleClick}>+</button> </div> );} setXxx((x)=>{ return 表达式}) 12345678910111213141516171819import { useState } from \"react\";let initialState = 1;export default function useStateHook() { const [count, setCount] = useState(initialState); function handleClick() { return setCount((count) => { return count * 2; }); } return ( <div> <h1>{count}</h1> <button onClick={handleClick}>X</button> </div> );} useReducer这个hook存在的价值当存在逻辑分支如:+ — * / => 都是为了计算count,也就是count=>多种操作方案,每一种方案可能有很多地方都需要使用。 reducer是一个非常好去解决,集成、对状态修改的方案集合的一种方法。 useReducer是什么useReducer含义、包含的概念含义useReducer => 会收集所有操作某一个数据的方案。 1const [ count , dispatch ] = useReducer(reducer,0) 包含的概念dispathdispatch派发器 => 传入的不同操作类型 => 调用不同的逻辑 dispatch({ type, payload }) 以下为拿计算器+ - * /为例: 12345678910111213141516171819202122/** * * @param {*} count --- 初始值 * @param {*} action --- { type, payload } * * type --- 动作类型 * * payload --- 动作携带的数据 * @returns */function countReducer(count, { type, payload }) { switch (type) { case \"PLUS\": return count + payload; case \"MINUS\": return count - payload; case \"TIMES\": return count * payload; case \"DIVIDE\": return count / payload; default: return count; }} useReducer用法useReducer(reducer,initialState)接受俩个参数,第一个是reducer的函数,第二个是initialState初始值。 1const [ count , dispatch ] = useReducer(reducer,0) 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162// usReducerexport default function Hook() { /** * * @param {*} count --- 初始值 * @param {*} action --- { type, payload } * * type --- 动作类型 * * payload --- 动作携带的数据 * @returns */ function countReducer(count, { type, payload }) { switch (type) { case \"PLUS\": return count + payload; case \"MINUS\": return count - payload; case \"TIMES\": return count * payload; case \"DIVIDE\": return count / payload; default: return count; } } /** * * count --- 状态 * * dispatch --- 触发动作 --dispatch({ type,payload}); * * type --- 动作类型 * * payload --- 动作携带的数据 * @returns */ const [count, dispatch] = useReducer(countReducer, 0); /** * dispatch 触发动作 * dispatch({ type: \"PLUS\", payload: 1 }); */ function plus() { dispatch({ type: \"PLUS\", payload: 1 }); } function minus() { dispatch({ type: \"MINUS\", payload: 1 }); } function times() { dispatch({ type: \"TIMES\", payload: 2 }); } function divide() { dispatch({ type: \"DIVIDE\", payload: 2 }); } return ( <div> <h1>Count: {count}</h1> <button onClick={plus}>+</button> <button onClick={minus}>-</button> <button onClick={times}>x</button> <button onClick={divide}>÷</button> </div> );} useEffect含义effect—副作用 => 处理视图状态不相关的逻辑 例如: 记时器; console.log(); 数据获取; 修改、操作DOM; 在React中所有副作用都必须在useEffect()中执行。 在组件挂载的时候存在一系列生命周期函数。在函数式组件中使用useEffect()代替类组件中生命周期的函数(简化)。 作用 手动收集依赖; 处理渲染副作用; 使用一个返回值(返回值)去处理副作用清理 代替生命周期函数; 需要根据传递依赖来去代替哪一个生命周期函数 使用回调函数 + 参数:useEffect(callback,depArr) 第二个参数为:depArr => the Array of dependencies 1234567useEffect(() => { // []:`depArr` => ` the Array of dependencies` // 清除函数 return(()=>{})},[]) 回调函数callback中的逻辑是否执行,是依赖depArr数组里面状态是否改变。因此depArr一定要在外界保存。 如果第二个参数为undefined => 任何状态改变时,都会重新执行。=> 组件更新的生命周期。 以下代码:每当按钮点击时,count变更时,useEffect()都会执行。 12345678910111213export default function useEffectHook() { const [count, setCount] = useState(0); useEffect(() => { console.log(\"useEffect\"); }); return ( <> <div>{count}</div> <button onClick={() => setCount((count) => count + 1)}>+</button> </> );} 如果第二个参数不是一个数组则 => 报警告。 当按钮点击时,会报以下warning: 1234567891011121314151617// react-dom.development.js:86 Warning: useEffect received a final argument that is not an array (instead, received `object`). When specified, the final argument must be an array.// Warning: useEffect received a final argument that is not an array (instead, received `object`). When specified, the final argument must be an array.export default function useEffectHook() { const [count, setCount] = useState(0); useEffect(() => { console.log(\"useEffect\"); }, {}); return ( <> <div>{count}</div> <button onClick={() => setCount((count) => count + 1)}>+</button> </> );} 如果第二个参数是一个数组,且是一个空数组 => 回调只会在函数组件调用时执行一次。(是在根组件执行的时候在执行) => componentDidMount 以下执行,只会执行一次。打印一次。 12345678910111213export default function useEffectHook() { const [count, setCount] = useState(0); useEffect(() => { console.log(\"useEffect\"); }, []); return ( <> <div>{count}</div> <button onClick={() => setCount((count) => count + 1)}>+</button> </> );} 如果第二个参数是一个有元素的数组 => 元素为状态的话,状态更新,回调重新执行一次。=> componentDidUpdate 以下代码会先执行一次,当按钮点击时,count更新的同时,也会执行打印。 1234567891011121314export default function useEffectHook() { const [count, setCount] = useState(0); useEffect(() => { console.log(\"useEffect\"); }, [count]); return ( <> <div>{count}</div> <button onClick={() => setCount((count) => count + 1)}>+</button> </> );} 清楚副作用1234useEffect(() => { // 当页面卸载时:compoentWillUnmount return(()=>{})},[]) 例子:清楚记时器 12345678910111213141516171819export default function useEffectHook() { const [count, setCount] = useState(0); useEffect(() => { let t = setInterval(() => { setCount((count) => count + 1); }, 1000); return () => { clearInterval(t); t = null; }; }, []); return ( <> <div>{count}</div> </> );} 对比Vue中watchEffect 不同 Vue watchEffect React useEffect 依赖收集 watchEffect自动收集,直接执行我们的回调 需要开发者手动收集 参数 没有第二个参数,第二个参数是watchEffect自动收集、提供的 有第二个参数,需手动追踪依赖 清楚副作用 回调不返回任何,清楚通过提供的宏(函数执行)onCleanup(xxx) 通过回调 return () => { };去清楚副作用 设计理念 观察副作用 更多去代替生命周期函数 相同点:都是观察我们的副作用,执行我们的回调","tags":[{"name":"react","slug":"react","permalink":"https://ayozoo.github.io/tags/react/"}]},{"title":"React入门之添加交互","date":"2023-11-02T15:21:22.000Z","path":"2023/11/02/ React-添加交互/","text":"React入门之添加交互界面上的控件对随着用户的输入而更新。例如点击按钮切换轮播图的展示。在React中,随着时间变化的数据称为状态(state)。可以向任何组件添加状态,按需去进行更新。 响应事件什么是响应事件React允许我们在JSX中添加时间处理程序。事件处理程序是我们自己定义的函数。比如我们界面交互时:点击、悬停、焦点聚焦等交互事件。 我们在自己的组件中可以定义我们自己的事件处理程序。做法是往我们的组件时间处理程序props指定特定应用的名称。 1234567891011121314151617181920212223242526272829303132333435// 定义button组件 通过props接收onclick事件// {children} 作为插槽接收function Button({ onClick, children }) { return ( <button onClick={onClick}> {children} </button> );}// 定义Toolbar 通过props接收onPlayMovie、onUploadImage事件// 这里的\"Play Movie、 Upload Image\" 作为子组件插入function Toolbar({ onPlayMovie, onUploadImage }) { return ( <div> <Button onClick={onPlayMovie}> Play Movie </Button> <Button onClick={onUploadImage}> Upload Image </Button> </div> );}// 导出最终组件// 这里的onPlayMovie、onUploadImage就是对应Toolbar组件接收的事件处理程序、最终传入Button的onClick事件中export default function App() { return ( <Toolbar onPlayMovie={() => alert('Playing!')} onUploadImage={() => alert('Uploading!')} /> );} 事件处理函数添加事件处理函数步骤: 首先定义一个函数组件 在函数组件中定义事件处理程序的函数,然后将其作为prop传入合适的JSX标签。 事件函数特点: 通常是在组件内部定义 名称handle开头,后面跟事件名称 事件处理函数可以在JSX中有俩种定义方式: 内联事件处理函数(函数体比较短使用较为方便) 简洁函数 这时候看个例子: 1234567891011121314151617181920212223export default function Button() { function handleClick() { alert('你点击了我!'); } return ( <button onClick={handleClick}> 点我 </button> );}// 简洁箭头函数 <button onClick={() => { alert('你点击了我!');}}> // 内联写法<button onClick={function handleClick() { alert('你点击了我!');}}> 事件处理函数传递的陷阱在Vue中会出现事件绑定时,直接触发事件处理程序。在这里存在陷阱。 传递给事件处理函数的函数应直接传递,而非调用。看下面的例子: 传递一个函数(正确) 调用一个函数(错误) 这样看来其实,区别很微妙。 左侧示例中handleClick函数作为onClick的事件处理函数传递。这个是告诉React这个事件是当用户点击按钮时才会触发函数。 右侧示例中handleClick()中最后的()会在渲染过程中立即触发函数,即使没有任何点击。这是因为JSX{和}之间的Javascript会立即执行。 当传入内联函数时,会出现不同的陷阱。 传递一个函数(正确) 调用一个函数(错误) <button onClick={() => alert(‘…’)}> <button onClick={alert(‘…’)}> 右侧的写法,将会导致组件渲染时,每次都触发。 左侧就是创建了一个稍后调用的函数,而不是在每次渲染时执行其内部的代码。 综上,就是想要定义内联函数事件处理函数,要将其包装在匿名函数中。 在事件处理函数中读取props事件函数声明于组件内部,因此他们可以直接访问组件的props。 例如:事件处理函数就可以接收到message 123456789101112131415161718192021function AlertButton({ message, children }) { return ( <button onClick={() => alert(message)}> {children} </button> );}export default function Toolbar() { return ( <div> <AlertButton message=\"正在播放!\"> 播放电影 </AlertButton> <AlertButton message=\"正在上传!\"> 上传图片 </AlertButton> </div> );} 将事件处理函数作为props传递通常,我们会在父组件中定义子组件的事件处理函数。为此将组件从父组件接收的prop作为事件处理函数传递。 1234567891011121314151617181920212223242526272829303132333435363738394041// 定义一个接收事件处理函数的子组件 function Button({ onClick, children }) { return ( <button onClick={onClick}> {children} </button> );}// 这里作为父组件// 定义一个内部事件处理函数// 作为props传递给button组件function PlayButton({ movieName }) { function handlePlayClick() { alert(`正在播放 ${movieName}!`); } return ( <Button onClick={handlePlayClick}> 播放 \"{movieName}\" </Button> );}function UploadButton() { return ( <Button onClick={() => alert('正在上传!')}> 上传图片 </Button> );}export default function Toolbar() { return ( <div> <PlayButton movieName=\"魔女宅急便\" /> <UploadButton /> </div> );} 命名事件处理函数prop对于浏览器内置组件(<button> 和 <div>),仅支持浏览器事件名称,例如,onclick。但是当我们构建自己的组件时,可以任意命名事件处理函数的prop。 当组件支持多种交互时,可以根据不同的应用程序命名事件处理函数props。 1234567891011121314151617181920212223// 这里onClick接收的还是浏览器内置的<button>(小写)// 仍然需要使用 onClick prop,而自定义的 Button 组件接收到的 prop 名称还是可以定义。function Button({ onSmash, children }) { return ( <button onClick={onSmash}> {children} </button> );}export default function App() { return ( <div> <Button onSmash={() => alert('正在播放!')}> 播放电影 </Button> <Button onSmash={() => alert('正在上传!')}> 上传图片 </Button> </div> );} 事件传播事件处理函数还将捕获来自任何子组件的事件。通常,我们会说事件沿着树向上“冒泡”或者“传播”:他从事件发生的地方开始,然后沿着树向上传播。 在React中所有的事件都会传播,除了onScroll,它仅适用于附加到的JSX标签中。 例如: 1234567891011121314151617// 当你点击button时,先触发他自身的onClick// 在执行父级div的onClick// 但是如果只点击了父级的那么只会触发父级本身的onClickexport default function Toolbar() { return ( <div className=\"Toolbar\" onClick={() => { alert('你点击了 toolbar !'); }}> <button onClick={() => alert('正在播放!')}> 播放电影 </button> <button onClick={() => alert('正在上传!')}> 上传图片 </button> </div> );} 阻止传播事件处理函数接收一个**事件对象作为唯一的参数。一般通常被称为e,代表`event(事件)。这个可以使用此对象读取事件的有关信息。** 这个事件对象还允许阻止传播。例如: 当你点击按钮时: React 调用了传递给 <button> 的 onClick 处理函数。 定义在中的处理函数执行了如下操作: 调用 e.stopPropagation(),阻止事件进一步冒泡。 调用 onClick 函数,它是从 Toolbar 组件传递过来的 prop。 在 Toolbar 组件中定义的函数,显示按钮对应的 alert。 由于传播被阻止,父级 <div> 的 onClick 处理函数不会执行。 由于调用了 e.stopPropagation(),点击按钮现在将只显示一个 alert(来自 <button>),而并非两个(分别来自 <button> 和父级 toolbar <div>)。点击按钮与点击周围的 toolbar 不同,因此阻止传播对这个 UI 是有意义的。 12345678910111213141516171819202122232425function Button({ onClick, children }) { return ( <button onClick={e => { e.stopPropagation(); onClick(); }}> {children} </button> );}export default function Toolbar() { return ( <div className=\"Toolbar\" onClick={() => { alert('你点击了 toolbar !'); }}> <Button onClick={() => alert('正在播放!')}> 播放电影 </Button> <Button onClick={() => alert('正在上传!')}> 上传图片 </Button> </div> );} 拓展: 少数情况下,你可能需要捕获子元素上的所有事件,即便它们阻止了传播。例如,你可能想对每次点击进行埋点记录,传播逻辑暂且不论。那么你可以通过在事件名称末尾添加 Capture 来实现这一点: onClickCapture捕获所有事件1234<div onClickCapture={() => { /* 这会首先执行 */ }}> <button onClick={e => e.stopPropagation()} /> <button onClick={e => e.stopPropagation()} /></div> 每个事件分三个阶段传播: 它向下传播,调用所有的 onClickCapture 处理函数。 它执行被点击元素的 onClick 处理函数。 它向上传播,调用所有的 onClick 处理函数。 捕获事件对于路由或数据分析之类的代码很有用,但你可能不会在应用程序代码中使用它们。 传递处理函数作为事件传播的代替方案看这一段代码 12345678910function Button({ onClick, children }) { return ( <button onClick={e => { e.stopPropagation(); onClick(); }}> {children} </button> );} 此处的点击事件处理函数先执行了一段代码,然后调用了父组件传递的 onClick prop。 也可以在调用父元素onClick函数之前,添加其他代码。此模式是事件传播的另一种 替代方案 。它让子组件处理事件,同时也让父组件指定一些额外的行为。与事件传播不同,它并非自动。但使用这种模式的好处是你可以清楚地追踪因某个事件的触发而执行的整条代码链。 如果你依赖于事件传播,而且很难追踪哪些处理程序在执行,及其执行的原因,可以尝试这种方法。 阻止默认行为某些浏览器事件具有与事件相关联的默认行为。例如,点击 <form> 表单内部的按钮会触发表单提交事件,默认情况下将重新加载整个页面。 可以调用事件对象中的 e.preventDefault() 来阻止这种情况发生: 123456789101112export default function Signup() { return ( <form onSubmit={e => { e.preventDefault(); alert('提交表单!'); }}> <input /> <button>发送</button> </form> );} 不要混淆 e.stopPropagation() 和 e.preventDefault()。它们都很有用,但二者并不相关: e.stopPropagation() 阻止触发绑定在外层标签上的事件处理函数。 e.preventDefault() 阻止少数事件的默认浏览器行为。 事件函数可以包含副作用吗当然可以!事件处理函数是执行副作用的最佳位置。 与渲染函数不同,事件处理函数不需要是 纯函数,因此它是用来 更改 某些值的绝佳位置。例如,更改输入框的值以响应键入,或者更改列表以响应按钮的触发。但是,为了更改某些信息,你首先需要某种方式存储它。在 React 中,这是通过 state(组件的记忆) 来完成的。 state:组件的记忆组件通常需要根据交互更改屏幕上显示的内容。在我们表单输入的时候应该更新字段、单机轮播图上的点击下一个应该更改的图片。组件需要“记住”这些东西:当前输入值、当前轮播图。在React中,这种组件持有的记忆被称为state。 当使用普通变量时,事件处理函数会更新局部的变量,但是没有达到预期的效果。 原因有二: 局部变量无法在多次渲染中持久化保存。当React在此渲染这个组件时,他会使用事件处理函数中最初的值重新开始渲染,他不会考虑之前局部变量的任何修改。 更新局部变量不会触发渲染。React没有意识它需要去使用新数据渲染数组。 如何使用新数据更新组件?需要做俩件事: 保留渲染之间的数据 触发React使用新数据渲染组件(重新渲染) 这时候就引出了,主人公:useStateHook。useStateHook提供了俩个功能: State变量用于保存渲染间的数据。 State setter函数更新变量并触发React再次渲染组件。 使用姿势(如何添加一个state变量) 现在顶部文件React导入useState 1import { useState } from 'react'; 定义state变量 1const [index, setIndex] = useState(0); 其中index为State变量,setIndex是对应的setter函数。 这里的 [ 和 ] 语法称为数组解构,它允许你从数组中读取值。 useState 返回的数组总是正好有两项。 在React中,useState以及其他以use开头的函数都被称为Hook Hook是特殊的函数,只在React渲染是有效。 注意: Hooks,以use开头的函数,只能在组件或者**自定义 Hook** 的最顶层调用。**不能在条件语句、循环语句或其他嵌套函数内调用 Hook。**Hook是函数。 深度剖析useState当调用useState时,是在告诉React你想让组件记住一些东西。 1const [index, setIndex] = useState(0); 在这段代码中,我们希望React记住index。 useState的唯一参数是state变量的初始值。这个例子中index初始值被useState(0)设置为0。 每当我们组件渲染时,useState都会返回一个包含俩个值的数组: state变量(index)会保存上次渲染的值。 state setter函数(setIndex)可以更新state变量并触发React重新渲染组件。 以下是具体的执行顺序: 组件进行第一次渲染。会将index初始值0传递给useState,他就会返回[0,setIndex]。这时候React会记住0是最新的值。 更新了state。当用户点击按钮的时候,他会调用setIndex(index+1)。index初始值0,所以就会变成setIndex(1)。这将告诉React记住index是1触发下一次渲染。 组件进行二次渲染。React仍然看到useState(0),但是这时候React记住了index设置为1,他将返回[1,setIndex]。 后续渲染过程如此反复。 我们可以为一个组件赋予多个state变量。并且这些组件可以拥有任意的多种类型的state变量。 123456789101112131415161718192021222324252627282930313233343536373839import { useState } from 'react';import { sculptureList } from './data.js';export default function Gallery() { const [index, setIndex] = useState(0); const [showMore, setShowMore] = useState(false); function handleNextClick() { setIndex(index + 1); } function handleMoreClick() { setShowMore(!showMore); } let sculpture = sculptureList[index]; return ( <> <button onClick={handleNextClick}> Next </button> <h2> <i>{sculpture.name} </i> by {sculpture.artist} </h2> <h3> ({index + 1} of {sculptureList.length}) </h3> <button onClick={handleMoreClick}> {showMore ? 'Hide' : 'Show'} details </button> {showMore && <p>{sculpture.description}</p>} <img src={sculpture.url} alt={sculpture.alt} /> </> );} 如果它们不相关,那么存在多个 state 变量是一个好主意,例如本例中的 index 和 showMore。但是,如果你发现经常同时更改两个 state 变量,那么最好将它们合并为一个。例如,如果你有一个包含多个字段的表单,那么有一个值为对象的 state 变量比每个字段对应一个 state 变量更方便。 选择 state 结构在这方面有更多提示。 拓展:为什么React如何知道要返回哪个state?? React Hooks: not magic, just arrays State是隔离且私有的State是屏幕上组件实例内部的状态。也就是说每次渲染都会产生完全隔离的state副本!一个改变会会影响另一个。 这是因为state与生命在模块顶部的普通变量不同的原因。State不依赖于特定的函数调用或在代码中的位置。他的作用域“只限于”屏幕上模块特定的区域。重复渲染组件,他们的state是分开存储的。 state完全私有于声明他的组件。父组件无法更改它。 State 变量仅用于在组件重渲染时保存信息。在单个事件处理函数中,普通变量就足够了。当普通变量运行良好时,不要引入 state 变量。 渲染和提交设想我们是一个厨师,把食材做成美味的菜肴。在这个场景下,React就是一个服务员。这种请求和提供UI的过程分为三步: 触发一次渲染(把客人的点单派发厨房) 渲染组件(厨房准备订单) 提交到DOM(将菜品放到桌子上) 步骤1:触发一次渲染触发渲染的原因有二: 组件的初次渲染。 组件(或者其祖先之一)的状态发生了改变 初次渲染当应用启动的时候,会触发初次渲染,框架和沙箱有时候会隐藏这段代码,但是它通过调用目标DOM节点的 createRoot,然后组件调用render函数完成的。 12345import Image from './Image.js';import { createRoot } from 'react-dom/client';const root = createRoot(document.getElementById('root'))root.render(<Image />); 状态更新时重现渲染一旦组件被初次渲染,我们就可以通过set 函数更新其状态来触发之后的渲染。更新组件的状态会自动将一次渲染送入队列。 步骤2:React渲染你的组件在触发初次渲染之后,React会调用组件的来确定屏幕上渲染显示的内容。“渲染中”即React在调用你的组件。 在初次渲染时,React会调用根组件。 对于后续的渲染,React会调用内部状态更新触发了渲染的函数组件。 这个过程是递归的:如果更新的组件会返回某个另外组件,那么React接下来就会渲染那个组件,如果哪个组件又返回了某个组件,那么React接下来会渲染那个组件。以此类推,这个过程会持续下去,知道没有更多的嵌套组件并且React确定知道哪些东西应该显示到屏幕上为止。 我们看个例子,React将会调用Gallery()和Image()若干次。 12345678910111213141516171819202122232425262728// Gallery组件export default function Gallery() { return ( <section> <h1>鼓舞人心的雕塑</h1> <Image /> <Image /> <Image /> </section> );}function Image() { return ( <img src=\"https://i.imgur.com/ZF6s192.jpg\" alt=\"'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals\" /> );}// appimport Gallery from './Gallery.js';import { createRoot } from 'react-dom/client';const root = createRoot(document.getElementById('root'))root.render(<Gallery />); 初次渲染中,React将会为<section>、<h1> 和三个 <img> 标签创建 DOM 节点 在一次重渲染过程中,React将计算它们的那些属性(如果有的话)自上次渲染以来已经更改。再下一步(提交阶段)之前,他不会对这些信息执行任何操作。 注意: 渲染必须是一次 纯计算: 输入相同,输出相同。给定相同的输入,组件应该始终返回相同的JSX。就好比,食客点了西红柿沙拉,不应该收到洋葱沙拉! 只做它们自己的事情。他不应该更改更改任何存在于渲染之前的对象或者变量。就好比一个订单不应该更改其他任何人的订单。 否则,随着代码库复杂性的增加,可能会遇到令人困惑的错误和不可预测的行为。在“严格模式‘下开发时,React会调用每个组件函数俩次,这可以检测不纯函数引起的错误。 性能优化如果更新的组件在树中的位置非常高,渲染更新后的组件内部所有嵌套组件的默认行为将不会获得最佳性能。如果你遇到了性能问题,性能 章节描述了几种可选的解决方案 。不要过早进行优化! 步骤3:React把更改提交到DOM上在渲染(调用)你的组件之后,React将会修改DOM 对于初次渲染,React会使用appendChild()DOM API将其创建所有的DOM节点放在屏幕上。 对于重渲染,React将应用最少的必要操作(在渲染时计算!),以使得DOM与最新的渲染输出互相匹配。 React仅在渲染之间存在差异时才会更新DOM节点。 有一个组件,它每秒使用从父组件传递下来的不同属性重新渲染一次。注意,你可以添加一些文本到 <input> 标签,更新它的 value,但是文本不会在组件重渲染时消失: 123456789export default function Clock({ time }) { return ( <> <h1>{time}</h1> <input /> </> );} 这个例子之所以会正常运行,是因为在最后一步中,React 只会使用最新的 time 更新 <h1> 标签的内容。它看到 <input> 标签出现在 JSX 中与上次相同的位置,因此 React 不会修改 <input> 标签或它的 value! 浏览器绘制在渲染完成并且React更新DOM之后,浏览器就会重新绘制屏幕。尽管这个过程称为“浏览器渲染”(“browser rendering”),这里还是称为“绘制”(“painting”),以避免在这些文档的其余部分中出现混淆。 state如同一张快照也许state变量看起来就和一般的可读写的JavaScript变量类似。但state在其表现出的特性上更像是一张快照。设置他不会更爱你已有的state变量,但会触发重新渲染。 设置state会触发渲染你可能会认为你的用户界面会直接对点击之类的用户输入做出相应并发生变化。在React中,他的工作方式与这种思维模型略有不同。上一章节我们知道通过设置state请求重新渲染。这就意味着要使界面对输入做出反应,需要使用设置state。 看个例子: 12345678910111213141516171819202122232425262728import { useState } from 'react';export default function Form() { const [isSent, setIsSent] = useState(false); const [message, setMessage] = useState('Hi!'); if (isSent) { return <h1>Your message is on its way!</h1> } return ( <form onSubmit={(e) => { e.preventDefault(); setIsSent(true); sendMessage(message); }}> <textarea placeholder=\"Message\" value={message} onChange={e => setMessage(e.target.value)} /> <button type=\"submit\">Send</button> </form> );}function sendMessage(message) { // ...} 当单击按钮时会发生以下情况: 执行 onSubmit 事件处理函数。 setIsSent(true) 将 isSent 设置为 true 并排列一个新的渲染。 React 根据新的 isSent 值重新渲染组件。 渲染会及时生出一张快照“正在渲染”就意味着React正在调用组件—- 一个函数。你从该函数返回的JSX就像是UI的一张及时的快照。它的props、事件处理函数和内部变量都是根据当前渲染时的state被计算出来的。 相较于照片或电影画面不同,你返回的UI“快照”是可交互的。他其中包含着类似事件处理函数的逻辑,这些逻辑对于指定如何输入作出响应。React随后会更新屏幕来匹配这张快照,并绑定事件处理函数。因此,按下按钮即会触发你的JSX的点击事件处理函数。 当React重新渲染一组件时: React会再次调用你的函数 函数会返回新的JSX快照 React会更新界面以匹配返回的快照 作为一个组件的记忆,state不同于在你的函数返回之后就会消失的普通变量。state实际是“活”在React本身– 就像摆在一个架子上!– 位于你的函数之外。当React调用组件时,他会为特定的那一次渲染提供一张state快照。你的组件会在会在JSX中返回一张包含一整套新的props和事件处理函数的UI快照,其中所有的值都是根据一次渲染中state的值被计算出来的! 下面看个例子: 试想下结果! 1234567891011121314151617import { useState } from 'react';export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 1); setNumber(number + 1); setNumber(number + 1); }}>+3</button> </> )} 点击按钮后其实发现,每次点击number递增一次!!! 设置state只会为下一次渲染变更state的值。在第一次渲染期间,number为0。这也解释了为什么在那次渲染中的 onClick 处理函数中,即便在调用了 setNumber(number + 1) 之后,number 的值也仍然是 0: 12345button onClick={() => { setNumber(number + 1); setNumber(number + 1); setNumber(number + 1);}}>+3</button> 分析下这个按钮点击事件处理函数通知React要做的事情: setNumber(number + 1):number 是 0 所以 setNumber(0 + 1)。 React 准备在下一次渲染时将 number 更改为 1。 setNumber(number + 1):number 是0 所以 setNumber(0 + 1)。 React 准备在下一次渲染时将 number 更改为 1。 setNumber(number + 1):number 是0 所以 setNumber(0 + 1)。 React 准备在下一次渲染时将 number 更改为 1。 尽管调用了三次 setNumber(number + 1),但是在这次渲染的的事件处理函数中number会一直是0,所以你会三次将state设置为1。这就是为什么你在事件处理函数执行完后,React重新渲染的组件中的number等于1而不是3 其实就是可以把state变量放入这次渲染中。由于这次渲染中的state变量就是0,其实事件处理函数就是以下这种: 12345<button onClick={() => { setNumber(0 + 1); setNumber(0 + 1); setNumber(0 + 1);}}>+3</button> 所以对于下一次渲染来说,number是1,因此那次渲染中的点击事件处理函数就是这样: 12345<button onClick={() => { setNumber(1 + 1); setNumber(1 + 1); setNumber(1 + 1);}}>+3</button> 以上这就是为什么每次都是递增1。 随着时间变化的state来看来段代码: 123456789101112131415import { useState } from 'react';export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); alert(number); }}>+5</button> </> )} 以上代码会出现 alert先显示0,页面显示累加后的。也就是alert会先显示上一次的数值,页面在显示。 在alert加上记时器,使得在组件重新渲染之后才触发。又会怎么样呢? 1234567891011121314151617import { useState } from 'react';export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); setTimeout(() => { alert(number); }, 3000); }}>+5</button> </> )} 这时候就会发现页面先渲染出累加后的数值,alert才会输出累计后的数值。 到提示框运行时,React中存储的state可能已经发生了改变,但他是使用用户与之交互时的快照进行调度的! 一个state变量的值永远不会在一次渲染的内部发生变化,即使事件处理函数的代码是异步的。在那个渲染的onClick内部,number的值即使在调用的setNumber(number + 5)之后也是0。它的值是在React通过调用你的组件“获取UI的快照”时就被“固定”了。 React 会使 state 的值始终”固定“在一次渲染的各个事件处理函数内部。 你无需担心代码运行时 state 是否发生了变化。 把一系列state更新加入队列React会对state更新进行批处理在上节这个示例中,我们发现当按钮点击后,组件页面渲染数值一直是每次累加一次的。 1234567891011121314151617import { useState } from 'react';export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 1); setNumber(number + 1); setNumber(number + 1); }}>+3</button> </> )} 每一次渲染的 state 值都是固定的,在第一次渲染的事件处理函数内部的number值总是0。 123setNumber(0 + 1);setNumber(0 + 1);setNumber(0 + 1); 在Reac机制里,React会等到事件处理函数中的所有代码都运行完毕在处理你的state更新。这也就是为什么重新渲染只会发生在所有这些setNumber()调用之后的原因。 就好比,点餐时。服务员不会在你说第一道菜的时候,就去厨房下单,而是等你,把菜点完、如有修改修改完后,再一次性去下单。 这样就可以更新多个state变量–甚至来自多个组件的state变量–而不会触发太多的 重新渲染。这样也意味着只有在我们的事件处理函数以及其中任何代码执行完成之后,UI才会更新。这种特性也就是批处理,他会使React应用运行得更快。这样也可以帮助我们避免处理只更新了一部分state变量的令人困惑的“半成品”渲染。 React不会垮多个需要刻意触发的事件(如点击)进行批处理–每次点击都是单独处理的。React只会在一般来说安全的情况下才进行批处理。例如,如果第一次点击按钮会禁用表单,那么第二次点击就不会再次提交它。 在下次渲染前读次更新同一个state如果想在下次渲染之前多次更新同一个state,我们可以使用setNumber(n => n + 1)这样传入一个根据队列中的前一个state计算下一个state函数,而不是像setNumber(number + 1)这样传入下一个state的值。这是告诉React“用state值做某事”而不是仅仅替换它的方法。 现在尝试递增计数器: 1234567891011121314151617import { useState } from 'react';export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(n => n + 1); setNumber(n => n + 1); setNumber(n => n + 1); }}>+3</button> </> )} 在这里,n => n + 1被称为更新函数。当我们给他传递一个state设置函数时: React会将此函数加入队列,以便在事件处理函数中的所有其他代码运行后进行处理。 在下一次渲染期间,React会遍历队列并更新之后的最终state。 123setNumber(n => n + 1);setNumber(n => n + 1);setNumber(n => n + 1); 以下是React在执行事件处理函数时处理这几行代码的过程: setNumber(n => n + 1):n => n + 1 是一个函数。React 将它加入队列。 setNumber(n => n + 1):n => n + 1 是一个函数。React 将它加入队列。 setNumber(n => n + 1):n => n + 1 是一个函数。React 将它加入队列。 在下次渲染期间调用useState时,React会遍历队列。之前的numberstate的值是0,所以这就是React作为参数n传递给第一个更新函数的值。然后React会获取上一个更新函数的返回值,并将其作为n传递给下一个更新函数,以此类推: 更新队列 n 返回值 n => n + 1 0 0 + 1 = 1 n => n + 1 1 1 + 1 = 2 n => n + 1 2 2 + 1 = 3 如果在替换state后更新state会发生什么看看下面这个例子,思考下下一次number渲染的值是什么? 1234<button onClick={() => { setNumber(number + 5); setNumber(n => n + 1);}}> 实际结果是:每次递增6 这个事件处理函数告诉React要做的事情: setNumber(number + 5):number 为 0,所以 setNumber(0 + 5)。React 将 “替换为 5” 添加到其队列中。 setNumber(n => n + 1):n => n + 1 是一个更新函数。 React 将 该函数 添加到其队列中。 在下一次渲染期间,React会遍历state队列: 更新队列 n 返回值 “替换为 5” 0(未使用) 5 n => n + 1 5 5 + 1 = 6 React会保存6为最终结果并从useState中返回。 注意:其实这时候就可以发现,setState(x)实际上会像setState(n => x)一样运行,只不过没有使用n! 如果在更新state后替换state会发生什么看看这例子,你认为number在下一次渲染中的值是什么 12345<button onClick={() => { setNumber(number + 5); setNumber(n => n + 1); setNumber(42);}}> 实际结果:第一次变更为42,后续一直为42。 以下是 React 在执行事件处理函数时处理这几行代码的过程: setNumber(number + 5):number 为 0,所以 setNumber(0 + 5)。React 将 “替换为 5” 添加到其队列中。 setNumber(n => n + 1):n => n + 1 是一个更新函数。React 将该函数添加到其队列中。 setNumber(42):React 将 “替换为 42” 添加到其队列中。 在下一次渲染期间,React 会遍历 state 队列: 更新队列 n 返回值 “替换为 5” 0(未使用) 5 n => n + 1 5 5 + 1 = 6 “替换为 42” 6(未使用) 42 然后 React 会保存 42 为最终结果并从 useState 中返回。 总而言之,以下是我们可以考虑传递给setNumberstate设置函数的内容: 一个更新函数(例如:n => n + 1)会被添加到队列中。 任何其他的值(例如:数字5)会导致“替换为5”被添加到队列中,已经在队列中的内容会被忽略。 事件处理函数执行完成之后,React将重新触发渲染。再重新渲染期间,React将处理队列。更新函数会在渲染期间执行,因此更新函数必须是 纯函数 并且只返回结果。不要尝试从他内部设置state或者执行其他副作用。在严格模式下,React会更新每个更新函数俩次(但是丢弃第二个结果),以便帮助发现错误。 命名惯例通常使用相应的state变量的第一个字母来命名更新函数的的参数: 123setEnabled(e => !e);setLastName(ln => ln.reverse());setFriendCount(fc => fc * 2); 另一个常见的惯例是重复使用完整的 state 变量名称,如 setEnabled(enabled => !enabled),或使用前缀,如 setEnabled(prevEnabled => !prevEnabled)。 更新state中的对象state中可以保存任意类型的JavaScript值,包括对象。但是,在修改对象的时候不应该直接修改存放在React state中的对象。当我们想更新一个对象时,需要创建一个新的对象(或者将其拷贝一份),然后将state更次为此对象。 什么是mutation我们可以在state中存放任意类型的JavaScript值。 我们在state中存放的数字、字符串和布尔值,这些类型的值在JavaScript中是不可变(immutable)的,这意味着它们不能被改变或是只读的。这些值可以通过替换它们的值来触发下一次重新渲染。 state存放数字statex从0到5,数字0本身没有发生改变。在JavaScript中,无法对内置的原始值,如数字、字符串和布尔值,进行任何更改。 1const [x,setX] = useState(0) state存放对象当我们改变对象本身的内容时,就制造了一个mutation 1const [position, setPosition] = useState({ x: 0, y: 0 }); 严格来说,React state中存放的对象是可变的,但是应该像处理数字、布尔值、字符串一样视为不可变。当要改变的时候,应该考虑去替换它们的值,而不是对他们进行修改。 将state视为只读的在React中我们应该将所有放在state中的JavaScript对象都视为只读的。 我们来看这个例子,我们使用存放在state中的对象来表示指针当前的位置。当我们在预览区域触发或移动光标时,红色移动。 123456789101112131415161718192021222324252627282930313233import { useState } from 'react';export default function MovingDot() { const [position, setPosition] = useState({ x: 0, y: 0 }); return ( <div onPointerMove={e => { setPosition({ x: e.clientX, y: e.clientY }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }}> <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> );} 为了真正 触发一次重新渲染,我们需要创建一个新的对象并把它传递给state的设置函数: 123456onPointerMove={e => { setPosition({ x: e.clientX, y: e.clientY });}} 通过使用setPosition,你在告诉React: 使用这个新的对象替换positon的值 然后再次渲染这个组件 局部的mutation是可以接受的像这样的代码是有问题的,因为它改变了state中现有的对象: 12position.x = e.clientX;position.y = e.clientY; 但是像这样的代码就没有任何问题,因为改变的是刚刚创建的一个循对象,并将这个对象传递给了state: 1234const nextPosition = {};nextPosition.x = e.clientX;nextPosition.y = e.clientY;setPosition(nextPosition); 这种写法完全等于这这写法: 12345setPosition({ x: e.clientX, y: e.clientY}); 只有改变处于state中的现有对象时,mutation才会成为问题。而修改一个刚刚创建的对象就不会出现任何问题,因为还没有其他代码引用它。改变它并不会意外的影响到其他依赖它的东西。这叫做“局部mutation”。我们也可以在 在渲染的过程中 进行“局部mutation”的操作。这种操作既便捷又没有任何问题! 使用展开语法复制对象在之前的例子,都会根据指针的位置创建出一个新的position对象。当我们只需要改变一个属性值的时候,也或者是将现有数据作为新对象的一部分。 看下面的例子,输入框并不会直接正常运行,因为onChange直接修改了state: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253import { useState } from 'react';export default function Form() { const [person, setPerson] = useState({ firstName: 'Barbara', lastName: 'Hepworth', email: '[email protected]' }); function handleFirstNameChange(e) { person.firstName = e.target.value; } function handleLastNameChange(e) { person.lastName = e.target.value; } function handleEmailChange(e) { person.email = e.target.value; } return ( <> <label> First name: <input value={person.firstName} onChange={handleFirstNameChange} /> </label> <label> Last name: <input value={person.lastName} onChange={handleLastNameChange} /> </label> <label> Email: <input value={person.email} onChange={handleEmailChange} /> </label> <p> {person.firstName}{' '} {person.lastName}{' '} ({person.email}) </p> </> );} 以上的,这段代码直接修改了上一次渲染中的state: 1person.firstName = e.target.value; 如果我们想在获取firstName的需求,最可靠的办法就是创建一个新的对象将它传递给stePerson。但是在这里,我们还需要将当前的数据复制到新对象中,因为我们只改了一个字段。 12345setPerson({ firstName: e.target.value, // 从 input 中获取新的 first name lastName: person.lastName, email: person.email}); 我们也可以使用...对象展开 语法,这样就不需要单独复制某个属性。这里注意:新的属性值应该放在最后。 1234setPerson({ ...person, // 复制上一个 person 中的所有字段 firstName: e.target.value // 但是覆盖 firstName 字段 }); 这样就可以看来,并没有为每个输入框单独生命一个state。对于大型表单,将所有数据都放在同一个对象中是非常方便的–前提是必须要正确地更新它! 请注意...展开愈发本质是“浅拷贝”—它只会复制一层。这样就使得它的执行速度很快,这也意味着我们要更新一个嵌套属性时,就必须多次使用展开语法。 使用一个事件处理函数来更新多个字段我们也可以在对象的定义中使用[xxx]括号来实现属性的动态命名。 看下面这个例子: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051import { useState } from 'react';export default function Form() { const [person, setPerson] = useState({ firstName: 'Barbara', lastName: 'Hepworth', email: '[email protected]' }); function handleChange(e) { setPerson({ ...person, [e.target.name]: e.target.value }); } return ( <> <label> First name: <input name=\"firstName\" value={person.firstName} onChange={handleChange} /> </label> <label> Last name: <input name=\"lastName\" value={person.lastName} onChange={handleChange} /> </label> <label> Email: <input name=\"email\" value={person.email} onChange={handleChange} /> </label> <p> {person.firstName}{' '} {person.lastName}{' '} ({person.email}) </p> </> );} 在这里,e.target.name 引用了 <input> 这个 DOM 元素的 name 属性。 更新一个嵌套对象如果出现了嵌套对象: 12345678const [person, setPerson] = useState({ name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: 'https://i.imgur.com/Sd1AgUOm.jpg', }}); 如果想要更新 person.artwork.city 的值,用mutation来实现的方法就很好理解了: 1person.artwork.city = 'New Delhi'; 但是在React中,需要将state是为不可变得!如果要修改city的值,先要创建一个新的artwork对象(其中预先填充上一个artworkd对象中的数据),然后创建一个你的person对象,并且使得其中的artwork属性指向新创建的artwork对象: 123const nextArtwork = { ...person.artwork, city: 'new ork'}const nextPerson = { ...person, artwork: nextArtwork}setPerson(nextPerson) 或者,也可以写成一个函数调用: 1234567setPerson({ ...person, // 复制其它字段的数据 artwork: { // 替换 artwork 字段 ...person.artwork, // 复制之前 person.artwork 中的数据 city: 'New Delhi' // 但是将 city 的值替换为 New Delhi! }}); 对象并非是嵌套的下面这个对象从代码上来看是“嵌套的”: 12345678let obj = { name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: 'https://i.imgur.com/Sd1AgUOm.jpg', }}; 其实,思考对象的特性时(key:value),“嵌套”并不是一个非常准确的方式。其实这个对象在运行时,会被解释为: 12345678910let obj1 = { title: 'Blue Nana', city: 'Hamburg', image: 'https://i.imgur.com/Sd1AgUOm.jpg',};let obj2 = { name: 'Niki de Saint Phalle', artwork: obj1}; 这么看来其实obj1并不处于obj2的内部。同时,下面代码中obj3中的属性也可以指向obj1: 12345678910111213141516let obj1 = { title: 'Blue Nana', city: 'Hamburg', image: 'https://i.imgur.com/Sd1AgUOm.jpg',};let obj2 = { name: 'Niki de Saint Phalle', artwork: obj1};let obj3 = { name: 'Copycat', artwork: obj1}; 如果你直接修改 obj3.artwork.city,就会同时影响 obj2.artwork.city 和 obj1.city。这是因为 obj3.artwork、obj2.artwork 和 obj1 都指向同一个对象。当你用“嵌套”的方式看待对象时,很难看出这一点。相反,它们是相互独立的对象,只不过是用属性“指向”彼此而已。 使用Immer编写简介的更新逻辑如果state有多层的嵌套,应该考虑将 将其扁平化。但是同时,不想改拜年state的数据结构,我们可以使用Immer 这个库,他可以让你使用简便但是可以直接修改的语法编写代码,并会处理好复制的过程。通过使用 Immer,你写出的代码看起来就像是你“打破了规则”而直接修改了对象: 123updatePerson(draft => { draft.artwork.city = 'Lagos';}); 不同于一般的mutation,他并不会覆盖之前的state! Immer 是如何运行的?由 Immer 提供的 draft 是一种特殊类型的对象,被称为 Proxy,它会记录你用它所进行的操作。这就是你能够随心所欲地直接修改对象的原因所在!从原理上说,Immer 会弄清楚 draft 对象的哪些部分被改变了,并会依照你的修改创建出一个全新的对象。 如何使用Immer尝试使用 Immer: 运行 npm install use-immer 添加 Immer 依赖 用 import { useImmer } from 'use-immer' 替换掉 import { useState } from 'react' 看以下例子: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081import { useImmer } from 'use-immer';export default function Form() { const [person, updatePerson] = useImmer({ name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: 'https://i.imgur.com/Sd1AgUOm.jpg', } }); function handleNameChange(e) { updatePerson(draft => { draft.name = e.target.value; }); } function handleTitleChange(e) { updatePerson(draft => { draft.artwork.title = e.target.value; }); } function handleCityChange(e) { updatePerson(draft => { draft.artwork.city = e.target.value; }); } function handleImageChange(e) { updatePerson(draft => { draft.artwork.image = e.target.value; }); } return ( <> <label> Name: <input value={person.name} onChange={handleNameChange} /> </label> <label> Title: <input value={person.artwork.title} onChange={handleTitleChange} /> </label> <label> City: <input value={person.artwork.city} onChange={handleCityChange} /> </label> <label> Image: <input value={person.artwork.image} onChange={handleImageChange} /> </label> <p> <i>{person.artwork.title}</i> {' by '} {person.name} <br /> (located in {person.artwork.city}) </p> <img src={person.artwork.image} alt={person.artwork.title} /> </> );} 这样,事件处理函数就变得简洁了。可以随意在一个组件中同时使用 useState 和 useImmer。如果你、想要写出更简洁的更新处理函数,Immer 会是一个不错的选择,尤其是当你的 state 中有嵌套,并且复制对象会带来重复的代码时。 更新state中的数组数组也是另外一种可以存储在state中的JavaScript对象,数组本身是可变的,但是应该视为不可变。同对象一样,如果想要更新存储于state中的数组时,需要创建一个新的数组(或者创意一份已有数组的拷贝值),并使用新数组设置state。 在没有mutation的前提下更新数组在JavaScript中,数组是另一种对象。同对象一样,需要将React state中的数值是为只读的。这意味着不应该使用类似于 arr[0] = 'bird'这样的方式来修改数组中的元素,也不应该使用会直接修改原数组的方法,例如push()和pop()。 相反,在需要更新一个数组时,需要将一个新的数组传入state中的setting方法中。为此我们可以通过 filter() 和 map() 这样不会直接修改原始值的方法,从原始数组生成一个新的数组。然后你就可以将 state 设置为这个新生成的数组。 以下是常见数组操作的api。当操作React state中的数组时,应该避免左列的方法,而选择右列的方法。 避免使用 (会改变原始数组) 推荐使用 (会返回一个新数组) 添加元素 push,unshift concat,[...arr] 展开语法(例子) 删除元素 pop,shift,splice filter,slice(例子) 替换元素 splice,arr[i] = ... 赋值 map(例子) 排序 reverse,sort 先将数组复制一份(例子) 或者,可以使用使用 Immer ,这样就可以使用表格中的所有方法了。 注意: slice和splice方法作用不同 slice可以拷贝数组或者数组的一部分。 splice会直接修改原始数组(插入或者删除元素) 向数组中添加元素如果利用push()会直接修改原始数组,而这个不是我们期望的。 123456789101112131415161718192021222324252627282930import { useState } from 'react';let nextId = 0;export default function List() { const [name, setName] = useState(''); const [artists, setArtists] = useState([]); return ( <> <h1>振奋人心的雕塑家们:</h1> <input value={name} onChange={e => setName(e.target.value)} /> <button onClick={() => { artists.push({ id: nextId++, name: name, }); }}>添加</button> <ul> {artists.map(artist => ( <li key={artist.id}>{artist.name}</li> ))} </ul> </> );} 以上例子中,我们应该创建一个新的数组,其包含了原始数组中的所有元素以及一个在末尾添加的新元素。可以有很多种办法实现。最简单的一种就是使用 ... 数组展开 语法: 123456setArtists( // 替换 state [ // 是通过传入一个新数组实现的 ...artists, // 新数组包含原数组的所有元素 { id: nextId++, name: name } // 并在末尾添加了一个新的元素 ]); 还可以通过展开运算符将新添的元素放在原始的...artists之前: 1234setArtists([ { id: nextId++, name: name }, ...artists // 将原数组中的元素放在末尾]); 这样看来,展开运算符就可以完成push()和unshift()的效果。 从数组中删除元素从数组中删除元素最简单的方法就是将它过滤出去。可以通过filter方法实现: 123456789101112131415161718192021222324252627282930313233343536import { useState } from 'react';let initialArtists = [ { id: 0, name: 'Marta Colvin Andrade' }, { id: 1, name: 'Lamidi Olonade Fakeye'}, { id: 2, name: 'Louise Nevelson'},];export default function List() { const [artists, setArtists] = useState( initialArtists ); return ( <> <h1>振奋人心的雕塑家们:</h1> <ul> {artists.map(artist => ( <li key={artist.id}> {artist.name}{' '} <button onClick={() => { setArtists( artists.filter(a => a.id !== artist.id ) ); }}> 删除 </button> </li> ))} </ul> </> );} 点击“删除”按钮几次,每次都会触发 123setArtists( artists.filter(a => a.id !== artist.id)); 与push()不同的是这里的filter()表示“创建一个新的数组”。也就是生成了新的数组,通过state更新,并触发渲染。但是filter()不会改变原数组。 转换数组如果想改变数组中的某些或者所有元素。使用map()创建一个新数组。传入的map函数据定了根据每个元素或者索引(或二者都作为条件)对元素进行处理。 在下面的例子中,一个数组记录了两个圆形和一个正方形的坐标。当你点击按钮时,仅有两个圆形会向下移动 100 像素。这是通过使用 map() 生成一个新数组实现的。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354import { useState } from 'react';let initialShapes = [ { id: 0, type: 'circle', x: 50, y: 100 }, { id: 1, type: 'square', x: 150, y: 100 }, { id: 2, type: 'circle', x: 250, y: 100 },];export default function ShapeEditor() { const [shapes, setShapes] = useState( initialShapes ); function handleClick() { const nextShapes = shapes.map(shape => { if (shape.type === 'square') { // 不作改变 return shape; } else { // 返回一个新的圆形,位置在下方 50px 处 return { ...shape, y: shape.y + 50, }; } }); // 使用新的数组进行重渲染 setShapes(nextShapes); } return ( <> <button onClick={handleClick}> 所有圆形向下移动! </button> {shapes.map(shape => ( <div key={shape.id} style={{ background: 'purple', position: 'absolute', left: shape.x, top: shape.y, borderRadius: shape.type === 'circle' ? '50%' : '', width: 20, height: 20, }} /> ))} </> );} 替换数组中的元素如果存在替换数组中或者多个元素需求的话。类似 arr[0] = 'bird' 这样的赋值语句会直接修改原始数组,所以在这种情况下,我们也应该使用 map。 要替换一个元素,请使用 map 创建一个新数组。在你的 map 回调里,第二个参数是元素的索引。使用索引来判断最终是返回原始的元素(即回调的第一个参数)还是替换成其他值: 1234567891011121314151617181920212223242526272829303132333435363738import { useState } from 'react';let initialCounters = [ 0, 0, 0];export default function CounterList() { const [counters, setCounters] = useState( initialCounters ); function handleIncrementClick(index) { const nextCounters = counters.map((c, i) => { if (i === index) { // 递增被点击的计数器数值 return c + 1; } else { // 其余部分不发生变化 return c; } }); setCounters(nextCounters); } return ( <ul> {counters.map((counter, i) => ( <li key={i}> {counter} <button onClick={() => { handleIncrementClick(i); }}>+1</button> </li> ))} </ul> );} 向数组中插入元素有时候也会存在向数组特定位置插入一个元素,这个位置既不在开头,又不在末尾。这时候就可以使用数组展开运算符 ... 和 slice() 方法一起使用。slice() 方法可以从数组中切出“一片”。为了将元素插入数组,需要先展开原数组在插入点之前的切片,然后插入新元素,最后展开原数组中剩下的部分。 下面的例子中,插入按钮总是会将元素插入到数组中索引为 1 的位置。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748import { useState } from 'react';let nextId = 3;const initialArtists = [ { id: 0, name: 'Marta Colvin Andrade' }, { id: 1, name: 'Lamidi Olonade Fakeye'}, { id: 2, name: 'Louise Nevelson'},];export default function List() { const [name, setName] = useState(''); const [artists, setArtists] = useState( initialArtists ); function handleClick() { const insertAt = 1; // 可能是任何索引 const nextArtists = [ // 插入点之前的元素: ...artists.slice(0, insertAt), // 新的元素: { id: nextId++, name: name }, // 插入点之后的元素: ...artists.slice(insertAt) ]; setArtists(nextArtists); setName(''); } return ( <> <h1>振奋人心的雕塑家们:</h1> <input value={name} onChange={e => setName(e.target.value)} /> <button onClick={handleClick}> 插入 </button> <ul> {artists.map(artist => ( <li key={artist.id}>{artist.name}</li> ))} </ul> </> );} 其他改变数组情况还会存在,依靠展开运算符和map()或者filter()等不回直接修改原值的方法无法做到。例如:翻转数组、数组排序。而因为JavaScript中的 reverse() 和 sort() 方法会改变原数组,所以你无法直接使用它们。 然而我们可以先拷贝这个数组,在改变这个拷贝后的值 例如: 1234567891011121314151617181920212223242526272829303132import { useState } from 'react';let nextId = 3;const initialList = [ { id: 0, title: 'Big Bellies' }, { id: 1, title: 'Lunar Landscape' }, { id: 2, title: 'Terracotta Army' },];export default function List() { const [list, setList] = useState(initialList); function handleClick() { const nextList = [...list]; nextList.reverse(); setList(nextList); } return ( <> <button onClick={handleClick}> 翻转 </button> <ul> {list.map(artwork => ( <li key={artwork.id}>{artwork.title}</li> ))} </ul> </> );} 在这里,我们现使用展开运算符进行了原数组的拷贝。有了这个拷贝值,就可以直接通过nextList.reverse() 或 nextList.sort() 这样直接修改原数组的方法。甚至可以通过 nextList[0] = \"something\" 这样的方式对数组中的特定元素进行赋值。 即使拷贝了数组,还是不能直接修改其内部的元素。这是因为数组的拷贝是浅拷贝–新的数组中依然保留了与原始数组相同的元素。 因此,修改了拷贝数组内部的某个对象,其实就是直接在修改当前的state。如下面的代码; 123const nextList = [...list];nextList[0].seen = true; // 问题:直接修改了 list[0] 的值setList(nextList); 虽然 nextList 和 list 是两个不同的数组,**nextList[0] 和 list[0] 却指向了同一个对象**。因此,通过改变 nextList[0].seen,list[0].seen 的值也被改变了。这是一种 state 的 mutation 操作,你应该避免这么做!可以用类似于 更新嵌套的 JavaScript 对象 的方式解决这个问题——拷贝想要修改的特定元素,而不是直接修改它。下面是具体的操作。 更新数组内部的对象对象并不是真的位于数组“内部”。可能在代码层面上来看像是在“内部”,但其实际数组中的每个对象都是这个数组“指向”的一个存储其他位置的值。所以在处理list[0]嵌套字段的时候需要格外小心,其他元素的值kennel也指向了数组的同一个元素。 当更新一个嵌套的 state 时,需要从想要更新的地方创建拷贝值,一直这样,直到顶层。 在下面的例子中,两个不同的艺术品清单有着相同的初始 state。他们本应该互不影响,但是因为一次 mutation,他们的 state 被意外地共享了,勾选一个清单中的事项会影响另外一个清单: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172import { useState } from 'react';let nextId = 3;const initialList = [ { id: 0, title: 'Big Bellies', seen: false }, { id: 1, title: 'Lunar Landscape', seen: false }, { id: 2, title: 'Terracotta Army', seen: true },];export default function BucketList() { const [myList, setMyList] = useState(initialList); const [yourList, setYourList] = useState( initialList ); function handleToggleMyList(artworkId, nextSeen) { const myNextList = [...myList]; const artwork = myNextList.find( a => a.id === artworkId ); artwork.seen = nextSeen; setMyList(myNextList); } function handleToggleYourList(artworkId, nextSeen) { const yourNextList = [...yourList]; const artwork = yourNextList.find( a => a.id === artworkId ); artwork.seen = nextSeen; setYourList(yourNextList); } return ( <> <h1>艺术愿望清单</h1> <h2>我想看的艺术清单:</h2> <ItemList artworks={myList} onToggle={handleToggleMyList} /> <h2>你想看的艺术清单:</h2> <ItemList artworks={yourList} onToggle={handleToggleYourList} /> </> );}function ItemList({ artworks, onToggle }) { return ( <ul> {artworks.map(artwork => ( <li key={artwork.id}> <label> <input type=\"checkbox\" checked={artwork.seen} onChange={e => { onToggle( artwork.id, e.target.checked ); }} /> {artwork.title} </label> </li> ))} </ul> );} 其实问题就是出现在 1234const myNextList = [...myList];const artwork = myNextList.find(a => a.id === artworkId);artwork.seen = nextSeen; // 问题:直接修改了已有的元素setMyList(myNextList); 虽然复制出来了新的数组myNextList,但是其内部的元素本身与原数组myList是相同的。因此修改了新数组myNextList中 artwork.seen,其实是在修改原始的 artwork 对象。而这个对象也在youList中使用,这样就导致存在问题。 其实可以使用map()在没有mutation的前提下将一个旧的元素替换成新的更新版本。 123456789setMyList(myList.map(artwork => { if (artwork.id === artworkId) { // 创建包含变更的*新*对象 return { ...artwork, seen: nextSeen }; } else { // 没有变更 return artwork; }})); 此处的 ... 是一个对象展开语法,被用来创建一个对象的拷贝. 通常来讲,我们应该只修改刚刚创建的对象。如果正在插入一个新的元素,可以修改它,但是如果想改变state中已经存在的东西,就需要先拷贝一份了。 使用Immer编写简洁的更新逻辑在没有 mutation 的前提下更新嵌套数组可能会变得有点重复。就像对对象一样: 通常情况下,我们应该不需要更新处于非常深层级的 state 。如果有此类需求,或许需要调整一下数据的结构,让数据变得扁平一些。 如果不想改变 state 的数据结构,使用 Immer ,它可以让我们继续使用方便的,但会直接修改原值的语法,并负责生成拷贝值。 下面是,使用Immer改写的例子 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475import { useState } from 'react';import { useImmer } from 'use-immer';let nextId = 3;const initialList = [ { id: 0, title: 'Big Bellies', seen: false }, { id: 1, title: 'Lunar Landscape', seen: false }, { id: 2, title: 'Terracotta Army', seen: true },];export default function BucketList() { const [myList, updateMyList] = useImmer( initialList ); const [yourList, updateYourList] = useImmer( initialList ); function handleToggleMyList(id, nextSeen) { updateMyList(draft => { const artwork = draft.find(a => a.id === id ); artwork.seen = nextSeen; }); } function handleToggleYourList(artworkId, nextSeen) { updateYourList(draft => { const artwork = draft.find(a => a.id === artworkId ); artwork.seen = nextSeen; }); } return ( <> <h1>艺术愿望清单</h1> <h2>我想看的艺术清单:</h2> <ItemList artworks={myList} onToggle={handleToggleMyList} /> <h2>你想看的艺术清单:</h2> <ItemList artworks={yourList} onToggle={handleToggleYourList} /> </> );}function ItemList({ artworks, onToggle }) { return ( <ul> {artworks.map(artwork => ( <li key={artwork.id}> <label> <input type=\"checkbox\" checked={artwork.seen} onChange={e => { onToggle( artwork.id, e.target.checked ); }} /> {artwork.title} </label> </li> ))} </ul> );} 请注意当使用 Immer 时,类似 artwork.seen = nextSeen 这种会产生 mutation 的语法不会再有任何问题了: 1234updateMyTodos(draft => { const artwork = draft.find(a => a.id === artworkId); artwork.seen = nextSeen;}); 这是因为并不是在修改原始的state,而是在修改Immer提供的特殊draft对象。同理,也可以为draft的内容使用push()``pop()这些会直接修改原值的方法。 幕后,Immer 总是会根据你对 draft 的修改来从头开始构建下一个 state。这使得你的事件处理程序非常的简洁,同时也不会直接修改 state。","tags":[{"name":"react","slug":"react","permalink":"https://ayozoo.github.io/tags/react/"}]},{"title":"React入门之描述UI","date":"2023-10-31T15:50:22.000Z","path":"2023/10/31/ React-描述UI/","text":"ReactReact应用就是被组件的独立UI片段构建,React组件本质就是可以添加任意标签的JavaScript函数。 React组件的基本要素 基本的组件js文件存在要素:1、可以return出存在任意标签的JavaScript函数2、函数中需要存在return(xxx) html结构3、最终需要将渲染的组件export出去 1234567891011121314151617181920function Profile() { return ( <img src=\"https://i.imgur.com/MK3eW3As.jpg\" alt=\"Katherine Johnson\" /> );}export default function Gallery() { return ( <section> <h1>Amazing scientists</h1> <Profile /> <Profile /> <Profile /> </section> );} React 组件导入导出 组件导入导出语法 语法 导出语句 导入语句 默认 export default function Button() {} import Button from ‘./Button.js’; 具名 export function Button() {} import { Button } from ‘./Button.js’ 组件导入导出案例解释 Gallery.js 定义了Profile组件 该组件采用默认导出(export default function) app.js 使用默认导入,导入Gallery文件的组件Gallery 使用默认导出方式将app组件导出 1234567891011121314151617181920212223242526272829303132## app.jsimport Gallery from './Gallery.js';export default function App() { return ( <Gallery /> );}## Gallery.jsfunction Profile() { return ( <img src=\"https://i.imgur.com/QIrZWGIs.jpg\" alt=\"Alan L. Hart\" /> );}export default function Gallery() { return ( <section> <h1>了不起的科学家们</h1> <Profile /> <Profile /> <Profile /> </section> );} JSX书写标签语言什么是JSXJSX 是 JavaScript 语法扩展,可以让在 JavaScript 文件中书写类似 HTML 的标签。JSX 看起来和 HTML 很像,但它的语法更加严格并且可以动态展示信息。 为什么出现JSX随着Web交互性越来越强,逻辑开始决定页面的内容。也就是可以说JavaScript负责HTML内容。因此,在React中,渲染逻辑和标签存在同一个组件文件中。 使用JSX的优势例如将一个按钮的渲染逻辑和标签放在一起,可以确保他们在编辑的时候保持同步。反之,也可以说彼此无关的细节是隔离的。 JSX与HTML的区别每个 React 组件都是一个 JavaScript 函数,它会返回一些标签,React 会将这些标签渲染到浏览器上。React 组件使用一种被称为 JSX 的语法扩展来描述这些标签。JSX对于HTML来说,语法更加严格并且可以动态的展示信息。 JSX and React 是相互独立的东西。通常配合使用,也可以单独使用它们中的任意一个,JSX 是一种语法扩展,而 React 则是一个 JavaScript 的库。 HTML转换为JSX现在存在一个html标签 1234567891011<h1>海蒂·拉玛的待办事项</h1><img src=\"https://i.imgur.com/yXOvdOSs.jpg\" alt=\"Hedy Lamarr\" class=\"photo\"><ul> <li>发明一种新式交通信号灯 <li>排练一个电影场景 <li>改进频谱技术</ul> 此时需要将它转化为React组件,可以这么做,是的没错。 采用 *** <> 或者其他标签*** 包裹。 123456789101112131415export default function TodoList() { return ( <> <h1>海蒂·拉玛的待办事项</h1> <img src=\"https://i.imgur.com/yXOvdOSs.jpg\" alt=\"Hedy Lamarr\" className=\"photo\" /> <ul> <li>发明一种新式交通信号灯</li> <li>排练一个电影场景</li> <li>改进频谱技术</li> </ul> </> JSX语法规则 只能返回一个根元素 就是return()中必须只有一个根元素 不想在标签中增加一个额外的 ,可以用 <> 和 </> 元素来代替 标签必须闭合 使用驼峰式命名法给大部分属性命名! JSX 最终会被转化为 JavaScript,而 JSX 中的属性也会变成 JavaScript 对象中的键值对。在组件中,经常会遇到需要用变量的方式读取这些属性的时候。但 JavaScript 对变量的命名有限制。例如,变量名称不能包含 - 符号或者像 class 这样的保留字。 JSX 虽然看起来很像 HTML,但在底层其实被转化为了 JavaScript 对象,你不能在一个函数中返回多个对象,除非用一个数组把他们包装起来。这就是为什么多个 JSX 标签必须要用一个父元素或者 Fragment 来包裹。 JSX中通过大括号使用javaScriptJSX可以在JavaScript中编写类似HTML的标签,保证渲染逻辑和内容绑定。在需要在标签中添加Js逻辑或者引用标签的动态属性的时候可以在JSX的大括号里面编写Js。 JSX使用引号字符串 当你想将字符串属性传递给JSX时,放在单引号或者双引号中 当你想动态传值时,可以使用{ 和 } 替代 “ 和 “ 123456789101112131415161718192021222324// 字符串export default function Avatar() { return ( <img className=\"avatar\" src=\"https://i.imgur.com/7vQD0fPs.jpg\" alt=\"Gregorio Y. Zara\" /> );}// 变量export default function Avatar() { const avatar = 'https://i.imgur.com/7vQD0fPs.jpg'; const description = 'Gregorio Y. Zara'; return ( <img className=\"avatar\" src={avatar} alt={description} /> );} 注意:当使用src={ } 这种写法会去读取花括号中js中这个变量的值。 标签插值 JSX中允许在标签中插入大括号{}中使用变量 JSX中允许在标签中插入大括号{}中使用函数表达式 1234567891011121314151617181920212223// 变量export default function TodoList() { const name = 'Gregorio Y. Zara'; return ( <h1>{name}'s To Do List</h1> );}// 函数表达式const today = new Date();function formatDate(date) { return new Intl.DateTimeFormat( 'zh-CN', { weekday: 'long' } ).format(date);}export default function TodoList() { return ( <h1>To Do List for {formatDate(today)}</h1> );} 大括号的使用场景主要场景是字符串、数字、变量、和js表达式。 用作JSX标签*内部的文本*:<h1>{name}'s To Do List</h1> 标签***=*后面紧跟的属性**:src={avatar} 会读取 avatar 变量,但是!!!对于src=\"{avatar}\"只会传一个字符串{avatar} 双大括号的使用场景双大括号其实是{}传递对象的传递方式。 对象也用大括号表示,例如 { name: \"Hedy Lamarr\", inventions: 5 }。因此,为了能在 JSX 中传递,必须用另一对额外的大括号包裹对象:person={{ name: \"Hedy Lamarr\", inventions: 5 }}。 也可使用嵌套对象,在jsx大括号中使用。 注意:内联 style 属性 使用驼峰命名法编写。例如,HTML <ul style=\"background-color: black\"> 在你的组件里应该写成 <ul style={{ backgroundColor: 'black' }}>。 12345678910111213141516171819202122232425262728293031323334353637383940// 对象传递export default function TodoList() { return ( <ul style={{ backgroundColor: 'black', color: 'pink' }}> <li>Improve the videophone</li> <li>Prepare aeronautics lectures</li> <li>Work on the alcohol-fuelled engine</li> </ul> );}// 嵌套对象传递const person = { name: 'Gregorio Y. Zara', theme: { backgroundColor: 'black', color: 'pink' }};export default function TodoList() { return ( <div style={person.theme}> <h1>{person.name}'s Todos</h1> <img className=\"avatar\" src=\"https://i.imgur.com/7vQD0fPs.jpg\" alt=\"Gregorio Y. Zara\" /> <ul> <li>Improve the videophone</li> <li>Prepare aeronautics lectures</li> <li>Work on the alcohol-fuelled engine</li> </ul> </div> );} Props组件传递React组件使用Props互相通信。每个父组件都可以提供props给他的自组件传递信息。可以通过Props传递认识JavaScript值,包括对象、数组、和函数。 Props传递类似HTML预定义的属性。像组件传递的时候可以传递任意的props。传递的props可以通过不同的方式进行渲染。 123456789101112131415161718192021222324// 其中person就是props/* 其中的花括号也就是props传递对象而已*/// 1、传递export default function Profile() { return ( <Avatar person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }} size={100} /> );}// 2、使用/读取/* 1.以下这种方式是采用解构,将person的props解构为单独的props传递 2.prop可以存在默认值,例如下面的prop size*/function Avatar({ person, size=100 }) { // 在这里 person 和 size 是可访问的} Props的使用,必须要关注你的父子组件。需要考虑好父组件需要向自组件传的props,而子组件需要考虑传递的props通过什么方式去进行渲染。事实上,**就可以总结出来,React函数组件就是接受一个参数,那就是Props对象。** Props使用妙计 prop可以指定默认值 如果渲染时不存在size的prop,那么size将被赋值100进行渲染 其实就是size的prop属性不存在或者值undefined时会生效 但是,如果传递size={null} 或 size={0},默认值将 不 被使用。 123function Avatar({ person, size = 100 }) { // ...} 可以使用JSX展开语法传递Props 会存在需要传递pros很多,需要声明prop传递 1234567891011function Profile({ person, size, isSepia, thickBorder }) { return ( <div className=\"card\"> <Avatar person={person} size={size} isSepia={isSepia} thickBorder={thickBorder} /> </div> ); 如果存在将所有的props传递给子组件可以采用简洁展开语法,这种写法,将父组件需要传递的props全部传递。如果滥用,那么建议拆分组件。 1234567function Profile(props) { return ( <div className=\"card\"> <Avatar {...props} /> </div> );} 将JSX作为子组件传递 类似HTML标签嵌套,以下例子在父组件card中将children组件作为prop传递,类似vue插槽 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849// app.jsimport Avatar from './Avatar.js';function Card({ children }) { return ( <div className=\"card\"> {children} </div> );}export default function Profile() { return ( <Card> <Avatar size={100} person={{ name: 'Katsuko Saruhashi', imageId: 'YfeOqp2' }} /> </Card> );}// Avatar.jsimport { getImageUrl } from './utils.js';export default function Avatar({ person, size }) { return ( <img className=\"avatar\" src={getImageUrl(person)} alt={person.name} width={size} height={size} /> );}// utils.jsexport function getImageUrl(person, size = 's') { return ( 'https://i.imgur.com/' + person.imageId + size + '.jpg' );} 注意一个组件可能会随着时间的推移收到不同的 props。 Props 并不总是静态的!Props 反映了组件在任何时间点的数据,并不仅仅是在开始时。 然而,props 是 不可变的(一个计算机科学术语,意思是“不可改变”)。当一个组件需要改变它的 props(例如,响应用户交互或新数据)时,它不得不“请求”它的父组件传递 不同的 props —— 一个新对象!它的旧 props 将被丢弃,最终 JavaScript 引擎将回收它们占用的内存。不要尝试“更改 props”。 条件渲染组件中会存在很多根据条件渲染的内容。在React中可以使用js中的if 语句、&& 和 ? : 运算符来选择性地渲染 JSX。 如何根据条件返回JSX根据组件Prop传入的属性值来动态展示内容 使用 if/else 语句 去判断 12345678910111213141516171819202122232425262728293031// 其中isPacked是组件接受的Prop// 用来动态显示 不同li内容的展示// 可以选择 return 的组件为null 那么他将不会显示任何内容function Item({ name, isPacked }) { if (isPacked) { return <li className=\"item\">{name} ✔</li>; } return <li className=\"item\">{name}</li>;}export default function PackingList() { return ( <section> <h1>Sally Ride 的行李清单</h1> <ul> <Item isPacked={true} name=\"宇航服\" /> <Item isPacked={true} name=\"带金箔的头盔\" /> <Item isPacked={false} name=\"Tam 的照片\" /> </ul> </section> );} 选择性包含JSX 一般的不同条件会return不同的HTML片段,此刻可以选择包含JSX的书写,一般使用以下运算符: 1、三目运算符(? :)123456789101112if (isPacked) { return <li className=\"item\">{name} ✔</li>;}return <li className=\"item\">{name}</li>;// 以上代码使用三目运算符修改为return ( <li className=\"item\"> {isPacked ? name + ' ✔' : name} </li>); **注意:**简单的内容展示适合使用三目这种运算符来展示,如果展示的逻辑较为复杂,可选择提取自组件来渲染。 2、与运算符(&&)1234567// 当 JavaScript && 表达式 的左侧(我们的条件)为 true 时,它则返回其右侧的值(在我们的例子里是勾选符号)。但条件的结果是 false,则整个表达式会变成 false。在 JSX 里,React 会将 false 视为一个“空值”,就像 null 或者 undefined,这样 React 就不会在这里进行任何渲染return ( <li className=\"item\"> {name} {isPacked && '✔'} </li>); 注意:切勿将数字放在 && 左侧. JavaScript 会自动将左侧的值转换成布尔类型以判断条件成立与否。然而,如果左侧是 0,整个表达式将变成左侧的值(0),React 此时则会渲染 0 而不是不进行渲染。 例如,一个常见的错误是 messageCount && <p>New messages</p>。其原本是想当 messageCount 为 0 的时候不进行渲染,但实际上却渲染了 0。 为了更正,可以将左侧的值改成布尔类型:messageCount > 0 && <p>New messages</p>。 选择性赋值JSX 1、可以选择将需要渲染的JSX表达式文本赋值给变量123456789101112131415161718192021222324252627282930313233343536// 其中 渲染的JSX文本 itemContent赋值变量function Item({ name, isPacked }) { let itemContent = name; if (isPacked) { itemContent = name + \" ✔\"; } return ( <li className=\"item\"> {itemContent} </li> );}export default function PackingList() { return ( <section> <h1>Sally Ride 的行李清单</h1> <ul> <Item isPacked={true} name=\"宇航服\" /> <Item isPacked={true} name=\"带金箔的头盔\" /> <Item isPacked={false} name=\"Tam 的照片\" /> </ul> </section> );} 2、对于任意的JSX也是适用的123456789101112131415161718192021222324252627282930313233343536373839// 对于JSX itemContent 提取为变量 在return时便可直接使用变量 迭代修改时容易扩展function Item({ name, isPacked }) { let itemContent = name; if (isPacked) { itemContent = ( <del> {name + \" ✔\"} </del> ); } return ( <li className=\"item\"> {itemContent} </li> );}export default function PackingList() { return ( <section> <h1>Sally Ride 的行李清单</h1> <ul> <Item isPacked={true} name=\"宇航服\" /> <Item isPacked={true} name=\"带金箔的头盔\" /> <Item isPacked={false} name=\"Tam 的照片\" /> </ul> </section> );} 渲染列表一般来说,我们经常通过JavaScript数组方法来操作数组中的数据,从而将数组中的数据渲染为多个类似的组件。 遍历渲染123456789101112131415161718192021222324252627282930// 首先我们拥有一个数组数据。const people = [ '凯瑟琳·约翰逊: 数学家', '马里奥·莫利纳: 化学家', '穆罕默德·阿卜杜勒·萨拉姆: 物理学家', '珀西·莱温·朱利亚: 化学家', '苏布拉马尼扬·钱德拉塞卡: 天体物理学家',];// 我们可以使用数组的遍历方法map这个数组来遍历元素const listItem = people.map(person=><li>{person}</li>) // 用ul将listItem包围return <ul>{listItem}</ul>// 最终代码const people = [ '凯瑟琳·约翰逊: 数学家', '马里奥·莫利纳: 化学家', '穆罕默德·阿卜杜勒·萨拉姆: 物理学家', '珀西·莱温·朱利亚: 化学家', '苏布拉马尼扬·钱德拉塞卡: 天体物理学家',];export default function List() { const listItems = people.map(person => <li>{person}</li> ); return <ul>{listItems}</ul>;} 注意:这样会导致没有唯一key报错。 过滤渲染我们有个需求需要将职业为化学家的元素渲染出来 12345678910111213141516171819202122232425262728293031323334353637383940// 数组const people = [ { id: 0, name: '凯瑟琳·约翰逊', profession: '数学家', }, { id: 1, name: '马里奥·莫利纳', profession: '化学家', }, { id: 2, name: '穆罕默德·阿卜杜勒·萨拉姆', profession: '物理学家', }, { name: '珀西·莱温·朱利亚', profession: '化学家', }, { name: '苏布拉马尼扬·钱德拉塞卡', profession: '天体物理学家', },];// 采用数组filter方法进行过滤const liItem = people.filter(person=>person.profession==='化学家')// 用ul将liIte包围return <ul>{liItem}</ul>// 完整代码export default function List() { const listItems = people.map(person => <li>{person}</li> ); return <ul>{listItems}</ul>;} 用key能保证渲染顺序直接放在 map() 方法里的 JSX 元素一般都需要指定 key 值! 这些key会告诉React,每个组件对应的数组数据那一项,去进行匹配。 这些将在数组项进行移动、插入或者删除等操作是非常重要的。合适的key将保证React正常的更新DMO树。 key值在渲染之前,要存在数据里面key必须存在。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869// app.jsimport { people } from './data.js';import { getImageUrl } from './utils.js';export default function List() { const listItems = people.map(person => <li key={person.id}> <img src={getImageUrl(person)} alt={person.name} /> <p> <b>{person.name}</b> {' ' + person.profession + ' '} 因{person.accomplishment}而闻名世界 </p> </li> ); return <ul>{listItems}</ul>;}// data.jsexport const people = [ { id: 0, // 在 JSX 中作为 key 使用 name: '凯瑟琳·约翰逊', profession: '数学家', accomplishment: '太空飞行相关数值的核算', imageId: 'MK3eW3A', }, { id: 1, // 在 JSX 中作为 key 使用 name: '马里奥·莫利纳', profession: '化学家', accomplishment: '北极臭氧空洞的发现', imageId: 'mynHUSa', }, { id: 2, // 在 JSX 中作为 key 使用 name: '穆罕默德·阿卜杜勒·萨拉姆', profession: '物理学家', accomplishment: '关于基本粒子间弱相互作用和电磁相互作用的统一理论', imageId: 'bE7W1ji', }, { id: 3, // 在 JSX 中作为 key 使用 name: '珀西·莱温·朱利亚', profession: '化学家', accomplishment: '开创性的可的松药物、类固醇和避孕药', imageId: 'IOjWm71', }, { id: 4, // 在 JSX 中作为 key 使用 name: '苏布拉马尼扬·钱德拉塞卡', profession: '天体物理学家', accomplishment: '白矮星质量计算', imageId: 'lrWQx8l', },];// util.jsexport function getImageUrl(person) { return ( 'https://i.imgur.com/' + person.imageId + 's.jpg' );} 如何在列表项显示多个DOM节点?? 我需要将每个列表多渲染其他DOM节点。使用Fragment的语法简写<></>无法接受key值。写法存在要不使用包裹,或者使用明确的<Fragment>。 Fragment标签不会出现在DOM上,代码会转成最终的<h1>、<p>、<h1>、<p>…… 的列表。 12345678910import { Fragment } from 'react';// ...const listItems = people.map(person => <Fragment key={person.id}> <h1>{person.name}</h1> <p>{person.bio}</p> </Fragment>); 如何设置key不同来源数据对应不同的key值获取方式: 来自数据库的数据:数据存在唯一的主键,可以直接使用。 本地产生的数据:可以使用自增计数器或者uuid的库生成唯一的key。 key值需要满足的条件 key在兄弟节点必须是唯一的。不需要全局唯一,不同数组可以使用相同的key。 key值不可改变。一定不可以在渲染时候的动态生成key。 为什么需要key??React的key作用,其实都是可以从众多兄弟元素中能够标识出某一项(JSX节点)。这个提供的key提供的信息不止是这个元素所在的位置。即使元素的位置在渲染工程中发现了改变,它对应的key能够让React在整个生命周期一直可以使用。 注意: 不可以直接使用数组的索引作为key值来使用。如果没有显式指定key,React会默认讲索引作为key。当数组项的顺序在插入、删除或者重新排序等操作中发生改变。此时会出现一些难以排查的bug。 不要使用随机数去动态生成key,这样会导致每次渲染key不一样,React这样会认为你在频繁修改DOM,将会导致所有的组件和DOM元素重新渲染。会导致渲染问题,还会导致用户输入丢失。一定要保证key值的稳定性。 组件不会将key当作props的一部分。key值只会对React起到提示作用。 保持组件纯粹纯函数纯函数具有以下特征: 只负责自己的任务。他不会更改在该函数调用前就已经存在的对象和变量。 输入相同,则输出相同。给定相同的输入,纯函数总是返回相同的结果。 React围绕纯函数的概念进行设计。React假设你编写的所有组件都是纯函数。也就是说对于相同的输入,你所编写的React组件将会总是返回相同的JSX。 就像数学公式一样。你传入的值相同,你将会永远得到相同的结果。 再比如,可以将组件比作食谱。如果你遵循正常的菜谱(也就是相同的规律或者规则),在烹饪过程中不引进新食材,那么每次得到的菜肴都是相同的。这个菜肴就是React组件渲染返回的JSX。 副作用:(不符合)预期的结果React的渲染过程自始至终都是纯粹的。组件就应该只返回他们的JSX,而不是改变,在渲染前,就存在的任何对象或者变量,都会是他们变得不纯粹。 例如: 1234567891011121314151617let guest = 0;function Cup() { // Bad: changing a preexisting variable! guest = guest + 1; return <h2>Tea cup for guest #{guest}</h2>;}export default function TeaSet() { return ( <> <Cup /> <Cup /> <Cup /> </> );} 以上组件,在读取外部guest的变量,这将导致React组件每次返回的JSX都是不同的。如果这时候,其他组件也在读取这个变量的话,他们也会产生不同的JSX。 这时候就可以这样修改: 我们将变量作为Props(输入),输入到我们的React组件中。现在对于Props输入的变量是一样的,这时候React组件返回的JSX都是一样的。这时候React组件返回的JSX只依赖这个guestprop。 1234567891011121314function Cup({ guest }) { return <h2>Tea cup for guest #{guest}</h2>;}export default function TeaSet() { return ( <> <Cup guest={1} /> <Cup guest={2} /> <Cup guest={3} /> </> );} 一般来说,你不应该期望你的组件以特定的顺序去渲染。你要注意,其实每个组件都应该是独立的,同样地,每个组件都应该去“独立思考”去考虑自己的输入,而不是在渲染过程中试图去与其他组件协调,或者依赖于其他组件。 使用严格模式检测不纯的计算当用户输入改变某些内容时,你应该设置状态,而不是直接写入变量。当组件渲染时,你永远不应该改变预先存在的变量或者对象。 React提供了“严格模式”,在严格模式下开发,他将会调用每个组件函数俩次。通过重复调用组件函数,严格模式有助找到违反这些规则的组件。 纯函数仅仅执行计算,因此调用它们两次不会改变任何东西 — 就像两次调用 double(2) 并不会改变返回值,两次求解 y = 2x 不会改变 y 的值一样。相同的输入,总是返回相同的输出。 严格模式在生产环境下不生效,因此它不会降低应用程序的速度。如需引入严格模式,你可以用 <React.StrictMode> 包裹根组件。一些框架会默认这样做。 局部mutation:组件的小秘密以上示例的问题是在渲染过程中,组件改变了预先存在的变量的值。 这种现象我们称之为突变(mutation)。纯函数不会改变函数作用域以外的变量、或在函数调用前创建的对象,这会使函数变得不纯粹! 但是,你完全可以在渲染时更改你**刚刚**创建的变量和对象。 看下面示例: 1234567891011function Cup({ guest }) { return <h2>Tea cup for guest #{guest}</h2>;}export default function TeaGathering() { let cups = []; for (let i = 1; i <= 12; i++) { cups.push(<Cup key={i} guest={i} />); } return cups;} 上面cups变量是在TeaGathering函数内部创建的,函数外部不知道发生了什么。这就被称为 “局部 mutation” — 如同藏在组件里的小秘密。 哪些地方可能引发副作用函数式编程很大程度依赖纯函数,但在某些事物在特定情况下不得不发生改变。这些变动包含更新屏幕、启动动画、更改数据等,他们被称为副作用。这些都是“额外”发生的事情,与渲染过程无关。 在React中,副作用通常属于事件处理程序。事件处理程序是React在你执行某些操作(如单击按钮)时运行的函数。即使事件处理程序是在组件内部定义的,他们也不会在渲染期间运行!**因此事件处理程序无需事纯函数。** 无法为副作用找到合适的事件处理程序,你还可以调用组件中的useEffect方法将其附加到返回的JSX中,这会将告诉React在渲染结束后执行他。然而,这种方法应该是最后的手段。 为什么React如此侧重于纯函数??纯函数的编写需要遵循一些习惯和规程。 你的组件在不同环境运行。这样针对相同的输入,都会返回相同的结果。 可以为那些未更改的组件来跳过渲染,可以提高性能。因为纯函数总是返回相同的结果,所以可以安全的缓存他们。 如果在渲染深层组件树的过程中,数据发生了变化,React可以重新开始渲染,不会浪费时间完成过时的渲染。纯粹性使得它随时可以安全的停止计算。 将UI视为树树是项目和UI之间的关系模型,通常使用树结构来表示UI。例如,浏览器使用树结构来构建HTM(DOM)与CSS(CSSOM)。移动平台也使用树来表示试图层次机构。 与浏览器和移动平台一样,React 还使用树结构来管理和建模 React 应用程序中组件之间的关系。这些树是有用的工具,用于理解数据如何在 React 应用程序中流动以及如何优化呈现和应用程序大小。 渲染树组件的主要特性是能够根据其他组件组合而成。组件是能够嵌套的,其中一个组件的父组件也可能是其他组件的自组件。 React在渲染的时候,会在渲染树中建模这种层级关系。这棵树是由节点组成,每个节点代表一个组件。 在React渲染树中,根节点是应用程序的跟组件。这种情况下,根组件是App,他是React渲染的第一个组件。树的每个箭头从父组件指向子组件。 渲染树表示 React 应用程序的单个渲染过程。在 条件渲染 中,父组件可以根据传递的数据渲染不同的子组件。 存在条件渲染的时候,渲染过程的渲染树可能都不同,但是这些树有助于识别React应用程序中的顶级和叶子组件。顶级组件是距离根组件最近的组件,他会影响其下所有的组件渲染性能,通常包含最多复杂性。叶子组件位于树的底部,没有子组件,通常会频繁重新渲染。 识别这些组件类别有助于应用程序的数据流和性能。 模块依赖树在React应用程序中,可以用树来建模的另一个关系是应用程序的模块依赖关系。当拆分组件和逻辑到不同的文件时,就创建了JavaScript模块,在这些模块中可以导出组件、函数或常量。 模块依赖树中的每个节点都是一个模块,每个分支代表该模块中的 import 语句。 以之前的 Inspirations 应用程序为例,可以构建一个模块依赖树,简称依赖树。 树的根节点是根模块,也称入口文件。它通常包含根组件的模块。 与同一应用程序的渲染树相比,存在相似的结构,但也存在一些显著的差异: 构成树的节点代表模块,而不是组件。 非组件模块。树中也有体现,渲染树仅封装组件。 组件嵌套中,父组件接受JSX作为children props,子组件渲染出来,但不导入该模块。 依赖树对于确定React应用程序所需要的模块是很有用的。在生产环境构建React应用时,通常会有构建步骤,该步骤将捆绑所有有必要的JavaScript以供客户端使用。负责操作的工具成为bundler(捆绑器),并且bunder将使用依赖树来确定应该包含哪些模块。 随着应用程序的增长,捆绑包大小通常也会增加。大型捆绑包大小对于客户端来说下载和运行成本高昂,并延迟 UI 绘制的时间。了解应用程序的依赖树可能有助于调试这些问题 现在存在一个html标签 1234567891011<h1>海蒂·拉玛的待办事项</h1><img src=\"https://i.imgur.com/yXOvdOSs.jpg\" alt=\"Hedy Lamarr\" class=\"photo\"><ul> <li>发明一种新式交通信号灯 <li>排练一个电影场景 <li>改进频谱技术</ul> 此时需要将它转化为React组件,可以这么做,是的没错。 采用 *** <> 或者其他标签*** 包裹。 123456789101112131415export default function TodoList() { return ( <> <h1>海蒂·拉玛的待办事项</h1> <img src=\"https://i.imgur.com/yXOvdOSs.jpg\" alt=\"Hedy Lamarr\" className=\"photo\" /> <ul> <li>发明一种新式交通信号灯</li> <li>排练一个电影场景</li> <li>改进频谱技术</li> </ul> </> JSX语法规则 只能返回一个根元素 就是return()中必须只有一个根元素 不想在标签中增加一个额外的 ,可以用 <> 和 </> 元素来代替 标签必须闭合 使用驼峰式命名法给大部分属性命名! JSX 最终会被转化为 JavaScript,而 JSX 中的属性也会变成 JavaScript 对象中的键值对。在组件中,经常会遇到需要用变量的方式读取这些属性的时候。但 JavaScript 对变量的命名有限制。例如,变量名称不能包含 - 符号或者像 class 这样的保留字。 JSX 虽然看起来很像 HTML,但在底层其实被转化为了 JavaScript 对象,你不能在一个函数中返回多个对象,除非用一个数组把他们包装起来。这就是为什么多个 JSX 标签必须要用一个父元素或者 Fragment 来包裹。 JSX中通过大括号使用javaScriptJSX可以在JavaScript中编写类似HTML的标签,保证渲染逻辑和内容绑定。在需要在标签中添加Js逻辑或者引用标签的动态属性的时候可以在JSX的大括号里面编写Js。 JSX使用引号字符串 当你想将字符串属性传递给JSX时,放在单引号或者双引号中 当你想动态传值时,可以使用{ 和 } 替代 “ 和 “ 123456789101112131415161718192021222324// 字符串export default function Avatar() { return ( <img className=\"avatar\" src=\"https://i.imgur.com/7vQD0fPs.jpg\" alt=\"Gregorio Y. Zara\" /> );}// 变量export default function Avatar() { const avatar = 'https://i.imgur.com/7vQD0fPs.jpg'; const description = 'Gregorio Y. Zara'; return ( <img className=\"avatar\" src={avatar} alt={description} /> );} 注意:当使用src={ } 这种写法会去读取花括号中js中这个变量的值。 标签插值 JSX中允许在标签中插入大括号{}中使用变量 JSX中允许在标签中插入大括号{}中使用函数表达式 1234567891011121314151617181920212223// 变量export default function TodoList() { const name = 'Gregorio Y. Zara'; return ( <h1>{name}'s To Do List</h1> );}// 函数表达式const today = new Date();function formatDate(date) { return new Intl.DateTimeFormat( 'zh-CN', { weekday: 'long' } ).format(date);}export default function TodoList() { return ( <h1>To Do List for {formatDate(today)}</h1> );} 大括号的使用场景主要场景是字符串、数字、变量、和js表达式。 用作JSX标签*内部的文本*:<h1>{name}'s To Do List</h1> 标签***=*后面紧跟的属性**:src={avatar} 会读取 avatar 变量,但是!!!对于src=\"{avatar}\"只会传一个字符串{avatar} 双大括号的使用场景双大括号其实是{}传递对象的传递方式。 对象也用大括号表示,例如 { name: \"Hedy Lamarr\", inventions: 5 }。因此,为了能在 JSX 中传递,必须用另一对额外的大括号包裹对象:person={{ name: \"Hedy Lamarr\", inventions: 5 }}。 也可使用嵌套对象,在jsx大括号中使用。 注意:内联 style 属性 使用驼峰命名法编写。例如,HTML <ul style=\"background-color: black\"> 在你的组件里应该写成 <ul style={{ backgroundColor: 'black' }}>。 12345678910111213141516171819202122232425262728293031323334353637383940// 对象传递export default function TodoList() { return ( <ul style={{ backgroundColor: 'black', color: 'pink' }}> <li>Improve the videophone</li> <li>Prepare aeronautics lectures</li> <li>Work on the alcohol-fuelled engine</li> </ul> );}// 嵌套对象传递const person = { name: 'Gregorio Y. Zara', theme: { backgroundColor: 'black', color: 'pink' }};export default function TodoList() { return ( <div style={person.theme}> <h1>{person.name}'s Todos</h1> <img className=\"avatar\" src=\"https://i.imgur.com/7vQD0fPs.jpg\" alt=\"Gregorio Y. Zara\" /> <ul> <li>Improve the videophone</li> <li>Prepare aeronautics lectures</li> <li>Work on the alcohol-fuelled engine</li> </ul> </div> );} Props组件传递React组件使用Props互相通信。每个父组件都可以提供props给他的自组件传递信息。可以通过Props传递认识JavaScript值,包括对象、数组、和函数。 Props传递类似HTML预定义的属性。像组件传递的时候可以传递任意的props。传递的props可以通过不同的方式进行渲染。 123456789101112131415161718192021222324// 其中person就是props/* 其中的花括号也就是props传递对象而已*/// 1、传递export default function Profile() { return ( <Avatar person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }} size={100} /> );}// 2、使用/读取/* 1.以下这种方式是采用解构,将person的props解构为单独的props传递 2.prop可以存在默认值,例如下面的prop size*/function Avatar({ person, size=100 }) { // 在这里 person 和 size 是可访问的} Props的使用,必须要关注你的父子组件。需要考虑好父组件需要向自组件传的props,而子组件需要考虑传递的props通过什么方式去进行渲染。事实上,**就可以总结出来,React函数组件就是接受一个参数,那就是Props对象。** Props使用妙计 prop可以指定默认值 如果渲染时不存在size的prop,那么size将被赋值100进行渲染 其实就是size的prop属性不存在或者值undefined时会生效 但是,如果传递size={null} 或 size={0},默认值将 不 被使用。 123function Avatar({ person, size = 100 }) { // ...} 可以使用JSX展开语法传递Props 会存在需要传递pros很多,需要声明prop传递 1234567891011function Profile({ person, size, isSepia, thickBorder }) { return ( <div className=\"card\"> <Avatar person={person} size={size} isSepia={isSepia} thickBorder={thickBorder} /> </div> ); 如果存在将所有的props传递给子组件可以采用简洁展开语法,这种写法,将父组件需要传递的props全部传递。如果滥用,那么建议拆分组件。 1234567function Profile(props) { return ( <div className=\"card\"> <Avatar {...props} /> </div> );} 将JSX作为子组件传递 类似HTML标签嵌套,以下例子在父组件card中将children组件作为prop传递,类似vue插槽 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849// app.jsimport Avatar from './Avatar.js';function Card({ children }) { return ( <div className=\"card\"> {children} </div> );}export default function Profile() { return ( <Card> <Avatar size={100} person={{ name: 'Katsuko Saruhashi', imageId: 'YfeOqp2' }} /> </Card> );}// Avatar.jsimport { getImageUrl } from './utils.js';export default function Avatar({ person, size }) { return ( <img className=\"avatar\" src={getImageUrl(person)} alt={person.name} width={size} height={size} /> );}// utils.jsexport function getImageUrl(person, size = 's') { return ( 'https://i.imgur.com/' + person.imageId + size + '.jpg' );} 注意一个组件可能会随着时间的推移收到不同的 props。 Props 并不总是静态的!Props 反映了组件在任何时间点的数据,并不仅仅是在开始时。 然而,props 是 不可变的(一个计算机科学术语,意思是“不可改变”)。当一个组件需要改变它的 props(例如,响应用户交互或新数据)时,它不得不“请求”它的父组件传递 不同的 props —— 一个新对象!它的旧 props 将被丢弃,最终 JavaScript 引擎将回收它们占用的内存。不要尝试“更改 props”。 条件渲染组件中会存在很多根据条件渲染的内容。在React中可以使用js中的if 语句、&& 和 ? : 运算符来选择性地渲染 JSX。 如何根据条件返回JSX根据组件Prop传入的属性值来动态展示内容 使用 if/else 语句 去判断 12345678910111213141516171819202122232425262728293031// 其中isPacked是组件接受的Prop// 用来动态显示 不同li内容的展示// 可以选择 return 的组件为null 那么他将不会显示任何内容function Item({ name, isPacked }) { if (isPacked) { return <li className=\"item\">{name} ✔</li>; } return <li className=\"item\">{name}</li>;}export default function PackingList() { return ( <section> <h1>Sally Ride 的行李清单</h1> <ul> <Item isPacked={true} name=\"宇航服\" /> <Item isPacked={true} name=\"带金箔的头盔\" /> <Item isPacked={false} name=\"Tam 的照片\" /> </ul> </section> );} 选择性包含JSX 一般的不同条件会return不同的HTML片段,此刻可以选择包含JSX的书写,一般使用以下运算符: 1、三目运算符(? :)123456789101112if (isPacked) { return <li className=\"item\">{name} ✔</li>;}return <li className=\"item\">{name}</li>;// 以上代码使用三目运算符修改为return ( <li className=\"item\"> {isPacked ? name + ' ✔' : name} </li>); **注意:**简单的内容展示适合使用三目这种运算符来展示,如果展示的逻辑较为复杂,可选择提取自组件来渲染。 2、与运算符(&&)1234567// 当 JavaScript && 表达式 的左侧(我们的条件)为 true 时,它则返回其右侧的值(在我们的例子里是勾选符号)。但条件的结果是 false,则整个表达式会变成 false。在 JSX 里,React 会将 false 视为一个“空值”,就像 null 或者 undefined,这样 React 就不会在这里进行任何渲染return ( <li className=\"item\"> {name} {isPacked && '✔'} </li>); 注意:切勿将数字放在 && 左侧. JavaScript 会自动将左侧的值转换成布尔类型以判断条件成立与否。然而,如果左侧是 0,整个表达式将变成左侧的值(0),React 此时则会渲染 0 而不是不进行渲染。 例如,一个常见的错误是 messageCount && <p>New messages</p>。其原本是想当 messageCount 为 0 的时候不进行渲染,但实际上却渲染了 0。 为了更正,可以将左侧的值改成布尔类型:messageCount > 0 && <p>New messages</p>。 选择性赋值JSX 1、可以选择将需要渲染的JSX表达式文本赋值给变量123456789101112131415161718192021222324252627282930313233343536// 其中 渲染的JSX文本 itemContent赋值变量function Item({ name, isPacked }) { let itemContent = name; if (isPacked) { itemContent = name + \" ✔\"; } return ( <li className=\"item\"> {itemContent} </li> );}export default function PackingList() { return ( <section> <h1>Sally Ride 的行李清单</h1> <ul> <Item isPacked={true} name=\"宇航服\" /> <Item isPacked={true} name=\"带金箔的头盔\" /> <Item isPacked={false} name=\"Tam 的照片\" /> </ul> </section> );} 2、对于任意的JSX也是适用的123456789101112131415161718192021222324252627282930313233343536373839// 对于JSX itemContent 提取为变量 在return时便可直接使用变量 迭代修改时容易扩展function Item({ name, isPacked }) { let itemContent = name; if (isPacked) { itemContent = ( <del> {name + \" ✔\"} </del> ); } return ( <li className=\"item\"> {itemContent} </li> );}export default function PackingList() { return ( <section> <h1>Sally Ride 的行李清单</h1> <ul> <Item isPacked={true} name=\"宇航服\" /> <Item isPacked={true} name=\"带金箔的头盔\" /> <Item isPacked={false} name=\"Tam 的照片\" /> </ul> </section> );} 渲染列表一般来说,我们经常通过JavaScript数组方法来操作数组中的数据,从而将数组中的数据渲染为多个类似的组件。 遍历渲染123456789101112131415161718192021222324252627282930// 首先我们拥有一个数组数据。const people = [ '凯瑟琳·约翰逊: 数学家', '马里奥·莫利纳: 化学家', '穆罕默德·阿卜杜勒·萨拉姆: 物理学家', '珀西·莱温·朱利亚: 化学家', '苏布拉马尼扬·钱德拉塞卡: 天体物理学家',];// 我们可以使用数组的遍历方法map这个数组来遍历元素const listItem = people.map(person=><li>{person}</li>) // 用ul将listItem包围return <ul>{listItem}</ul>// 最终代码const people = [ '凯瑟琳·约翰逊: 数学家', '马里奥·莫利纳: 化学家', '穆罕默德·阿卜杜勒·萨拉姆: 物理学家', '珀西·莱温·朱利亚: 化学家', '苏布拉马尼扬·钱德拉塞卡: 天体物理学家',];export default function List() { const listItems = people.map(person => <li>{person}</li> ); return <ul>{listItems}</ul>;} 注意:这样会导致没有唯一key报错。 过滤渲染我们有个需求需要将职业为化学家的元素渲染出来 12345678910111213141516171819202122232425262728293031323334353637383940// 数组const people = [ { id: 0, name: '凯瑟琳·约翰逊', profession: '数学家', }, { id: 1, name: '马里奥·莫利纳', profession: '化学家', }, { id: 2, name: '穆罕默德·阿卜杜勒·萨拉姆', profession: '物理学家', }, { name: '珀西·莱温·朱利亚', profession: '化学家', }, { name: '苏布拉马尼扬·钱德拉塞卡', profession: '天体物理学家', },];// 采用数组filter方法进行过滤const liItem = people.filter(person=>person.profession==='化学家')// 用ul将liIte包围return <ul>{liItem}</ul>// 完整代码export default function List() { const listItems = people.map(person => <li>{person}</li> ); return <ul>{listItems}</ul>;} 用key能保证渲染顺序直接放在 map() 方法里的 JSX 元素一般都需要指定 key 值! 这些key会告诉React,每个组件对应的数组数据那一项,去进行匹配。 这些将在数组项进行移动、插入或者删除等操作是非常重要的。合适的key将保证React正常的更新DMO树。 key值在渲染之前,要存在数据里面key必须存在。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869// app.jsimport { people } from './data.js';import { getImageUrl } from './utils.js';export default function List() { const listItems = people.map(person => <li key={person.id}> <img src={getImageUrl(person)} alt={person.name} /> <p> <b>{person.name}</b> {' ' + person.profession + ' '} 因{person.accomplishment}而闻名世界 </p> </li> ); return <ul>{listItems}</ul>;}// data.jsexport const people = [ { id: 0, // 在 JSX 中作为 key 使用 name: '凯瑟琳·约翰逊', profession: '数学家', accomplishment: '太空飞行相关数值的核算', imageId: 'MK3eW3A', }, { id: 1, // 在 JSX 中作为 key 使用 name: '马里奥·莫利纳', profession: '化学家', accomplishment: '北极臭氧空洞的发现', imageId: 'mynHUSa', }, { id: 2, // 在 JSX 中作为 key 使用 name: '穆罕默德·阿卜杜勒·萨拉姆', profession: '物理学家', accomplishment: '关于基本粒子间弱相互作用和电磁相互作用的统一理论', imageId: 'bE7W1ji', }, { id: 3, // 在 JSX 中作为 key 使用 name: '珀西·莱温·朱利亚', profession: '化学家', accomplishment: '开创性的可的松药物、类固醇和避孕药', imageId: 'IOjWm71', }, { id: 4, // 在 JSX 中作为 key 使用 name: '苏布拉马尼扬·钱德拉塞卡', profession: '天体物理学家', accomplishment: '白矮星质量计算', imageId: 'lrWQx8l', },];// util.jsexport function getImageUrl(person) { return ( 'https://i.imgur.com/' + person.imageId + 's.jpg' );} 如何在列表项显示多个DOM节点?? 我需要将每个列表多渲染其他DOM节点。使用Fragment的语法简写<></>无法接受key值。写法存在要不使用包裹,或者使用明确的<Fragment>。 Fragment标签不会出现在DOM上,代码会转成最终的<h1>、<p>、<h1>、<p>…… 的列表。 12345678910import { Fragment } from 'react';// ...const listItems = people.map(person => <Fragment key={person.id}> <h1>{person.name}</h1> <p>{person.bio}</p> </Fragment>); 如何设置key不同来源数据对应不同的key值获取方式: 来自数据库的数据:数据存在唯一的主键,可以直接使用。 本地产生的数据:可以使用自增计数器或者uuid的库生成唯一的key。 key值需要满足的条件 key在兄弟节点必须是唯一的。不需要全局唯一,不同数组可以使用相同的key。 key值不可改变。一定不可以在渲染时候的动态生成key。 为什么需要key??React的key作用,其实都是可以从众多兄弟元素中能够标识出某一项(JSX节点)。这个提供的key提供的信息不止是这个元素所在的位置。即使元素的位置在渲染工程中发现了改变,它对应的key能够让React在整个生命周期一直可以使用。 注意: 不可以直接使用数组的索引作为key值来使用。如果没有显式指定key,React会默认讲索引作为key。当数组项的顺序在插入、删除或者重新排序等操作中发生改变。此时会出现一些难以排查的bug。 不要使用随机数去动态生成key,这样会导致每次渲染key不一样,React这样会认为你在频繁修改DOM,将会导致所有的组件和DOM元素重新渲染。会导致渲染问题,还会导致用户输入丢失。一定要保证key值的稳定性。 组件不会将key当作props的一部分。key值只会对React起到提示作用。 保持组件纯粹纯函数纯函数具有以下特征: 只负责自己的任务。他不会更改在该函数调用前就已经存在的对象和变量。 输入相同,则输出相同。给定相同的输入,纯函数总是返回相同的结果。 React围绕纯函数的概念进行设计。React假设你编写的所有组件都是纯函数。也就是说对于相同的输入,你所编写的React组件将会总是返回相同的JSX。 就像数学公式一样。你传入的值相同,你将会永远得到相同的结果。 再比如,可以将组件比作食谱。如果你遵循正常的菜谱(也就是相同的规律或者规则),在烹饪过程中不引进新食材,那么每次得到的菜肴都是相同的。这个菜肴就是React组件渲染返回的JSX。 副作用:(不符合)预期的结果React的渲染过程自始至终都是纯粹的。组件就应该只返回他们的JSX,而不是改变,在渲染前,就存在的任何对象或者变量,都会是他们变得不纯粹。 例如: 1234567891011121314151617let guest = 0;function Cup() { // Bad: changing a preexisting variable! guest = guest + 1; return <h2>Tea cup for guest #{guest}</h2>;}export default function TeaSet() { return ( <> <Cup /> <Cup /> <Cup /> </> );} 以上组件,在读取外部guest的变量,这将导致React组件每次返回的JSX都是不同的。如果这时候,其他组件也在读取这个变量的话,他们也会产生不同的JSX。 这时候就可以这样修改: 我们将变量作为Props(输入),输入到我们的React组件中。现在对于Props输入的变量是一样的,这时候React组件返回的JSX都是一样的。这时候React组件返回的JSX只依赖这个guestprop。 1234567891011121314function Cup({ guest }) { return <h2>Tea cup for guest #{guest}</h2>;}export default function TeaSet() { return ( <> <Cup guest={1} /> <Cup guest={2} /> <Cup guest={3} /> </> );} 一般来说,你不应该期望你的组件以特定的顺序去渲染。你要注意,其实每个组件都应该是独立的,同样地,每个组件都应该去“独立思考”去考虑自己的输入,而不是在渲染过程中试图去与其他组件协调,或者依赖于其他组件。 使用严格模式检测不纯的计算当用户输入改变某些内容时,你应该设置状态,而不是直接写入变量。当组件渲染时,你永远不应该改变预先存在的变量或者对象。 React提供了“严格模式”,在严格模式下开发,他将会调用每个组件函数俩次。通过重复调用组件函数,严格模式有助找到违反这些规则的组件。 纯函数仅仅执行计算,因此调用它们两次不会改变任何东西 — 就像两次调用 double(2) 并不会改变返回值,两次求解 y = 2x 不会改变 y 的值一样。相同的输入,总是返回相同的输出。 严格模式在生产环境下不生效,因此它不会降低应用程序的速度。如需引入严格模式,你可以用 <React.StrictMode> 包裹根组件。一些框架会默认这样做。 局部mutation:组件的小秘密以上示例的问题是在渲染过程中,组件改变了预先存在的变量的值。 这种现象我们称之为突变(mutation)。纯函数不会改变函数作用域以外的变量、或在函数调用前创建的对象,这会使函数变得不纯粹! 但是,你完全可以在渲染时更改你**刚刚**创建的变量和对象。 看下面示例: 1234567891011function Cup({ guest }) { return <h2>Tea cup for guest #{guest}</h2>;}export default function TeaGathering() { let cups = []; for (let i = 1; i <= 12; i++) { cups.push(<Cup key={i} guest={i} />); } return cups;} 上面cups变量是在TeaGathering函数内部创建的,函数外部不知道发生了什么。这就被称为 “局部 mutation” — 如同藏在组件里的小秘密。 哪些地方可能引发副作用函数式编程很大程度依赖纯函数,但在某些事物在特定情况下不得不发生改变。这些变动包含更新屏幕、启动动画、更改数据等,他们被称为副作用。这些都是“额外”发生的事情,与渲染过程无关。 在React中,副作用通常属于事件处理程序。事件处理程序是React在你执行某些操作(如单击按钮)时运行的函数。即使事件处理程序是在组件内部定义的,他们也不会在渲染期间运行!**因此事件处理程序无需事纯函数。** 无法为副作用找到合适的事件处理程序,你还可以调用组件中的useEffect方法将其附加到返回的JSX中,这会将告诉React在渲染结束后执行他。然而,这种方法应该是最后的手段。 为什么React如此侧重于纯函数??纯函数的编写需要遵循一些习惯和规程。 你的组件在不同环境运行。这样针对相同的输入,都会返回相同的结果。 可以为那些未更改的组件来跳过渲染,可以提高性能。因为纯函数总是返回相同的结果,所以可以安全的缓存他们。 如果在渲染深层组件树的过程中,数据发生了变化,React可以重新开始渲染,不会浪费时间完成过时的渲染。纯粹性使得它随时可以安全的停止计算。 将UI视为树树是项目和UI之间的关系模型,通常使用树结构来表示UI。例如,浏览器使用树结构来构建HTM(DOM)与CSS(CSSOM)。移动平台也使用树来表示试图层次机构。 与浏览器和移动平台一样,React 还使用树结构来管理和建模 React 应用程序中组件之间的关系。这些树是有用的工具,用于理解数据如何在 React 应用程序中流动以及如何优化呈现和应用程序大小。 渲染树组件的主要特性是能够根据其他组件组合而成。组件是能够嵌套的,其中一个组件的父组件也可能是其他组件的自组件。 React在渲染的时候,会在渲染树中建模这种层级关系。这棵树是由节点组成,每个节点代表一个组件。 在React渲染树中,根节点是应用程序的跟组件。这种情况下,根组件是App,他是React渲染的第一个组件。树的每个箭头从父组件指向子组件。 渲染树表示 React 应用程序的单个渲染过程。在 条件渲染 中,父组件可以根据传递的数据渲染不同的子组件。 存在条件渲染的时候,渲染过程的渲染树可能都不同,但是这些树有助于识别React应用程序中的顶级和叶子组件。顶级组件是距离根组件最近的组件,他会影响其下所有的组件渲染性能,通常包含最多复杂性。叶子组件位于树的底部,没有子组件,通常会频繁重新渲染。 识别这些组件类别有助于应用程序的数据流和性能。 模块依赖树在React应用程序中,可以用树来建模的另一个关系是应用程序的模块依赖关系。当拆分组件和逻辑到不同的文件时,就创建了JavaScript模块,在这些模块中可以导出组件、函数或常量。 模块依赖树中的每个节点都是一个模块,每个分支代表该模块中的 import 语句。 以之前的 Inspirations 应用程序为例,可以构建一个模块依赖树,简称依赖树。 树的根节点是根模块,也称入口文件。它通常包含根组件的模块。 与同一应用程序的渲染树相比,存在相似的结构,但也存在一些显著的差异: 构成树的节点代表模块,而不是组件。 非组件模块。树中也有体现,渲染树仅封装组件。 组件嵌套中,父组件接受JSX作为children props,子组件渲染出来,但不导入该模块。 依赖树对于确定React应用程序所需要的模块是很有用的。在生产环境构建React应用时,通常会有构建步骤,该步骤将捆绑所有有必要的JavaScript以供客户端使用。负责操作的工具成为bundler(捆绑器),并且bunder将使用依赖树来确定应该包含哪些模块。 随着应用程序的增长,捆绑包大小通常也会增加。大型捆绑包大小对于客户端来说下载和运行成本高昂,并延迟 UI 绘制的时间。了解应用程序的依赖树可能有助于调试这些问题 什么是JSXJSX 是 JavaScript 语法扩展,可以让在 JavaScript 文件中书写类似 HTML 的标签。JSX 看起来和 HTML 很像,但它的语法更加严格并且可以动态展示信息。 为什么出现JSX随着Web交互性越来越强,逻辑开始决定页面的内容。也就是可以说JavaScript负责HTML内容。因此,在React中,渲染逻辑和标签存在同一个组件文件中。 使用JSX的优势例如将一个按钮的渲染逻辑和标签放在一起,可以确保他们在编辑的时候保持同步。反之,也可以说彼此无关的细节是隔离的。 JSX与HTML的区别每个 React 组件都是一个 JavaScript 函数,它会返回一些标签,React 会将这些标签渲染到浏览器上。React 组件使用一种被称为 JSX 的语法扩展来描述这些标签。JSX对于HTML来说,语法更加严格并且可以动态的展示信息。 JSX and React 是相互独立的东西。通常配合使用,也可以单独使用它们中的任意一个,JSX 是一种语法扩展,而 React 则是一个 JavaScript 的库。 HTML转换为JSX现在存在一个html标签 1234567891011<h1>海蒂·拉玛的待办事项</h1><img src=\"https://i.imgur.com/yXOvdOSs.jpg\" alt=\"Hedy Lamarr\" class=\"photo\"><ul> <li>发明一种新式交通信号灯 <li>排练一个电影场景 <li>改进频谱技术</ul> 此时需要将它转化为React组件,可以这么做,是的没错。 采用 *** <> 或者其他标签*** 包裹。 123456789101112131415export default function TodoList() { return ( <> <h1>海蒂·拉玛的待办事项</h1> <img src=\"https://i.imgur.com/yXOvdOSs.jpg\" alt=\"Hedy Lamarr\" className=\"photo\" /> <ul> <li>发明一种新式交通信号灯</li> <li>排练一个电影场景</li> <li>改进频谱技术</li> </ul> </> JSX语法规则 只能返回一个根元素 就是return()中必须只有一个根元素 不想在标签中增加一个额外的 ,可以用 <> 和 </> 元素来代替 标签必须闭合 使用驼峰式命名法给大部分属性命名! JSX 最终会被转化为 JavaScript,而 JSX 中的属性也会变成 JavaScript 对象中的键值对。在组件中,经常会遇到需要用变量的方式读取这些属性的时候。但 JavaScript 对变量的命名有限制。例如,变量名称不能包含 - 符号或者像 class 这样的保留字。 JSX 虽然看起来很像 HTML,但在底层其实被转化为了 JavaScript 对象,你不能在一个函数中返回多个对象,除非用一个数组把他们包装起来。这就是为什么多个 JSX 标签必须要用一个父元素或者 Fragment 来包裹。 JSX中通过大括号使用javaScriptJSX可以在JavaScript中编写类似HTML的标签,保证渲染逻辑和内容绑定。在需要在标签中添加Js逻辑或者引用标签的动态属性的时候可以在JSX的大括号里面编写Js。 JSX使用引号字符串 当你想将字符串属性传递给JSX时,放在单引号或者双引号中 当你想动态传值时,可以使用{ 和 } 替代 “ 和 “ 123456789101112131415161718192021222324// 字符串export default function Avatar() { return ( <img className=\"avatar\" src=\"https://i.imgur.com/7vQD0fPs.jpg\" alt=\"Gregorio Y. Zara\" /> );}// 变量export default function Avatar() { const avatar = 'https://i.imgur.com/7vQD0fPs.jpg'; const description = 'Gregorio Y. Zara'; return ( <img className=\"avatar\" src={avatar} alt={description} /> );} 注意:当使用src={ } 这种写法会去读取花括号中js中这个变量的值。 标签插值 JSX中允许在标签中插入大括号{}中使用变量 JSX中允许在标签中插入大括号{}中使用函数表达式 1234567891011121314151617181920212223// 变量export default function TodoList() { const name = 'Gregorio Y. Zara'; return ( <h1>{name}'s To Do List</h1> );}// 函数表达式const today = new Date();function formatDate(date) { return new Intl.DateTimeFormat( 'zh-CN', { weekday: 'long' } ).format(date);}export default function TodoList() { return ( <h1>To Do List for {formatDate(today)}</h1> );} 大括号的使用场景主要场景是字符串、数字、变量、和js表达式。 用作JSX标签*内部的文本*:<h1>{name}'s To Do List</h1> 标签***=*后面紧跟的属性**:src={avatar} 会读取 avatar 变量,但是!!!对于src=\"{avatar}\"只会传一个字符串{avatar} 双大括号的使用场景双大括号其实是{}传递对象的传递方式。 对象也用大括号表示,例如 { name: \"Hedy Lamarr\", inventions: 5 }。因此,为了能在 JSX 中传递,必须用另一对额外的大括号包裹对象:person={{ name: \"Hedy Lamarr\", inventions: 5 }}。 也可使用嵌套对象,在jsx大括号中使用。 注意:内联 style 属性 使用驼峰命名法编写。例如,HTML <ul style=\"background-color: black\"> 在你的组件里应该写成 <ul style={{ backgroundColor: 'black' }}>。 12345678910111213141516171819202122232425262728293031323334353637383940// 对象传递export default function TodoList() { return ( <ul style={{ backgroundColor: 'black', color: 'pink' }}> <li>Improve the videophone</li> <li>Prepare aeronautics lectures</li> <li>Work on the alcohol-fuelled engine</li> </ul> );}// 嵌套对象传递const person = { name: 'Gregorio Y. Zara', theme: { backgroundColor: 'black', color: 'pink' }};export default function TodoList() { return ( <div style={person.theme}> <h1>{person.name}'s Todos</h1> <img className=\"avatar\" src=\"https://i.imgur.com/7vQD0fPs.jpg\" alt=\"Gregorio Y. Zara\" /> <ul> <li>Improve the videophone</li> <li>Prepare aeronautics lectures</li> <li>Work on the alcohol-fuelled engine</li> </ul> </div> );} Props组件传递React组件使用Props互相通信。每个父组件都可以提供props给他的自组件传递信息。可以通过Props传递认识JavaScript值,包括对象、数组、和函数。 Props传递类似HTML预定义的属性。像组件传递的时候可以传递任意的props。传递的props可以通过不同的方式进行渲染。 123456789101112131415161718192021222324// 其中person就是props/* 其中的花括号也就是props传递对象而已*/// 1、传递export default function Profile() { return ( <Avatar person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }} size={100} /> );}// 2、使用/读取/* 1.以下这种方式是采用解构,将person的props解构为单独的props传递 2.prop可以存在默认值,例如下面的prop size*/function Avatar({ person, size=100 }) { // 在这里 person 和 size 是可访问的} Props的使用,必须要关注你的父子组件。需要考虑好父组件需要向自组件传的props,而子组件需要考虑传递的props通过什么方式去进行渲染。事实上,**就可以总结出来,React函数组件就是接受一个参数,那就是Props对象。** Props使用妙计 prop可以指定默认值 如果渲染时不存在size的prop,那么size将被赋值100进行渲染 其实就是size的prop属性不存在或者值undefined时会生效 但是,如果传递size={null} 或 size={0},默认值将 不 被使用。 123function Avatar({ person, size = 100 }) { // ...} 可以使用JSX展开语法传递Props 会存在需要传递pros很多,需要声明prop传递 1234567891011function Profile({ person, size, isSepia, thickBorder }) { return ( <div className=\"card\"> <Avatar person={person} size={size} isSepia={isSepia} thickBorder={thickBorder} /> </div> ); 如果存在将所有的props传递给子组件可以采用简洁展开语法,这种写法,将父组件需要传递的props全部传递。如果滥用,那么建议拆分组件。 1234567function Profile(props) { return ( <div className=\"card\"> <Avatar {...props} /> </div> );} 将JSX作为子组件传递 类似HTML标签嵌套,以下例子在父组件card中将children组件作为prop传递,类似vue插槽 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849// app.jsimport Avatar from './Avatar.js';function Card({ children }) { return ( <div className=\"card\"> {children} </div> );}export default function Profile() { return ( <Card> <Avatar size={100} person={{ name: 'Katsuko Saruhashi', imageId: 'YfeOqp2' }} /> </Card> );}// Avatar.jsimport { getImageUrl } from './utils.js';export default function Avatar({ person, size }) { return ( <img className=\"avatar\" src={getImageUrl(person)} alt={person.name} width={size} height={size} /> );}// utils.jsexport function getImageUrl(person, size = 's') { return ( 'https://i.imgur.com/' + person.imageId + size + '.jpg' );} 注意一个组件可能会随着时间的推移收到不同的 props。 Props 并不总是静态的!Props 反映了组件在任何时间点的数据,并不仅仅是在开始时。 然而,props 是 不可变的(一个计算机科学术语,意思是“不可改变”)。当一个组件需要改变它的 props(例如,响应用户交互或新数据)时,它不得不“请求”它的父组件传递 不同的 props —— 一个新对象!它的旧 props 将被丢弃,最终 JavaScript 引擎将回收它们占用的内存。不要尝试“更改 props”。 条件渲染组件中会存在很多根据条件渲染的内容。在React中可以使用js中的if 语句、&& 和 ? : 运算符来选择性地渲染 JSX。 如何根据条件返回JSX根据组件Prop传入的属性值来动态展示内容 使用 if/else 语句 去判断 12345678910111213141516171819202122232425262728293031// 其中isPacked是组件接受的Prop// 用来动态显示 不同li内容的展示// 可以选择 return 的组件为null 那么他将不会显示任何内容function Item({ name, isPacked }) { if (isPacked) { return <li className=\"item\">{name} ✔</li>; } return <li className=\"item\">{name}</li>;}export default function PackingList() { return ( <section> <h1>Sally Ride 的行李清单</h1> <ul> <Item isPacked={true} name=\"宇航服\" /> <Item isPacked={true} name=\"带金箔的头盔\" /> <Item isPacked={false} name=\"Tam 的照片\" /> </ul> </section> );} 选择性包含JSX 一般的不同条件会return不同的HTML片段,此刻可以选择包含JSX的书写,一般使用以下运算符: 1、三目运算符(? :)123456789101112if (isPacked) { return <li className=\"item\">{name} ✔</li>;}return <li className=\"item\">{name}</li>;// 以上代码使用三目运算符修改为return ( <li className=\"item\"> {isPacked ? name + ' ✔' : name} </li>); **注意:**简单的内容展示适合使用三目这种运算符来展示,如果展示的逻辑较为复杂,可选择提取自组件来渲染。 2、与运算符(&&)1234567// 当 JavaScript && 表达式 的左侧(我们的条件)为 true 时,它则返回其右侧的值(在我们的例子里是勾选符号)。但条件的结果是 false,则整个表达式会变成 false。在 JSX 里,React 会将 false 视为一个“空值”,就像 null 或者 undefined,这样 React 就不会在这里进行任何渲染return ( <li className=\"item\"> {name} {isPacked && '✔'} </li>); 注意:切勿将数字放在 && 左侧. JavaScript 会自动将左侧的值转换成布尔类型以判断条件成立与否。然而,如果左侧是 0,整个表达式将变成左侧的值(0),React 此时则会渲染 0 而不是不进行渲染。 例如,一个常见的错误是 messageCount && <p>New messages</p>。其原本是想当 messageCount 为 0 的时候不进行渲染,但实际上却渲染了 0。 为了更正,可以将左侧的值改成布尔类型:messageCount > 0 && <p>New messages</p>。 选择性赋值JSX 1、可以选择将需要渲染的JSX表达式文本赋值给变量123456789101112131415161718192021222324252627282930313233343536// 其中 渲染的JSX文本 itemContent赋值变量function Item({ name, isPacked }) { let itemContent = name; if (isPacked) { itemContent = name + \" ✔\"; } return ( <li className=\"item\"> {itemContent} </li> );}export default function PackingList() { return ( <section> <h1>Sally Ride 的行李清单</h1> <ul> <Item isPacked={true} name=\"宇航服\" /> <Item isPacked={true} name=\"带金箔的头盔\" /> <Item isPacked={false} name=\"Tam 的照片\" /> </ul> </section> );} 2、对于任意的JSX也是适用的123456789101112131415161718192021222324252627282930313233343536373839// 对于JSX itemContent 提取为变量 在return时便可直接使用变量 迭代修改时容易扩展function Item({ name, isPacked }) { let itemContent = name; if (isPacked) { itemContent = ( <del> {name + \" ✔\"} </del> ); } return ( <li className=\"item\"> {itemContent} </li> );}export default function PackingList() { return ( <section> <h1>Sally Ride 的行李清单</h1> <ul> <Item isPacked={true} name=\"宇航服\" /> <Item isPacked={true} name=\"带金箔的头盔\" /> <Item isPacked={false} name=\"Tam 的照片\" /> </ul> </section> );} 渲染列表一般来说,我们经常通过JavaScript数组方法来操作数组中的数据,从而将数组中的数据渲染为多个类似的组件。 遍历渲染123456789101112131415161718192021222324252627282930// 首先我们拥有一个数组数据。const people = [ '凯瑟琳·约翰逊: 数学家', '马里奥·莫利纳: 化学家', '穆罕默德·阿卜杜勒·萨拉姆: 物理学家', '珀西·莱温·朱利亚: 化学家', '苏布拉马尼扬·钱德拉塞卡: 天体物理学家',];// 我们可以使用数组的遍历方法map这个数组来遍历元素const listItem = people.map(person=><li>{person}</li>) // 用ul将listItem包围return <ul>{listItem}</ul>// 最终代码const people = [ '凯瑟琳·约翰逊: 数学家', '马里奥·莫利纳: 化学家', '穆罕默德·阿卜杜勒·萨拉姆: 物理学家', '珀西·莱温·朱利亚: 化学家', '苏布拉马尼扬·钱德拉塞卡: 天体物理学家',];export default function List() { const listItems = people.map(person => <li>{person}</li> ); return <ul>{listItems}</ul>;} 注意:这样会导致没有唯一key报错。 过滤渲染我们有个需求需要将职业为化学家的元素渲染出来 12345678910111213141516171819202122232425262728293031323334353637383940// 数组const people = [ { id: 0, name: '凯瑟琳·约翰逊', profession: '数学家', }, { id: 1, name: '马里奥·莫利纳', profession: '化学家', }, { id: 2, name: '穆罕默德·阿卜杜勒·萨拉姆', profession: '物理学家', }, { name: '珀西·莱温·朱利亚', profession: '化学家', }, { name: '苏布拉马尼扬·钱德拉塞卡', profession: '天体物理学家', },];// 采用数组filter方法进行过滤const liItem = people.filter(person=>person.profession==='化学家')// 用ul将liIte包围return <ul>{liItem}</ul>// 完整代码export default function List() { const listItems = people.map(person => <li>{person}</li> ); return <ul>{listItems}</ul>;} 用key能保证渲染顺序直接放在 map() 方法里的 JSX 元素一般都需要指定 key 值! 这些key会告诉React,每个组件对应的数组数据那一项,去进行匹配。 这些将在数组项进行移动、插入或者删除等操作是非常重要的。合适的key将保证React正常的更新DMO树。 key值在渲染之前,要存在数据里面key必须存在。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869// app.jsimport { people } from './data.js';import { getImageUrl } from './utils.js';export default function List() { const listItems = people.map(person => <li key={person.id}> <img src={getImageUrl(person)} alt={person.name} /> <p> <b>{person.name}</b> {' ' + person.profession + ' '} 因{person.accomplishment}而闻名世界 </p> </li> ); return <ul>{listItems}</ul>;}// data.jsexport const people = [ { id: 0, // 在 JSX 中作为 key 使用 name: '凯瑟琳·约翰逊', profession: '数学家', accomplishment: '太空飞行相关数值的核算', imageId: 'MK3eW3A', }, { id: 1, // 在 JSX 中作为 key 使用 name: '马里奥·莫利纳', profession: '化学家', accomplishment: '北极臭氧空洞的发现', imageId: 'mynHUSa', }, { id: 2, // 在 JSX 中作为 key 使用 name: '穆罕默德·阿卜杜勒·萨拉姆', profession: '物理学家', accomplishment: '关于基本粒子间弱相互作用和电磁相互作用的统一理论', imageId: 'bE7W1ji', }, { id: 3, // 在 JSX 中作为 key 使用 name: '珀西·莱温·朱利亚', profession: '化学家', accomplishment: '开创性的可的松药物、类固醇和避孕药', imageId: 'IOjWm71', }, { id: 4, // 在 JSX 中作为 key 使用 name: '苏布拉马尼扬·钱德拉塞卡', profession: '天体物理学家', accomplishment: '白矮星质量计算', imageId: 'lrWQx8l', },];// util.jsexport function getImageUrl(person) { return ( 'https://i.imgur.com/' + person.imageId + 's.jpg' );} 如何在列表项显示多个DOM节点?? 我需要将每个列表多渲染其他DOM节点。使用Fragment的语法简写<></>无法接受key值。写法存在要不使用包裹,或者使用明确的<Fragment>。 Fragment标签不会出现在DOM上,代码会转成最终的<h1>、<p>、<h1>、<p>…… 的列表。 12345678910import { Fragment } from 'react';// ...const listItems = people.map(person => <Fragment key={person.id}> <h1>{person.name}</h1> <p>{person.bio}</p> </Fragment>); 如何设置key不同来源数据对应不同的key值获取方式: 来自数据库的数据:数据存在唯一的主键,可以直接使用。 本地产生的数据:可以使用自增计数器或者uuid的库生成唯一的key。 key值需要满足的条件 key在兄弟节点必须是唯一的。不需要全局唯一,不同数组可以使用相同的key。 key值不可改变。一定不可以在渲染时候的动态生成key。 为什么需要key??React的key作用,其实都是可以从众多兄弟元素中能够标识出某一项(JSX节点)。这个提供的key提供的信息不止是这个元素所在的位置。即使元素的位置在渲染工程中发现了改变,它对应的key能够让React在整个生命周期一直可以使用。 注意: 不可以直接使用数组的索引作为key值来使用。如果没有显式指定key,React会默认讲索引作为key。当数组项的顺序在插入、删除或者重新排序等操作中发生改变。此时会出现一些难以排查的bug。 不要使用随机数去动态生成key,这样会导致每次渲染key不一样,React这样会认为你在频繁修改DOM,将会导致所有的组件和DOM元素重新渲染。会导致渲染问题,还会导致用户输入丢失。一定要保证key值的稳定性。 组件不会将key当作props的一部分。key值只会对React起到提示作用。 什么是JSXJSX 是 JavaScript 语法扩展,可以让在 JavaScript 文件中书写类似 HTML 的标签。JSX 看起来和 HTML 很像,但它的语法更加严格并且可以动态展示信息。 为什么出现JSX随着Web交互性越来越强,逻辑开始决定页面的内容。也就是可以说JavaScript负责HTML内容。因此,在React中,渲染逻辑和标签存在同一个组件文件中。 使用JSX的优势例如将一个按钮的渲染逻辑和标签放在一起,可以确保他们在编辑的时候保持同步。反之,也可以说彼此无关的细节是隔离的。 JSX与HTML的区别每个 React 组件都是一个 JavaScript 函数,它会返回一些标签,React 会将这些标签渲染到浏览器上。React 组件使用一种被称为 JSX 的语法扩展来描述这些标签。JSX对于HTML来说,语法更加严格并且可以动态的展示信息。 ***JSX and React 是相互独立的东西。通常配合使用,也可以单独使用它们中的任意一个,JSX 是一种语法扩展,而 React 则是一个 JavaScript 的库。 *** HTML转换为JSX现在存在一个html标签 1234567891011<h1>海蒂·拉玛的待办事项</h1><img src=\"https://i.imgur.com/yXOvdOSs.jpg\" alt=\"Hedy Lamarr\" class=\"photo\"><ul> <li>发明一种新式交通信号灯 <li>排练一个电影场景 <li>改进频谱技术</ul> 此时需要将它转化为React组件,可以这么做,是的没错。 采用 *** <> 或者其他标签*** 包裹。 123456789101112131415export default function TodoList() { return ( <> <h1>海蒂·拉玛的待办事项</h1> <img src=\"https://i.imgur.com/yXOvdOSs.jpg\" alt=\"Hedy Lamarr\" className=\"photo\" /> <ul> <li>发明一种新式交通信号灯</li> <li>排练一个电影场景</li> <li>改进频谱技术</li> </ul> </> JSX语法规则 只能返回一个根元素 就是return()中必须只有一个根元素 不想在标签中增加一个额外的 ,可以用 <> 和 </> 元素来代替 ***JSX 虽然看起来很像 HTML,但在底层其实被转化为了 JavaScript 对象,你不能在一个函数中返回多个对象,除非用一个数组把他们包装起来。这就是为什么多个 JSX 标签必须要用一个父元素或者 Fragment 来包裹。 *** 标签必须闭合 使用驼峰式命名法给大部分属性命名! JSX 最终会被转化为 JavaScript,而 JSX 中的属性也会变成 JavaScript 对象中的键值对。在组件中,经常会遇到需要用变量的方式读取这些属性的时候。但 JavaScript 对变量的命名有限制。例如,变量名称不能包含 - 符号或者像 class 这样的保留字。 JSX中通过大括号使用javaScriptJSX可以在JavaScript中编写类似HTML的标签,保证渲染逻辑和内容绑定。在需要在标签中添加Js逻辑或者引用标签的动态属性的时候可以在JSX的大括号里面编写Js。 JSX使用引号字符串 当你想将字符串属性传递给JSX时,放在单引号或者双引号中 当你想动态传值时,可以使用{ 和 } 替代 “ 和 “ 123456789101112131415161718192021222324// 字符串export default function Avatar() { return ( <img className=\"avatar\" src=\"https://i.imgur.com/7vQD0fPs.jpg\" alt=\"Gregorio Y. Zara\" /> );}// 变量export default function Avatar() { const avatar = 'https://i.imgur.com/7vQD0fPs.jpg'; const description = 'Gregorio Y. Zara'; return ( <img className=\"avatar\" src={avatar} alt={description} /> );} 注意:当使用src={ } 这种写法会去读取花括号中js中这个变量的值。 标签插值 JSX中允许在标签中插入大括号{}中使用变量 JSX中允许在标签中插入大括号{}中使用函数表达式 1234567891011121314151617181920212223// 变量export default function TodoList() { const name = 'Gregorio Y. Zara'; return ( <h1>{name}'s To Do List</h1> );}// 函数表达式const today = new Date();function formatDate(date) { return new Intl.DateTimeFormat( 'zh-CN', { weekday: 'long' } ).format(date);}export default function TodoList() { return ( <h1>To Do List for {formatDate(today)}</h1> );} 大括号的使用场景主要场景是字符串、数字、变量、和js表达式。 用作JSX标签*内部的文本*:<h1>{name}'s To Do List</h1> 标签***=*后面紧跟的属性**:src={avatar} 会读取 avatar 变量,但是!!!对于src=\"{avatar}\"只会传一个字符串{avatar} 双大括号的使用场景双大括号其实是{}传递对象的传递方式。 对象也用大括号表示,例如 { name: \"Hedy Lamarr\", inventions: 5 }。因此,为了能在 JSX 中传递,必须用另一对额外的大括号包裹对象:person={{ name: \"Hedy Lamarr\", inventions: 5 }}。 也可使用嵌套对象,在jsx大括号中使用。 注意:内联 style 属性 使用驼峰命名法编写。例如,HTML <ul style=\"background-color: black\"> 在你的组件里应该写成 <ul style={{ backgroundColor: 'black' }}>。 12345678910111213141516171819202122232425262728293031323334353637383940// 对象传递export default function TodoList() { return ( <ul style={{ backgroundColor: 'black', color: 'pink' }}> <li>Improve the videophone</li> <li>Prepare aeronautics lectures</li> <li>Work on the alcohol-fuelled engine</li> </ul> );}// 嵌套对象传递const person = { name: 'Gregorio Y. Zara', theme: { backgroundColor: 'black', color: 'pink' }};export default function TodoList() { return ( <div style={person.theme}> <h1>{person.name}'s Todos</h1> <img className=\"avatar\" src=\"https://i.imgur.com/7vQD0fPs.jpg\" alt=\"Gregorio Y. Zara\" /> <ul> <li>Improve the videophone</li> <li>Prepare aeronautics lectures</li> <li>Work on the alcohol-fuelled engine</li> </ul> </div> );} Props组件传递React组件使用Props互相通信。每个父组件都可以提供props给他的自组件传递信息。可以通过Props传递认识JavaScript值,包括对象、数组、和函数。 Props传递类似HTML预定义的属性。像组件传递的时候可以传递任意的props。传递的props可以通过不同的方式进行渲染。 123456789101112131415161718192021222324// 其中person就是props/* 其中的花括号也就是props传递对象而已*/// 1、传递export default function Profile() { return ( <Avatar person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }} size={100} /> );}// 2、使用/读取/* 1.以下这种方式是采用解构,将person的props解构为单独的props传递 2.prop可以存在默认值,例如下面的prop size*/function Avatar({ person, size=100 }) { // 在这里 person 和 size 是可访问的} Props的使用,必须要关注你的父子组件。需要考虑好父组件需要向自组件传的props,而子组件需要考虑传递的props通过什么方式去进行渲染。事实上,**就可以总结出来,React函数组件就是接受一个参数,那就是Props对象。** Props使用妙计 prop可以指定默认值 如果渲染时不存在size的prop,那么size将被赋值100进行渲染 其实就是size的prop属性不存在或者值undefined时会生效 但是,如果传递size={null} 或 size={0},默认值将 不 被使用。 123function Avatar({ person, size = 100 }) { // ...} 可以使用JSX展开语法传递Props 会存在需要传递pros很多,需要声明prop传递 1234567891011function Profile({ person, size, isSepia, thickBorder }) { return ( <div className=\"card\"> <Avatar person={person} size={size} isSepia={isSepia} thickBorder={thickBorder} /> </div> ); 如果存在将所有的props传递给子组件可以采用简洁展开语法,这种写法,将父组件需要传递的props全部传递。如果滥用,那么建议拆分组件。 1234567function Profile(props) { return ( <div className=\"card\"> <Avatar {...props} /> </div> );} 将JSX作为子组件传递 类似HTML标签嵌套,以下例子在父组件card中将children组件作为prop传递,类似vue插槽 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849// app.jsimport Avatar from './Avatar.js';function Card({ children }) { return ( <div className=\"card\"> {children} </div> );}export default function Profile() { return ( <Card> <Avatar size={100} person={{ name: 'Katsuko Saruhashi', imageId: 'YfeOqp2' }} /> </Card> );}// Avatar.jsimport { getImageUrl } from './utils.js';export default function Avatar({ person, size }) { return ( <img className=\"avatar\" src={getImageUrl(person)} alt={person.name} width={size} height={size} /> );}// utils.jsexport function getImageUrl(person, size = 's') { return ( 'https://i.imgur.com/' + person.imageId + size + '.jpg' );} 注意一个组件可能会随着时间的推移收到不同的 props。 Props 并不总是静态的!Props 反映了组件在任何时间点的数据,并不仅仅是在开始时。 然而,props 是 不可变的(一个计算机科学术语,意思是“不可改变”)。当一个组件需要改变它的 props(例如,响应用户交互或新数据)时,它不得不“请求”它的父组件传递 不同的 props —— 一个新对象!它的旧 props 将被丢弃,最终 JavaScript 引擎将回收它们占用的内存。不要尝试“更改 props”。","tags":[{"name":"react","slug":"react","permalink":"https://ayozoo.github.io/tags/react/"}]},{"title":"http vs websocket","date":"2023-08-27T13:22:45.000Z","path":"2023/08/27/ websocket/","text":"通信串行通信中,数据通常是在俩个终端之间进行传送,根据数据流的传输方向分为以下三种基本传送方式:单工、半双工和全双工。 基本区别为: 单工:单工通信只有一根数据线,通信只在一个方向上进行,这种方式的应用实例有:监视器、打印机、电视机等。 半双工:半双工通信也只有一根数据线,它也单工的区别是这根数据线既可作发送又可作发接收,虽然数据可在两个方向上传送,但通信双方不能同时收发数据。http协议采用的就是这个通信方式 全双工: 数据的发送和接收用两根不同的数据线,通信双方在同一时刻都能进行发送和接收,这一工作方式称为全双工通信。在这种方式下,通信双方都有发送器和接收器,发送和接收可同时进行,没有时间延迟。websocket采用的就是这个通信方式 http http协议主要关注的是 客户端——>服务器(获取资源) 特点:无状态协议; 每个请求都是独立的; 请求应答模式,服务器无法主动给客户端推送消息(单工,半双工,全双工) http受浏览器同源策略的影响 websocket双向通信(全双工协议)每次不需要重新建立连接,可以一直相互通信 不使用websocket 以前的双向通信的实现方式Comet,主要是为了是实现服务端可以像客户端桶送数据,为了继绝实时性比较高的情况。 1.轮询(客户端定期向服务端发送请求采用方式,前端setInterval定时器发送请求)轮询会在的问题: 轮询方式会存在竞速问题,无法保证请求的先后顺序,可能会存在多个请求返回的结果同时修改资源。 频繁的网络请求 会导致服务器负荷增加 同时频繁的客户端发请求也会影响客户端性能问题 http 发送的时候 会增加http报文(headers、鉴权、内容类型) 会出现额外的数据消耗 实时性比较低 定时轮询前端定时轮询(定时器发请求)无法处理即时处理的请求轮询的优点 容易实现 不适合实时性比较高的,低并发 2.长轮询(前端接口递归调用) 想解决短轮询的缺点(想将实时性更强)长轮询存在的问题 实时性强了,同时也造成了更多的网络请求 链接堆积问题,链接需要在服务端中保持打开,占有服务器资源(前端需要大量数据从服务端访问,会一直从服务端获取)优点 实时性强了,但是要求服务端的并发能力要强 3.iframe流(使用ifream存在的沙箱模式)存在的问题 单通信(服务端直接推送客户端消息)优点 具有实时性,且不需要客户端和服务端频繁发请求 4.sse EventSource(html提供的,单项通信,客户端可以监控服务端推送的事件。只能推送文本消息,适合小数据)123456789101112131415161718192021222324// 服务端代码 app.get('/clock',function(res,req){ // 这里表明服务器传递的是时间流 res.setHeader('Content-Type','text/event-stream'); setInterval(()=>{ // 和http协议一样,按照行的方式传输 // Content-Type:xxx // Authorization:xxx res.write(`data:hello\\n\\n`) },1000) }) // 客户端代码 script中const eventsource = new EventSource('xxx接口地址')eventsource.onopen = function(){ console.log('Connection opened');}// 发送消息eventsource.onmessage = function(e){ console.log(e.data)}存在的问题- 单项传输,客户端无法给服务端传递数据 5.webSocket(h5提供的api)优点 双向通信 持久连接 发送的消息增加帧是非常小的 支持多种数据格式 天生支持跨域 1234567891011121314151617181920212223242526272829303132333435// 客户端代码const ws = new Websocket('ws://loacalhost:3000')ws.onopen = function(){ console.log('Content opened') ws.send('hello Serve')}ws.onmessage = function(e){ console.log('服务器响应数据:'+ e.data)}// 服务端代码import express from 'express'import http from 'http'// webSocket首先基于http协议import { WebSocket } from 'ws'const wsServer = new WebSocket({ server })wsServer.on('connection',(ws)=>{ console.log('Connection opend') // 给客户端发送消息 ws.send('hellow client') ws.on('message',(message)=>{ console.log('客户端发送的数据:' + message) })})server.listen(3000) 拓展: 协议的表示方式? 以http为例子就是要了解http各种header的使用怎么实现握手的,数据长什么样子,怎么通信 查看网络得知:webSocket协议 请求行显示:请求方式 GET ws://localhost:3000 协议版本 HTTP/1.1 Connection : Upgrade Upgrade: websocket 升级的协议是什么 Sec-Websocket-Version: 13 协议的版本 生成Sec-Websocket-Key:用于生成唯一的,保证安全的websocket连接 防止恶意连接 可以用于握手 Sec-Websocket-Accept 是根据key算出来的 表示握手成功 通过wireshark工具可以抓包,了解会生成key->·GBUN9IA5TYXPYgQehlxEUw== 握手的时候创建一个随机的keyaccept-> TpUkC2LowejLbA6ZRgwSL8Rk4FI= 服务端要响应一个值 每一次的key都不一样,采用以下方法创建安全的握手连接 1234567// 加密库import crypto from 'crypto'const number = '258EAFA5-E914-47DA-95CA-C5ABODC85B11const'; const websocketKey ='GBIN9IA5TYXPYgQehlxEUw=='// 采用hah算法生成更新生成摘要输出base64格式 响应给客户端const websocketAccept = crypto.createHash( 'sha1' ).update(websocketKey + number).digest( 'base64' ) 完整的握手过程 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748// 服务端代码import net from 'net'// 可以接受原始的消息// 每个人连接都会产生一个socketconst server = net.createServer(function(socket){ // 客户端发消息 先握手 socket.once('data',function(data){ // 发送的报文 data /** * data包含 GET / HTIP/1.1 *请求行* Host: localhost:3000 Connection: UpgradePragma: no-cacheCache Control: no-cacheUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac 0S X 10 15 7) AppleWebKit/537.36 (KHTMLlike Gecko) Chrome/115.0.0.0 Safari/537.36Upgrade: websocket 0rigin: http://127.0.0.1:5500 Sec WebSocket-Version: 13 Accept-Encoding: gzip,deflate,brAccept-Language: zh-CN, zh;g=0.9Sec-WebSocket-Key: LFD4X3DrVLhObMnKL0b5K0 Sec-WebSocket-Extensions: permessage-deflate; client max window bits */ data = data.toString() // 说明要升级成websocket协议 再报文中读取是否已经是websocket协议 if(data.match(/Upgrade:websocket/)){ /** * 在抓包工具中读取到 key用来解析 ***在报文中都是字符串存在的*** Host; localhost:3000\\r\\n Connection: Upgrade rn Pragma: no-cache rin Cache Control: nocache\\rin User-Agent; Mozilla/5.0 (Macintosh; Intel Mac 05 X 10 15 7) AppleWebkit/537,36 (KHTML, like Gecko Upgrade: websocket r\\n 0rigin: http://127.0.0.1:5500\\r\\n Sec-WebSocket-Version: 13r'nAccept-Encoding: gzip, deflate, bririnAccept-Language: zh-CN zh;a=0.9\\r\\n Sec-WebSocket-Key:P2P2F9kEf/wg18RkzXM8eA==\\rin Sec-WebSocket-Extensions: permessage-deflate; client max window bits'rinrin */ let rows = data.split('\\r\\n') console.log(rows) /** * rows 打印得出 GETHTTP/1.1Host: localhost:3000'Connection: Uggrade'Pragma; no-cache'Cache-Control; no-cache'\"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac 0S X 10 15 7) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/115.0.0.0 Safari/537.36''Uparade: websocket' */ } })})server.listen(3000,function(){ console.log('server start port 3000')})","tags":[]},{"title":"webpack、vite的自动导包的\"爱恨情仇\"","date":"2022-10-19T13:21:48.000Z","path":"2022/10/19/ webpack、vite的自动导包的-爱恨情仇/","text":"webpack、vite的自动导包的”爱恨情仇”前言最近在开发Vue3+Vite的统一基座工程,遇到了模块自动导入的不同构建工具的引入方式,特此记录。 在项目中,为了避免频繁导入。大家都会在对应,例如项目使用Webpack 打包工具的。在其项目store文件下其中的index.js 文件中使用webpack提供的apirequire.context的方法实现自动导入。 ES 模块规范在实现自动导入模块前,我们先了解下ES 模块规范。 所谓的ES模块规范,即 JavaScript 的标准模块系统,它允许您使用 import 和 export 关键字来导入和导出模块。这是现代 JavaScript 中推荐使用的模块化方式。 在ES模块规范中,提供了import.meta.glob功能。它允许在项目运行中动态匹配特定模式的模块。例如以下代码就可以动态匹配到module模块下的所有js文件。 1import.meta.glob('./module/*.js') 话不多说,上干货例如,你的项目sotre文件目录为 12345678- store - module - module1.js - module2.js - ... - moduleN.js - main.js 其中module下的moduleX文件你的main.js为你的sotre入口文件, webpack中的自动导入的实现方式在main.js入口文件中你可以使用webpack提供require.context的方法实现module文件目录下的模块自动导入。 1234567const files = require.context('./model', false, /\\.js$/)const modules = {}files.keys().forEach((key) => { modules[key.replace(/(\\.\\/|\\.js)/g, '')] = files(key).default})export default modules 其中files.keys()是require.context方法返回的一个函数,它会返回一个包含所有匹配模块路径的数组。 require.context是Webpack提供的一个方法,它允许你在构建时动态地导入模块。该方法接收三个参数: directory: 表示要搜索的目录路径。 useSubdirectories: 表示是否搜索子目录。 regExp: 表示匹配文件的正则表达式。 在上面代码中,files就是通过require.context动态导入了./model目录下所有的.js文件,并使用正则表达式/\\.js$/来匹配文件。这样,files.keys()返回一个包含所有匹配模块路径的数组。例如控制台打印files.keys()可获得以下数组 123456[ \"./module1.js\", \"./module2.js\", \"./。。。.js\", \"./moduleN.js\"] 接着,我们可以使用.forEach()遍历这个数组,对每个匹配的模块进行处理,提取模块名,并将模块添加到modules对象中。这样,你就得到了一个以模块名为键、模块对象为值的modules对象,它包含了所有从./model目录中动态导入的模块。 vite自动导入的实现方式在main.js入口文件中你可以使用ES模块规范提供的import.meta.glob方法来获取特定模块的匹配模式,来实现自动导入。 12345678910111213141516171819// main.jsconst modules = {};async function importAllModules() { const files = import.meta.glob('./module/*.js'); for (const path in files) { const key = path.replace(/\\.\\/module\\/|\\.js/g, ''); const module = await files[path](); modules[key] = module.default; }}importAllModules().then(() => { console.log(modules); // 所有模块已导入并组合成对象});// 现在可以导出组合后的 modules 对象供其他地方使用export default modules; 在这种情况下,使用异步操作是因为模块导入是一个异步操作。import.meta.glob()方法会返回一个对象,该对象的键是匹配到的文件路径,值是一个函数,调用该函数将异步导入对应的模块。因此,我们需要使用异步操作来等待模块导入完成,然后再将其添加到modules对象中。 如果我们不使用异步操作,而是直接将模块导入的结果添加到module对象中,那么由于模块导入是异步的,modules对象可能在模块导入完成之前被导出,导致modules对象不完整或为空。使用异步操作可以确保在所有模块导入完成后再导出modules对象,保证其包含所有模块导出的内容。 总结为什么都是打包工具,会有不同的自动导入方式呢?在使用 Webpack 的情况下,与使用 Vite 或其他原生支持 ES 模块的项目相比,自动导入模块的实现会有一些区别。主要区别在于 Webpack 不支持 import.meta.glob,因为它是 ES 模块的一个特殊功能,而 Webpack 是一个打包工具,不完全符合 ES 模块的规范。 那这里就浅浅对比下俩种打包工具的差异吧webpack 成熟度高。Webpack是一个成熟且应用相当广泛的打包构建工具,具有强大的生态系统和社区支持。 打包速度较慢。Webpack打包速度在大型项目打包速度比较慢,从入口文件开始,基于代码中的import、export、require构建依赖树,将所有的模块打包到一个或者几个少数文件中。因此,项目规模庞大的话,启动和热更新更慢。每次代码变更构建都需要生成新的Bundle文件。 配置复杂。官网配置很多,需要处理不同的Loader和Plugin来管理不同的资源文件。 插件系统丰富。Webpack具有强大的插件系统,允许开发者根据需求扩展定义。 Tree Shaking。Webpack通过使用UglifyJS等工具进行Tree Shaking,消除未使用的代码 热模块替换(HMR)。Webpack支持热模块替换,但在某些情况下需要手动配置。 Vite 新兴技术。Vite是一个相较新的构建工具,旨在提供更快的开发体验和构建速度。 打包速度极快。Vite在开发环境下具有极快的启动和热更新速度,因为它采用了原生ES模块的方式,并且将依赖项保持为独立的文件,而不是打包到一个大文件中。 配置简单。Vite的配置比Webpack简单,尤其是对于常见的项目结构,大部分任务都无需额外配置。 热模块替换(HMR)。Vite对热模块替换的支持非常好,在开发过程中几乎不需要手动配置即可实现HMR。 Tree Shaking。Vite使用Rollup进行Tree Shaking,这使得未使用的代码更容易被消除。 插件系统还不够完善。Vite的插件系统仍在发展中,目前没有Webpack那么丰富的插件支持。 总之,Webpack更加适合与大型、复杂项目的构建工作,拥有成熟和完善的生态系统和社区。Vite更适合用于一些热更新快速的程序。","tags":[{"name":"前端构建工具","slug":"前端构建工具","permalink":"https://ayozoo.github.io/tags/%E5%89%8D%E7%AB%AF%E6%9E%84%E5%BB%BA%E5%B7%A5%E5%85%B7/"}]},{"title":"vue3+vite环境变量踩雷","date":"2022-07-18T12:12:44.000Z","path":"2022/07/18/ vue3-vite环境变量踩雷/","text":"vue3 + vite 环境变量踩坑前言众所周知,项目搭建过程中,存在环境的区分。一般项目中都会存在env的俩个配置环境变量的配置文件。… 例如以下生产环境、开发环境的环境变量文件: 1234567891011121314//.env.development 生产环境变量文件# 开发环境配置ENV = 'development'# 开发环境VUE_APP_BASE_API = '/dev-api'// .env.production# 生产环境配置ENV = 'production'# 生产环境VUE_APP_BASE_API = '/web-api' 然后在package.json中配置对应环境启动命令用以启动不同环境 1234\"scripts\": { \"dev\": \"vue-cli-service --mode development\", \"build\": \"vue-cli-service build --mode production\", }, 什么是环境变量?根据代码运行环境变化而变化的变量就是所谓的环境变量。 在生产环境和开发环境中最多用以区分接口公共BASE_URL_API路径来请求不同接口。 环境变量一般在全局可以访问得到。在vue项目中,常见的环境变量访问方式是以下方式进行获取的: 12345// webpack.config.jsmodule.exports = { mode: process.env.NODE_ENV === 'production' ? 'production' : 'development'} 其中的process.env.NODE_ENV就是环境变量,他是Node.js提供的API,用以返回获取当前Shall(操作系统和运行环境)所有的环境变量。 vue2+webPack 与 vue3+vite项目搭建获取环境变量的区别:webpack、vite相似之处配置都是按照前言所述,创建对应的环境变量的env文件,配置package.json文件中对应环境的启动命令。 webpack中的环境变量在vue2项目中,webpack做了处理,使得浏览器可以直接识别获取到node环境的process.env变量。 创建方式:配置在项目中env环境变量配置文件中,webpack环境变量可随意命名 获取方式:可以通过***process.env***获取 所以在页面中可以获取相应的环境变量: 123//这里就是获取启动或者打包的命令获取对应的配置文件中的变量 console.log('当前环境是:',process.env.NODE_ENV) vite中的环境变量 创建方式:vite中环境创建存在俩种,都是在项目中env环境变量配置文件中配置: 创建以VITE_开头的环境变量 123456789# 开发环境配置NODE_ENV ='development'# 开发环境VITE_APP_BASE_API = '/dev-api'VITE_APP_URL = \"http://193.1.1.107:8042\"VITE_APP_BASE_PORT = \"80\" 更换VITE_前缀 以 envPrefix 开头的环境变量会通过 import.meta.env 暴露在你的客户端源码中。参考官网: [共享配置]: https://vitejs.cn/vite3-cn/config/shared-options.html#envdir “VIte官方文档” 以 envPrefix 开头的环境变量会通过 import.meta.env 暴露在你的客户端源码中。 类型: string | string[] 默认: VITE_ 安全注意事项 envPrefix 不应被设置为空字符串 ‘ ‘ ,这将暴露你所有的环境变量,导致敏感信息的意外泄漏。 检测到配置为 ‘ ‘ 时 Vite 将会抛出错误. 获取方式: Vite是在 import.meta.env对象上暴露当前Shall的环境变量。 以下是获取对象中常用的内在变量: 1234567891011// 应用运行的模式。import.meta.env.MODE: {string} // 部署应用时的基本 URL。他由base配置项决定。import. meta.env.BASE_URL: {string} // 应用是否运行在生产环境。import. meta.env.PROD: {boolean}// 应用是否运行在开发环境 (永远与 import.meta.env.PROD相反)。import. meta.env.DEV: {boolean} // 应用是否运行在 import.meta.env.SSR: {boolean} server 上。 注意:以上的变量存在于运行环境中,对应的vite.config中获取不到。 在 Vite 的配置文件 vite.config.js 中,默认是不加载 .env 文件中的环境变量的。因为正常情况下,只有在评估完 Vite 配置之后,才知道哪些文件需要被加载,比如之前我们提到的root和envDir都会影响到加载的行为。不管如何,如果想要在配置中加载环境变量,可以使用 loadEnv 去加载环境变量。 1234567891011121314151617181920212223242526272829303132333435363738394041import { defineConfig, loadEnv } from \"vite\";import vue from \"@vitejs/plugin-vue\";import path from \"path\";// https://vitejs.dev/config/export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), \"\"); console.log(env.VITE_APP_BASE_API); const resolve = (dir) => path.resolve(__dirname, dir); return { base: \"./\", publicPath: resolve(\"./static\"), assetsPublicPath: \"./\", plugins: [vue()], extensions: [\".js\", \".ts\", \".jsx\", \".tsx\", \".json\", \".vue\"], resolve: { alias: { \"@\": resolve(\"src\"), }, }, // 本地运行配置,及反向代理配置 server: { host: \"127.0.0.1\", port: 80, open: false, strictPort: false, https: false, cors: true, proxy: { // 代理规则直接放在 server 对象下,而非嵌套的 server 对象中 [env.VITE_APP_BASE_API]: { target: \"http://172.1.1.210:8080\", changeOrigin: true, rewrite: (path) => path.replace(new RegExp(\"^\" + env.VITE_APP_BASE_API), \"/web-api\"), // 替换 /dev-api 为 target 接口地址 }, }, }, };}); 注意,mode必须解构后传入loadEnv mode变量中","tags":[{"name":"前端构建工具","slug":"前端构建工具","permalink":"https://ayozoo.github.io/tags/%E5%89%8D%E7%AB%AF%E6%9E%84%E5%BB%BA%E5%B7%A5%E5%85%B7/"}]}]