一、加注释后的源码全文
<script>
export default {
// 组件名,用于递归组件、devtools 里显示
name: 'ElAvatar',
// 定义组件的对外属性(props)
props: {
/**
* 头像大小:
* - 字符串类型:只允许 'large' | 'medium' | 'small'
* - 数字类型:自定义像素值,如 40
*/
size: {
type: [Number, String],
// 自定义验证器
validator(val) {
// 如果是字符串,必须是预设的三个尺寸之一
if (typeof val === 'string') {
return ['large', 'medium', 'small'].includes(val);
}
// 如果是数字,直接通过(任意数字)
return typeof val === 'number';
}
},
/**
* 头像形状:
* - circle:圆形
* - square:方形
*/
shape: {
type: String,
default: 'circle',
validator(val) {
return ['circle', 'square'].includes(val);
}
},
/**
* 图标类名,用于展示图标头像
* 示例:icon="el-icon-user-solid"
*/
icon: String,
/**
* 图片地址,用于展示图片头像
* 示例:src="https://example.com/avatar.jpg"
*/
src: String,
/**
* 图片的 alt 属性,提升可访问性
*/
alt: String,
/**
* 图片的 srcset 属性,用于响应式图片
*/
srcSet: String,
/**
* 图片加载失败时的回调函数
* 返回 false 可以阻止组件内置的“隐藏图片”行为
*/
error: Function,
/**
* 图片的 object-fit 样式,和 CSS object-fit 一致
* 默认 'cover':保持比例填充容器,可能裁剪
*/
fit: {
type: String,
default: 'cover'
}
},
// 组件内部数据
data() {
return {
/**
* 标记当前图片是否加载成功
* - true:显示 <img>
* - false:隐藏 <img>,回退到 icon 或 slot
*/
isImageExist: true
};
},
computed: {
/**
* 计算最终要渲染在根元素上的 class 字符串
* 根据不同的 size / shape / icon 拼接对应的类名
*/
avatarClass() {
const { size, icon, shape } = this;
// 基础类名
let classList = ['el-avatar'];
// 如果 size 是字符串预设值,拼接尺寸修饰类
if (size && typeof size === 'string') {
classList.push(`el-avatar--${size}`);
}
// 如果传了 icon,说明是图标头像,添加图标修饰类
if (icon) {
classList.push('el-avatar--icon');
}
// 根据 shape 添加形状修饰类
if (shape) {
classList.push(`el-avatar--${shape}`);
}
// 拼接成字符串返回
return classList.join(' ');
}
},
methods: {
/**
* 处理图片加载失败逻辑
* 1. 调用用户传入的 error 回调
* 2. 根据返回值决定是否将 isImageExist 置为 false
*/
handleError() {
const { error } = this;
// 如果用户传了 error 函数,执行它;否则 errorFlag 为 undefined
const errorFlag = error ? error() : undefined;
// 只有当 errorFlag !== false 时,才隐藏图片
// 这样用户可以通过 error() 返回 false 来阻止“隐藏图片”的默认行为
if (errorFlag !== false) {
this.isImageExist = false;
}
},
/**
* 真正渲染头像内容的函数
* 返回一个 VNode,在 render() 中被使用
* 优先级:
* 1. 图片存在且 src 有值 → <img>
* 2. 否则,如果有 icon → <i class="icon">
* 3. 否则,使用默认插槽内容
*/
renderAvatar() {
const { icon, src, alt, isImageExist, srcSet, fit } = this;
// 情况 1:图片存在且 src 有值 → 渲染 <img>
if (isImageExist && src) {
return (
<img
src={src}
onError={this.handleError} // 图片加载失败时触发
alt={alt}
srcSet={srcSet}
// 使用 JSX 方式设置 style 对象
style={{ 'object-fit': fit }}
/>
);
}
// 情况 2:没有图片,但有 icon → 渲染图标
if (icon) {
return (<i class={icon} />);
}
// 情况 3:既没有图片,也没有 icon → 使用默认插槽
return this.$slots.default;
}
},
/**
* 渲染函数
* - 返回一个 <span> 作为根节点
* - 根据 size 是数字时,设置行内样式(高、宽、行高)
* - 内部调用 renderAvatar() 渲染具体内容
*/
render() {
const { avatarClass, size } = this;
// 如果 size 是数字,生成一个包含尺寸样式的对象
const sizeStyle = typeof size === 'number' ? {
height: `${size}px`,
width: `${size}px`,
lineHeight: `${size}px`
} : {};
return (
<span class={ avatarClass } style={ sizeStyle }>
{
this.renderAvatar()
}
</span>
);
}
};
</script>
二、从整体到细节
1. 组件整体在做什么?
一句话概括:
ElAvatar 是一个“头像容器”,根据 props 决定里面放:
- 图片
<img>- 图标
<i>- 或者任意插槽内容(文字、其他组件) 并通过
size/shape/fit等属性控制外观。 从渲染流程看,逻辑大致如下:
flowchart LR
A[开始渲染 ElAvatar] --> B{有 src 且 isImageExist?}
B -- 有 --> C[渲染 img]
B -- 无 --> D{有 icon?}
D -- 有 --> E[渲染 i.icon]
D -- 无 --> F[渲染 this.$slots.default]
2. class 类名的作用
源码里通过 avatarClass 拼接出来的类名,主要依赖 CSS:
classList = ['el-avatar'];
if (size && typeof size === 'string') {
classList.push(`el-avatar--${size}`);
}
if (icon) {
classList.push('el-avatar--icon');
}
if (shape) {
classList.push(`el-avatar--${shape}`);
}
在 Element 的样式文件中,这些类大致做了这些事(结合官方文档和 SCSS 源码):
| 类名 | 作用(简化理解) |
|---|---|
el-avatar | 基础类:设置 display: inline-block、基础尺寸、字体大小、文字居中、背景色等 |
el-avatar--large | 大尺寸预设:宽高变大,通常用于“大头像” |
el-avatar--medium | 中等尺寸 |
el-avatar--small | 小尺寸 |
el-avatar--circle | 圆形:border-radius: 50% |
el-avatar--square | 方形:border-radius 较小或无 |
el-avatar--icon | 当内容是图标时,调整图标大小、垂直居中 |
重点记忆:
- 组件内部不直接写死尺寸,而是通过类名 + 预设尺寸,好处是统一风格、方便主题定制。
- 如果
size是数字,则不走预设类,而是用行内样式写死宽高。
3. data 中数据的作用
源码:
data() {
return {
isImageExist: true
};
}
- 作用:标记当前图片是否“存在/加载成功”。
- 初始为
true,表示假设图片一开始是好的。 - 当
<img>触发onError时,调用handleError,如果error()没有返回false,就把isImageExist置为false,从而在renderAvatar中不再渲染<img>。 重点理解: - 这是“受控状态”:组件内部维护一个“图片是否可用”的标志,再结合用户传入的
error回调,决定是否回退到icon或默认插槽。 - 一个典型场景:
- 传入
src加载失败 → 回退到icon或文字。 - 如果用户
error()返回false,则组件继续保留<img>(用户可能想自己处理错误,比如显示默认占位图)。
- 传入
4. computed 方法详解:avatarClass
computed: {
avatarClass() {
const { size, icon, shape } = this;
let classList = ['el-avatar'];
if (size && typeof size === 'string') {
classList.push(`el-avatar--${size}`);
}
if (icon) {
classList.push('el-avatar--icon');
}
if (shape) {
classList.push(`el-avatar--${shape}`);
}
return classList.join(' ');
}
}
逐行解释:
- 从
this中解构出size、icon、shape。 - 初始
classList = ['el-avatar'],这是所有头像都有的基础类。 - 如果
size是字符串,说明是预设尺寸(large | medium | small),拼接对应尺寸类。 - 如果有
icon,说明是图标头像,加el-avatar--icon类,方便样式针对图标做调整。 - 如果有
shape,拼接形状类circle/square。 难点 / 重点:
- 这里 没有处理数字 size,数字尺寸是在
render()中用sizeStyle行内样式实现的。 - 这也是很多 UI 组件库的常见设计:
- 预设尺寸 → 类名
- 自定义尺寸 → 行内样式 记忆点:
avatarClass只负责“类名逻辑”,不负责“尺寸计算”。- 以后你自己在写组件时,也可以模仿:
- 一部分样式用类名(主题化、可复用),
- 一部分精确尺寸用行内样式。
5. methods 详解:handleError & renderAvatar
5.1 handleError
methods: {
handleError() {
const { error } = this;
const errorFlag = error ? error() : undefined;
if (errorFlag !== false) {
this.isImageExist = false;
}
}
}
逻辑拆解:
- 取出用户传入的
error函数。 - 如果有,执行
error();没有,errorFlag为undefined。 - 只有当
errorFlag !== false时,才设置isImageExist = false。 关键点:
- 用户可以通过
error返回false,阻止组件隐藏图片。 - 默认行为:加载失败 →
isImageExist = false→ 不再渲染<img>。 典型用法示例:
<template>
<el-avatar
:src="avatarUrl"
:error="handleAvatarError"
/>
</template>
<script>
export default {
data() {
return {
avatarUrl: 'invalid-url.jpg'
};
},
methods: {
handleAvatarError() {
console.log('头像加载失败,使用默认头像');
// 返回 false,阻止 el-avatar 内部隐藏图片的行为
return false;
}
}
};
</script>
5.2 renderAvatar
renderAvatar() {
const { icon, src, alt, isImageExist, srcSet, fit } = this;
if (isImageExist && src) {
return <img
src={src}
onError={this.handleError}
alt={alt}
srcSet={srcSet}
style={{ 'object-fit': fit }}/>;
}
if (icon) {
return (<i class={icon} />);
}
return this.$slots.default;
}
逻辑拆解:
- 解构出需要的属性。
- 如果
isImageExist为真且src有值 → 渲染<img>:onError绑定handleError,处理加载失败。style={{ 'object-fit': fit }}控制图片在容器内的填充方式,和 CSSobject-fit一致。
- 否则,如果有
icon→ 渲染<i class="icon">。 - 否则,返回
this.$slots.default,即默认插槽内容。 重点 & 难点:
- 优先级:图片 > 图标 > 插槽。
- 这里用的是 JSX 写法,在 Vue 2 中需要配置
babel-plugin-transform-vue-jsx。 srcSet是原生<img>属性,用于根据设备像素密度选择不同分辨率图片。 需要记忆:- 如果需要“根据条件渲染不同内容”,可以模仿这种“函数返回 VNode”的模式:
- 先写一个
renderXxx()方法, - 在
render()或<template>中调用。
- 先写一个
6. render() 函数详解
render() {
const { avatarClass, size } = this;
const sizeStyle = typeof size === 'number' ? {
height: `${size}px`,
width: `${size}px`,
lineHeight: `${size}px`
} : {};
return (
<span class={ avatarClass } style={ sizeStyle }>
{
this.renderAvatar()
}
</span>
);
}
逐行解释:
- 从
this中取出avatarClass(computed)和size(prop)。 - 如果
size是数字,生成一个包含height、width、lineHeight的样式对象。- 注意:
lineHeight也设置成相同数值,保证文字内容垂直居中。
- 注意:
- 如果
size不是数字,sizeStyle为空对象,只使用类名预设尺寸。 - 返回一个
<span>,根节点上:class为avatarClassstyle为sizeStyle- 内部调用
this.renderAvatar()渲染具体内容。 重点:
size为数字时,使用行内样式覆盖预设尺寸,这样更灵活。- 根节点是
<span>,所以默认是行内块元素,适合放在文字旁边。
7. 和官方 API 的对应关系
结合 Element 官方文档(Element Plus 的 Avatar API 与 Element UI 基本一致):
| 官方属性 | 源码中的体现 | 说明 |
|---|---|---|
icon | icon: String + renderAvatar 中 <i class={icon}> | 图标类名 |
size | size: [Number, String] + avatarClass / sizeStyle | 支持预设字符串或数字像素 |
shape | shape: String + avatarClass 中 el-avatar--${shape} | circle / square |
src | src: String + <img src={src}> | 图片地址 |
src-set | srcSet: String + <img srcSet={srcSet}> | 原生 srcset |
alt | alt: String + <img alt={alt}> | 可访问性 |
fit | fit: String + style={{ 'object-fit': fit }} | 图片填充方式 |
error | error: Function + handleError | 图片加载失败回调 |
8. 重点、难点、需要记忆的部分总结
8.1 重点
- 三个渲染优先级:图片 > 图标 > 插槽
- 这是
renderAvatar的核心逻辑,一定要记住。
- 这是
size的两种处理方式- 字符串预设尺寸 → 类名
el-avatar--large/medium/small - 数字自定义尺寸 → 行内样式
height / width / lineHeight
- 字符串预设尺寸 → 类名
isImageExist的作用- 控制图片是否渲染,配合
error回调实现“错误回退”逻辑。
- 控制图片是否渲染,配合
8.2 难点
- JSX 写法
- 如果你不熟悉 JSX,可能对
<img ... />、<i class={icon} />感到陌生。 - 建议:先在项目中配置好 JSX,然后自己写几个简单组件练手。
- 如果你不熟悉 JSX,可能对
error回调与默认行为的交互- 用户可以通过返回
false阻止组件内部隐藏图片。 - 这是“组件默认行为 + 用户自定义控制”的典型设计模式。
- 用户可以通过返回
8.3 需要记忆的代码模式
- 类名拼接模式
后面很多组件都会用到这种“基础类 + 条件修饰类”的模式。const classList = ['el-avatar']; if (size && typeof size === 'string') { classList.push(`el-avatar--${size}`); } if (icon) { classList.push('el-avatar--icon'); } if (shape) { classList.push(`el-avatar--${shape}`); } return classList.join(' '); - 条件渲染 + 回退逻辑
这是“优先级渲染”的经典写法。if (isImageExist && src) { return <img ... onError={this.handleError} />; } if (icon) { return <i class={icon} />; } return this.$slots.default; - 数字尺寸生成行内样式
const sizeStyle = typeof size === 'number' ? { height: `${size}px`, width: `${size}px`, lineHeight: `${size}px` } : {};
如果你愿意,下一步可以:
- 我可以帮你写一个“简化版 ElAvatar”,把 JSX 改成
<template>版本,方便你在 Vue 2 项目中直接使用。 - 或者我们再往下挖一层:看 Element 的
avatar.scss具体样式实现,学习它是如何用 CSS 实现各种状态和尺寸的。