普通视图
图片预览的两种方式
图片预览的方式
1.base64
<input type="file" id="fileInput">
<img id="previewImage" src="" alt="Preview Image">
<script>
const fileInput = document.getElementById('fileInput');
const previewImage = document.getElementById('previewImage');
fileInput.addEventListener('change', function () {
const file = fileInput.files[0];
const reader = new FileReader();
reader.onload = function (e) {
const base64String = e.target.result;
previewImage.src = base64String;
console.log('图片读取的Base64的值为--->', base64String);
};
reader.readAsDataURL(file);
});
</script>
2.bloburl
<input type="file" id="fileInput">
<img id="previewImage" src="" alt="Preview Image">
<script>
const fileInput = document.getElementById('fileInput');
const previewImage = document.getElementById('previewImage');
fileInput.addEventListener('change', function () {
const file = fileInput.files[0];
let tempUrl = window.URL.createObjectURL(file)
console.log('blob--->', tempUrl); // blob:http://127.0.0.1:5500/84d2e951-33dc-4fea-840a-f3d8f3396766
previewImage.src = tempUrl;
});
</script>
```tu
为什么组件再次渲染了?
Props 改变导致重新渲染
我来先看一个简单的例子,当我点击这个按钮时,count 会加 1。
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return <div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
}
需要注意的是,这里讨论的 Props 改变并非指 initialCount 或其他 Props 的值发生变化,而是指 Props 对象本身被重新创建了。
让我们深入理解一下:当 React 渲染这个组件时,本质上是在调用 Counter({initialCount: 0})
。每次调用时,都会创建一个全新的对象作为 Props 传入,即使对象的内容完全相同。
function App() {
return <>
<Counter initialCount={0} />
{/* 其他组件 */}
</>
}
当我们的父组件重新渲染时,Props 对象本身被重新创建,所以 Counter 和其他组件都会重新渲染。
这显然不是我们希望看到的,那么我们如何优化这个组件呢?
使用 React.memo
const Counter = React.memo(({ initialCount }: { initialCount: number }): JSX.Element => {
const [count, setCount] = useState(initialCount)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
})
它会对 Props 的值进行一个浅比较,如果值没有发生变化,则不会重新渲染。
提升到组件外部、useMemo、useCallback
然而需要注意的是,即使使用了 memo,我们也要谨慎处理 Props 的值。 由于 memo 只进行浅比较,如果我们在传递 Props 时直接使用对象或内联函数,组件仍然会重新渲染
function App() {
return <>
// 都使用 memo 包裹
<SlowComponent data={{foo:"bar"}} />
<SlowComponentTwo data={["foo","bar"]} />
<OtherSlowComponent setCount={() => setCount(count + 1)} />
{/* 其他组件 */}
</>
}
以上可能是我们在日常开发中经常会写的代码,这些代码会造成不必要的重新渲染,那么我们如何优化这些代码呢?
对于静态数据,我们可以将其提升到组件外部。而对于依赖 state、props 或需要在 useEffect 中使用的数据, 我们可以通过 useMemo 或 useCallback 这些 Hook 来进行性能优化。这样可以避免在每次渲染时重新创建数据或函数。
const data = ["foo","bar"]
function App() {
const setCount = useCallback(() => {
setCount(count + 1)
}, [count])
const memoizedData = useMemo(() => data, [count])
return <>
<SlowComponent data={data} />
<SlowComponentTwo data={memoizedData} />
<OtherSlowComponent setCount={setCount} />
{/* 其他组件 */}
</>
}
显然,这种写法与我们编写代码的初衷相去甚远。作为开发者,我们希望能够简单直接地传递函数或对象,而不是因此导致组件频繁地重新渲染。 虽然我们可以通过手动优化或使用 lint 规则来规避这些问题,但这并不是一个理想的解决方案。这种复杂的优化过程,反而增加了开发的心智负担。
React Compiler
React Compiler 为这些问题提供了一个优雅的解决方案。尽管目前仍处于实验阶段,但是我觉得它是一个非常值得期待的解决方案。
只需启用 React Compiler,我们就可以摆脱 memo、useMemo、useCallback 等优化方案的困扰。它支持 React 17/18/19 版本,可以在项目或指定目录级别启用。
这种优化方式如此简单直接,可能这就是它讨论热度很低的原因。未来关于组件优化的面试题可能会变得很简单 - 启用 React Compiler 就够了。
Ryan Carniato 在直播中有一个关于 React Compiler 的性能测试
用原生 js 做为基准值,通过加权几何平均值可以看出,React 相关实现相比原生都有一定的性能损耗
- 经过手动优化后 react-hooks: 慢 1.51 倍
- 经过 react compiler 优化后的 react-hooks: 慢 1.57 倍
- 没有经过优化的 react-hooks: 慢 1.85 倍
从这次 benchmark 可以看出,React Compiler 并不一定可以让 react 更快,但是他大概率让你的 react 代码更快
State 改变导致重新渲染
还是第一个例子,当 count 改变时,组件会重新渲染,这当然是没问题
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return <div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
}
然而,随着代码层级的增加和多个自定义 hooks 的引入,state 的管理会变得愈发复杂。 这种复杂性往往会导致组件树中出现不必要的多层级渲染,影响应用性能。为了有效应对这个问题,我们可以采取以下策略:
- 降低组件复杂度,将 state 下移到真正需要它的子组件
- 将不依赖 state 的 JSX 上移,这样这部分在 state 改变时不会重新渲染
- 如果数据不需要响应式更新,考虑使用 useRef 替代 useState
Context 变化导致重新渲染
prop drilling
有了以上这些优化,我们还需要解决最后一个问题,那就是 prop drilling
。那么什么是 prop drilling
呢?
当多个组件需要共享同一个 state 数据时,传统的做法是将这个 state 提升到它们最近的共同父组件中,
然后通过 props 层层传递给需要使用该数据的子组件。这种将数据在组件树中自上而下逐级传递的模式被称为 prop drilling
。
然而,这种模式的一个明显缺陷是:即使某些中间组件完全不需要这个 state,它们也会因为 props 的变化而被迫重新渲染。
通过创建 Context,我们可以实现数据的跨层级传递,使数据能够直接到达需要它的组件,而无需通过中间组件的 props 层层传递,简化了数据流, 避免不必要的中间组件渲染,
Context 的问题
Context 也有一些问题,当我们的应用变得复杂的时候。
当我们的 Context 值被多个组件共享时,一旦 Context 的值发生变化,即使只有 A 组件的 UI 需要更新, 所有订阅了该 Context 的组件及其子组件都会被触发重新渲染。为此,使用 context 时,也有一些建议
-
将 Context Provider 放在组件树顶层
建议将 Provider 放置在根组件或 App 组件中,这样可以确保 Provider 只在应用初始挂载时渲染一次, 避免频繁的重新渲染和不必要的性能开销
-
合理拆分 Context
避免创建一个包含所有状态的大型 Context 例如,不要将用户信息、主题设置等所有状态都放在同一个 Context 中 应该根据功能将其拆分为独立的 Context(如 ThemeContext、UserContext) 这样可以实现更细粒度的更新,提高渲染性能
-
谨慎在自定义 Hook 中使用嵌套过深的 state (context,useState)
随着应用复杂度增加,过多的嵌套过深的 state 会导致:
- 代码可维护性降低
- 调试难度增加
- 渲染逻辑难以追踪
建议保持 hooks 结构扁平化,适度使用组合 这样不仅提高了代码可读性,也便于性能优化和问题排查
总结
Props 改变导致的重新渲染
- 使用 React.memo 包裹组件
- 将静态数据提升到组件外部
- 使用 useMemo 和 useCallback 缓存数据和函数
- 使用 React Compiler(实验阶段)自动优化
State 改变导致的重新渲染
- 将 state 下移到真正需要的子组件
- 将不依赖 state 的 JSX 上移
- 考虑使用 useRef 替代不需要响应式更新的数据
Context 改变导致的重新渲染
- 将 Context Provider 放在组件树顶层
- 根据功能合理拆分 Context
- 谨慎在自定义 hooks 中使用嵌套过深的 state (context,useState)
以上文字参考
the big problem with React useContext
how does React re-render?
为了解决内存泄露,我把 vue 源码改了
前言
彦祖们,好久不见,最近一直忙于排查单位业务的终端内存泄露问题,已经吃了不下 10 个 bug
了
但是排查内存泄露在前端领域属于比较冷门的领域了
这篇文章笔者将带你一步步分享业务实践中遇到的内存泄露问题以及如何修复的经历
本文涉及技术栈
- vue2
场景复现
如果之前有看过我文章的彦祖们,应该都清楚
笔者所在的单位有一个终端叫做工控机(类似于医院挂号的终端),没错!所有的 bug 都源自于它😠
因为内存只有 1G
所以一旦发生内存泄露就比较可怕
不过没有这个机器 好像也不会创作这篇文章😺
复现 demo
彦归正传,demo 其实非常简单,只需要一个最简单的 vue2 demo 就可以了
- App.vue
<template>
<div id="app">
<button @click="render = true">render</button>
<button @click="render = false">destroy</button>
<Test v-if="render"/>
</div>
</template>
<script>
import Test from './test.vue'
export default {
name: 'App',
components: {
Test
},
data () {
return {
render: false
}
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
</style>
- test.vue
<template>
<div class="test">
<div>{{ total }}</div>
<div
v-for="(item,index) in 1000"
:key="`${item}-${index}`"
class="item"
>
{{ item }}ipc-prod2.8
</div>
</div>
</template>
<script>
export default {
name: 'Test',
data () {
return {
total: 1000
}
},
mounted () {
this.timer = setTimeout(() => {
this.total = 10000
}, 500)
},
beforeDestroy () {
clearTimeout(this.timer)
}
}
</script>
复现流程
以下流程建议彦祖们在 chrome 无痕模式
下执行
- 我们点击
render
按钮渲染test
组件,此时我们发现dom
节点的个数来到了2045
考虑到有彦祖可能之前没接触过这块面板,下图展示了如何打开此面板
-
500ms
后(定时器执行完成后,如果没复现可以把500ms 调整为 1000ms, 1500ms
),我们点击destroy
按钮 - 我们点击面板这里的强制回收按钮(发现节点并没有回收,已发生内存泄露)
如果你的浏览器是最新的 chrome
,还能够点击这里的 已分离的元素
(detached dom),再点击录制
我们会发现此时整个 test
节点已被分离
问题分析
那么问题到底出在哪里呢?
vue 常见泄露场景
笔者搜遍了全网,网上所说的不外乎以下几种场景
1.未清除的定时器
2.未及时解绑的全局事件
3.未及时清除的 dom 引用
4.未及时清除的 全局变量
好像第一种和笔者的场景还比较类似,但是仔细看看代码好像也加了
beforeDestroy () {
clearTimeout(this.timer)
}
这段代码啊,就算不加,timer
执行完后,事件循环也会把它回收掉吧
同事提供灵感
就这样笔者这段代码来回测试了半天也没发现猫腻所在
这时候同事提供了一个想法说"total 更新的时候是不是可以提供一个 key
"
改了代码后就变成了这样了
- test.vue
<template>
<div class="test">
<div :key="renderKey">{{ total }}</div>
<div
v-for="(item,index) in 1000"
:key="`${item}-${index}`"
class="item"
>
{{ item }}ipc-prod2.8
</div>
</div>
</template>
<script>
export default {
name: 'Test',
data () {
return {
renderKey: 0,
total: 1000
}
},
mounted () {
this.timer = setTimeout(() => {
this.total = 10000
this.renderKey = Date.now()
}, 500)
},
beforeDestroy () {
clearTimeout(this.timer)
}
}
</script>
神奇的事情就这样发生了,笔者还是按以上流程测试了一遍,直接看结果吧
我们看到这个 DOM
节点曲线,在 destroy
的时候能够正常回收了
问题复盘
最简单的 demo
问题算是解决了
但是应用到实际项目中还是有点困难
难道我们要把每个更新的节点都手动加一个 key
吗?
其实仔细想想,有点 vue
基础的彦祖应该了解这个 key
是做什么的?
不就是为了强制更新组件吗?
等等,强制更新组件?更新组件不就是 updated
吗?
updated
涉及的不就是八股文中我们老生常谈的 patch
函数吗?(看来八股文也能真有用的时候😺)
那么再深入一下, patch
函数内部不就是 patchVnode
其核心不就是 diff
算法吗?
首对首比较,首对尾比较,尾对首比较,尾对尾比较
这段八股文要是个 vuer
应该都不陌生吧?😺
动手解决
其实有了问题思路和想法
那么接下来我们就深入看看 vue
源码内部涉及的 updated
函数到底在哪里吧?
探索 vue 源码
我们找到 node_modules/vue/vue.runtime.esm.js
我们看到了 _update
函数真面目,其中有个 __patch__
函数,我们再重点查看一下
createPatchFunction
最后 return 了这个函数
我们最终来看这个 updateChildren
函数
其中多次出现了上文中所提到的八股文,每个都用 sameVnode
进行了对比
- function sameVnode
function sameVnode (a, b) {
return (a.key === b.key &&
a.asyncFactory === b.asyncFactory &&
((a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)) ||
(isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error))));
}
果然这里我们看到了上文中 key
的作用
key
不一样就会认作不同的 vnode
那么就会强制更新节点
对应方案
既然找到了问题的根本
在判定条件中我们是不是直接加个 || a.text !== b.text
强制对比下文本节点不就可以了吗?
修改 sameVnode
看下我们修改后的 sameVnode
function sameVnode (a, b) {
if(a.text !== b.text) return false // 文本不相同 直接 return
return (a.key === b.key &&
a.asyncFactory === b.asyncFactory &&
((a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)) ||
(isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error))));
}
方案效果
让我们用同样的代码来测试下
测试了几次发现非常的顺利,至此我们本地的修改算是完成了
如何上线?
以上的方案都是基于本地开发的,那么我们如何把代码应用到线上呢?
其他开发者下载的 vue
包依旧是 老的 sameVnode
啊
不慌,接着看
patch-package
对比了好几种方式,最终我们选择了这个神器
其实使用也非常简单
1.npm i patch-package
2.修改 node_modules/vue
源码
3.在根目录执行 npx patch-package vue
(此时如果报错,请匹配对应 node 版本的包)
我们会发现新增了一个这样的文件
4.我们需要在package.json
scripts
新增以下代码
- package.json
"scripts": {
+"postinstall":"patch-package"
}
至此上线后,其他开发者执行 npm i
后便能使变动的补丁生效了
优化点
其实我们的改造还有一定的进步空间,比如说在指定节点上新增一个 attribute
在函数内部判断这个 attribute
再 return false
这样就不用强制更新每个节点了
当然方式很多种,文章的意义在于解决问题的手段和耐心
写在最后
最后再次感谢同事 juejin.cn/user/313102… 的提供的灵感和协助
感谢彦祖们的阅读
个人能力有限
如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟
2025真实面试题
1.9下午三点
1.为什么离职
2.js数据类型
3.如何判断数据类型
4.js == 和 === 区别
== 为什么不判断类型
5.js数组操作方法有哪些,那些会改变原数组
6.es6新增特性
7.let,const,var区别
8.箭头函数和普通函数区别
9.promise是什么?promise的静态方法有什么区别
10.vue2和vue3区别
11.vue生命周期
12.vue组件通信
13.vue路由跳转方式,vue路由模式
14.如何实现一个自定义指令
15.如何实现响应式布局,rem是什么的尺寸
16.实现一个左侧固定宽度,右侧宽度自适应布局
17.组件库实现考虑那些东西
1.16上午10点
1.vite打包配置
2.事件循环
3.跨域,什么算跨域
4.call apply,bind
5.数组常用api
6.vue2vue3区别
7.vue2vue3响应式原理区别
8.v-model
9.ts中any,never,unknow区别
10.es6新特性
1.16下午三点
1.vue2,vue3区别
2.teleport使用(项目中)
3.token无感刷新,静默登录(项目中)
4.前端存储方案,区别,存在cookie里,关闭浏览器会消失吗
5.路由守卫权限验证
6.按钮权限控制如何实现,v-if,v-show区别
7.组件通信方式
8.pinia用来做什么
9.react中怎么更新组件的状态
10.websocket 项目中使用场景,websocket有哪些事件
11.行内元素有哪些
12.行内元素设置宽高会生效吗,如果让他生效怎么做
13.插槽的作用
14.按钮如何实现防抖
15.那些比较有挑战性的工作
Vue3: 二次封装组件的原则与方法
原则
- 保持单一职责
原则:每个组件应专注于完成一种特定功能,不要添加与组件主要功能无关的逻辑。
举例:封装一个按钮组件时,应该专注于样式和事件绑定,而不是处理复杂的表单逻辑。 - 尽量减少重复代码
原则:通过封装复用逻辑,减少多处使用同样代码的情况。
举例:可以对常用的输入框组件进行封装,将其通用功能提取为单一组件。 - 提供清晰的接口
原则:定义合理的 props 和 events,避免过多的参数,保持易用性和清晰度。
举例:组件需要的所有配置项应通过 props 提供,事件通过 $emit 通知父组件。 - 支持扩展性
原则:为封装组件提供插槽、动态样式或回调接口,让开发者可以扩展功能。
举例:提供 slot 和 props,使组件可以适应不同场景。 - 遵循团队规范
原则:封装组件时应符合团队的命名、代码风格和功能设计规范。
举例:Vue 组件的 props 使用驼峰命名,事件使用 update:modelValue 风格
方法
-
继承属性:使用
v-bind="$attrs"
来接收和传递父组件传递的属性。常用于向原组件传递未声明的属性。 -
继承事件:使用
emits
配置声明事件,或者通过$emit
进行事件的触发和传播。 -
继承方法:使用
ref
来暴露内部方法给父组件,通过defineExpose
明确暴露。 -
继承插槽:使用
slots
对象来接收和转发插槽内容。 -
vue2与vue3的区别:在 vue3 中,取消了listeners这个组件实例的属性,将其事件的监听都整合到了attrs上,因此直接通过v-bind=$attrs属性就可以进行props属性和event事件的透传
-
在 vue2 中,需要用到
$slots
(插槽) 和$scopedSlots
(作用域插槽) -
在 vue3 中,取消了作用域插槽
$scopedSlots
,将所有插槽都统一在$slots
当中
v-model
大家应该都知道v-model
只是一个语法糖,实际就是给组件定义了modelValue
属性和监听update:modelValue
事件,所以我们以前要实现数据双向绑定需要给子组件定义一个modelValue
属性,并且在子组件内要更新modelValue
值时需要emit
出去一个update:modelValue
事件,将新的值作为第二个字段传出去。
原因是因为从vue2
开始就已经是单向数据流,在子组件中是不能直接修改props
中的值。而是应该由子组件中抛出一个事件,由父组件去监听这个事件,然后去修改父组件中传递给props
的变量。如果这里我们给input
输入框直接加一个v-model="props.modelValue"
,那么其实是在子组件内直接修改props
中的modelValue
。
如果父组件和子组件中都使用了 v-model,并且绑定的是同一个变量,这个时候就会出问题了,因为子组件直接更改了父组件的数据,违背了单向数据流,这样会导致如果出现数据问题不好调试,无法定位出现问题的根源。
第一种方法:将 v-model 拆开,通过 emit 让父组件去修改数据
第二种方法:使用计算属性的 get set 方法
但是如果子组件中有多个表单项,不管是上面哪种方法,都要写很多重复的代码
<!-- 父组件 -->
<template>
<my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
text: '',
password: '',
name: ''
})
</script>
<!-- 子组件 -->
<template>
<el-input v-model="name"></el-input>
<el-input v-model="text"></el-input>
<el-input v-model="password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
modelValue: {
type: Object,
default: () => {},
}
});
const emit = defineEmits(['update:modelValue']);
const name = computed(() => {
get() {
return props.modelValue.name
},
set(val) {
emit('update:modelValue', {
...props.modelValue,
name: val
})
}
})
const text = computed(() => {
get() {
return props.modelValue.text
},
set(val) {
emit('update:modelValue', {
...props.modelValue,
text: val
})
}
})
const password = computed(() => {
get() {
return props.modelValue.password
},
set(val) {
emit('update:modelValue', {
...props.modelValue,
password: val
})
}
})
</script>
上面使用计算属性监听单个属性,所以需要每个属性都写一遍,我们可以考虑在计算属性中监听整个对象:
<!-- 父组件 -->
<template>
<my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
text: '',
password: '',
name: ''
})
</script>
<!-- 子组件 -->
<template>
<el-input v-model="modelList.name"></el-input>
<el-input v-model="modelList.text"></el-input>
<el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
modelValue: {
type: Object,
default: () => {},
}
});
const emit = defineEmits(['update:modelValue']);
const modelList = computed(() => {
get() {
return props.modelValue
},
set(val) {
emit('update:modelValue', val)
}
})
</script>
读取属性的时候能正常调用 get,但是设置属性的时候却无法触发 set,原因是 modelList.value = xxx
,才会触发 set,而 modelList.value.name = xxx
,无法触发。这个时候,Proxy
代理对象可以完美的解决这个问题:
<!-- 父组件 -->
<template>
<my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
text: '',
password: '',
name: ''
})
</script>
<!-- 子组件 -->
<template>
<el-input v-model="modelList.name"></el-input>
<el-input v-model="modelList.text"></el-input>
<el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
modelValue: {
type: Object,
default: () => {},
}
});
const emit = defineEmits(['update:modelValue']);
const modelList = computed(() => {
get() {
return new Proxy(props.modelValue, {
get(target, key) {
return Reflect.get(target, key)
},
set(target, key, value) {
emit('update:modelValue',{
...target,
[key]: value
})
return true
}
})
},
set(val) {
emit('update:modelValue', val)
}
})
</script>
我们还可以考虑把这段代码进行封装,可以在多处引入进行使用: useVModel.ts
,其实 vueuse 里面有提供了这么一个方法,基本的逻辑是一样的。
useVModel
<!-- 父组件 -->
<template>
<my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
text: '',
password: '',
name: ''
})
</script>
<!-- 子组件 -->
<template>
<el-input v-model="modelList.name"></el-input>
<el-input v-model="modelList.text"></el-input>
<el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useVModel } from './useVModel.ts'
const props = defineProps({
modelValue: {
type: Object,
default: () => {},
}
});
const emit = defineEmits(['update:modelValue']);
const modelList = useVModel(props, 'modelValue', emit)
</script>
使用defineModel实现数据双向绑定
defineModel是一个宏,所以不需要从vue中import导入,直接使用就可以了。这个宏可以用来声明一个双向绑定 prop,通过父组件的 v-model 来使用
<template>
<CommonInput v-model="inputValue" />
</template>
<script setup lang="ts">
import { ref } from "vue";
const inputValue = ref();
</script>
<template>
<input v-model="model" />
</template>
<script setup lang="ts">
const model = defineModel();
model.value = "xxx";
</script>
在上面的例子中我们直接将defineModel
的返回值使用v-model
绑定到input输入框上面,无需定义 modelValue
属性和监听 update:modelValue
事件,代码更加简洁。defineModel
的返回值是一个ref
,我们可以在子组件中修改model
变量的值,并且父组件中的inputValue
变量的值也会同步更新,这样就可以实现双向绑定。
原理:defineModel其实就是在子组件内定义了一个叫model的ref变量和modelValue的props,并且watch了props中的modelValue。当props中的modelValue的值改变后会同步更新model变量的值。并且当在子组件内改变model变量的值后会抛出update:modelValue事件,父组件收到这个事件后就会更新父组件中对应的变量值。
例子
<template>
<my-input v-model="inputValue" ref="myInputRef" :name="'lyw'" :year="18" @input="onInput">
<!-- 动态插槽定义 -->
<template #prefix>
<span>Prefix Content</span>
</template>
<template #suffix="{ year }">
<span>END: {{ year }}</span>
</template>
<template #default="{ name }">
<p>Default Content: {{ name }}</p>
</template>
</my-input>
<div>当前输入的值是:{{ inputValue }}</div>
</template>
<script lang="ts" setup>
import MyInput from '@/components/MyInput.vue'
import { onMounted } from 'vue'
import { ref } from 'vue'
const inputValue = ref('')
const myInputRef = ref()
function onInput(value: string) {
console.log('Input value:', value)
}
onMounted(() => {
// 光标聚焦
// 避免ref 链式调用,比如 this.$refs.tableRef.$refs.table.clearSort()
// 前提:子组件把方法暴露
myInputRef.value.focus()
})
</script>
<template>
<!-- 简化:直接使用 computed 绑定到 v-bind -->
<el-input v-model="localValue" v-bind="$attrs" ref="inputRef">
<!-- 动态插槽 -->
<template v-for="(_slot, slotName) in $slots" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps"></slot>
</template>
</el-input>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue'
import { computed, ref } from 'vue'
// 定义 props 和 emits
const props = defineProps({
modelValue: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue'])
// 使用 computed 来同步 props 和 emits
const localValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const inputRef = ref('')
let exposeObj: Record<string, any> = {}
const getMethod = () => {
const entries = Object.entries(inputRef.value)
for (const [method, fn] of entries) {
exposeObj[method] = fn
}
}
onMounted(getMethod)
defineExpose(exposeObj)
</script>