阅读视图

发现新文章,点击刷新页面。

vue3项目搭建基础

element-plus

安装依赖(确保版本适配 Vue 3)

npm install element-plus --save

plugins/element.js 实现按需引入组件

import { ElButton } from 'element-plus'  // 导入 Element Plus 的 Button(Vue 3 版本)
import 'element-plus/dist/index.css' //全局引入样式,避免找不到

 // 接收 main.js 传入的 app 实例
export default function setupElement(app) {
 // 注册 Button 组件(Vue 3 用 app.component 注册单个组件)
  app.component('ElButton', ElButton)
}

main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import setupElement from './plugins/element.js'

//创建应用实例
const app = createApp(App)
//注册 Element Plus 插件
setupElement(app)
app.use(router)
//挂载应用
app.mount('#app')

App.vue

<template>
  <div id="app">
        <el-button type="danger">Danger</el-button>
  </div>
</template>

<script>
export default {
  name: 'app'
}
</script>

<style>
        html,body,#app{
                width: 100%;
                height: 100%;
                padding: 0;
                margin: 0;
        }
</style>

代码是从上往下一行一行执行的

  • 执行 main.js中引入代码

  • import { createApp } from 'vue' → 从 Vue 库导入 createApp 方法;

  • import App from './App.vue' → 导入根组件 App.vue(仅加载,未渲染);

  • import router from './router' → 导入路由(如果有配置);

  • 当执行到import setupElement from './plugins/element.js'会立即进入 element.js 执行里面的代码,但注意:element.js 中只有「导入组件 + 定义函数」的逻辑会执行,app.component 注册组件的逻辑要等 main.js 中调用 setupElement(app) 时才执行。

从 Element Plus 加载按钮组件的代码;

执行 import 'element-plus/dist/index.css' → 加载 Element Plus 的全局样式(CSS 生效);

执行 export default function setupElement(app) {...} → 定义 setupElement 函数(仅定义,不执行函数内部代码);

执行完 element.js 后,将 setupElement 函数作为返回值,赋值给 main.js 中的 setupElement 变量

  • 执行 main.js 中的实例创建和注册逻辑

  • const app = createApp(App) → 创建 Vue 应用实例(此时还未挂载组件);

  • setupElement(app)调用 element.js 中定义的函数

    • 将 Vue 实例 app 传入函数,执行 app.component('ElButton', ElButton) → 全局注册 ElButton 组件;
  • app.use(router) → 注册路由(如果有);

  • app.mount('#app') → 将 Vue 实例挂载到页面的 #app 元素上:

    • 渲染 App.vue 组件;
    • 解析 App.vue 中的 <el-button> 标签 → 匹配到全局注册的 ElButton 组件 → 渲染出带样式的危险按钮。
import setupElement from './plugins/element.js'

//创建应用实例
const app = createApp(App)
//注册 Element Plus 插件
setupElement(app)

相当于这里要引用setupElement这个方法,创建实例时,调用setupElement这个方法,并将创建的实例当成参数传入,实现vue组件的注册。

简单说:setupElement 是一个 “组件注册工具函数”,调用它并传入 Vue 实例 app,本质就是把 ElButton 组件挂载到 Vue 应用实例上,这样整个项目的所有组件(比如 App.vue)都能使用 <el-button> 标签。

element.js

import { ElCard,ElCol,ElRow } from 'element-plus'
import 'element-plus/dist/index.css'

export default function setupElement(app) {
  app.component('ElCard', ElCard)
  app.component('ElCol', ElCol)
  app.component('ElRow', ElRow)

}

进行优化

import { ElCard,ElCol,ElRow } from 'element-plus'
import 'element-plus/dist/index.css'

export default function setupElement(app) {

    //批量注册
    const components = { ElCard, ElCol, ElRow }
    Object.entries(components).forEach(([name, component]) => {
      app.component(name, component)
    })
}

将需要注册的组件放入对象中,这里是简写语法。 const components = { ElCard, ElCol, ElRow }

const components = { ElCard: ElCard, ElCol: ElCol, ElRow: ElRow }

Object.entries(components)把对象转成「键值对数组」:转换成「二维数组」,每个子数组包含「键、值」

[     ['ElCard', ElCard], 
    ['ElCol', ElCol],
    ['ElRow', ElRow]
   ]

.forEach(...) → 遍历这个二维数组

([name, component]):ES6 数组解构,把遍历到的子数组 ['ElCard', ElCard] 拆成两个变量:

  • name = 'ElCard'(组件注册名);
  • component = ElCard(组件对象)

app.component(name, component):调用 Vue 3 的组件注册方法,等价于 app.component('ElCard', ElCard)

echarts

安装依赖

ESLint 相关的配置包版本太旧,和新版的 Vue ESLint 插件不兼容,连带导致 echarts 安装失败.直接在安装命令后加 --legacy-peer-deps,强制忽略版本冲突,快速安装 echarts

npm i -S echarts --legacy-peer-dep

正常安装

npm i -S echarts

彻底解决依赖冲突

如果想从根本上修复版本冲突,执行以下步骤(不影响已安装的 Element Plus):

步骤 1:卸载旧的 ESLint 配置包

bash

运行

npm uninstall @vue/eslint-config-standard --save-dev
步骤 2:安装兼容新版 eslint-plugin-vue@8.x 的配置包

bash

运行

npm install @vue/eslint-config-standard@latest --save-dev --legacy-peer-deps
步骤 3:重新安装 echarts

bash

运行

npm i -S echarts

main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import setupElement from './plugins/element.js'
import * as echarts from 'echarts'

const app = createApp(App)
setupElement(app)
app.config.globalProperties.$echarts = echarts
app.use(router)
app.mount('#app')

Vue 3 中移除 Vue.prototype,改用 app.config.globalProperties 的本质是:Vue 3 不再基于构造函数原型链扩展,而是基于应用实例的全局配置来挂载全局属性 / 方法,更贴合 Vue 3 的「实例化」设计(每个 createApp() 都是独立的应用实例)。app.config.globalProperties 挂载到这个对象上的属性 / 方法,会被注入到所有组件的「选项式 API」上下文(即 this)中,效果和 Vue 2 的 Vue.prototype 完全一致,但作用域仅限当前应用实例。

// 创建应用实例 
const app = createApp(App) 
// 挂载全局属性/方法 
app.config.globalProperties.自定义属性名 = 要挂载的内容


特性 Vue 2 Vue 3
核心载体 全局构造函数 Vue(所有组件共享同一个原型链) 应用实例 appcreateApp() 创建,多实例隔离)
全局挂载 Vue.prototype.$xxx = xxx(挂载到构造函数原型,全局共享) app.config.globalProperties.$xxx = xxx(挂载到单个应用实例,实例隔离)
  • 多实例隔离:Vue 3 支持一个页面创建多个独立的 Vue 应用实例(比如 app1 = createApp(), app2 = createApp()),如果用原型链,会导致多个实例的全局属性互相污染;而 globalProperties 是绑定到单个 app 实例的,不同实例的全局属性互不影响。

  • 更符合模块化:Vue 3 推崇「模块化」「按需使用」,原型链扩展是侵入式的(修改全局构造函数),而 globalProperties 是配置式的(仅修改当前应用实例),更灵活。

import * as echarts from 'echarts'

import * as echarts from 'echarts' 的作用是:把 echarts 库中所有导出的内容,整体导入并挂载到 echarts 这个变量上。这么写的根本原因是:echarts v5+ 采用「命名导出」(Named Export),而非「默认导出」(Default Export),所以不能用 import echarts from 'echarts' 这种默认导入方式

导出方式 语法示例(库作者写的代码) 导入方式(你写的代码) 适用场景
默认导出(Default Export) export default { init: () => {} } import echarts from 'echarts' 库只有一个核心导出(比如 Vue、React)
命名导出(Named Export) export const init = () => {}``export const dispose = () => {} import * as echarts from 'echarts'import { init } from 'echarts' 库有多个独立导出(比如 echarts、lodash)

echarts 源码的核心导出逻辑类似这样(简化版):

// echarts 源码中的导出逻辑(模拟)
export const init = (dom) => { /* 初始化图表 */ }
export const dispose = (instance) => { /* 销毁图表 */ }
export const registerMap = (name, data) => { /* 注册地图 */ }
// ... 还有上百个命名导出的方法/对象

:echarts 没有写 export default,只有一堆 export const/function 的「命名导出」—— 这就是为什么你用 import echarts from 'echarts' 会报错(找不到默认导出)

import * as echarts from 'echarts' 语法拆解

语法片段 含义
import * 导入目标模块中所有命名导出的内容(init/dispose/registerMap 等)
as echarts 把这些导入的内容,统一挂载到一个名为 echarts命名空间对象
from 'echarts' echarts 这个模块导入

相当于给 echarts 库的所有导出内容做了一个 “收纳箱”,箱子名字叫 echarts

  • 原本分散的 init 方法 → 现在是 echarts.init
  • 原本分散的 dispose 方法 → 现在是 echarts.dispose
  • 所有 echarts 的功能,都可以通过 echarts.xxx 访问,既整洁又不会污染全局变量。

如果想只导入部分方法,也可以写:

// 只导入 init 方法(命名导出的精准导入) 
import { init } from 'echarts'
// 使用时直接写 init(),而非 echarts.init() 
const chart = init(document.getElementById('chart'))

echarts 极致按需导入

// 极致按需导入(推荐生产环境用)
import { init } from 'echarts/core'
import { BarChart } from 'echarts/charts'
import { TitleComponent, TooltipComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'

// 注册需要的模块
init.use([BarChart, TitleComponent, TooltipComponent, CanvasRenderer])

// 使用 init 方法创建图表
const chart = init(document.getElementById('chart'))

echarts使用

<div ref="totalOrderRef" :style="{width:'100%',height:'100px'}" />

<script setup>
        import { onMounted,ref,getCurrentInstance } from 'vue'
        // 1. 定义 ref 变量,和模板中的 ref="totalOrderRef" 对应
        const totalOrderRef = ref(null)
        // 2. 在 onMounted 中获取元素(DOM 渲染完成)
        onMounted(() => {
          // 通过 ref 获取元素(Vue3 推荐方式)
          const chartDom = totalOrderRef.value
          if (chartDom) {
                 const instance = getCurrentInstance()
                 const $echarts = instance.appContext.config.globalProperties.$echarts
                 const chart = $echarts.init(chartDom)
                 chart.setOption({
                         xAxis: {
                                 type: 'value',
                                 show: false
                         },
                         yAxis: {
                                 type: 'category',
                                 show: false
                         },
                         series: [{
                                name: '上月平台用户数',
                                type: 'bar',
                                stack: '总量',
                                data: [300],
                                barWidth: 10,
                                itemStyle: {
                                        color: '#45c946'
                                }
                         },{
                                name: '今日平台用户数',
                                type: 'bar',
                                stack: '总量',
                                data: [200],
                                itemStyle: {
                                        color: '#ddd'
                                }
                         },{
                                 type: 'custom',
                                 stack: '总量',
                                 data: [300],
                                 renderItem: (params,api) => {
                                        const value = api.value(0)
                                        const point = api.coord([value,0])
                                        return {
                                                type: 'group',
                                                position: [point[0] - 10, point[1]] ,
                                                children: [{
                                                        type: 'path',
                                                        shape: {
                                                                d: 'M0 767.909l512.029-511.913L1024 767.909 0 767.909z',
                                                                x: 0,
                                                                y: 2,
                                                                width: 20,
                                                                height: 20
                                                        },
                                                        style: {
                                                                fill: '#45c946'
                                                        }
                                                },{
                                                        type: 'path',
                                                        shape: {
                                                                d: 'M1024 255.996 511.971 767.909 0 255.996 1024 255.996z',
                                                                x: 0,
                                                                y: -22,
                                                                width: 20,
                                                                height: 20
                                                        },
                                                        style: {
                                                                fill: '#45c946'
                                                        }
                                                }]
                                        }
                                 }
                         }],
                         grid: {
                                top: 0,
                                bottom: 0,
                                left: 0,
                                right: 0
                         }
                 })
          }
        })
</script>



getCurrentInstance()

<script setup> 中 “接触” 到组件底层实例的唯一官方入口(<script setup> 是封闭作用域,无法直接访问 this,所以无法像 Vue2 那样用 this.$echarts,只能通过 getCurrentInstance() 获取 appContext(应用上下文),进而访问全局挂载的 $echarts。必须在组件生命周期内调用,onMountedonCreated<script setup> 顶层调用。异步回调(如 setTimeout、接口请求回调)中调用可能获取不到实例。

创建 ECharts 实例

$echarts.init() 方法会创建一个独立的 ECharts 实例,并返回给变量 chart。这个实例是操作图表的 “唯一入口”

  • 每个 DOM 容器对应一个独立的 ECharts 实例(避免多个图表冲突);

  • 实例包含图表的所有配置、数据、渲染状态等核心信息。

setOption

向 ECharts 实例传入图表的配置项(Option),让 ECharts 根据配置渲染 / 更新图表

    chart.setOption({
      xAxis: { type: 'value', show: false }, // x轴配置
      yAxis: { type: 'category', show: false }, // y轴配置
      series: [/* 柱状图/自定义图形系列配置 */], // 图表数据和样式
      grid: { top: 0, bottom: 0 } // 网格布局
    })

配置合并规则

   // 完全替换旧配置,重新渲染图表 
   chart.setOption(newOption, true)            
  • 默认:setOption合并新旧配置(新配置覆盖旧配置,未修改的保留);

  • 强制替换:如需完全替换配置(而非合并),可传第二个参数 true

renderItem

它是 ECharts 「自定义系列(custom series)」的核心渲染函数,作用是「告诉 ECharts 如何手动绘制每一个数据项的图形」

renderItem 是自定义系列(type: 'custom')的必填配置,ECharts 渲染自定义系列时,会对每一个数据项调用一次 renderItem,你需要在这个函数中返回「图形描述对象」,ECharts 会根据这个描述画出对应的图形。

当 ECharts 渲染 type: 'custom' 的系列时,会遍历该系列的 data 数组(比如写的 data: [200]),对每一个数据项(这里是 200)执行一次 renderItem 函数。

通过 renderItem,你可以突破 ECharts 内置图表(如柱状图、折线图)的限制,实现:

  • 自定义形状(如三角形、五角星、不规则路径);
  • 精准控制图形位置(如定位到 200 数值处);
  • 组合多个图形(如一个三角形 + 一个文本标签);
  • 动态调整图形样式(如根据数据值改变颜色 / 大小)。

api对象提供 ECharts 内置的「坐标转换 / 数据获取」工具方法:

api.value(dimIndex):获取当前数据项指定维度的值(如 api.value(0) 取 200);

api.coord([x, y]):把逻辑坐标(如 [200, 0])转换成画布像素坐标;

api.size([width, height]):把逻辑尺寸转换成像素尺寸;

api.style():获取系列默认样式

params对象包含当前数据项的上下文信息:

params.dataIndex:当前数据项的索引(如 0)

params.value:当前数据项的原始值(如 200);

params.seriesIndex:当前系列的索引;

params.coordSys:坐标系信息(如 x/y 轴类型)

renderItem 必须返回一个「图形描述对象」(或数组),ECharts 会根据这个对象绘制图形

    return {
     type: 'group', // 图形类型:组合图形(可包含多个子图形)
     position: point, // 图形的基准位置(像素坐标)
     children: [{ // 子图形列表
       type: 'path', // 图形类型:路径(自定义形状)
       shape: { // 形状配置
         d: 'M0 767.909...', // 路径指令(三角形的绘制路径)
         x: 0,//路径的偏移
         y: 0, //路径的偏移
         width: 20, // 尺寸
         height: 20 // 尺寸
       },
       style: { fill: 'red' } // 样式:填充红色
     }]
   }
类型 作用 示例场景
path 绘制自定义路径(如三角形、不规则图形) 你画红色三角形的核心
rect 绘制矩形 自定义柱状图
circle 绘制圆形 自定义散点图
text 绘制文本 给图形加标签
group 组合多个图形 三角形 + 文本标签

和前面用 type: 'bar' 画柱状图,ECharts 会自动渲染;而 type: 'custom' 则是把渲染权完全交给你,核心差异

维度 内置系列(bar/line) 自定义系列(custom)
渲染逻辑 ECharts 自动绘制(固定形状) 你通过 renderItem 手动定义
灵活性 低(只能改样式,不能改形状) 高(可画任意形状)
复杂度 简单(只需配置 data/style) 稍高(需手动计算坐标 / 形状)
适用场景 标准图表(柱状图、折线图) 非标准图形(自定义标记、组合图形)

配置

stack

它是 ECharts 中用于实现「堆叠式图表」的关键配置,作用是「将多个同系列类型(如 bar)、同 stack 值的系列,在同一类目下按数值累加堆叠显示」 —— 两个柱状图系列因为都配置了 stack: '总量',才会从 0 开始依次累加(200+260),形成绿色 + 灰色的分段堆叠效果

组件引用

把导入的 TopView 组件 “注册” 到 Home 组件的作用域中,只有注册后,才能在 <template> 中使用;

  <template>
      <div class="home">
              <!-- 引用组件 -->
              <top-view/>  
      </div>
</template>

<script>
        import TopView from '../components/TopView'
        export default {
                name: 'Home',
                components: {
                        TopView,// 局部注册组件
                }
        }
</script>

<style>
        .home{
                width: 100%;
                height: 100%;
                padding: 0 20px;
                background: #eee;
                box-sizing: border-box;
        }
</style>

仅在当前 Home 组件内可用,其他组件(如 About.vue)不能直接用 <top-view>,需重新导入 + 注册。不会污染全局作用域,适合只在单个页面使用的组件

公共组件提取

如CommonCard这个组件很多地方用到,进行提取

创建一个mixins文件夹,创建对应的文件名card.js

import CommonCard from '../components/CommonCard/index'
export default {
        components: {
                CommonCard
        }
}

需要调用时引入card.js

<template>
        <common-card/>
</template>

<script>
        import CommonCard from '../../mixins/card'
        export default {
                mixins: [CommonCard]
        }
</script>

<style>
</style>

vue3调用公共组件

<template>
        <common-card
                title="累计订单量"
                value="2,124,223"
        >
        </common-card>
</template>

<script setup>
        import { onMounted, ref,defineOptions } from 'vue'
        import commonCardMixin from '../../mixins/commonCardMixin'
        defineOptions({
          mixins: [commonCardMixin]
        })

</script>

需要使用defineOptions

Mixin 是 Vue2 的经典写法,Vue3 更推荐用 “组件导入函数”“全局注册组件” 替代 mixin(减少隐式依赖)

// utils/importComponents.js
export const useCommonCard = () => {
  const CommonCard = defineAsyncComponent(() => import('../components/CommonCard/index'))
  return { CommonCard }
}

业务组件中使用:

 <script setup>
    import { useCommonCard } from '../../utils/importComponents'
    const { CommonCard } = useCommonCard() // 显式获取组件
</script>

全局注册组件(适合全项目高频使用的组件)

// main.js
import { createApp } from 'vue'
import CommonCard from './components/CommonCard/index'

const app = createApp(App)
app.component('CommonCard', CommonCard) // 全局注册,所有组件可直接使用

插槽

引用子组件common-card时,自定义两个插槽的内容及样式。下面代码存在有误地方,

    <template>
      <div class="compare-wrapper">
        <div class="compare">
          <span>同比</span>
          <span class="emphasis">7.3%</span>
        </div>
      </div> 
    </template>

这段代码无法正常显示出默认插槽中的内容,<template> 无任何指令(如 v-slot)直接包裹默认插槽 → 编译时会被忽略,内容不渲染.

父组件:
<template>
  <common-card 
    title="销售额"
  >

    <template>
      <div class="compare-wrapper">
        <div class="compare">
          <span>同比</span>
          <span class="emphasis">7.3%</span>
        </div>
      </div> 
    </template>
    <!-- footer 具名插槽 -->
    <template v-slot:footer>
      <span>昨日销售额:</span>
      <span class="money">¥40,123</span>
    </template> 
  </common-card>
</template>

子组件:
<template>
<div class="common-card">
<div class="title">{{title}}</div>
<div>
                <slot></slot>
</div>
<div class="total">
    <slot name="footer"></slot>
</div>
</div>
</template>

<template>...</template>,Vue 不知道这是 “默认插槽”,就不会把内容插入到 <slot></slot> 位置;加 v-slot 指令后,Vue 才会识别并渲染。

    <template v-slot>
      <div class="compare-wrapper">
        <div class="compare">
          <span>同比</span>
          <span class="emphasis">7.3%</span>
        </div>
      </div> 
    </template>
    

或者不需要template包裹

        <div class="compare-wrapper">
        <div class="compare">
          <span>同比</span>
          <span class="emphasis">7.3%</span>
        </div>
      </div> 
      
写法 是否生效 原因
<template>内容</template> ❌ 不生效 Vue 无法识别这是默认插槽,编译时忽略该 template
<template v-slot>内容</template> ✅ 生效 v-slot 指令明确标识这是「默认插槽」,Vue 会把内容插入到 <slot></slot> 位置
直接写内容(无 template) ✅ 生效 Vue 自动把未包裹的内容识别为默认插槽,是最简洁的写法

Vue2 和 Vue3 在「<template> 包裹默认插槽」的语法上确实有差别 ——Vue2 中 <template> 包裹默认插槽无需加 v-slot 就能生效,而 Vue3 必须显式加 v-slot 指令

场景 Vue2 写法(生效) Vue3 写法(生效) Vue3 错误写法(不生效)
template 包裹默认插槽 <template>内容</template> <template v-slot>内容</template> <template>内容</template>
直接写默认插槽内容 直接写内容(生效) 直接写内容(生效) -
具名插槽 <template slot="footer"> / <template v-slot:footer> <template #footer> / <template v-slot:footer> -

vue3这样都通过 v-slot 指令管理,逻辑更清晰,也避免了 “无指令 template 被误解析” 的问题。

slot作用

Vue3 中的插槽,本质是组件对外暴露的 “自定义渲染接口” —— 组件开发者给组件预留 “空白位置”(<slot>),组件使用者可以往这个位置插入任意内容(HTML、子组件、逻辑渲染的内容),实现「组件固定结构复用 + 自定义内容灵活定制」。

插槽就像你买的 “定制化蛋糕胚”,蛋糕胚的大小、形状(组件的基础 UI / 逻辑)是固定的,但你可以往上面加水果、奶油、巧克力(插槽内容),做出不同样式的蛋糕,而不用重新做蛋糕胚。

复用性:

没有插槽的组件,只能显示固定内容

<!-- 无插槽的 CommonCard 组件 -->
<template>
  <div class="card">
    <div class="title">累计销售额</div>
    <div class="content">¥1,211,312</div>
  </div>
</template>

有插槽,复用性拉满

<!-- 有插槽的 CommonCard 组件 -->
<template>
  <div class="card">
    <div class="title">{{ title }}</div>
    <!-- 插槽:预留自定义位置 -->
    <div class="content">
      <slot></slot>
    </div>
  </div>
</template>
<script>
export default { props: ['title'] }
</script>

可自定义内容,同一个组件适配多场景

<!-- 场景1:显示销售额 -->
<common-card title="累计销售额">
  <div>¥1,211,312</div>
</common-card>

<!-- 场景2:显示订单数 -->
<common-card title="累计订单数">
  <div>123,456 单</div>
</common-card>

<!-- 场景3:显示复杂内容(同比数据+箭头) -->
<common-card title="累计销售额">
  <div class="compare">
    <span>日同比 7.3%</span>
    <div class="arrow-up"></div>
  </div>
</common-card>


逻辑与 UI 解耦,降低维护成本,比如修改卡片的基础样式只需改 CommonCard,修改销售额的显示格式只需改插槽内容。

插槽可以插入任意内容:

  • 普通 HTML 标签(<div>/<span>);
  • 其他 Vue 组件(<el-button>/<chart>);
  • 带逻辑的渲染内容(v-if/v-for/{{ 变量 }});
  • 甚至是 JSX/TSX(复杂场景)。

Vue3 把插槽分为 3 类,覆盖所有自定义场景,且废弃了 Vue2 的 slot 属性,统一用 v-slot 指令(简写 #

1. 默认插槽(匿名插槽)

  • 定义:无 name 属性的 <slot>,是组件的 “默认自定义位置”;

  • 用法

    • 简洁写法(推荐):直接把内容写在组件标签内,无需 <template>
    • 完整写法:用 <template v-slot> 包裹(Vue3 必须加 v-slot)。

2. 具名插槽

  • 定义:有 name 属性的 <slot>,用于组件内多位置自定义(比如卡片的 header、body、footer);

  • 用法:用 <template #插槽名>(或 <template v-slot:插槽名>)包裹内容,和组件内的 name 一一对应。

    <!-- 组件内定义多个具名插槽 -->
    <template>
      <div class="card">
        <div class="header">
          <slot name="header"></slot> <!-- 具名插槽:header -->
        </div>
        <div class="body">
          <slot></slot> <!-- 默认插槽 -->
        </div>
        <div class="footer">
          <slot name="footer"></slot> <!-- 具名插槽:footer -->
        </div>
      </div>
    </template>
    
    <!-- 使用具名插槽 -->
    <common-card>
      <template #header>
        <h3>累计销售额</h3> <!-- 对应 header 插槽 -->
      </template>
      <div>¥1,211,312</div> <!-- 对应默认插槽 -->
      <template #footer>
        <span>昨日销售额:¥40,123</span> <!-- 对应 footer 插槽 -->
      </template>
    </common-card>
    
    

3. 作用域插槽(带数据的插槽)

  • 定义:组件可以给插槽传递数据(作用域数据),使用者可以接收并基于这些数据自定义渲染 —— 这是插槽的 “高级玩法”,实现「组件传数据 + 使用者自定义渲染」;

  • 用法

    1. 组件内:给 <slot> 绑定属性(:数据名="数据值");

    2. 使用者:用 <template v-slot="插槽变量"> 接收数据,在插槽内使用。

      <!-- 组件内定义作用域插槽(给插槽传数据) -->
      <template>
        <div class="card">
          <!-- 给默认插槽传数据:salesData -->
          <slot :salesData="sales"></slot>
          <!-- 给 footer 插槽传数据:yesterdaySales -->
          <slot name="footer" :yesterdaySales="yesterday"></slot>
        </div>
      </template>
      <script>
      export default {
        data() {
          return {
            sales: { total: 1211312, dayRatio: 7.3 }, // 组件内部数据
            yesterday: 40123
          }
        }
      }
      </script>
      
      <!-- 使用作用域插槽(接收并使用数据) -->
      <common-card>
        <!-- 接收默认插槽的 data,自定义渲染 -->
        <template v-slot="slotProps">
          <div>
            累计销售额:¥{{ slotProps.salesData.total.toLocaleString() }}
            <span>日同比:{{ slotProps.salesData.dayRatio }}%</span>
          </div>
        </template>
        <!-- 接收 footer 插槽的 data,自定义渲染 -->
        <template #footer="footerProps">
          <span>昨日销售额:¥{{ footerProps.yesterdaySales.toLocaleString() }}</span>
        </template>
      </common-card>
      

Vue3 支持对插槽变量解构,让代码更简洁:

        <!-- 解构默认插槽数据 -->
            <template v-slot="{ salesData }">
              <div>累计销售额:¥{{ salesData.total }}</div>
            </template>

            <!-- 解构具名插槽数据 + 重命名 -->
            <template #footer="{ yesterdaySales: ys }">
              <span>昨日销售额:¥{{ ys }}</span>
            </template>  
            

4.支持动态插槽名

可以用变量作为插槽名

<template #[dynamicSlotName]>
  <div>动态插槽内容</div>
</template>
<script>
export default {
  data() {
    return { dynamicSlotName: 'footer' } // 动态指定插槽名
  }
}
</script>

插槽的实战场景

数据可视化组件:封装图表组件时,用作用域插槽传递图表数据,使用者自定义 tooltip、图例的显示样式

列表渲染组件:装通用列表组件时,用作用域插槽传递列表项数据,使用者自定义列表项的渲染样式

通用组件封装:封装卡片、表格、弹窗、导航栏等通用组件时,用插槽预留自定义位置

vue-echarts

vue-echarts 完全兼容 Vue3,且专门为 Vue3 做了适配(支持 <script setup>、组合式 API 等),相比直接使用原生 ECharts,它的优势是:

  • 自动处理 ECharts 实例的创建 / 销毁,避免内存泄漏;
  • 响应式更新配置,数据变化时自动重绘图表;
  • 无需手动获取 DOM 元素,直接通过组件属性传参。
    npm install echarts vue-echarts -S

    npm install vue-echarts echarts --save

全局注册

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import setupElement from './plugins/element.js'

// 1. 核心补充:按需导入 ECharts 核心模块(必填)
import { use } from 'echarts/core'
// 导入你需要的图表类型(根据业务场景添加,比如柱状图、自定义系列)
import { BarChart, CustomChart, LineChart } from 'echarts/charts'
// 导入渲染器(必须,推荐 CanvasRenderer)
import { CanvasRenderer } from 'echarts/renderers'
// 可选:导入交互组件(如提示框、图例,按需添加)
import { TooltipComponent, LegendComponent } from 'echarts/components'

// 2. 核心补充:注册 ECharts 模块(必填)
use([
  BarChart, CustomChart, LineChart, // 图表类型
  CanvasRenderer,                  // 渲染器(必须)
  TooltipComponent, LegendComponent // 可选交互组件
])

// 3. 导入 vue-echarts 组件(你的原有代码)
import VueECharts from 'vue-echarts'

const app = createApp(App)
setupElement(app)

// 4. 全局注册 <v-chart> 组件(你的原有代码,正确)
app.component('v-chart', VueECharts)

app.use(router)
app.mount('#app')    

v-echarts

npm i v-charts echarts -S

或安装(直接执行以下命令,纠正包名 + 忽略 peer 依赖冲突

npm install vue-echarts echarts -S --legacy-peer-deps

vue2和vue3支持状态

维度 v-charts(第三方) vue-echarts(官方)
Vue3 适配 ❌ 无官方支持 ✅ 完全适配(v6+ 版本专为 Vue3 设计)
维护状态 ❌ 停止维护 ✅ 持续更新,和 ECharts 版本同步
打包体积 ❌ 内置全量 ECharts,体积大 ✅ 按需导入模块,体积可控
功能完整性 ❌ 仅封装部分 ECharts 功能 ✅ 支持 ECharts 所有功能(包括自定义系列、交互)
文档 / 社区 ❌ 文档陈旧,无社区支持 ✅ 官方文档完善,问题可在 ECharts 社区解决

v-charts(第三方)和 vue-echarts(官方),前者是 Vue2 专属,后者是 Vue3 首选

Element Plus

el-menu

<el-menu> 组件的基础使用方式,用于实现水平导航菜单(如页面顶部的销售额 / 访问量切换)

<el-menu 
  mode="horizontal" 
  :default-active="'1'"
  @select="pnSelect"
>
配置项 类型 作用 & 意义
mode="horizontal" 字符串 定义菜单的布局模式: horizontal:水平布局(顶部导航); vertical:垂直布局(侧边栏); inline:内嵌布局(侧边栏子菜单展开)
:default-active="'1'" 字符串 / 数字(响应式绑定) 设置菜单的默认选中项,值需和 <el-menu-item>index 匹配;注意:index 本质是字符串,所以这里用 '1'(加引号)更规范(也可写 1,Element Plus 会自动转换) 默认选中 “销售额”(index="1"),页面加载后该菜单项会高亮
@select="pnSelect" 事件绑定 监听菜单选中事件:点击 <el-menu-item> 时触发,回调函数会接收当前选中项的 index 点击 “销售额”/“访问量” 时,pnSelect 方法会拿到 index(1/2),用于后续业务逻辑(如切换图表数据)

JavaScript 基础理解一

变量

变量是可变的量。将编程思想转换为现实生活中的例子进行理解。可变的量存在一个容器中,就像一个苹果箱里面有着许多苹果,箱子的作用就是用于存放量,而里面的苹果就是实际的值。

如:var apples = 20

var: 相当于制作了一个空箱子

apples: 给这个空箱子贴上苹果的标签,用于识别里面存放的是什么

20: 箱子里放了20个苹果

apples = 30 把20个苹果拿走,换成30个(重新赋值)

整个过程就像是工厂制作好箱子贴上标签,放入苹果,等待客户过来订单拿走。由此可以理解变量的本质:计算机内存中一块有名字的存储空间(“箱子”),变量名是 “用于方便识别的标签”,变量值是 “箱子里的东西”;

var/let

制作空箱子的方式有两种var和let,这两个关键字来声明变量。通过var或let制作出一个空箱子,贴上用于识别的标签。

声明变量语法: var 变量名;  或 let 变量名; 

两者的区别:核心差异集中在作用域、变量提升、重复声明、全局绑定

1.作用域:var 是 “函数 / 全局作用域”,let 是 “块级作用域”。

var 无视块级作用域,只认 “函数” 或 “全局” 边界,会从块内 “泄露” 到外部,污染全局。

块级作用域:就是 {} 包裹的区域(比如 ifforwhile 或直接写的 {}),let 声明的变量只在当前块内有效,出了块就 “消失”,避免了全局污染。

  1. 变量提升:var 完全提升,let 提升但有 “暂时性死区”,变量提升:JS 引擎会把变量声明 “提前” 到作用域顶部,但初始化(赋值)还在原来的位置。
  • var:声明 + 初始化都被提升(提前造了箱子,还往里面放了 “空”),声明前访问不会报错,只会得到 undefined

  • let:只有声明被提升,但初始化未完成,声明前访问会报错(这个阶段叫 “暂时性死区”)—— 相当于 “提前说要造箱子,但箱子还没做好,不能用”。

  1. 重复声明:var 允许,let 禁止
  • var:同一作用域内可以重复声明同一个变量(相当于给同一个箱子反复贴标签,不会报错);

  • let:同一作用域内禁止重复声明(同一个区域不能有两个贴一样标签的箱子,会直接报错)。

  1. 全局作用域绑定:var 挂到 window,let 不挂,在全局作用域(函数外)声明变量时:
  • var 声明的变量会成为 window 对象的属性(相当于把箱子直接挂在 “房子” 墙上,所有人都能看到);
  • let 声明的变量不会绑定到 window(箱子放在房子的公共区域,但不挂墙,不属于房子的属性)。

1.作用域

// 用 let 声明(块级作用域)

{ 
   let apple1 = 10;
   console.log(apple1);   //输出10
}

console.log(apple1);  // 报错:apple1 is not defined

生活中的例子来理解:

块级作用域 = 超市的 “分区管理”

  • {} 就对应超市里的水果区(一个独立的块);

  • let apple1 = 10 就是 “水果区专属的苹果箱”,这个箱子被明确规定 “只能在水果区范围内”;

  • 出了水果区(也就是 } 之后),到蔬菜区 / 日用品区(块外部),自然找不到这个 “水果区专属箱子”,所以 console.log(apple1) 会报错。箱子里的苹果只能在水果区域。不能在蔬菜或其他日用品区域出现。进行了规定及区域限制。

再举一个例子:

// 只有上午10点-11点(条件满足),试吃区(块)才开放 
if (new Date().getHours() >= 10 && new Date().getHours() < 11) { 
    let trialApple = 1; 
    console.log(trialApple); // 试吃区能拿,输出1 
  } 
  // 过了11点离开试吃区,就拿不能拿到试吃的食物
  console.log(trialApple);  // 报错:trialApple is not defined

对比 var(无块级作用域)= 小卖部的随意摆放

{ 
    var apple2 = 20; 
    console.log(apple2); //输出20

 }

console.log(apple2) //输出20

var 声明的变量就像小卖部没有区域区分和限制,不管是水果区、日用品区,整个小卖部(函数 / 全局作用域)都能用到。

典型场景:for 循环

// var 版:循环结束后 i 会泄露,且所有循环体共享同一个 i 
for (var i = 0; i < 3; i++) { 
    setTimeout(() => console.log(i), 100);
    // 输出 333(因为共享一个i,最后i=3) 
 } 


// let 版:每次循环都会创建新的 i,块级作用域隔离 
for (let i = 0; i < 3; i++) { 
    setTimeout(() => console.log(i), 100); // 输出 012(每个循环有自己的i) 
}

进行分析var版:

  • 同步代码优先执行:for 循环是 “同步代码”,需要从头到尾跑完。

  • 异步代码延后执行setTimeout 是 “异步代码”,要等同步代码全部跑完,且等待 100ms 后才执行。

  • var 声明的 i 是 “共享的” :var 没有块级作用域,整个 for 循环里只有一个 i 变量(相当于一个公共的本子),循环中每次修改的都是这个本子上的数字。

步骤 1:初始化变量(同步)

执行 var i = 0:创建一个全局 / 函数作用域的变量 i,值为 0(公共本子上先写 0)。

步骤 2:第一次循环(同步)
  • 判断条件 i < 3:0 < 3,条件成立;
  • 执行循环体:调用 setTimeout,把回调函数 () => console.log(i) 放入 “异步任务队列”,此时回调函数还没执行
  • 执行 i++:把公共本子上的 i 改成 1。
步骤 3:第二次循环(同步)
  • 判断条件 i < 3:1 < 3,条件成立;
  • 执行循环体:再放一个回调函数到异步队列;
  • 执行 i++:公共本子上的 i 改成 2。
步骤 4:第三次循环(同步)
  • 判断条件 i < 3:2 < 3,条件成立;
  • 执行循环体:放第三个回调函数到异步队列;
  • 执行 i++:公共本子上的 i 改成 3。
步骤 5:循环结束(同步)
  • 判断条件 i < 3:3 < 3,条件不成立,for 循环彻底跑完;
  • 此时同步代码全部执行完毕,公共本子上的 i 固定为 3。
步骤 6:执行异步回调(延后)

等待 100ms 后,JS 依次执行异步队列里的 3 个回调函数:

  • 第一个回调:去查公共本子上的 i → 3,输出 3;
  • 第二个回调:还是查同一个公共本子 → 3,输出 3;
  • 第三个回调:依旧查这个本子 → 3,输出 3。

核心原因:var 声明的 i全局 / 函数作用域,整个循环只有一个 i,同步循环跑完后 i 已经变成 3

进行分析let版:

let 在 for 循环中有个特殊设计 ——每次循环迭代都会创建一个全新的、独立的 i 变量(而非共享同一个),每个 setTimeout 回调会 “绑定” 当前迭代的这个独立 i

步骤 1:第一次循环迭代(同步执行)
  1. 创建第一个独立的 i 变量(块级作用域),初始值为 0;
  2. 判断条件 i < 3(0 < 3,成立);
  3. 执行 setTimeout:把回调函数 () => console.log(i) 放入异步队列,这个回调会 “记住” 当前这个独立的 i=0
  4. 执行 i++:本次迭代的 i 变成 1(但这个变化只属于当前迭代的独立 i)。
步骤 2:第二次循环迭代(同步执行)
  1. 创建第二个独立的 i 变量(全新的,和上一个无关),初始值继承上一次的结果(1);
  2. 判断条件 i < 3(1 < 3,成立);
  3. 执行 setTimeout:回调 “记住” 当前这个独立的 i=1,放入异步队列;
  4. 执行 i++:本次迭代的 i 变成 2。
步骤 3:第三次循环迭代(同步执行)
  1. 创建第三个独立的 i 变量,初始值为 2;
  2. 判断条件 i < 3(2 < 3,成立);
  3. 执行 setTimeout:回调 “记住” 当前这个独立的 i=2,放入异步队列;
  4. 执行 i++:本次迭代的 i 变成 3。
步骤 4:循环终止(同步执行)

判断条件 i < 3(3 < 3,不成立),for 循环彻底跑完。

步骤 5:执行异步回调(延后执行)

100ms 后,JS 依次执行异步队列里的 3 个回调函数:

  • 第一个回调:调用 “记住” 的第一个 i=0 → 输出 0;
  • 第二个回调:调用 “记住” 的第二个 i=1 → 输出 1;
  • 第三个回调:调用 “记住” 的第三个 i=2 → 输出 2。

核心运行逻辑差异:

var 整个循环只有1 个共享的 i(函数 / 全局作用域),let 每次循环创建新的独立 i(块级作用域)。

var 无块级作用域,i 泄露到循环外,let 块级作用域,每个 i 仅限当前迭代使用。

var回调绑定 “唯一的共享 i”,最终取到 i=3。let绑定 “当前迭代的独立 i”,分别是 0/1/2。

2. 暂时性死区

// var 提升:提前造了箱子,里面是空的 
console.log(banana); // 输出 undefined(箱子存在但没装苹果) 
var banana = 15; // 声明+赋值 

// let 暂时性死区:箱子还没造好,不能用 
console.log(orange); // 报错:Cannot access 'orange' before initialization 
let orange = 25; // 声明+赋值

3.重复声明

// var 重复声明:没问题 
var pear = 5; 
var pear = 8; // 覆盖之前的值,不会报错 
console.log(pear); // 输出 8 

// let 重复声明:报错 
let grape = 6; 
let grape = 9; // 报错:Identifier 'grape' has already been declared

4.var 挂到 window,let 不挂

// 全局 
var mango = 30;
console.log(window.mango); // 输出 30(挂在window上) 

// 全局 
let cherry = 40; 
console.log(window.cherry); // 输出 undefined(不挂在window上)

var变量提升

代码是从上一行一行往下执行

//执行顺序声明一个变量num 并赋值为20 
var num = 20
console.log(num) //再打印输出这个变量的值


//根据代码从上往下执行,sun输出时没有声明变量,应该报错,但是输出的是undefined
console.log(sun)
var sun = 30   //是因为浏览器会将var sun 放到最顶部,变量提升

//如下形式
var sun;
console.log(sun);
sun = 30;

const

const 声明的变量,指向的内存地址不可变(简单说就是 “箱子不能换,但箱子里的内容可能能改”)

  1. 声明时必须初始化(不能造 “空箱子”)

const 声明变量时,必须立刻赋值(往箱子里放东西),不能像 let/var 那样先声明、后赋值,否则直接报错。

错误:const 不能声明空变量 const apple;

正确:声明时必须初始化 const apple = 10;

2.声明后不能给 const 变量重新赋值(相当于不能把整个箱子换成新的),否则报错

const total = 20; total = 30;// 错误:不能重新赋值(换箱子)

  1. 块级作用域(和 let 完全一致)

在所在的 {} 块内有效,出块即失效

4.引用类型(对象 / 数组):内容可改,指向不可改

const 绑定的是简单类型(数字、字符串、布尔值),因为值直接存在 “箱子” 里,指向不可变 = 值不可变;

如果绑定的是复杂类型(对象、数组),“箱子” 里装的是 “指向果篮的地址”,地址不可改(不能换果篮),但果篮里的内容可以改。

// 简单类型(值不可变) 
const num = 10; 
num = 20; // 报错(换箱子=改值) 

// 复杂类型(对象)—— 内容可改,指向不可改 
const fruitBasket = { red: 10, green: 5 }; // 可以改箱子里的内容(调整果篮里的苹果数量) fruitBasket.red = 15;
console.log(fruitBasket.red); // 输出 15 
fruitBasket = { orange: 8 }; //不能换箱子(不能改指向的地址) 报错

// 示复杂类型(数组)
const arr = [1, 2, 3]; 
arr.push(4); //可以改内容,输出 [1,2,3,4] 
arr = [5,6]; //  不能换数组(改指向),报错


  • const 核心是 “指向不可变”,而非 “值不可变”—— 简单类型值不可改,复杂类型内容可改、指向不可改;

  • const 声明必须初始化、不可重新赋值、有块级作用域,禁止重复声明;

数据类型

js中的数据类型,将编程思维变成生活中思维可以理解成归类,用于更加快捷,方便的区分,通过统一标签降低代码混乱,根据其特性进行使用。

比如将水果和蔬菜放在一个大筐里,想要从里面拿出一个苹果,需要在一堆各种各样的水果和蔬菜混装中找到,不方便。如果一次性要找出五个苹果,那么花费的时间更长。

但将水果放在一个大筐里,蔬菜单独放在一个大筐里,这样比较好找一些。如果再细分下,划分两个区域,一个区域放水果,苹果单独一筐,香蕉单独一筐。另一个区域放蔬菜,青菜单独一筐,胡萝卜单独一筐,这样既不混乱也方便找到需要的东西。

同时蔬菜和水果不能进行炒菜,这样也区分了特性。

基于上面的理解,那么js数据类型也可以分为两个大区域:基本类型和引用类型。为了更好使用分别又进行了划分,7种基本数据类型,引用类型Object

1.基本数据类型(原始类型)

基本数据类型:值直接存在变量指向的内存地址(箱子里直接装东西),箱子里直接装苹果、香蕉(值),拿取直接用

1.String 字符串

定义:文本内容,用单引号 / 双引号 / 反引号包裹;

const fruitName = "苹果"; // 双引号 
const desc = '红富士苹果'; // 单引号 
const priceDesc = `苹果单价:8.99元`; // 反引号

2. Number 数字

定义:包含整数、小数、特殊值(NaN、Infinity);

const appleCount = 20; // 整数 
const applePrice = 8.99; // 小数 
const invalidNum = 10 / "苹果"; // NaN(Not a Number,非数字,注意:NaN 不等于任何值,包括自己) 
const bigNum = 1 / 0; // Infinity(无穷大)

3. Boolean 布尔值

定义:只有两个值:true(真)、false(假),用于条件判断;

  1. Undefined 未定义

定义:变量声明了但未赋值时的默认值;

let apple; // 只声明,没赋值 
console.log(apple); // 输出 undefined

5. Null 空值

定义:主动声明的 “空”,表示变量指向的内存地址无内容;

const emptyBox = null; // 主动表示箱子是空的

6. Symbol 符号

定义:唯一的、不可重复的值,用于创建唯一标识;

const id1 = Symbol("apple"); 
const id2 = Symbol("apple"); 
console.log(id1 === id2); // 输出 false

7. BigInt 大整数

定义:解决 Number 的精度问题,处理超大整数,后缀加 n,不能和 Number 直接运算,需先转换;

const bigNum = 9007199254740993n; // 大整数 
const sum = bigNum + 1n; // 运算时也要加 n,输出 9007199254740994n

2.引用数据类型

  1. Object 普通对象

定义:键值对(key-value)集合,key 是字符串 / Symbol,value 可以是任意类型;

const fruit = { 
    name: "苹果", // key: name,value: 字符串
    price: 8.99, // key: price,value: 数字 
    hasStock: true // key: hasStock,value: 布尔值 
    }; 
    // 修改对象内容(允许,因为只是改地址指向的内容) 
    fruit.price = 7.99; 
    console.log(fruit.price); // 输出 7.99
    

2. Array 数组

定义:有序的集合,索引从 0 开始,本质是特殊的 Object;

const fruits = ["苹果", "香蕉", "橙子"]; // 修改数组内容(允许) 
fruits.push("葡萄"); // 新增元素 
console.log(fruits); // 输出 ["苹果", "香蕉", "橙子", "葡萄"]

3. Function 函数

定义:可执行的代码块,本质是特殊的 Object(可以作为参数、返回值);

  1. 其他引用类型
  • Date(日期):处理时间,const now = new Date();

  • RegExp(正则):处理字符串匹配,const reg = /apple/;

堆和栈

  • 栈(Stack,执行栈 / 调用栈) :像取餐口 —— 空间小、存取快、顺序先进后出,只能放固定大小的物品。

  • 堆(Heap) :像仓库 —— 空间大、能放大小不固定的物品,存取稍慢,物品位置无序,需要标记(地址)才能找到。

JS 引擎正是通过这两个空间的配合,完成所有数据的存储和管理。

  • 堆的内存不会自动释放,需要 JS 的垃圾回收机制(GC)定期清理无引用的对象;

  • 堆中的数据没有固定顺序,每个数据会有一个「内存地址」(指针),通过这个地址才能找到数据。

    // 1. 堆中创建对象本体:{ name: "张三" },分配地址(比如 0x123) 
    // 2. 栈中存储:obj1 → 0x123(指针指向堆的地址0x123) 
    let obj1 = { name: "张三" }; //将地址赋值给到变量,变量拿到的是地址而非真正的值
    // 3. 栈中拷贝指针:obj2 → 0x123(obj1和obj2指向堆中同一个对象) 
    let obj2 = obj1; 
    // 4. 通过obj2修改堆中的数据本体 
    obj2.name = "李四"; 
    // 5. obj1通过指针访问堆中同一数据,所以值也变了 
    console.log(obj1.name); // 输出 李四
    
  • 赋值阶段let obj1 = { name: "张三" }

  • JS 引擎先在堆内存里开辟一块空间,存入 { name: "张三" } 这个对象本体,并给这块空间分配唯一的内存地址(比如0x123); - 然后在栈内存里创建变量 obj1,并把「地址 0x123」这个指针赋值给 obj1 —— 所以 obj1 本身存的不是对象,而是指向对象的 “门牌号”。

  • 拷贝阶段let obj2 = obj1

    • 这一步并不是把堆里的对象复制一份,而是把栈里 obj1 存的地址(0x123)拷贝给 obj2
    • 此时栈里 obj1obj2 都指向 0x123,相当于两个人拿着同一个门牌号,能找到同一个房子(堆里的对象)。
  • 修改阶段obj2.name = "李四"

    • 引擎先读取栈里 obj2 的地址(0x123),然后根据这个地址找到堆里的对象;
    • 直接修改堆里这个对象的 name 属性 —— 因为房子只有一个,不管用哪个门牌号进去改,房子里的东西都会变。
  • 访问阶段console.log(obj1.name)

    • 引擎读取栈里 obj1 的地址(0x123),找到堆里的对象,读取 name 属性 —— 自然就是修改后的「李四」。

代码2:

  let obj1 = { name: "张三" }; 
  let obj2 = obj1; // 注意:这是给obj2重新赋值,不是修改属性 obj2 = { name: "王五" };
  console.log(obj1.name); // 输出 张三(而非王五)
  • 堆内存:有一个对象 { name: "张三" },地址 0x123

  • 栈内存:obj1 → 0x123obj2 → 0x123(两个变量都指向同一个堆地址)

  • JS 引擎看到你写了 { name: "王五" } —— 这是一个「全新的对象字面量」,引擎会默认认为你需要一个新对象,因此会在堆里重新开辟一块新空间(比如地址 0x456),并把 { name: "王五" } 存入这个新地址;

  • 修改栈里的指针:把栈中 obj2 原来存储的地址 0x123 替换成新地址 0x456; 此时 obj1 仍指向 0x123(原堆对象),obj2 指向 0x456(新堆对象);堆里同时存在 0x1230x456 两个独立的对象,互不影响。

  • JS 中只要写 {}/[]/function(){} 等引用类型字面量,引擎就会在堆里新建一块空间存储这个新数据;所以obj2 = { name: "王五" } 是 “赋值新对象”,而非 “修改原对象”,所以会先创建新堆地址,再更新栈里 obj2 的指针;

基本数据类型放在栈中

基本类型放在栈里,是 JS 引擎为了「性能最优」做的设计,栈的存取速度远高于堆,栈内存的核心特征之一是:只能存储「大小固定、已知」的数据

栈是 “先进后出” 的线性结构,数据的存入(压栈)、取出(弹栈)只需要操作栈顶指针,不需要像堆那样遍历、查找内存地址,CPU 能直接缓存栈的连续内存,访问速度极快;

基本类型是 JS 中使用最频繁的数据(比如数字计算、布尔判断、简单字符串拼接),把它们放在最快的栈里,能最大程度减少内存访问耗时,提升代码执行效率。

如果把基本类型放堆里,每次访问都要先查栈里的指针,再找堆里的数据。

栈是一块连续的线性内存空间,像一排编号固定的小格子。7 种基本数据类型(Number、String、Boolean、Undefined、Null、Symbol、BigInt),它们的值在创建时大小就是固定的:

-   Number:不管是 10 还是 100000,都占用 8 字节(JS 中统一用 64 位浮点数存储);
-   Boolean:只有 true/false 两种可能,占用 1 字节;
-   String:虽然看起来长度可变,但 JS 中字符串是「不可变的」
-   Undefined/Null:占用极小且固定的内存空间。

引用类型(对象、数组等)大小不固定(比如数组可以无限 push 元素),无法提前确定占用多少字节,所以只能放在 “不限制大小、无序存储” 的堆里。

栈是自动释放:函数执行时,变量被压入栈;函数执行结束,对应的栈帧(包含变量)会立即被销毁,内存自动释放。

// 执行函数时,栈中创建栈帧,存入a、b(固定大小,快速分配)
    function add() { 
        let a = 10; // 栈:a → 10 
        let b = 20; // 栈:b → 20 
        return a + b; 
    } 
add(); // 函数执行结束,栈帧被立即销毁,a、b的内存自动释放,无残留


同步和异步

  • 同步:像去奶茶店排队买奶茶 —— 必须等前面的人都买完、你拿到奶茶,才能做下一件事,一步等一步,完全按顺序来;

  • 异步:像点外卖 —— 下单后不用等外卖送到,你可以先去看电视,等外卖到了(有结果了),再处理收外卖这件事,不用全程等待。

同步(Synchronous):按顺序执行,阻塞线程

同步是 JS 代码的「默认执行模式」,核心规则是:代码严格按照书写顺序依次执行,前一行代码执行完成(不管是简单计算、函数调用),后一行代码才会开始执行

在执行同步代码时,JS 的主线程会被「阻塞」—— 直到当前同步任务完成,才能处理下一个任务。

为何要使用同步,是因为JS可修改DOM结构,JS和DOM共用一个线程。

2. 异步(Asynchronous):不等待,不阻塞线程

异步是为了解决「同步阻塞」问题设计的执行模式,核心规则是:耗时的异步任务不会阻塞主线程,JS 会先跳过它执行后面的同步代码,等异步任务有结果了(比如定时器到时间、网络请求返回),再回头执行对应的回调函数

console.log('1. 主线程开始执行'); // 异步任务:定时器(延迟1秒执行回调)
setTimeout(() => {
    console.log('2. 异步定时器回调执行'); 
}, 1000); // 不会等定时器,直接执行这行同步代码 
console.log('3. 主线程继续执行,不等异步任务');

1. 主线程开始执行
3. 主线程继续执行,不等异步任务 
2. 异步定时器回调执行 // 1秒后才输出

梳理

  • JS 是单线程:同一时间确实只能执行一个任务;

  • 执行优先级:先同步,后异步:同步任务全部执行完,才会处理异步任务;

  • 异步不会 “插队”:哪怕异步任务先 “准备好”(比如定时器设 0 秒),也得等同步任务全执行完才会运行。

  • 如果没有异步,单线程的 JS 面对任何耗时操作(比如网络请求、定时器)都会卡死,而异步的核心好处就是「不阻塞主线程,让程序 / 页面始终可交互,同时高效利用资源」。

  • 比如:点击 “加载数据” 按钮后,用异步请求数据,用户依然可以滚动页面、点击其他按钮,不会出现 “卡死”;页面加载时异步加载图片 / 数据,用户能先看到页面骨架,再逐步加载内容,而非白屏等待。

  • 同步模式下,CPU 会在耗时操作(比如网络请求)期间 “空等”(因为要等服务器返回数据,CPU 没事可做);

  • 异步模式下,CPU 会把耗时操作交给浏览器 / Node 的异步模块(比如网络线程、定时器线程)处理,自己继续执行其他任务,直到异步任务完成后再回调 ——CPU 始终在干活,不会闲置

  • JS 是单线程,但异步能让 JS “看起来像同时处理多个任务”(伪并发)

  • 同时发起 3 个网络请求(用户信息、商品列表、分类列表),异步模块会并行处理这 3 个请求,谁先完成谁先回调,总耗时≈最慢的那个请求的时间(而非 3 个请求时间相加);如果是同步,总耗时 = 请求 1 + 请求 2 + 请求 3,效率极低

调用栈(同步任务区) :奶茶店的「制作台」—— 只能做一杯奶茶(单线程),按顺序做完一个,才能接下一个;JS 引擎扫描代码,把所有同步任务(比如变量赋值、console.log、普通函数)依次推入「调用栈」,逐个执行。

任务队列(异步任务区, 队列结构,先进先出) :奶茶店的「取餐叫号机」—— 异步任务(比如外卖单)不会直接进制作台,而是先在叫号机排队,等制作台空了(同步任务做完),再按顺序叫号处理;

事件循环(协调者) :奶茶店的「店员」—— 不停检查制作台(调用栈)是否空,空了就去叫号机(任务队列)取一个异步单来做。

同步任务在「调用栈」执行,异步回调在「任务队列」排队,由「事件循环」协调执行。

执行异步任务:只有当「调用栈为空」(所有同步任务都执行完),事件循环才会把任务队列里的异步回调函数逐个推入调用栈执行 —— “同步执行结束后,找到异步执行”。

思考的问题:当异步任务未完成是否影响到下一个异步任务。

JS 的任务队列是「先进先出」的独立队列,每个异步任务的回调都是独立排队、独立执行的。异步任务只要 “有结果了(不管是好结果还是坏结果)”,对应的回调就会被放进任务队列,等调用栈空了执行;只有异步任务 “没完成”(比如网络请求还在 pending、定时器还在计时),回调才不会入队。

一个异步回调执行失败(比如报错),JS 引擎只会终止当前这个回调的执行,调用栈清空后,依然会继续执行任务队列里的下一个异步回调;

一号顾客的奶茶做砸了(回调报错),只会重新给一号做(如果处理了错误),但二号、三号顾客的奶茶依然会按顺序做,不会因为一号砸了就停。所以单个异步的成功 / 失败(或回调报错),不会影响任务队列里其他异步回调的执行。

当 JS 主线程遇到多个异步任务时,会把它们分别交给对应的异步模块,这些异步模块是多线程的,能同时处理多个任务(比如一个定时器线程计时的同时,另一个网络线程发请求);

每个异步任务只有自己 “完成”(成功 / 失败)后,才会把回调放进任务队列;未完成的异步任务,只是在自己的线程里 “等待”,不会占用主线程,也不会阻止其他异步模块的工作。

多个异步任务的执行顺序,由它们各自完成的时间决定(谁先完成谁先入队执行)。

问题思考:多个异步任务按 “完成时间先到先得” 的方式执行,在需要「有序逻辑」的场景下是否造成影响

如果业务逻辑依赖固定执行顺序(比如先查用户、再查订单),会导致逻辑混乱、数据错误;如果业务逻辑不依赖顺序(比如同时加载两张无关的图片)。

先请求 “用户信息”(拿到用户 ID),再用用户 ID 请求 “用户订单”。如果订单请求网络更快,先完成入队执行,就会因为没有用户 ID 导致请求失败 / 数据错误。

let userId = null;

 // 异步1:请求用户信息(假设网络慢,2秒完成) 
setTimeout(() => {
    userId = 1001; // 拿到用户ID
    console.log('异步1完成:拿到用户ID', userId); 
}, 2000); 

// 异步2:请求用户订单(依赖userId,假设网络快,1秒完成) 
setTimeout(() => { 
    console.log('异步2执行:请求订单,用户ID为', userId); // 此时userId还是null  
}, 1000);

异步2执行:请求订单,用户IDnull ( 先完成的异步2先执行,拿到无效数据 )
异步1完成:拿到用户ID 1001

如何解决

让异步任务按「业务逻辑顺序」执行,而非「完成时间顺序」。异步执行顺序从 “时间驱动” 变回 “逻辑驱动”;

方案 1:串行执行(依赖型异步,用 async/await)

“必须先 A 后 B” 的场景,让 B 等待 A 完成后再执行:

 async function f() { 

     let userId = null; 
     
     await new Promise((resolve) => { 
         setTimeout(() => { 
             userId = 1001; 
             console.log('异步1完成:拿到用户ID', userId);
             resolve(); // 标记异步1完成 
         }, 2000); 
     }); 

     // 异步2:等异步1完成后再执行
     await new Promise((resolve) => { 
         setTimeout(() => { 
             console.log('异步2执行:请求订单,用户ID为', userId); // 此时ID=1001 
             resolve(); 
             }, 1000); 
         }); 
     } 

 f();
 

方案 2:并行等待(需要所有异步完成,用 Promise.all)

适合 “需要所有数据到齐再处理” 的场景,不管谁先完成,都等全部完成后统一执行:

 // 异步1:请求商品列表(2秒完成) 
 const fetchGoods = new Promise((resolve) => { 
     setTimeout(() => { resolve(['商品1', '商品2']); }, 2000); 
  });

 // 异步2:请求分类列表(1秒完成) 
 const fetchCate = new Promise((resolve) => { 
     setTimeout(() => { resolve(['分类1', '分类2']); }, 1000); 
 }); // 等待所有异步完成,再统一处理 

 Promise.all([fetchGoods, fetchCate]).then(([goods, cate]) => { 
     console.log('所有数据到齐:', { goods, cate }); // 这里渲染页面,数据完整 
 });
 
 所有数据到齐: { goods: ['商品1', '商品2'], cate: ['分类1', '分类2'] }
 
 
 
 

promise

callback hell 回调地狱,在了解回调地狱时先了解下什么是回调。

回调 & 回调函数到底是什么?

你去蛋糕店定一个蛋糕,跟店员说:“蛋糕做好了叫我一声,我过来取”。

  • 这里的你就是程序主逻辑,而 “叫我一声” 这个动作就是回调函数,这件事情交给了店员(执行异步操作的函数);

  • 店员不用一直等蛋糕做好,忙别的事(异步执行),蛋糕做好了才会 “回头调用”,执行“叫你” 这个动作;

  • 这个 “被交给别人、等时机到了再执行的动作”,就是回调函数;“回头调用” 这个动作本身,就是回调

  • 回调(Callback) :指 “回头调用” 的行为 —— 一个函数执行完成后,“回头” 调用另一个函数的过程。

  • 回调函数:被作为参数传递给另一个函数(我们称这个函数为 “主函数”),并由主函数在合适的时机(同步 / 异步操作完成后)调用执行的函数。

回调函数的本质是:把函数当作参数传递,让其他函数决定它的执行时机。

通过上面例子可以理解:

  • “我把函数给你,你用完了再叫我” :回调函数的执行权不在自己手里,而是交给了接收它的主函数;

  • 同步 / 异步都能用:异步场景(定时器、AJAX)是为了等结果,同步场景(forEach、sort)是为了自定义逻辑;

  • 本质是 “参数” :回调函数只是一个 “以函数形式存在的参数”,和数字、字符串参数没有本质区别,只是类型不同。

回调函数的同步 / 异步,由执行回调的主函数决定:

  • 如果主函数在执行过程中立刻、无延迟地调用回调函数 → 这是同步回调
  • 如果主函数先执行完,等某个异步操作(定时器、网络请求、文件读取)完成后延迟调用回调函数 → 这是异步回调

async/await

异步编程在一些业务逻辑下存在问题,async/await是一种解决方案。

核心作用是把 “回调式 / 链式” 的异步代码改写成 “同步风格” ,大幅提升异步代码的可读性、可维护性,同时简化错误处理和异步顺序控制。

  1. 回调地狱:多层异步嵌套(比如 “请求用户→请求订单→请求订单详情”),代码缩进层层嵌套,可读性极差;
  2. Promise.then 链:虽然解决了回调地狱,但多步异步会形成长长的.then链式调用,逻辑分散,依然不够直观;
  3. 错误处理繁琐:Promise 需要用.catch单独捕获错误,多层异步的错误处理会分散在不同位置。

async/await正是为解决这些问题而生 —— 让异步代码 “看起来像同步代码”,同时保留异步非阻塞的特性。

❌