普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月7日首页

TypeScript 泛型从轻松入门到看懂源码

作者 SuperEugene
2026年2月7日 02:02

从「完全不懂泛型」一路走到「看懂下面这段代码到底在干嘛」:

//此代为为VxeTable组件库Grid配置式表格数据分页示例代码部分片段
<script lang="ts" setup>
import { reactive } from 'vue'
import type { VxeGridProps, VxeGridListeners } from 'vxe-table'

interface RowVO {  
  id: number  
  name: string  
  role: string  
  sex: string  
  age: number  
  address: string
}

const gridOptions = reactive<VxeGridProps<RowVO>>({  
  showOverflow: true,  
  border: true,  
  loading: false,  
  height: 500,  
  pagerConfig: pagerVO,  
  columns: [    
    { type: 'seq', width: 70, fixed: 'left' },    
    { field: 'name', title: 'Name', minWidth: 160 },    
    { field: 'email', title: 'Email', minWidth: 160 },    
    { field: 'nickname', title: 'Nickname', minWidth: 160 },    
    { field: 'age', title: 'Age', width: 100 },    
    { field: 'role', title: 'Role', minWidth: 160 },    
    { field: 'amount', title: 'Amount', width: 140 },    
    { field: 'updateDate', title: 'Update Date', visible: false },    
    { field: 'createDate', title: 'Create Date', visible: false },  
  ],  
  data: [],
})
</script>

VxeTable组件库简介:

  • 由于这篇文章引用到了VxeTable组件库的代码,所以在这里给没接触过的小伙伴做一个简单的介绍,老司机可自行跳过。
  • VxeTable是一个基于 Vue 的表格组件库,提供表格、表单、工具栏、分页等组件,适合中后台场景。性能与功能都较强,但学习成本和按需引入的配置需要投入时间。如果你的项目以表格为核心,且需要虚拟滚动、复杂交互等功能,VxeTable 是合适的选择。感兴趣的小伙伴可以通过下方贴上的官网链接学习了解。

(PS·即使没用过VxeTable也不影响你看懂这篇文章)

官网链接:VxeTable官网

一、什么是泛型?一句话版本

泛型 = 给 “类型” 加参数。

  • 函数可以有参数:function fn(x: number) {}
  • 类型也可以有 “参数”:Array<string>

这里 Array 就是一个「带类型参数」的类型,<string> 就是「传给它的类型参数」。

用人话说:

泛型就是:我写一份通用的类型 / 函数,真正用的时候再告诉它具体用什么类型

二、最普通的一层泛型

1. 最熟悉的例子:数组

// 这俩是完全等价的
const list1: string[] = []
const list2: Array<string> = []
  • Array<T> 是一个泛型类型
  • T 是它的类型参数
  • Array<string> 表示「元素类型是 string 的数组」

2. 自己写一个泛型函数

function identity<T>(value: T): T {  
  return value
}
identity<number>(1)      // T 被替换成 number
identity<string>('hi')   // T 被替换成 string

你可以理解为:

  • 定义:identity<T>T 是一个「占位的类型」
  • 使用:identity<number> → 这次调用里「把 T 换成 number

或许有同学不理解为什么要在函数名称后面写<T>。不用纠结,这是固定的写法,就像你要使用变量,就要先声明一样,如:

let data = []
data.push(123)

如果此处没有声明data,便用不了data。同理如果不在函数名称后面写<T>声明一下这是泛型参数,TypeScript 无法识别 T 是什么,如下:

//  错误:找不到名称 'T'
function identity(value: T): T {
  return value
}
// 报错:Cannot find name 'T'

TypeScript 会把 T 当作一个未声明的类型,因此报错。

三、类型也可以是泛型:接口 /type

1. 泛型接口

// 使用时传入不同的 T
interface ApiResponse<T> {  
  code: number  
  msg: string  
  data: T
}

interface User {  
  id: number  
  name: string
}

const res1: ApiResponse<User> = {  
  code: 0,  
  msg: 'ok',  
  data: { id: 1, name: '张三' },
}

const res2: ApiResponse<string[]> = {  
  code: 0,  
  msg: 'ok',  
  data: ['a', 'b'],
}

观察:

  • ApiResponse<T> 自己并不知道 T 是啥
  • 真正用的时候写 ApiResponse<User> / ApiResponse<string[]>
  • TypeScript 在这一刻才把 T 替换掉

四、嵌套泛型:泛型里面再套泛型

其实很简单,就是「类型参数本身也是一个泛型类型」。

// 一层:数组里放字符串
Array<string>

// 两层:Promise 里放数组,数组里放字符串
Promise<Array<string>>

// 换个写法更直观
type StringArray = Array<string>
type StringArrayPromise = Promise<StringArray>

你可以这么想:

  • 第一层:Array<T>
  • 第二层:Promise<第一层>

五、回到文章最开始的例子:VxeGridProps<RowVO>

先看定义的行数据类型:

interface RowVO {  
  id: number  
  name: string  
  role: string  
  sex: string  
  age: number  
  address: string
}

然后:

const gridOptions = reactive<VxeGridProps<RowVO>>({...})

拆开理解:

  • VxeGridProps<D = any> 是 vxe-table 提供的泛型接口
  • 你写的是 VxeGridProps<RowVO>
  • 这一刻,D 就被替换成了 RowVO

也就是在这一整次使用里,可以把它脑补成:

// 伪代码,仅用于理解
interface VxeGridProps_RowVO extends VxeTableProps<RowVO> {  
  columns?: VxeGridPropTypes.Columns<RowVO>  
  proxyConfig?: VxeGridPropTypes.ProxyConfig<RowVO>  
  // ...
}

可能很多同学看到这里会感到些疑惑,怎么一会儿T一会儿D的。其实不管是T还是D都是类型变量的自定义名称,叫什么都无所谓,语法上没有任何固定含义,就像你写 JS 时给变量起名num/name/age一样,只是前端社区形成了「约定俗成的命名习惯」,用不同字母对应不同语义,让代码更易读。

字母 全称 含义/使用场景 例子
T Type 通用类型(最常用,无特殊语义时都用 T) first<T>(arr: T[])
D Default/Date 通常指 “默认类型” 或 “日期类型”(小众) 泛型接口里的默认类型:interface Config<D = string>
K KeyKey 表示对象的「键」类型 getKey<K extends string>(obj: { [k: K]: any }, key: K)
V Value 表示对象的「值」类型 Map<K, V>(TS 内置的 Map 泛型)
E Element 表示数组 / 集合的「元素」类型 Array<E>(TS 内置的数组泛型)
P Parameter 表示函数的「参数」类型 function wrap<P>(fn: (arg: P) => void, arg: P)

六、类型参数是怎么一层一层 “传下去” 的?

到这一步为了更好的理解泛型,我将带着同学们追溯源码。一起来追踪一下源码看看吧。

1. 第一层:VxeGridProps<D>

源码里(简化):

export interface VxeGridProps<D = any> extends VxeTableProps<D> {  
  columns?: VxeGridPropTypes.Columns<D>  
  proxyConfig?: VxeGridPropTypes.ProxyConfig<D>  
  // ...
}

当你用 VxeGridProps<RowVO>

  • extends VxeTableProps<D> → 变成 extends VxeTableProps<RowVO>
  • columns?: Columns<D> → 变成 columns?: Columns<RowVO>
  • proxyConfig?: ProxyConfig<D> → 变成 proxyConfig?: ProxyConfig<RowVO>

记忆:哪里写了 <D>,就会被替换成 <RowVO>

2. 第二层:Columns<D> = Column<D>[]

export namespace VxeGridPropTypes {  
  export type Column<D = any> = VxeTableDefines.ColumnOptions<D>  
  export type Columns<D = any> = Column<D>[]
}

当你用的是 Columns<RowVO> 时:

  • Columns<D>Columns<RowVO>
  • = Column<D>[] 这一行里的 D 同样被替换成 RowVO,变成:
  • Columns<RowVO> = Column<RowVO>[]

接着:

  • Column<D> = VxeTableDefines.ColumnOptions<D>
  • 也会变成:Column<RowVO> = VxeTableDefines.ColumnOptions<RowVO>

所以:

columns 的每一项类型就是 ColumnOptions<RowVO>

七、第三层:ColumnOptions<D>D 真正用在哪里?

export interface ColumnOptions<D = any> extends VxeColumnProps<D> {  
  children?: ColumnOptions<D>[]  
  slots?: VxeColumnPropTypes.Slots<D>
}

继续替换:

  • ColumnOptions<D>ColumnOptions<RowVO>
  • extends VxeColumnProps<D> → 变成 extends VxeColumnProps<RowVO>
  • children?: ColumnOptions<D>[] → 变成 children?: ColumnOptions<RowVO>[]
  • slots?: Slots<D> → 变成 slots?: Slots<RowVO>

关键点:ColumnOptions<RowVO> 本身定义了「列配置」的结构它继承的 VxeColumnProps<RowVO> + Slots<RowVO> 等地方,会在「需要行数据的回调」里用到 RowVO,比如:

formatter(params: { row: RowVO; ... })
className(params: { row: RowVO; ... })

八、我在学习时候的疑惑?

我当时并不理解TypeScript 做的是统一替换

// 把 D 换成 RowVO:
type Columns<RowVO> = Column<RowVO>[]

// 再把 Column 展开:
type Column<RowVO> = VxeTableDefines.ColumnOptions<RowVO>

// 合起来就是:
type Columns<RowVO> = VxeTableDefines.ColumnOptions<RowVO>[]

就拿文章示例的代码来看,TypeScript 会把函数体中所有的<T>都 替换成你制定的类型。

不理解的代码:

export type Column<D = any> = VxeTableDefines.ColumnOptions<D>
export type Columns<D = any> = Column<D>[]

我当特别不能理解 Column<D>[]<D>是怎么变成<RowVO>的。直到我明白了TypeScript会做统一替换,根本不是按数据传参的逻辑去做的。

九、把整个链路串起来(从外到内)

你写了:

reactive<VxeGridProps<RowVO>>({...})

于是:

VxeGridProps<D> → VxeGridProps<RowVO>
extends VxeTableProps<D> → extends VxeTableProps<RowVO>
columns?: Columns<D> → columns?: Columns<RowVO>

然后:

Columns<D> = Column<D>[] → Columns<RowVO> = Column<RowVO>[]
Column<D> = ColumnOptions<D> → Column<RowVO> = ColumnOptions<RowVO>

再往下:

ColumnOptions<D> extends VxeColumnProps<D> → ColumnOptions<RowVO> extends VxeColumnProps<RowVO>

最终效果:

  • data 的类型是:RowVO[]
  • 所有回调里涉及「行数据」的地方,类型参数是 RowVO

十、总结这次案例

  • RowVO:描述 “一行数据长什么样”
  • VxeGridProps<RowVO>:告诉表格「我的每一行数据都是 RowVO
  • 泛型参数 <RowVO> 会一层层往下传,凡是类型里写了 <D> 的地方,就会变成 <RowVO>

你现在已经不是 “不懂泛型的小白” 了,你已经能:

  • 看懂「类型参数是怎么一层一层传下去的」
  • 顺着 VxeGridProps<RowVO> → Columns<RowVO> → ColumnOptions<RowVO> 这一整条链路往下追

这就已经是非常扎实的泛型理解了。

总结

  1. 泛型的核心是「给类型加参数」,使用时再指定具体类型,如 Array<string>VxeGridProps<RowVO>
  2. 嵌套泛型的本质是「类型参数本身也是泛型」,参数会逐层传递替换(DRowVO);

以上便是对泛型的分享,欢迎大家指正讨论,与大家共勉。

昨天 — 2026年2月6日首页

你不知道的 v-on

作者 SuperEugene
2026年2月6日 14:18

v-onVue事件绑定指令,近期在使用Vxe Table组件库的时候看见了一个就职公司项目场景不常用的写法,在此分享给同样不常用或不知道的同学们。

//此处复制的是vxetbale组件库的示例代码
<vxe-grid v-bind="gridOptions" v-on="gridEvents"></vxe-grid>

const gridEvents: VxeGridListeners = { 
    pageChange ({ pageSize, currentPage }) { 
        pagerVO.currentPage = currentPage
        pagerVO.pageSize = pageSize
        loadList()
    } 
}

v-on绝大部分人只知道是vue提供的事件绑定api,通常用法:v-on:click="getInfo" 或者简写 @click="handleClick"。在上述案例代码中v-on后面直接就是="gridEvents"这并不是错误写法, 而是v-on对象式事件绑定写法。和常用的 @click="handleClick" 属于同一套事件绑定机制,仅写法形式不同。

两种写法对比

1. 单个事件(常规熟悉写法)

@v-on: 的语法糖,两种写法完全等价:

<button @click="handleClick">点击</button>
<!-- 等价于 -->
<button v-on:click="handleClick">点击</button>

2. 对象式绑定(v-on="对象" 用法)

直接通过 v-on 绑定一个事件对象,适用于多个事件绑定的场景:

<vxe-grid v-on="gridEvents"></vxe-grid>

其中 gridEvents 是一个键值对对象

  • 键:事件名(如 pageChangecellClick
  • 值:该事件对应的处理函数Vue 会自动遍历这个对象,将每个键值对解析为「v-on:事件名=处理函数」的形式完成绑定。

在代码中的实际含义

vxe-table的分页事件为例,实际定义的事件对象如下(包含TypeScript类型约束):

const gridEvents: VxeGridListeners = {
  pageChange({ pageSize, currentPage }) {
    pagerVO.currentPage = currentPage
    pagerVO.pageSize = pageSize
    loadList()
  },
}

此时 v-on="gridEvents" 完全等价于单个事件绑定的写法

<vxe-grid v-on:pageChange="gridEvents.pageChange"></vxe-grid>

如果 gridEvents 中包含多个事件Vue会自动完成所有事件的批量绑定,例如:

// 包含多个事件的处理对象
const gridEvents = {
  pageChange: (e) => { ... },
  editClosed: (e) => { ... },
  cellClick: (e) => { ... },
}

等价于手动为每个事件单独绑定:

<vxe-grid
  v-on:pageChange="gridEvents.pageChange"
  v-on:editClosed="gridEvents.editClosed"
  v-on:cellClick="gridEvents.cellClick"
></vxe-grid>

为什么使用对象式事件绑定写法?

  1. 事件多时更简洁:无需在模板中重复书写大量 v-on:xxx="xxx",仅需一个 v-on="对象" 即可完成批量绑定,简化模板代码;
  2. 便于维护:所有事件的处理函数都集中在一个对象中,事件名和对应逻辑一一对应,后续新增 / 修改 / 删除事件时,只需操作该对象,无需改动模板;
  3. 适配组件库场景vxe-tableElement Plus这类 UI 组件库的复杂组件(如表格、树形控件)通常提供大量事件,使用对象统一配置事件,代码结构会更清晰。

以上便是对v-on的分享,欢迎大家指正讨论,与大家共勉。

昨天以前首页

Promise对象、同步和异步代码、回调地狱的讲解

作者 SuperEugene
2026年2月5日 10:38

简单粗暴一点的说 Promise 就是一个为了解决异步代码的东西,它可以让代码按照你想要的顺序去执行。

我们先来说说什么是同步代码,什么是异步代码。

  • 同步代码就是按顺序执行,如:12345 按顺序往下走。只有前一条代码执行完毕之后才会去执行后一条代码。
  • 异步代码就是可以不按照顺序执行的代码,如:213465,例如:网络请求、定时器、文件的读写等均是异步的。我们直接上代码直观的去感受一下
setTimeout(() => {
  
}, 1000)

↑这是一个定时器,这是一个可以将包裹在方法体内的代码延迟 1000 毫秒以后再执行的定时器。

console.log('任务1', moment(new Date()).format('HH:mm:ss'))
setTimeout(() => {
  console.log('任务2', moment(new Date()).format('HH:mm:ss'))
}, 1000)

//moment (new Date ()).format ('HH:mm:ss') 如果你是初学者不明白 moment () 是什么,
//直接复制代码发现报错,没关系。并不影响你继续学习,这条代码的功能就是输出博主当前执行代码的时间,
//你可以删掉这条代码,只输出 ' 任务1、任务2' 即可。感兴趣的同学可以自行搜索 moment 的使用方法学习

image.png

可以看到任务 2 比任务 1 的打印时间晚了一秒

前面我们说到定时器是一个异步的代码,我们来验证一下。

setTimeout(() => {
  console.log('任务2', moment(new Date()).format('HH:mm:ss'))
}, 1000)
console.log('任务1', moment(new Date()).format('HH:mm:ss'))

按照代码自上而下的执行顺序,在控制台中应该是一秒钟以后先输出的任务 2 再输出的任务 1

image.png

从控制台的输出得知并不是这样的。代码跳过了任务 2 先输出的任务 1,然后一秒钟之后才输出的任务 2,证实定时器确实是一个异步的代码,它并没有按照顺序执行。

异步代码的好处就在于不会造成代码的堵塞

  • 例如:现在我们是交通参与者的身份开着车行驶在路上,突然前方出现了交通事故,此时边上有辅道可以绕过去,难道我们就要等到这起交通事故处理完成之后才能通行吗?当然是没有这个必要的,我们可以先通过边上的辅道绕过去,无需等到交通事故处理完毕之后再通行,如果等到交通事故处理之后再通行那就会造成交通瘫痪大堵车的结局。

还有的时候异步任务也需要同步的去执行。还是以这个交通事故举例子,例如:我们是交警,我们接到了调度中心发来的任务请求,告知我们某路段发生交通事故需要我们前往现场进行处理。在接警没多久之后调度中心又发来请求,告知我们现在警力紧张,在我们的不远处还有一起交通事故希望我们处理完手里的这个事故之后立马前往下一个路段处理第二起交通事故。那此时任务二就要等待任务一执行完毕之后才能执行。其他的交通参与者当然不需要等待交警的任务执行完毕之后才能通过,可以自行从边上绕过去。上代码,我们把这个小故事以代码的形式呈现出来

console.log('社会车辆1')
console.log('社会车辆2')
setTimeout(() => {
  console.log('交警处理事故1', moment(new Date()).format('HH:mm:ss'))
  setTimeout(() => {
    console.log('交警处理事故2', moment(new Date()).format('HH:mm:ss'))
  }, 1000)
}, 1000)
console.log('社会车辆3')
console.log('社会车辆4')

image.png

通过控制台的输出我们可以看到社会车辆 1、2、3、4 先行通过了,交警处理事故 1 之后一秒钟事故 2 才执行处理。

但其实这种写法并不美观,如果有很多事故需要处理呢,那是不是就要嵌套很多层呢。

console.log('社会车辆1')
console.log('社会车辆2')
setTimeout(() => {
  console.log('交警处理事故1', moment(new Date()).format('HH:mm:ss'))
  setTimeout(() => {
    console.log('交警处理事故2', moment(new Date()).format('HH:mm:ss'))
    setTimeout(() => {
      console.log('交警处理事故3', moment(new Date()).format('HH:mm:ss'))
      setTimeout(() => {
        console.log('交警处理事故4', moment(new Date()).format('HH:mm:ss'))
        setTimeout(() => {
          console.log('交警处理事故5', moment(new Date()).format('HH:mm:ss'))
          setTimeout(() => {
            console.log('交警处理事故6', moment(new Date()).format('HH:mm:ss'))
          }, 1000)
        }, 1000)
      }, 1000)
    }, 1000)
  }, 1000)
}, 1000)
console.log('社会车辆3')
console.log('社会车辆4')

这样的写法会导致代码的可读性非常的差,你可能说我现在一眼就能看出哪儿条输出语句对应着哪儿个代码块。没错,就以现在的这个代码是可以做到。但如果当每一个代码块中的代码复杂起来多起来了以后呢。你还能一眼就看出来吗?当然是很费力的嘛。这就是我们常说的回调地狱

Promise 的出现就可以很好的规避掉这个问题,让我们来进入 Promise 的学习吧。

new Promise()

Promise 就长这样↑

Promise 接收一个函数,在函数中接收两个参数:resolvereject

new Promise((resolve, reject) => {})

resolvereject 是由 Promise 对象传入的。resolve 直译过来的意思是:决定、解决,是在程序执行成功的时候调用的,而 reject 直译过来的意思是:拒绝,是在程序调用失败的时候调用的,故此你可以将

  • resolve 理解为成功
  • reject 理解为失败

例如:现在我们正在登陆某一个网站,当你的用户名或密码输入错误之时,身份验证不通过就会返回错误信息,此时就可以调用 reject。反之用户名和密码都正确身份认证通过了,此时便可调用 resolve

咱们先打印一下这个 Promise 对象,看看长什么样

const promiseObj = new Promise((resolve, reject) => {})
 
console.log(promiseObj)

image.png

我们看到在控制台打印出来的数据中有一个pending(等待的意思),promise 对象中有一个状态的概念。你看现在的状态是一个默认状态 pending,你可以理解为这个 promise 里的 resolvereject 一个都没有被触发。

我们来触发 resolve 参数看看状态

const promiseObj = new Promise((resolve, reject) => {
  resolve()
})
console.log(promiseObj)

image.png

观察发现 PromiseStatepending 等待变成了fulfilledfulfilled 就表示完成或者成功

我们再来触发一下 reject 参数看看状态

const promiseObj = new Promise((resolve, reject) => {
  reject()
})
console.log(promiseObj)

image.png

观察发现 PromiseState 的状态变成了rejected 表示拒绝或者失败了。

promise 对象给我们提供了三个方法,咱们自己可以如图一样通过对象点的形式来看看是不是出来 catchfinallythen 这三个方法。

简单粗暴的讲一下这三个方法。还是以登陆某一网站为例:

  • 假设用户名和密码都填写正确则会走到 then() 方法里,你可以理解为then() 方法是成功时候调用的。假设用户名和密码有一个或者全都填写错误时服务器便会返回错误信息,此时代码就会走到 catch() 方法里,你可以理解为catch()方法是在错误时候调用的。那么finally() 方法则是无论服务器返回的是 true 还是 false 都会走到 finally 里面,你可以理解为不论对错都会调用 finally。用户名密码填写正确,走完 then() 还会走 finally。用户名密码填写错误,走完 catch() 也还会走 finally()

让我们来一一验证一下:

const promiseObj = new Promise((resolve, reject) => {
  resolve('身份认证通过!')
})
promiseObj
  .then((data) => { //data就是resolve传过来的内容 名称没有规定自定义即可
    console.log(data)
  })
  .catch((error) => {
    console.log(error)
  })
  .finally(() => {
    console.log('我是finally')
  })

image.png

通过代码可以看得出这三个方法接收的都是一个回调函数。通过控制台的输出可以得知当我们调用了 resolve 时代码走到了then() 方法之后又走到了 finally() 方法,验证通过。

const promiseObj = new Promise((resolve, reject) => {
  reject('身份认证失败!')
})
promiseObj
  .then((data) => { 
    console.log(data)
  })
  .catch((error) => { //error就是reject传过来的内容  名称没有规定自定义即可
    console.log(error)
  })
  .finally(() => {
    console.log('我是finally')
  })

image.png

通过控制台的输出可以得知当我们调用了 reject 时代码走到了 catch() 方法之后又走到了 finally() 方法,验证通过。

到这儿为止你已经对 Promise 的使用有了一个简单的认识。我们现在用 Promise 来解决一下前面举例说明的小故事。看看你是否会觉得优雅很多,代码更可读。

虽然 promise 本身是同步的,但是 promise.then().catch (). finally () 这些方法中的回调是异步的,所以在这里我们就不用定时器了。

console.log('社会车辆1')
console.log('社会车辆2')
const promiseObj = new Promise((resolve, reject) => {
  resolve('交警处理事故1')
})
promiseObj
  .then((data) => {
    console.log(data)
    return new Promise((resolve, reject) => {
      resolve('交警处理事故2')
    })
  })
  .then((data) => {
    console.log(data)
    return new Promise((resolve, reject) => {
      resolve('交警处理事故3')
    })
  })
  .then((data) => {
    console.log(data)
    return new Promise((resolve, reject) => {
      resolve('交警处理事故4')
    })
  })
  .then((data) => {
    console.log(data)
  })
  .catch()
console.log('社会车辆3')
console.log('社会车辆4')

image.png

有的人会说 这也嵌套了呀。没错这确实是也嵌套了,但是只有一层,无论多少次都只有一层

promiseObj.then().then().then().then().then().then().catch()

↑这样看是不是就更直观了呢。

还有另一种写法可以给每一个.then() 都设置一个单独的 catch() 直接上代码

image.png 从这张图↑可以看到.then() 里面其实可以传递两个参数的,第一个表示成功时候的调用,第二个表示失败时候的调用

promiseObj
  .then((data) => {}, (error) => {})
  .then((data) => {}, (error) => {})
  .then((data) => {}, (error) => {})
  .then((data) => {}, (error) => {})
  .then((data) => {}, (error) => {})

↑这便是大致的一个结构

const promiseObj = new Promise((resolve, reject) => {
  resolve('交警处理事故1')
})
console.log('社会车辆1')
console.log('社会车辆2')
promiseObj
  .then(
    (data) => {
      console.log(data)
      return new Promise((resolve, reject) => {
        resolve('交警处理事故2')
      })
    },
    (error) => {
      console.log(error)
    }
  )
  .then(
    (data) => {
      console.log(data)
      let errorBtn = false
      return new Promise((resolve, reject) => {
        if (errorBtn) {
          resolve('交警处理事故3')
        } else {
          reject('交通事故处理时候遇到特殊状况')
        }
      })
    },
    (error) => {
      console.log(error)
    }
  )
  .then(
    (data) => {
      console.log(data)
      return new Promise((resolve, reject) => {
        resolve('交警处理事故4')
      })
    },
    (error) => {
      console.log(error)
    }
  )
  .then()
 
console.log('社会车辆3')
console.log('社会车辆4')

我们在交警事故处理 3 处加了一个 reject 的调用

image.png

通过控制台的输出可以得知咱们的 reject 起作用了。当交警处理事故 3 处报错时代码就终止了,后面的程序就不执行了。

以上就是本章的知识点讲解分享,感谢大家的耐心观看学习,欢迎大家在评论区讨论纠错,与大家共勉。

v-bind 你用对了吗?

作者 SuperEugene
2026年2月4日 19:18

先极简总结(2 句话记死,终身受用)

  1. 绑 1 个属性:用缩写 :属性名="值"(原生标签 / Vue 组件通用,日常 90% 用这个);
  2. 绑 N 个属性:用 v-bind="属性对象"(无冒号、无属性名,属性越多越简洁,封装组件 / 配置驱动场景必用)。

核心前提先讲透(一句话干货)

  • v-bind是 Vue 用来给HTML 标签 / Vue 组件绑定动态属性值的指令,只有 2 种核心用法,本质都是「动态传值」,区别仅在于绑 1 个属性还是绑多个属性
  1. 单个属性绑定v-bind:属性名="值" → 缩写 :属性名="值"(日常 90% 高频用);

  2. 批量属性绑定:直接v-bind="属性对象"(无属性名、无冒号,把多个属性打包成对象一次性绑定);

关键:批量绑定的属性对象键 = HTML 标签 / Vue 组件的属性名值 = 要绑定的动态数据,Vue 会自动解析成「键 = 值」的单个属性,一一绑定到标签上。

例子 :原生img图片标签(最易理解的通用场景)

img标签的src(图片地址)、alt(占位文字)、width(宽度)是所有人都认识的原生属性,用它举例最直观,重点看单个绑定批量绑定的等价关系。

步骤 :先定义 Vue 里的动态数据(脚本部分,通用)

<script setup> 
    // Vue3基础响应式数据,不用纠结ref,知道是动态值就行 
    import { ref } from 'vue' 
    // 图片地址 
    const imgUrl = ref('https://picsum.photos/200/200')
    // 图片占位文字 
    const imgText = ref('风景图') 
    // 图片宽度 
    const imgW = ref(200)
</script>

用法 1:单个属性绑定(缩写:,逐个绑)

最常用的写法,给img逐个绑定动态属性,每个属性都用:缩写,模板清晰:

<template> 
    <!-- 核心:每个原生属性前加:,绑定对应的动态值 --> 
    <img :src="imgUrl" :alt="imgText" :width="imgW" /> 
</template>

等价于完整v-bind写法(繁琐,几乎没人用):

<script setup> 
    import { ref, reactive } from 'vue' 
    // 1. 先定义零散动态值 
    const imgUrl = ref('https://picsum.photos/200/200') 
    const imgText = ref('风景图') 
    const imgW = ref(200) 
    // 2. 打包成「属性对象」:键=img原生属性名,值=动态值 
    const imgProps = reactive({ src: imgUrl, alt: imgText, width: imgW }) 
</script> 
<template> 
    <!-- 核心:无属性名、无冒号,v-bind直接绑属性对象 --> 
    <img v-bind="imgProps" /> 
</template>

用法 2:批量属性绑定(v-bind="对象",打包绑)

img需要的所有动态属性打包成一个对象,用v-bind直接绑定这个对象,Vue 会自动解析:

<script setup> 
    import { ref, reactive } from 'vue' 
    // 1. 先定义零散动态值 
    const imgUrl = ref('https://picsum.photos/200/200') 
    const imgText = ref('风景图') 
    const imgW = ref(200) 
    // 2. 打包成「属性对象」:键=img原生属性名,值=动态值 
    const imgProps = reactive({ src: imgUrl, alt: imgText, width: imgW }) 
</script> 
<template> 
    <!-- 核心:无属性名、无冒号,v-bind直接绑属性对象 --> 
    <img v-bind="imgProps" /> 
</template>

完全等价于单个绑定的写法,Vue 会自动把imgProps里的src/alt/width逐个绑定到img标签上,效果一模一样。

单个和批量这两种用法是 Vue 最基础、最高频的语法,不用记复杂概念,只要知道「单个用:,多个用 v-bind = 对象」,就能搞定所有动态属性绑定场景。

❌
❌