[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"sidebar-data":3,"post-一文掌握javascript事件循环机制-event-loop":351,"comments-POST-93de6c59-fc6d-49d6-877d-4405ab187652-1":364},{"categories":4,"tags":93,"postCount":277,"tagCount":278,"hotPosts":279},[5,13,20,27,33,40,47,51,58,62,66,72,78,83,87],{"id":6,"name":7,"slug":8,"description":9,"sortOrder":10,"createdAt":11,"updatedAt":11,"postCount":12},"e8d0bd45-d10c-46d3-8afb-0c072df7f8a7","技术","tech","技术文章",0,"2026-06-27T04:18:37.371Z",1,{"id":14,"name":15,"slug":16,"description":15,"sortOrder":12,"createdAt":17,"updatedAt":18,"postCount":19},"15ac46ad-edf7-4435-9a64-ff78117d58c5","Vue3 生态","vue3-生态","2022-05-21T08:05:39.000Z","2023-02-08T02:49:14.000Z",6,{"id":21,"name":22,"slug":23,"description":22,"sortOrder":24,"createdAt":25,"updatedAt":18,"postCount":26},"11d4d397-685c-4180-a7b3-9b0e3a1e411e","Css","css",2,"2022-05-23T07:19:37.000Z",9,{"id":28,"name":29,"slug":29,"description":29,"sortOrder":30,"createdAt":31,"updatedAt":18,"postCount":32},"d10456a5-e649-4741-a38f-f07f266ce5f2","开发环境",3,"2022-05-24T01:52:41.000Z",13,{"id":34,"name":35,"slug":36,"description":35,"sortOrder":37,"createdAt":38,"updatedAt":18,"postCount":39},"5ed5cc62-43ea-49a2-b0b2-38bc7aae52a0","Vue3","vue3",4,"2022-05-24T01:55:05.000Z",8,{"id":41,"name":42,"slug":43,"description":42,"sortOrder":44,"createdAt":45,"updatedAt":18,"postCount":46},"da130ba9-d4f4-49f3-aa0f-149078097ef0","JavaScript","javascript",5,"2022-05-24T02:22:57.000Z",18,{"id":48,"name":49,"slug":49,"description":49,"sortOrder":19,"createdAt":50,"updatedAt":18,"postCount":12},"d8cbe380-54b3-4a61-a12d-5438c2918574","限时优惠","2022-05-25T07:18:03.000Z",{"id":52,"name":53,"slug":54,"description":53,"sortOrder":55,"createdAt":56,"updatedAt":18,"postCount":57},"e0f3b8d8-cfe7-41fb-802b-a79699d95968","JavaScript插件","javascript插件",7,"2022-06-01T14:08:31.000Z",16,{"id":59,"name":60,"slug":60,"description":60,"sortOrder":39,"createdAt":61,"updatedAt":18,"postCount":19},"4ea3d8af-9cc3-49bb-a9cd-34dbcdc3bd85","构建工具","2022-06-02T07:28:13.000Z",{"id":63,"name":64,"slug":64,"description":64,"sortOrder":26,"createdAt":65,"updatedAt":18,"postCount":39},"9ed9827c-9cbb-42da-80e4-d04c7fdba886","开发工具","2022-06-21T03:35:05.000Z",{"id":67,"name":68,"slug":69,"description":68,"sortOrder":70,"createdAt":71,"updatedAt":18,"postCount":44},"6b9179c3-17b2-43ff-a431-a03d6eb32d89","Vue2 生态","vue2-生态",10,"2022-07-16T13:14:29.000Z",{"id":73,"name":74,"slug":75,"description":74,"sortOrder":76,"createdAt":77,"updatedAt":18,"postCount":44},"73a5f62c-3c47-45b9-9ae2-f29953ae8dc0","Node","node",11,"2022-07-16T13:15:39.000Z",{"id":79,"name":80,"slug":80,"description":80,"sortOrder":81,"createdAt":82,"updatedAt":18,"postCount":24},"2b696c16-48ef-403b-a88b-6e57cfc79596","开发问题",12,"2022-07-16T14:06:54.000Z",{"id":84,"name":85,"slug":85,"description":85,"sortOrder":32,"createdAt":86,"updatedAt":18,"postCount":12},"c0f0561e-a47a-4ecd-8caa-cc1df2315d57","算法","2022-07-16T14:22:34.000Z",{"id":88,"name":89,"slug":90,"description":89,"sortOrder":91,"createdAt":92,"updatedAt":18,"postCount":30},"a629c1f7-29f1-439e-be3c-29670b17ba20","Vue2","vue2",15,"2022-07-16T14:41:51.000Z",[94,100,105,108,113,118,122,126,131,136,141,146,149,154,158,162,167,172,177,182,186,190,194,196,200,205,210,213,216,219,222,225,229,232,234,236,239,243,246,250,254,257,260,262,265,267,271,274],{"id":95,"name":96,"slug":97,"createdAt":98,"updatedAt":99},"076bd8b9-293e-45cb-9dc3-e162007ca474","Axios","axios","2022-06-05T07:41:56.000Z","2025-12-30T07:26:21.000Z",{"id":101,"name":102,"slug":103,"createdAt":104,"updatedAt":18},"2aa7f6d0-1fac-4ed1-b9bb-f3afc813f42c","Axure","axure","2022-06-21T03:35:15.000Z",{"id":106,"name":22,"slug":23,"createdAt":107,"updatedAt":18},"b084ddd8-09be-4e57-98f0-cf4e376aecd7","2022-05-21T09:59:55.000Z",{"id":109,"name":110,"slug":111,"createdAt":112,"updatedAt":18},"78a62bff-ff77-4878-8c25-3e6aae18c668","Docker","docker","2022-07-16T14:34:37.000Z",{"id":114,"name":115,"slug":116,"createdAt":117,"updatedAt":18},"2de16806-ef3f-4e54-a259-d1e1e182468c","Git","git","2022-07-16T14:25:15.000Z",{"id":119,"name":120,"slug":121,"createdAt":107,"updatedAt":18},"994cc226-578b-4a72-a57e-a47a63d2793e","JavaScript生态","javascript生态",{"id":123,"name":124,"slug":125,"createdAt":107,"updatedAt":18},"5086e93c-23b9-43d3-9643-cc87f0e9ee94","JenKins","jenkins",{"id":127,"name":128,"slug":129,"createdAt":130,"updatedAt":18},"b73007a8-bb5c-42a8-9fd9-163033a5b45d","Linux","linux","2022-07-16T14:40:17.000Z",{"id":132,"name":133,"slug":134,"createdAt":135,"updatedAt":18},"0b658b92-dd6b-4db3-a398-9f6d69950a02","Markdown","markdown","2022-07-16T14:39:25.000Z",{"id":137,"name":138,"slug":139,"createdAt":140,"updatedAt":18},"ab034d3a-6e5b-4db5-a2dc-faf4ccbb63f5","Nest","nest","2022-07-16T13:15:49.000Z",{"id":142,"name":143,"slug":144,"createdAt":145,"updatedAt":18},"52c41978-da06-4962-9636-45bbaeedda80","Nginx","nginx","2022-05-21T09:59:56.000Z",{"id":147,"name":148,"slug":148,"createdAt":145,"updatedAt":18},"0f1cc678-40e4-44b1-b2cf-a6fd8a1c867a","npm",{"id":150,"name":151,"slug":152,"createdAt":153,"updatedAt":18},"a4370d78-70e1-4073-a8f6-3dc5d81fd8fd","Nuxt","nuxt","2022-06-01T13:07:07.000Z",{"id":155,"name":156,"slug":157,"createdAt":107,"updatedAt":18},"d232e01f-048e-4151-8a0a-fff9561f946f","Pinia","pinia",{"id":159,"name":160,"slug":161,"createdAt":145,"updatedAt":18},"14e9ab02-b0bb-4c85-8604-fe6f1f0f33cd","Pnpm","pnpm",{"id":163,"name":164,"slug":165,"createdAt":166,"updatedAt":166},"399d1d38-cc0d-43ce-8baf-c769447a2ebd","React生态","react生态","2023-02-21T02:03:09.000Z",{"id":168,"name":169,"slug":170,"createdAt":171,"updatedAt":18},"c95bbe84-bdd0-410a-86a9-e87958c55f4f","Redis","redis","2022-10-05T05:14:14.000Z",{"id":173,"name":174,"slug":175,"createdAt":176,"updatedAt":18},"6d05f9df-e116-450f-af57-85ed710c4870","Swiper","swiper","2022-06-01T14:08:46.000Z",{"id":178,"name":179,"slug":180,"createdAt":181,"updatedAt":18},"66f3aeb0-84ef-45f6-a43a-944eefc9895a","Vite","vite","2022-06-02T07:28:24.000Z",{"id":183,"name":184,"slug":185,"createdAt":107,"updatedAt":18},"bf5b94d3-090b-4098-a03c-4bc69781fb2d","Vue","vue",{"id":187,"name":188,"slug":189,"createdAt":145,"updatedAt":18},"2f7fb1be-b9c5-4606-b54f-e9f66f2653b2","Vue-Router","vue-router",{"id":191,"name":192,"slug":193,"createdAt":107,"updatedAt":18},"2fef3b91-1c1c-4ae8-b2c1-0e04b4f9b3a2","Vue2生态","vue2生态",{"id":195,"name":35,"slug":36,"createdAt":107,"updatedAt":18},"62b94c93-724f-488d-8fc5-0449971d9204",{"id":197,"name":198,"slug":199,"createdAt":107,"updatedAt":18},"20bff9cd-7848-4c16-8775-42cf12b44b30","Vue3生态","vue3生态",{"id":201,"name":202,"slug":203,"createdAt":204,"updatedAt":18},"c807b2c6-cb12-4409-a1f1-6bea9f330a6b","Vuex","vuex","2022-07-16T13:14:59.000Z",{"id":206,"name":207,"slug":208,"createdAt":209,"updatedAt":18},"5782dff5-2ea2-4427-9696-d4363a7fd5bc","Webpack","webpack","2022-07-16T14:33:41.000Z",{"id":211,"name":212,"slug":212,"createdAt":107,"updatedAt":18},"d0aa41f4-68f8-48d4-a4ed-3a503ea90451","下载",{"id":214,"name":215,"slug":215,"createdAt":107,"updatedAt":18},"a046060c-39ef-474a-8c85-2546aca0e2e5","代码片段",{"id":217,"name":218,"slug":218,"createdAt":107,"updatedAt":18},"fee73435-b2be-4b55-85b1-d133ea96aea4","伪元素",{"id":220,"name":221,"slug":221,"createdAt":107,"updatedAt":18},"436bd369-8c57-4869-8827-e88e50e5e0ab","伪类",{"id":223,"name":224,"slug":224,"createdAt":107,"updatedAt":18},"4c6be544-8a00-4445-92a3-e3dcbaf6142e","动画",{"id":226,"name":227,"slug":227,"createdAt":228,"updatedAt":18},"9321a12e-ea72-49a9-a32d-5566149f812f","图片压缩","2022-08-02T00:37:47.000Z",{"id":230,"name":231,"slug":231,"createdAt":145,"updatedAt":18},"512b16fb-576a-4397-a7c5-dd20e6a8f9ca","布局",{"id":233,"name":64,"slug":64,"createdAt":107,"updatedAt":18},"f32faa96-f2ec-45c6-9a17-2c76062edcb0",{"id":235,"name":29,"slug":29,"createdAt":107,"updatedAt":18},"3c46ed3f-6d6b-4f91-bcb3-af5112860bf5",{"id":237,"name":238,"slug":238,"createdAt":107,"updatedAt":18},"dbfc086a-73a6-4560-814d-593acb61cf98","性能优化",{"id":240,"name":241,"slug":241,"createdAt":242,"updatedAt":18},"1831cd06-0d6b-48f7-94fa-324782fe23cb","拖拽","2022-07-28T12:39:13.000Z",{"id":244,"name":245,"slug":245,"createdAt":145,"updatedAt":18},"9a74300d-06f7-46d0-80d9-8fe67ec0539b","数组",{"id":247,"name":248,"slug":248,"createdAt":249,"updatedAt":18},"19ac8998-7e0a-459b-9702-bb1adca70e8c","文本复制","2022-07-17T01:54:45.000Z",{"id":251,"name":252,"slug":252,"createdAt":253,"updatedAt":18},"5ff33473-71a4-4e02-8c82-f9ea369a768f","时间","2022-07-17T01:51:12.000Z",{"id":255,"name":256,"slug":256,"createdAt":107,"updatedAt":18},"aa47ca4d-d3f6-4cac-b495-2c67c9592c36","最新优惠",{"id":258,"name":259,"slug":259,"createdAt":145,"updatedAt":18},"f6766d54-54fc-405e-932d-b7d550559125","服务器",{"id":261,"name":60,"slug":60,"createdAt":107,"updatedAt":18},"d856559a-03ff-40b4-980d-3f272b998c3c",{"id":263,"name":264,"slug":264,"createdAt":107,"updatedAt":18},"692d5d68-b188-4e5c-aca8-65d0229399a1","渐变",{"id":266,"name":85,"slug":85,"createdAt":107,"updatedAt":18},"38e1fd6b-d7c6-4d62-bf70-7bacc175bea9",{"id":268,"name":269,"slug":269,"createdAt":270,"updatedAt":18},"be7b10bc-49eb-4a03-bea7-ceb915d500fe","规范","2022-07-16T14:41:06.000Z",{"id":272,"name":273,"slug":273,"createdAt":145,"updatedAt":18},"b42e2916-ad62-4b8a-a863-cd8c19a829de","面试",{"id":275,"name":276,"slug":276,"createdAt":145,"updatedAt":18},"7069add9-b636-44f1-9cd4-ea3a6d2b85d3","面试题",104,48,[280,297,309,322,335],{"id":281,"title":282,"slug":282,"content":283,"excerpt":282,"coverImage":284,"status":285,"isPinned":286,"pinnedAt":287,"viewCount":288,"publishedAt":289,"createdAt":289,"updatedAt":290,"category":291,"author":292,"tags":295},"a1bd1f49-6f6d-4fea-9789-b5636e19a6b3","uni-app瀑布流","## 实现思路\n> 获取父组件的列表数组，watch监听数组长度变化，截取后面新的数据，创建两个左右数组，比较左右dom的长度，哪个短，就push一条数据进去，源数组删除一条数据。利用img的load（加载成功）和error方法（加载失败），触发数组的push，实现瀑布流\n\n## 代码实现\n\n```language\n\u003Ctemplate>\n    \u003Cview class=\\\"waterfall\\\">\n        \u003Cview class=\\\"waterfall_left\\\">\n            \u003Cview class=\\\"waterfall_list\\\" v-for=\\\"(item, index) in leftList\\\" :key=\\\"index\\\">\n                \u003CSearch\n                    :name=\\\"item.name\\\"\n                    :image=\\\"item.src\\\"\n                    :label=\\\"item.label\\\"\n                    :item=\\\"item\\\"\n                    @considerPush=\\\"considerPush\\\"\n                >\n                \u003C\u002FSearch>\n            \u003C\u002Fview>\n        \u003C\u002Fview>\n        \u003Cview class=\\\"waterfall_right\\\">\n            \u003Cview class=\\\"waterfall_list\\\" v-for=\\\"(item, index) in rightList\\\" :key=\\\"index\\\">\n                \u003CSearch\n                    :name=\\\"item.name\\\"\n                    :image=\\\"item.src\\\"\n                    :label=\\\"item.label\\\"\n                    :item=\\\"item\\\"\n                    @considerPush=\\\"considerPush\\\"\n                >\n                \u003C\u002FSearch>\n            \u003C\u002Fview>\n        \u003C\u002Fview>\n    \u003C\u002Fview>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nimport Search from '@\u002Fcomponents\u002Fsearch\u002Fsearch.vue';\nexport default {\n    components: { Search },\n    props: {\n        list: {\n            type: Array,\n            default: () => []\n        }\n    },\n    data() {\n        return {\n            \u002F\u002F 左侧列表\n            leftList: [],\n            \u002F\u002F 右侧列表\n            rightList: [],\n            \u002F\u002F 组件数据备份\n            newList: [],\n            \u002F\u002F默认请求数,主要为了正常排序\n            interceptNumber: 10\n        };\n    },\n    created() {\n        this.touchOff(); \u002F\u002F 触发排列\n    },\n    mounted() {},\n    watch: {\n        list(newValue, oldValue) {\n            this.interceptNumber = newValue.length - oldValue.length;\n            this.touchOff();\n        }\n    },\n    computed: {},\n    methods: {\n        \u002F\u002F 触发重新排列\n        touchOff() {\n            this.newList = [...this.list.slice(-this.interceptNumber)];\n            if (this.newList.length !== 0) {\n                this.leftList.push(this.newList.shift()); \u002F\u002F触发排列\n            }\n        },\n        \u002F\u002F 计算排列\n        considerPush() {\n            this.$nextTick(() => {\n                if (this.newList.length == 0) return; \u002F\u002F没有数据了\n                let leftH = 0;\n                let rightH = 0; \u002F\u002F左右高度\n                let query = uni.createSelectorQuery().in(this);\n                query.selectAll('.waterfall_left').boundingClientRect();\n                query.selectAll('.waterfall_right').boundingClientRect();\n                query.exec(res => {\n                    leftH = res[0].length != 0 ? res[0][0].height : 0; \u002F\u002F防止查询不到做个处理\n                    rightH = res[1].length != 0 ? res[1][0].height : 0;\n                    if (leftH == rightH || leftH \u003C rightH) {\n                        \u002F\u002F 相等 || 左边小\n                        this.leftList.push(this.newList.shift());\n                    } else {\n                        \u002F\u002F 右边小\n                        this.rightList.push(this.newList.shift());\n                    }\n\n                    \u002F\u002F console.log('左右高度：', leftH, rightH, leftH == rightH || leftH \u003C rightH);\n                });\n            });\n        }\n    }\n};\n\u003C\u002Fscript>\n\n\u003Cstyle scoped lang=\\\"scss\\\">\n.waterfall {\n    display: flex;\n    align-items: flex-start;\n    justify-content: flex-start;\n    .waterfall_left {\n        flex: 1;\n    }\n\n    .waterfall_right {\n        flex: 1;\n    }\n}\n\u003C\u002Fstyle>\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F7d4fa8d1d2775177b882a4656e3a5ed5.png","PUBLISHED",false,null,1819,"2022-11-22T07:54:41.000Z","2026-06-27T11:37:20.140Z",{"id":41,"name":42,"slug":43},{"id":293,"name":294,"avatar":287},"f9d0f2de-c700-4f90-b535-afd3dbe78128","Admin",[296],{"id":119,"name":120,"slug":121},{"id":298,"title":299,"slug":299,"content":300,"excerpt":299,"coverImage":301,"status":285,"isPinned":286,"pinnedAt":287,"viewCount":302,"publishedAt":303,"createdAt":303,"updatedAt":304,"category":305,"author":306,"tags":307},"b2c46bf6-d971-4cce-b21b-052dbea8e8a2","v-html使用img点击实现放大效果","## 代码实现\n```js\n\u002F**\n * JS获取html代码中所有的图片地址\n * @param htmlstr\n * @returns arr 数组\n *\u002F\n\nexport function getimgsrc(htmlstr) {\n    let reg = \u002F\u003Cimg.+?src=('|\\\")?([^'\\\"]+)('|\\\")?(?:\\s+|>)\u002Fg;\n    let arr = [];\n    let tem = 0;\n    \u002F\u002Feslint-disable-next-line\n    while ((tem = reg.exec(htmlstr))) {\n        arr.push(tem[2]); \u002F\u002F eslint-disable-line\n    }\n\n    return arr;\n}\n\n```\n\n\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\\\"image-expansion\\\" :class=\\\"classArr\\\">\n        \u003Cdiv @click.stop=\\\"hanldeImage($event)\\\" v-html=\\\"formatHtmlData\\\">\u003C\u002Fdiv>\n\n        \u003Cel-image-viewer\n            v-if=\\\"imgPreviewUrl\\\"\n            :initial-index=\\\"subscript\\\"\n            :src=\\\"imgPreviewUrl\\\"\n            :on-close=\\\"closeViewer\\\"\n            :url-list=\\\"imgList\\\"\n        >\u003C\u002Fel-image-viewer>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nimport { getimgsrc } from '..\u002F..\u002Futils\u002Fgetimgsrc';\nimport ElImageViewer from 'element-ui\u002Fpackages\u002Fimage\u002Fsrc\u002Fimage-viewer';\nexport default {\n    components: {\n        ElImageViewer\n    },\n    props: {\n        htmlData: {\n            type: String,\n            default: () => {\n                return '';\n            }\n        },\n        classArr: {\n            type: Array,\n            default: () => {\n                return ['min'];\n            }\n        },\n\n        isArticle: {\n            type: Boolean,\n            default: () => {\n                return false;\n            }\n        }\n    },\n    data() {\n        return {\n            imgList: [],\n            formatHtmlData: '',\n            imgPreviewUrl: '',\n            subscript: 0\n        };\n    },\n    computed: {},\n\n    watch: {\n        \u002F\u002F监听数据，防止数据不更新\n        htmlData: {\n            handler(newName, oldName) {\n                \u002F\u002F判断是否为文章\n                if (this.isArticle) {\n                    newName ? (this.formatHtmlData = newName.replace(\u002F(\u003C([^>]+)>)\u002Fgi, '').replace(\u002F[\\\r\\\n]\u002Fg, '')) : '';\n                } else {\n                    \u002F\u002F剔除strong和p标签\n                    newName ? (this.formatHtmlData = newName.replace(\u002F(\u003C\\\u002F?strong.*?>)|(\u003C\\\u002F?p.*?>)\u002Fg, '')) : '';\n\n                    \u002F\u002F获取html全部图片，push成图片数组\n                    this.imgList = Object.values(getimgsrc(this.formatHtmlData));\n                    \u002F\u002F获取图片下标\n                    let subscript = this.imgList.indexOf(this.imgPreviewUrl);\n                    this.subscript = subscript > -1 ? subscript : 0;\n                }\n            },\n\n            immediate: true\n        }\n    },\n\n    mounted() {},\n\n    methods: {\n        \u002F\u002F监听点击事件\n        hanldeImage(event) {\n            if (event.target.nodeName === 'IMG' || event.target.nodeName === 'img') {\n                \u002F\u002F获取点击的图片url,decodeURIComponent转码一下，防禁url转码\n                this.imgPreviewUrl = decodeURIComponent(event.target.currentSrc);\n\n                \u002F\u002F获取图片下标\n                let subscript = this.imgList.indexOf(this.imgPreviewUrl);\n                this.subscript = subscript > -1 ? subscript : 0;\n\n                \u002F\u002F禁止遮罩层后面的内容滚动\n                document.documentElement.style.overflowY = 'hidden';\n            } else {\n                this.$emit('goDetail');\n            }\n        },\n\n        \u002F\u002F关闭弹框\n        closeViewer() {\n            this.imgPreviewUrl = '';\n            \u002F\u002F恢复遮罩层后面的内容滚动\n            document.documentElement.style.overflowY = 'auto';\n        }\n    }\n};\n\u003C\u002Fscript>\n\n\u003Cstyle lang=\\\"scss\\\" scoped>\n.image-expansion {\n}\n\n.min {\n    \u002Fdeep\u002F img {\n        cursor: pointer;\n        height: 28px;\n        padding: 0 10px 3px;\n    }\n}\n\n.max {\n    \u002Fdeep\u002F img {\n        cursor: pointer;\n    }\n}\n\n.class1 {\n    \u002Fdeep\u002F div {\n        font-size: 15px;\n        font-family: Microsoft YaHei;\n        font-weight: 400;\n        color: #888888;\n        line-height: 30px;\n    }\n}\n\u003C\u002Fstyle>\n\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002Fc77c3fb113d1ab2f67e7afba1ca33b95.png",1572,"2023-01-10T07:22:29.000Z","2026-06-27T11:07:40.499Z",{"id":67,"name":68,"slug":69},{"id":293,"name":294,"avatar":287},[308],{"id":226,"name":227,"slug":227},{"id":310,"title":311,"slug":312,"content":313,"excerpt":311,"coverImage":314,"status":285,"isPinned":286,"pinnedAt":287,"viewCount":315,"publishedAt":316,"createdAt":316,"updatedAt":317,"category":318,"author":319,"tags":320},"9f33b49b-0785-4404-9a4e-d85937e7fc92","在 vue3 中优雅的使用 jsx\u002Ftsx","在-vue3-中优雅的使用-jsx-tsx","## 安装插件（@vitejs\u002Fplugin-vue-jsx）\n```shell\npnpm add @vitejs\u002Fplugin-vue-jsx -D\n```\n\n## 配置vite.config.ts\n```ts\nimport vueJsx from \\\"@vitejs\u002Fplugin-vue-jsx\\\";\n\nexport default defineConfig({\n  plugins: [\n    vueJsx(),\n  ]\n})\n```\n\n## 插值\n```language\n\u002F\u002F vue3模板语法\n\u003Cspan>{{ a + b }}\u003C\u002Fspan>\n\n\u002F\u002F jsx\u002Ftsx\n\u003Cspan>{ a + b }\u003C\u002Fspan>\n```\n\n\n## class与style 绑定\n\n```ts\n\u002F\u002F 模板字符串\n\u003Cdiv className={`header ${ isBg ? 'headerBg' : '' }`}>header\u003C\u002Fdiv>\n\u002F\u002F数组\n\u003Cdiv class={ [ 'header', isBg && 'headerBg' ] } >header\u003C\u002Fdiv>\n```\n\n```ts\nconst color = 'red'\nconst element = \u003Csapn style={{ color, fontSize: '16px' }}>style\u003C\u002Fsapn>\n```\n\n## 条件渲染\n\n```ts\n   setup() {\n       const isShow = false\n       const element = () => {\n           if (isShow) {\n               return \u003Cspan>我是if\u003C\u002Fspan>\n           } else {\n               return \u003Cspan>我是else\u003C\u002Fspan>\n           }\n       }\n       return () => (\n           \u003Cdiv>\n               \u003Cspan v-show={isShow}>我是v-show\u003C\u002Fspan>\n               {\n                   element()\n               }\n               {\n                   isShow ? \u003Cp>我是三目1\u003C\u002Fp> : \u003Cp>我是三目2\u003C\u002Fp>\n               }\n           \u003Cdiv>\n       )\n   }\n```\n\n## 列表渲染\n```language\nsetup() {\n   const listData = [\n       {name: 'Tom', age: 18},\n       {name: 'Jim', age: 20},\n       {name: 'Lucy', age: 16}\n   ]\n   return () => (\n       \u003Cdiv>\n           \u003Cdiv class={'box'}>\n               \u003Cspan>姓名\u003C\u002Fspan>\n               \u003Cspan>年龄\u003C\u002Fspan>\n           \u003C\u002Fdiv>\n           {\n               prop.listData.map(item => {\n                   return \u003Cdiv class={'box'}>\n                       \u003Cspan>{item.name}\u003C\u002Fspan>\n                       \u003Cspan>{item.age}\u003C\u002Fspan>\n                   \u003C\u002Fdiv>\n               })\n           }\n       \u003C\u002Fdiv>\n   )\n}\n\n```\n\n## 事件处理\n\n```language\nsetup() {\n    const clickBox = val => {\n        console.log(val)\n    }\n    return () => (\n        \u003Cdiv class={'box1'} onClick={() => clickBox('box1')}>\n            \u003Cspan>我是box1\u003C\u002Fspan>\n            \u003Cdiv class={'box2'} onClick={() => clickBox('box2')}>\n                \u003Cspan>我是box2\u003C\u002Fspan>\n                \u003Cdiv class={'box3'} onClick={withModifiers(() => clickBox('box3'), ['stop'])}>我是box3\u003C\u002Fdiv>\n            \u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n    )\n}\n```\n\n## v-model\n\n```ts\n\u002F\u002F 正常写法\n\u003Cinput v-model=\\\"value\\\" \u002F> \u002F\u002F vue\n\u003Cinput v-model={value} \u002F> \u002F\u002F jsx\n\n\u002F\u002F 指定绑定值写法\n\u003Cinput v-model:modelValue=\\\"value\\\" \u002F> \u002F\u002F vue\n\u003Cinput v-model={[value,'modelValue']} \u002F> \u002F\u002F jsx\n\n\u002F\u002F 修饰符写法\n\u003Cinput v-model:modelValue.trim=\\\"value\\\" \u002F> \u002F\u002F vue\n\u003Cinput v-model={[value,'modelValue',['trim']]} \u002F> \u002F\u002F jsx\n```\n\n## slot插槽\n\n### 定义插槽\n```ts\nimport { renderSlot } from \\\"vue\\\"\nexport default defineComponent({\n    \u002F\u002F 从ctx中解构出来 slots\n    setup(props, { slots }) {\n        return () => (\n            \u003Cdiv>\n                { renderSlot(slots, 'default') }\n                { slots.title?.() }\n            \u003C\u002Fdiv>\n        )\n    }\n})\n\n```\n\n### 使用插槽\n\n```ts\nimport Vslot from '.\u002FslotTem'\nexport default defineComponent({\n    setup() {\n        return () => (\n            \u003Cdiv class={'box'}>\n                \u003CVslot v-slots={{\n                    title: () => {\n                        return \u003Cp>我是title插槽\u003C\u002Fp>\n                    },\n                    default: () => {\n                        return \u003Cp>我是default插槽\u003C\u002Fp>\n                    }\n                }} \u002F>\n            \u003C\u002Fdiv>\n        )\n    }\n})\n\n```\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F3e4cb0d3df611ee3f57b8ed503e1015e.png",1392,"2023-04-19T09:23:27.000Z","2026-06-27T11:08:34.303Z",{"id":34,"name":35,"slug":36},{"id":293,"name":294,"avatar":287},[321],{"id":195,"name":35,"slug":36},{"id":323,"title":324,"slug":325,"content":326,"excerpt":324,"coverImage":327,"status":285,"isPinned":286,"pinnedAt":287,"viewCount":328,"publishedAt":329,"createdAt":329,"updatedAt":330,"category":331,"author":332,"tags":333},"1dcdd755-0e46-45a3-8998-9a213fd3fcd5","Vue3 导入导出Excel","vue3-导入导出excel","## 安装\n```shell\npnpm add -S XLSX\n```\n\n\n## 方法封装\n```js\nimport * as XLSX from 'xlsx';\n\n\u002F\u002F参数说明\n\u002F\u002Fconfiguration: {\n\u002F\u002F  data: [], \u002F\u002F 导出的数据\n\u002F\u002F  head: {}, \u002F\u002F 导出的数据对应的表头\n\u002F\u002F  name: '', \u002F\u002F 导出的文件名\n\u002F\u002F  label: '', \u002F\u002F 导出的表单名\n\u002F\u002F  widthArr: [], \u002F\u002F 导出的表单列宽\n\u002F\u002F}\n\n\u002F\u002F 导出excel\nexport const ExportXlsx = (configuration) => {\n  const { data, head, name, label, widthArr } = configuration;\n\n  const list = data.map((item) => {\n    const obj = {};\n    for (const k in item) {\n      if (head[k]) {\n        obj[head[k]] = item[k];\n      }\n    }\n    return obj;\n  });\n\n  \u002F\u002F 创建工作表\n  const xLSXData = XLSX.utils.json_to_sheet(list);\n  \u002F\u002F 创建工作簿\n  const wb = XLSX.utils.book_new();\n  \u002F\u002F 将工作表放入工作簿中\n  XLSX.utils.book_append_sheet(wb, xLSXData, label);\n  xLSXData['!cols'] = [];\n  \u002F\u002F 设置列宽\n  widthArr.forEach((item) => {\n    xLSXData['!cols'].push({ wpx: item });\n  });\n\n  \u002F\u002F 生成文件并下载\n  XLSX.writeFile(wb, `${name}.xlsx`);\n};\n\n\u002F\u002F 导入excel\nexport const ImportXlsx = (e) => {\n  const file = e.target.files[0];\n  const reader = new FileReader();\n  reader.readAsArrayBuffer(file);\n  reader.onload = (e) => {\n    const data = e.target.result;\n    const workbook = XLSX.read(data, { type: 'binary', cellDates: true });\n    const wsname = workbook.SheetNames[0];\n    const outdata = XLSX.utils.sheet_to_json(workbook.Sheets[wsname]);\n    console.log(outdata);\n  };\n};\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F80231a326d3846b895a7d46c99e738ce.png",1336,"2023-03-14T06:18:59.000Z","2026-06-27T11:37:23.273Z",{"id":41,"name":42,"slug":43},{"id":293,"name":294,"avatar":287},[334],{"id":119,"name":120,"slug":121},{"id":336,"title":337,"slug":338,"content":339,"excerpt":337,"coverImage":340,"status":285,"isPinned":286,"pinnedAt":287,"viewCount":341,"publishedAt":342,"createdAt":342,"updatedAt":343,"category":344,"author":348,"tags":349},"80920598-a452-4357-bf53-842b200560e8","React18入门到精通教程","react18入门到精通教程","\n## JSX实现列表渲染\n> 使用`map()`方法遍历数组，必须添加`key`属性提高性能\n```jsx\nconst songs = [\n  { id: 1, name: \\\"helo1\\\" },\n  { id: 2, name: \\\"helo2\\\" },\n  { id: 3, name: \\\"helo3\\\" },\n];\n\nfunction App() {\n  return (\n    \u003Cdiv>\n      \u003Cul>\n        {songs.map((song) => (\n          \u002F\u002F 关键：添加唯一key标识符（避免使用索引）\n          \u003Cli key={song.id}>  \n            {song.id}-{song.name}\n          \u003C\u002Fli>\n        ))}\n      \u003C\u002Ful>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n**最佳实践：**\n1. 使用`\u003Cul>`包裹列表项\n2. `key`应使用稳定唯一标识（如ID），避免数组索引\n3. 空列表处理：`{songs.length > 0 && ...}` 或 `{songs.map(...) || \u003CEmptyView\u002F>}`\n\n## JSX实现条件渲染\n### 简单逻辑\n```jsx\n\u002F\u002F 三元表达式\n{isLoggedIn ? \u003CDashboard \u002F> : \u003CLoginForm \u002F>}\n\n\u002F\u002F 逻辑短路\n{hasNotification && \u003CNotificationBadge count={5} \u002F>}\n\n\u002F\u002F 空值处理\n{userProfile?.avatar || \u003CDefaultAvatar \u002F>}\n```\n\n### 复杂逻辑\n```jsx\nconst renderContent = (type) => {\n  switch(type) {\n    case 'success': \n      return \u003CSuccessAlert \u002F>;\n    case 'error':\n      return \u003CErrorAlert \u002F>;\n    default:\n      return \u003CInfoAlert \u002F>;\n  }\n}\n\nfunction App() {\n  return (\n    \u003Cdiv className=\\\"container\\\">\n      {renderContent(status)}\n      \n      {\u002F* 另一种模式：立即执行函数 *\u002F}\n      {(() => {\n        if (isLoading) return \u003CSpinner \u002F>;\n        if (isEmpty) return \u003CEmptyState \u002F>;\n        return \u003CDataTable \u002F>;\n      })()}\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n## JSX样式处理\n### 行内样式\n```jsx\n\u002F\u002F 直接对象\n\u003Cdiv style={{ \n  color: 'white', \n  backgroundColor: 'teal',\n  padding: '1rem'\n}}>\n\n\u002F\u002F 样式对象复用\nconst alertStyle = {\n  padding: '15px',\n  borderRadius: '4px',\n  margin: '10px 0'\n};\n\nfunction Alert({ type }) {\n  return (\n    \u003Cdiv style={{\n      ...alertStyle,  \u002F\u002F 扩展运算符合并样式\n      background: type === 'error' ? '#f8d7da' : '#d4edda'\n    }}>\n      {message}\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n### 类名控制（推荐）\n```css\n\u002F* styles.module.css *\u002F\n.card {\n  border: 1px solid #ddd;\n  border-radius: 8px;\n  padding: 20px;\n  box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\n\n.highlight {\n  background-color: #ffffe0;\n}\n```\n\n```jsx\nimport styles from '.\u002Fstyles.module.css';\n\nfunction ProductCard({ featured }) {\n  return (\n    \u003Cdiv className={`${styles.card} ${featured ? styles.highlight : ''}`}>\n      {\u002F* 内容 *\u002F}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## React 18新特性\n### 并发模式（Concurrent Mode）\n```jsx\nimport { startTransition } from 'react';\n\n\u002F\u002F 非紧急状态更新\nfunction handleSearch(query) {\n  startTransition(() => {\n    setSearchQuery(query); \u002F\u002F 可中断的渲染\n  });\n}\n```\n\n### 自动批处理（Automatic Batching）\n```jsx\n\u002F\u002F React 17及之前：两次渲染\n\u002F\u002F React 18：自动批处理，一次渲染\nfunction handleClick() {\n  setCount(c => c + 1);\n  setFlag(f => !f);\n}\n```\n\n## Redux状态管理（现代写法）\n### 安装依赖\n```bash\nnpm install @reduxjs\u002Ftoolkit react-redux\n```\n\n### 创建Store\n```js\n\u002F\u002F store.js\nimport { configureStore, createSlice } from '@reduxjs\u002Ftoolkit';\n\nconst counterSlice = createSlice({\n  name: 'counter',\n  initialState: { value: 0 },\n  reducers: {\n    increment: state => { state.value += 1 },\n    decrement: state => { state.value -= 1 },\n    incrementByAmount: (state, action) => {\n      state.value += action.payload\n    }\n  }\n});\n\n\u002F\u002F 异步操作示例\nexport const fetchUserData = () => async (dispatch) => {\n  const response = await fetch('\u002Fapi\u002Fuser');\n  dispatch(setUser(await response.json()));\n};\n\nexport const store = configureStore({\n  reducer: {\n    counter: counterSlice.reducer,\n    \u002F\u002F 其他reducer...\n  }\n});\n\nexport const { increment, decrement } = counterSlice.actions;\n```\n\n### 组件集成\n```jsx\n\u002F\u002F index.js\nimport { Provider } from 'react-redux';\nimport { store } from '.\u002Fstore';\n\nroot.render(\n  \u003CProvider store={store}>\n    \u003CApp \u002F>\n  \u003C\u002FProvider>\n);\n\n\u002F\u002F Counter.js\nimport { useSelector, useDispatch } from 'react-redux';\nimport { increment, decrement } from '.\u002Fstore';\n\nfunction Counter() {\n  const count = useSelector(state => state.counter.value);\n  const dispatch = useDispatch();\n\n  return (\n    \u003Cdiv>\n      \u003Cbutton onClick={() => dispatch(decrement())}>-\u003C\u002Fbutton>\n      \u003Cspan>{count}\u003C\u002Fspan>\n      \u003Cbutton onClick={() => dispatch(increment())}>+\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## 最佳实践总结\n1. **组件设计**：遵循单一职责原则，拆分智能组件（容器组件）和展示组件\n2. **状态管理**：\n   - 局部状态用`useState`\u002F`useReducer`\n   - 全局共享状态用Redux\n   - 避免过度使用状态提升\n3. **性能优化**：\n   - 使用`React.memo`记忆组件\n   - 使用`useCallback`\u002F`useMemo`避免不必要的重渲染\n   - 虚拟化长列表（react-window）\n4. **Hooks规范**：\n   - 避免在循环\u002F条件中使用Hook\n   - 自定义Hook以`use`前缀命名\n5. **TypeScript集成**：\n   ```tsx\n   interface UserCardProps {\n     name: string;\n     age: number;\n     onSelect: (id: string) => void;\n   }\n   \n   const UserCard: React.FC\u003CUserCardProps> = ({ name, age }) => (\n     \u003Cdiv>{name} ({age})\u003C\u002Fdiv>\n   )\n   ```\n\n下面为您完善教程，增加React路由和生命周期相关内容：\n\n## React路由管理（React Router v6）\n\n### 安装与基础配置\n```bash\nnpm install react-router-dom@6\n```\n\n### 路由基础结构\n```jsx\n\u002F\u002F index.js\nimport { BrowserRouter } from 'react-router-dom';\n\nconst root = ReactDOM.createRoot(document.getElementById('root'));\nroot.render(\n  \u003CBrowserRouter>\n    \u003CApp \u002F>\n  \u003C\u002FBrowserRouter>\n);\n\n\u002F\u002F App.js\nimport { Routes, Route, Link } from 'react-router-dom';\n\nfunction App() {\n  return (\n    \u003Cdiv>\n      \u003Cnav>\n        \u003CLink to=\\\"\u002F\\\">首页\u003C\u002FLink>\n        \u003CLink to=\\\"\u002Fabout\\\">关于\u003C\u002FLink>\n        \u003CLink to=\\\"\u002Fusers\\\">用户列表\u003C\u002FLink>\n      \u003C\u002Fnav>\n      \n      \u003CRoutes>\n        \u003CRoute path=\\\"\u002F\\\" element={\u003CHome \u002F>} \u002F>\n        \u003CRoute path=\\\"\u002Fabout\\\" element={\u003CAbout \u002F>} \u002F>\n        \u003CRoute path=\\\"\u002Fusers\\\" element={\u003CUserList \u002F>} \u002F>\n        \u003CRoute path=\\\"\u002Fusers\u002F:id\\\" element={\u003CUserDetail \u002F>} \u002F>\n        \u003CRoute path=\\\"*\\\" element={\u003CNotFound \u002F>} \u002F>\n      \u003C\u002FRoutes>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### 嵌套路由\n```jsx\n\u002F\u002F Dashboard.js\nimport { Outlet } from 'react-router-dom';\n\nfunction Dashboard() {\n  return (\n    \u003Cdiv>\n      \u003Ch2>仪表盘\u003C\u002Fh2>\n      \u003COutlet \u002F> {\u002F* 子路由渲染位置 *\u002F}\n    \u003C\u002Fdiv>\n  );\n}\n\n\u002F\u002F 路由配置\n\u003CRoutes>\n  \u003CRoute path=\\\"\u002Fdashboard\\\" element={\u003CDashboard \u002F>}>\n    \u003CRoute index element={\u003CDashboardHome \u002F>} \u002F>\n    \u003CRoute path=\\\"settings\\\" element={\u003CDashboardSettings \u002F>} \u002F>\n    \u003CRoute path=\\\"analytics\\\" element={\u003CDashboardAnalytics \u002F>} \u002F>\n  \u003C\u002FRoute>\n\u003C\u002FRoutes>\n```\n\n### 编程式导航\n```jsx\nimport { useNavigate, useParams, useLocation } from 'react-router-dom';\n\nfunction UserCard({ user }) {\n  const navigate = useNavigate();\n  \n  return (\n    \u003Cdiv onClick={() => navigate(`\u002Fusers\u002F${user.id}`)}>\n      {user.name}\n    \u003C\u002Fdiv>\n  );\n}\n\nfunction UserDetail() {\n  const { id } = useParams(); \u002F\u002F 获取URL参数\n  const location = useLocation(); \u002F\u002F 获取位置对象\n  \n  return (\n    \u003Cdiv>\n      \u003Ch2>用户ID: {id}\u003C\u002Fh2>\n      \u003Cp>当前路径: {location.pathname}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### 路由守卫（认证保护）\n```jsx\nimport { Navigate } from 'react-router-dom';\n\nfunction ProtectedRoute({ children }) {\n  const { isAuthenticated } = useAuth();\n  \n  if (!isAuthenticated) {\n    return \u003CNavigate to=\\\"\u002Flogin\\\" replace \u002F>;\n  }\n  \n  return children;\n}\n\n\u002F\u002F 使用\n\u003CRoute \n  path=\\\"\u002Fdashboard\\\" \n  element={\n    \u003CProtectedRoute>\n      \u003CDashboard \u002F>\n    \u003C\u002FProtectedRoute>\n  } \n\u002F>\n```\n\n## React生命周期\n\n### 类组件生命周期方法\n\n```jsx\nclass LifecycleDemo extends React.Component {\n  \u002F\u002F 1. 初始化阶段\n  constructor(props) {\n    super(props);\n    this.state = { count: 0 };\n    console.log('Constructor');\n  }\n\n  \u002F\u002F 2. 挂载阶段\n  componentDidMount() {\n    console.log('Component did mount');\n    \u002F\u002F 适合进行API调用、事件订阅\n    this.timer = setInterval(() => {\n      this.setState(prev => ({ count: prev.count + 1 }));\n    }, 1000);\n  }\n\n  \u002F\u002F 3. 更新阶段\n  shouldComponentUpdate(nextProps, nextState) {\n    console.log('Should component update?');\n    return nextState.count !== this.state.count;\n  }\n\n  componentDidUpdate(prevProps, prevState) {\n    console.log('Component did update');\n  }\n\n  \u002F\u002F 4. 卸载阶段\n  componentWillUnmount() {\n    console.log('Component will unmount');\n    \u002F\u002F 清理操作\n    clearInterval(this.timer);\n  }\n\n  render() {\n    console.log('Render');\n    return \u003Cdiv>Count: {this.state.count}\u003C\u002Fdiv>;\n  }\n}\n```\n\n### 函数组件生命周期（Hooks实现）\n\n```jsx\nimport { useState, useEffect } from 'react';\n\nfunction FunctionLifecycle() {\n  const [count, setCount] = useState(0);\n  const [data, setData] = useState(null);\n\n  \u002F\u002F 相当于componentDidMount + componentDidUpdate\n  useEffect(() => {\n    console.log('每次渲染后执行');\n  });\n\n  \u002F\u002F 相当于componentDidMount\n  useEffect(() => {\n    console.log('组件挂载后执行');\n    \n    \u002F\u002F 数据获取\n    fetch('\u002Fapi\u002Fdata')\n      .then(res => res.json())\n      .then(setData);\n    \n    \u002F\u002F 相当于componentWillUnmount\n    return () => {\n      console.log('组件卸载前执行');\n    };\n  }, []); \u002F\u002F 空依赖数组\n\n  \u002F\u002F 依赖变化时执行\n  useEffect(() => {\n    console.log('count变化时执行:', count);\n    \n    document.title = `Count: ${count}`;\n    \n    return () => {\n      console.log('清理count效果');\n    };\n  }, [count]); \u002F\u002F count依赖\n\n  return (\n    \u003Cdiv>\n      \u003Cp>Count: {count}\u003C\u002Fp>\n      \u003Cbutton onClick={() => setCount(c => c + 1)}>增加\u003C\u002Fbutton>\n      {data && \u003Cpre>{JSON.stringify(data, null, 2)}\u003C\u002Fpre>}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### 生命周期阶段对比\n\n| 阶段 | 类组件方法 | 函数组件Hook |\n|------|------------|--------------|\n| **挂载** | constructor | useState初始化 |\n|       | render      | 函数体执行    |\n|       | componentDidMount | useEffect(() => {}, []) |\n| **更新** | shouldComponentUpdate | React.memo, useMemo |\n|       | render      | 函数体执行    |\n|       | componentDidUpdate | useEffect(() => {}) |\n| **卸载** | componentWillUnmount | useEffect返回函数 |\n| **错误处理** | componentDidCatch | 暂无直接等效，需错误边界组件 |\n\n### 现代React开发建议\n\n1. **优先使用函数组件+Hooks**：\n   - 90%的场景可替代类组件\n   - 更简洁的代码结构\n   - 更好的逻辑复用\n\n2. **关键生命周期替代**：\n   - `componentDidMount` → `useEffect(() => {}, [])`\n   - `componentDidUpdate` → `useEffect(() => {})` 或带依赖的 `useEffect`\n   - `componentWillUnmount` → `useEffect(() => { return cleanup }, [])`\n   - `shouldComponentUpdate` → `React.memo` 或 `useMemo`\n\n3. **数据获取最佳实践**：\n```jsx\nuseEffect(() => {\n  let isMounted = true;\n  \n  const fetchData = async () => {\n    try {\n      const result = await api.getData();\n      if (isMounted) setData(result);\n    } catch (error) {\n      if (isMounted) setError(error);\n    }\n  };\n  \n  fetchData();\n  \n  return () => {\n    isMounted = false; \u002F\u002F 避免组件卸载后设置状态\n  };\n}, []);\n```\n\n## 路由与生命周期整合示例\n\n```jsx\nfunction UserProfile() {\n  const { id } = useParams();\n  const [user, setUser] = useState(null);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    let isActive = true;\n    \n    const fetchUser = async () => {\n      try {\n        setLoading(true);\n        const data = await fetchUserById(id);\n        if (isActive) {\n          setUser(data);\n          setLoading(false);\n        }\n      } catch (error) {\n        if (isActive) {\n          setError(error.message);\n          setLoading(false);\n        }\n      }\n    };\n    \n    fetchUser();\n    \n    return () => {\n      isActive = false; \u002F\u002F 清理效果\n    };\n  }, [id]); \u002F\u002F id变化时重新获取\n\n  if (loading) return \u003CSpinner \u002F>;\n  \n  return (\n    \u003Cdiv>\n      \u003Ch2>{user.name}\u003C\u002Fh2>\n      \u003Cp>Email: {user.email}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## 完整项目结构建议\n\n```\nsrc\u002F\n├── components\u002F      # 通用UI组件\n├── pages\u002F           # 页面组件\n├── layouts\u002F         # 布局组件\n├── hooks\u002F           # 自定义Hooks\n├── store\u002F           # Redux状态\n│   ├── slices\u002F\n│   └── store.js\n├── services\u002F        # API服务\n├── routers\u002F         # 路由配置\n├── utils\u002F           # 工具函数\n├── assets\u002F          # 静态资源\n└── App.js           # 主应用组件\n```\n\n这些新增内容涵盖了React路由的现代用法（v6版本）以及React生命周期的详细解释，包括类组件和函数组件的实现方式对比。同时还提供了路由与生命周期整合的实际示例，帮助开发者理解如何在真实项目中应用这些概念。","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F46e205aa1bd33d1bb7201019fc2fdf43.png",1148,"2023-02-21T02:01:52.000Z","2026-06-27T11:00:41.557Z",{"id":345,"name":346,"slug":347},"f1701085-b8c1-413a-8750-58e7a0a33832","React","react",{"id":293,"name":294,"avatar":287},[350],{"id":163,"name":164,"slug":165},{"id":352,"title":353,"slug":354,"content":355,"excerpt":353,"coverImage":356,"status":285,"isPinned":286,"pinnedAt":287,"viewCount":357,"publishedAt":358,"createdAt":358,"updatedAt":359,"category":360,"author":361,"tags":362},"93de6c59-fc6d-49d6-877d-4405ab187652","一文掌握JavaScript事件循环机制（event loop）","一文掌握javascript事件循环机制-event-loop","## 简介\n> javascript是一个单线程语言\n\n## 任务\n|任务| 优先级 | 执行位置 | \n| - | - | - | \n|同步任务| 只要被扫描到，就可以被主线程马上执行的任务。（优先于所有异步任务） |主线程 | \n|异步任务| 即使被扫描到，也不会马上执行，异步任务会被压入异步任务队列中，等待主线程中的任务全部清空了，再被召唤执行。 | macrotask或者microtask| \n\n## 常见的任务有如下几种\n| 任务 | 举例 |\n| - | - |\n| 同步任务 | new promise()、console.log() |\n| 微任务(micro task) | promise、async、await、process.nextTick(node)、mutationObserver(html5新特性) |\n| 宏任务(macro task) | script(整体代码)、setTimeout()、setInterval()、setImmediate 、I\u002FO、UI render |\n\n\n```js\nsetTimeout(() => {\nconsole.log(\\\"1\\\");\n},0)\n\nconsole.log(2)\n\u002F\u002F输出结果是 2，1  虽然setTimeout的延迟是0，但setTimeout是一个异步任务，他一定会在所有同步任务执行完毕之后再去执行。\n```\n\n\n> 当有异步任务被压入异步任务队列时候，javascript会将这些异步任务分为宏任务和微任务两个新的队列。然后，在所有同步任务执行完毕之后，异步任务会优先执行所有已经存在任务队列中的微任务。在所有的微任务执行完毕之后，再去宏任务队列中执行一个（注意是一个,注意是一个,注意是一个）宏任务，执行完一个宏任务之后会再去微任务队列中检查是否有新的微任务，有则全部执行，再回到宏任务队列执行一个宏任务，以此循环。这一套流程，就是事件循环（event loop）\n\n![](https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F1a5f1dc1a249b951eb6c5ebf879e140e.webp)\n\n```js\n  setTimeout(() => { console.log(\\\"1\\\") }, 0);  \u002F\u002F异步任务 - 宏任务\n\n  console.log(2);   \u002F\u002F同步任务\n\n  Promise.resolve().then(() => { console.log(3) }) \u002F\u002F异步任务 - 微任务\n\n  console.log(6);   \u002F\u002F同步任务\n\n```\n> 输出结果：2 6 3 1\n> 首先，同步任务必定优先于所有所有异步任务并按顺序执行。所以输出 2 6。同步任务执行完毕后，还剩下一个宏任务和一个微任务。微任务优先于宏任务执行，所以先输出 3 再输出 1\n\n\n```js\n        \u002F\u002F第一个宏任务\n         setTimeout(() => {\n              console.log(1); \u002F\u002F宏任务中的同步任务\n              Promise.resolve().then(() => { console.log(7) }) \u002F\u002F宏任务中的微任务\n         }, 0);  \u002F\u002F异步任务 - 宏任务\n\n        console.log(2);   \u002F\u002F同步任务\n\n        Promise.resolve().then(() => { console.log(3) }) \u002F\u002F异步任务 - 微任务\n\n        \u002F\u002F第二个宏任务\n        setTimeout(() => { \n          console.log(8); \u002F\u002F宏任务中的同步任务\n          setTimeout(() => { console.log(5) }, 0)      \u002F\u002F宏任务中的宏任务 第四个宏任务\n        }, 0);\n\n        \u002F\u002F第三个宏任务\n        setTimeout(() => { \n          Promise.resolve().then(() => { console.log(4) })  \u002F\u002F宏任务中的微任务\n        }, 0);\n        \n         console.log(6);   \u002F\u002F同步任务\n```\n> 输出结果：2 6 3 1 7 8 4 5\n\n\n> 首先，同步任务必定优先于所有所有异步任务并按顺序执行。所以输出 2 6。\n然后同一批次中剩下一个微任务和一个三个宏任务。\n因为宏任务必定会在同一批次环境中的微任务全部执行完毕后再执行，所以场上当前批次中唯一一个微任务先执行。输出3\n还剩下三个宏任务。执行第一个宏任务，宏任务中有一个同步任务和一个异步任务。这里要注意两点。\n统一批次宏任务中按顺序执行一次只执行一个宏任务，然后同步任务当场执行。微任务压入队列。然后就要去检查有没有微任务，有则执行\n所以，第一个宏任务执行的时候，产生了一个同步任务和一个微任务。需要注意,宏任务一次只执行一个。执行完之后发现同步任务当场执行（输出1），然后查看微任务队列中有没有微任务可以执行。发现有，则执行微任务（输出7）\n然后,才开始执行第二个宏任务。执行第二个宏任务产生了一个同步任务，同步任务当场执行（输出8），产生一个宏任务（宏任务压入红任务执行队列，也就是所有宏任务之后），按事件循环，再次检查是否存在未执行的微任务，发现没有，不执行。\n然后执行第三个宏任务，第三个宏任务中产生一个微任务，按事件循环，再去寻找是否存在未执行的微任务，发现有，则执行（输出4）\n最后执行第四个宏任务（第二个宏任务产生的）。走一遍事件循环的流程，输出5\n\n\n\n```js\nconsole.log('script start'); \u002F\u002F同步任务，顺序 1\n\nasync function async1() {\n    await async2(); \u002F\u002F异步任务 - 微任务 (async\u002Fawait 底层依然是 Promise，所以是微任务，只是 await 比较特殊)\n    console.log('async1 end'); \u002F\u002F顺序 5\n}\nasync function async2() {\n    console.log('async2 end');\n}\nasync1(); \u002F\u002F异步任务，顺序 2\n\nsetTimeout(function () {\n    console.log('setTimeout'); \u002F\u002F异步任务 - 宏任务，顺序 7\n}, 0);\n\nnew Promise(resolve => {\n    console.log('Promise'); \u002F\u002F同步任务，顺序 3\n    resolve();\n})\n    .then(function () {\n        console.log('promise1'); \u002F\u002F异步任务 - 微任务，顺序 5\n    })\n    .then(function () {\n        console.log('promise2'); \u002F\u002F异步任务 - 微任务，顺序 6\n    });\n\nconsole.log('script end'); \u002F\u002F同步任务，顺序 4\n\n\n```\n> 新版输出(新版的chrome浏览器优化了,await变得更快了,输出为)\n> 输出答案：script start、async2 end、Promise、script end、async1 end、promise1、promise2、setTimeout\n\n\n\n## async\u002Fawait （重点）\n### async\n> 当我们在函数前使用async的时候，使得该函数返回的是一个Promise对象\n```js\nasync function test() {\n    return 1   \u002F\u002F async的函数会在这里帮我们隐士使用Promise.resolve(1)\n}\n\u002F\u002F 等价于下面的代码\nfunction test() {\n   return new Promise(function(resolve, reject) {\n       resolve(1)\n   })\n}\n\u002F\u002F 可见async只是一个语法糖，只是帮助我们返回一个Promise而已\n```\n\n### await\n> await表示等待，是右侧「表达式」的结果，这个表达式的计算结果可以是 Promise 对象的值或者一个函数的值（换句话说，就是没有特殊限定）。并且只能在带有async的内部使用\n使用await时，会从右往左执行，当遇到await时， ★★★★★会阻塞函数内部处于它后面的代码，去执行该函数外部的同步代码，当外部同步代码执行完毕，再回到该函数内部执行剩余的代码★★★★★, 并且当await执行完毕之后，会先处理微任务队列的代码\n\n```js\n\u002F\u002F1\nconsole.log('1');    \n\u002F\u002F2\nsetTimeout(function() {\n    console.log('2');\n    process.nextTick(function() {\n        console.log('3');\n    })\n    new Promise(function(resolve) {\n        console.log('4');\n        resolve();\n    }).then(function() {\n        console.log('5')\n    })\n})\n\u002F\u002F3\nprocess.nextTick(function() {\n    console.log('6');\n})\n\u002F\u002F4\nnew Promise(function(resolve) {\n    console.log('7');\n    resolve();\n}).then(function() {\n    console.log('8')\n})\n\u002F\u002F5\nsetTimeout(function() {\n    console.log('9');\n    process.nextTick(function() {\n        console.log('10');\n    })\n    new Promise(function(resolve) {\n        console.log('11');\n        resolve();\n    }).then(function() {\n        console.log('12')\n    })\n})\n\n\u002F\u002F 先执行1 输出1\n\u002F\u002F 执行到2，把setTimeout放入异步的任务队列中（宏任务）\n\u002F\u002F 执行到3，把process.nextTick放入异步任务队列中（微任务）\n\u002F\u002F 执行到4，上面提到promise里面是同步任务，所以输出7，再将then放入异步任务队列中（微任务）\n\u002F\u002F 执行到5，同2\n\u002F\u002F 上面的同步任务全部完成，开始进行异步任务\n\u002F\u002F 先执行微任务，发现里面有两个微任务，分别是3，4压入的，所以输出6 8\n\u002F\u002F 再执行一个宏任务，也就是第一个setTimeout\n\u002F\u002F 先输出2，把process.nextTick放入微任务中，再如上promise先输出4，再将then放入微任务中\n\u002F\u002F 再执行所以微任务输出输出3 5\n\u002F\u002F 同样的，再执行一个宏任务setTImeout2，输出9 11 在执行微任务输出10 12\n\u002F\u002F 所以最好的顺序为：1 7 6 8 2 4 3 5 9 11 10 12\n```\n\n```js\nasync function async1() {\n    console.log('async1 start');\n    await async2();\n    console.log('async1 end');\n}\n\nasync function async2() {\n    console.log('async2');\n}\nconsole.log('script start');\nsetTimeout(function () {\n    console.log('setTimeout');\n}, 0);\nasync1();\nnew Promise(function (resolve) {\n    console.log('promise1');\n    resolve();\n}).then(function () {\n    console.log('promise2');\n});\nconsole.log('script end');\n\n\u002F\u002F 首先执行同步代码，console.log( 'script start' )\n\u002F\u002F 遇到setTimeout,会被推入宏任务队列\n\u002F\u002F 执行async1(), 它也是同步的，只是返回值是Promise，在内部首先执行console.log( 'async1 start' )\n\u002F\u002F 然后执行async2(), 然后会打印console.log( 'async2' )\n\u002F\u002F 从右到左会执行, 当遇到await的时候，阻塞后面的代码，去外部执行同步代码\n\u002F\u002F 进入new Promise,打印console.log( 'promise1' )\n\u002F\u002F 将.then放入事件循环的微任务队列\n\u002F\u002F 继续执行，打印console.log( 'script end' )\n\u002F\u002F 外部同步代码执行完毕，接着回到async1()内部, 继续执行 await async2() 后面的代码，执行 console.log( 'async1 end' ) ，所以打印出 async1 end 。（个人理解：async\u002Fawait本质上也是Promise，也是属于微任务的，所以当遇到await的时候，await后面的代码被阻塞了，应该也是被放到微任务队列了，当同步代码执行完毕之后，然后去执行微任务队列的代码，执行微任务队列的代码的时候，也是按照被压入微任务队列的顺序执行的）\n\u002F\u002F 执行微任务队列的代码, 打印 console.log( 'promise2' )\n\u002F\u002F 进入第二次事件循环，执行宏任务队列, 打印console.log( 'setTimeout' )\n\u002F**\n * 执行结果为：\n * script start\n * async1 start\n * async2\n * promise1\n * script end\n * async1 end\n * promise2\n * setTimeout\n *\u002F\n\n```\n\n```js\nconsole.log(1);\nasync function fn(){\n    console.log(2)\n    await console.log(3)\n    await console.log(4)\n    await console.log(\\\"await之后的：\\\",11)\n    await console.log(\\\"await之后的：\\\",22)\n    await console.log(\\\"await之后的：\\\",33)\n    await console.log(\\\"await之后的：\\\",44)\n}\nsetTimeout(()=>{\n    console.log(5)\n},0)\nfn();\nnew Promise((resolve)=>{\n    console.log(6)\n    resolve();\n}).then(()=>{\n    console.log(7)\n})\nconsole.log(8)\n\n\u002F**\n * 执行结果为：\n * 1\n * 2\n * 3\n * 6\n * 8\n * 4\n * 7\n * await之后的： 11\n * await之后的： 22\n * await之后的： 33\n * await之后的： 44\n * 5\n *\u002F\n\u002F*\n由此可见，代码执行的时候，只要碰见 await ，都会执行完当前的 await 之后，\n把 await 后面的代码放到微任务队列里面。但是定时器里面的 5 是最后打印出来的，\n可见当不断碰见 await ，把 await 之后的代码不断的放到微任务队列里面的时候，\n代码执行顺序是会把微任务队列执行完毕，才会去执行宏任务队列里面的代码。\n*\u002F\n```\n\n\n\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F52c39c689c44243cc9e821da5c7fa0b1.png",488,"2023-02-08T09:04:22.000Z","2026-06-27T11:18:49.943Z",{"id":41,"name":42,"slug":43},{"id":293,"name":294,"avatar":287},[363],{"id":119,"name":120,"slug":121},{"list":365,"total":10,"page":12,"pageSize":366},[],20]