爱站seo工具包官网,pc网站是什么,怎么做淘宝客网站赚钱,开发一个小程序一般需要多少钱呢前言 官网地址#xff1a;Vue.js - 渐进式 JavaScript 框架 | Vue.js (vuejs.org)下面只讲Vue3与Vue2有差异的地方#xff0c;一些相同的地方我会忽略或者一笔带过与Vue3一同出来的还有Vite#xff0c;但是现在不使用它#xff0c;等以后会有单独的教程使用。目前仍旧使用v…前言 官网地址Vue.js - 渐进式 JavaScript 框架 | Vue.js (vuejs.org)下面只讲Vue3与Vue2有差异的地方一些相同的地方我会忽略或者一笔带过与Vue3一同出来的还有Vite但是现在不使用它等以后会有单独的教程使用。目前仍旧使用vue-cli创建vue3项目Vue3有两种代码编写形式。下面演示主要使用第二种 一种是和vue2相仿的选项式API写法另一种是组合式API的写法。具体两种代码编写的区别与优缺点可以自行去官网查看。 vue3版本向下兼容。所以Vue2的写法依旧可以继续使用不过只能搭配第一种选项是API的写法一同使用。 1、基础
1、初始化Vue 使用vue-cli创建好项目后打开main.js文件可以看到和vue2的区别。vue3中使用起来更加语义明显并且不需要使用new Vue那么笨重的构造函数而是封装好了需要的函数只把这些函数提供给我们。 1.1、vue2中
import Vue from vue
import App from ./App.vue
import router from ./router
import store from ./storenew Vue({router,store,render: h h(App)
}).$mount(#app)1.2、vue3中
import { createApp } from vue
import App from ./App.vue
import router from ./router
import store from ./storecreateApp(App).use(store).use(router).mount(#app)打印下createApp()函数返给我们的内容看看可以看到几个常用的函数 2、生命周期 vue2中组件挂载会执行mounted钩子函数对应的卸载会使用beforeDestroy、destroyed钩子函数。vue3中其他钩子函数依旧不变包括挂载mounted但是卸载改为了beforeUnmounted、unmounted。 3、模板语法 模板语法中v-model、v-bind、v-on等包括其对应的简写形式都没有改变。唯一的区别就是标签template中不再强制必须有一个外层标签嵌套里面所有内容了。 3.1、vue2中
template!-- 模板中只能有一个主标签如果有两个则会报错 --divh1HomeView/h1!-- ... --/div
/template3.2、vue3中
template!-- 不强制必须有外层的主标签了 --h1HomeView/h1!-- ... --
/template4、响应式基础(重要) vue2中响应式数据全部存放到data函数中。vue3中也可以使用vue2的方式不过我们使用的是组合式API就不能那么写了。根据数据类型可以分为两种方式 ref(xxx)可以将所有类型的数据定义为响应式、基础类型以及对象。不过在js中访问数据需要添加.value。返回的值为RefImpl对象reactive(xxx)只能将对象\数组类型数据转为响应式。但是在js中访问数据不需要添加.value 其实ref函数中会根据参数数据类型进行判断如果是对象其内部还是使用的reactive将对象转为响应式不过会将转换完的数据封装起来外面包一层。 4.1、基本数据类型(ref)
代码
template!-- 在模板中使用的话则不需要.valuevue自动执行 --h2checkbox的值{{ flag }}/h2input typecheckbox v-modelflagh2input的值{{ str }}/h2input typetext v-modelstr
/template!-- 组合式API只需要在script标签上使用 setup 即可 --
script setup// 使用什么就导入什么import { ref } from vue;// 此时flag就是一个响应式对象const flag ref(true);// 访问的是包装以后的对象并不是真正的值需要.value才能获取到真正的值console.log(flag, flag);const str ref(123);/* 真正的str的值 */console.log(str, str.value);
/script效果 4.2、对象类型(ref)
代码
templatebutton clickchangeAge姓名1/buttonh2姓名:/h2{{ user.name}}h2年龄:/h2{{ user.age}}
/templatescript setupimport { ref } from vue;const user ref({name: 张三,age: 18})// Proxy对象console.log(user, user.value);function changeAge() {// 必须要使用.value才能获取到对象user.value.age;}
/script
效果 4.3、对象类型(reactive)
代码
templatebutton clickchangeAge姓名1/buttonh2姓名:/h2{{ user.name}}h2年龄:/h2{{ user.age}}
/templatescript setupimport { ref, reactive } from vue;const user reactive({name: 张三,age: 18})// 还是Proxy对象console.log(user, user);function changeAge() {// 不用使用.value就可以获取到对象user.age;}
/script
效果 5、响应式基础总结 vue2中 不管是基本数据类型还是对象类型都会使用Object.defineProperty的最后一个参数get、set方法实现对数据的响应式。基本类型的数据只能进行获取或者修改操作实现起来毫无问题。 对象或数组不仅有获取、修改操作还可以动态的添加属性或者删除属性等操作这种操作使用Object.defineProperty是监听不到的。下面第5.1章有演示 针对上面这种操作vue提供了几种解决办法 Vue重新实现了数组的一系列函数才实现数组的响应式。另外Vue还提供了$set(object,field,val)针对字面的动态添加对象属性/数组值的响应式 vue3中 现在ES6提出了Proxy代理使用代理我们就可以监听到任何针对对象的操作。上面的问题才得以解决Vue3针对数组和对象就采取了Proxy的方式实现了对象数据响应式。不需要再使用$set等方法了。下面5.2章有演示针对上面演示也可以发现针对对象数据类型返回的值为Proxy类型的数据 ref和reactive区别 ref将所有类型的数据转换为响应式对象。在script使用时需要.value访问在模板语法中使用时则不需要。reactive只能将对象/数组类型数据转为响应式。在script使用时不需要.value访问直接访问即可。 建议 不管是对象/数组还是基本类型数据都建议使用ref进行封装。详细请看为什么建议使用ref 5.1、vue2响应式失效演示
代码
templatediv classhomebutton clickaddUserSex添加用户性别-男/buttonbutton clickaddUserHobby添加用户爱好-羽毛球/buttonh2用户名:/h2{{ user.name }}h2密码:/h2{{ user.password }}h2性别:/h2{{ user.sex }}h2爱好:/h2{{ user.hobby }}/div
/templatescript
export default {data() {return {user: {name: admin,password: 123456,hobby:[篮球, 足球]}};},methods: {addUserSex() {this.user.sex 男;console.log(this.user);},addUserHobby() {this.user.hobby[2] 羽毛球;console.log(this.user);}},
};
/script效果 5.2、vue3相同赋值方式生效演示
代码
templatebutton clickaddUserSex添加用户性别-男/buttonbutton clickaddUserHobby添加用户爱好-羽毛球/buttonh2用户名:/h2{{ user.name }}h2密码:/h2{{ user.password }}h2性别:/h2{{ user.sex }}h2爱好:/h2{{ user.hobby }}
/templatescript setupimport { ref, reactive } from vue;const user reactive({name: admin,password: 123456,hobby:[篮球, 足球]})// 还是Proxy对象console.log(user, user);function addUserSex() {user.sex 男;console.log(user);}function addUserHobby() {user.hobby[2] 羽毛球;console.log(user);}
/script
效果 6、条件渲染与列表渲染 条件渲染依旧使用v-if、v-else-if、v-else目标组件必须相邻列表渲染也依旧使用v-for(item, index) in hobbys。下面主要讲的是同时使用。其实vue是不建议同时使用的。 在vue2中v-if、v-for同时使用的话v-for的优先级更高v-if是可以拿v-for中的变量去动态判断的。在vue3中v-if、v-for同时使用的话则相反了v-if的优先级更高意味着在v-if中是拿不到item去判断的 6.1、vue2中演示
代码
templatediv classhomeul!-- 如果不是过滤不是打球的选项 --li v-foritem in hobby :keyitem v-ifitem ! 打球{{ item }}/li/ul/div
/templatescript
export default {data() {return {hobby:[篮球, 足球, 排球, 打球]};},
};
/script效果 6.2、vue3中演示
代码
templateul!-- 如果不是过滤不是打球的选项 --li v-foritem in hobby :keyitem v-ifitem ! 打球{{ item }}/liul
/templatescript setupimport { ref, reactive } from vue;const hobby reactive([篮球, 足球, 排球, 打球])
/script效果 7、模板引用 在vue2中我们给dom标签上使用refxxx然后在js中使用this.$refs.xxx获取绑定的dom元素。在vue3中我们需要在script中定义和ref绑定的同名的变量(ref响应式函数返回)vue即可自动绑定上。 7.1、vue2中
代码
templatediv classhomebutton clickfocusInput激活光标/buttoninput typetext refinput/div
/templatescript
export default {methods: {focusInput() {console.log(this.$refs.input);this.$refs.input.focus();}}
};
/script效果 7.3、vue3中
代码
templatebutton clickfocusInput激活光标/buttoninput typetext refinput
/templatescript setupimport { ref, reactive } from vue;// 变量名必须与dom标签中ref绑定的变量一致 参数不管写不写值都是ref绑定的dom。且必须放到script中不能放到函数中const input ref();console.log(input, input);function focusInput() {input.value.focus();}
/script效果 8、计算属性 vue2中我们使用的是选项式API(computed{})定义此时它是一个对象。vue3中我们使用的是vue提供的computed()函数定义此时它是一个函数并且没有参数。 如果参数使用的是函数的话那么默认是get方法如果参数是一个对象的话那么可以单独定义get、set方法 8.1、vue2中
代码
templatediv classhomebutton clickchangeValval值1/buttonh2val值/h2{{ val }}h2val是否为偶数/h2{{ flag }}/div
/templatescript
export default {computed: {flag() {return this.val % 2 0;}},data() {return {val: 1,}},methods: {changeVal() {this.val;}}
};
/script效果 8.2、vue3中
代码
templatebutton clickchangeValval值1/buttonh2val值/h2{{ val }}h2val是否为偶数/h2{{ flag }}
/templatescript setupimport { ref, reactive, computed } from vue;const val ref(1);const flag computed(() {// 记得使用.valuereturn val.value % 2 0;})function changeVal() {val.value;}
/script
效果 9、侦听器-watch 监听按数量可以分为两类单个数据监听、多个数据监听按监听的数据类型分类可以分为四类基本数据类型、对象、对象基本属性、对象中对象/数组属性注意如果监听由reactive创建出来的对象的话不需要什么配置默认就是深度监听。如果是ref创建出来的对象的话需要手动打开deep:true配置监听对象的话newValue和oldValue的值相同 9.1、语法
watch(被监听的双向绑定的数据, 数据改变的回调, 配置)9.2、监听基本数据类型
代码
templatebutton clickchangeValval值1/buttonh2val值/h2{{ val }}
/templatescript setupimport { ref, reactive, computed, watch } from vue;const val ref(1);watch(val, (newVal, oldVal) {console.log(val的值改变了, newVal, oldVal);})function changeVal() {val.value;}
/script效果 9.3、监听对象 默认是深度监听如果想监听整个user改变的话就不能使用reactive了要使用ref然后修改user.value ...即可监听到 代码
templatebutton clickchangeAge用户年龄1/buttonh2用户名/h2{{ user.name }}h2用户年龄/h2{{ user.age }}
/templatescript setupimport { ref, reactive, computed, watch } from vue;let user reactive({name: 张三,age: 18})// 只有由reactive创建出来的对象才不需要手动开启deep配置默认深度监听watch(user, (newVal, oldVal) {console.log(user的值改变了, newVal, oldVal);})function changeAge() {user.age;}
/script效果 9.4、监听对象中基本属性 监听对象中的属性要提供一个get函数才可以 代码
templatebutton clickchangeAge用户年龄1/buttonh2用户名/h2{{ user.name }}h2用户年龄/h2{{ user.age }}
/templatescript setupimport { ref, reactive, computed, watch } from vue;let user reactive({name: 张三,age: 18})// 参数1为get函数watch(() user.age, (newVal, oldVal) {console.log(userAge的值改变了, newVal, oldVal);})// 错误因为得到的是一个基本数据类型number。如果监听的是user中的对象/数组的话则这么写//watch(user.age, (newVal, oldVal) {// console.log(userAge的值改变了, newVal, oldVal);//})function changeAge() {user.age;}
/script效果 9.5、监听对象中对象/数组
代码
templatebutton clickchangeAge用户年龄1/buttonbutton clickchangeHobby修改用户爱好/buttonbutton clickchangeAddress修改用户地址/buttonh2用户名/h2{{ user.name }}h2用户年龄/h2{{ user.age }}h2爱好/h2{{ user.hobbys }}
/templatescript setupimport { ref, reactive, computed, watch } from vue;let user reactive({name: 张三,age: 18,hobbys: [吃饭, 睡觉, 打豆豆],address: {province: 浙江省,city: 杭州市,county: 西湖区}})// 不需要get函数了watch(user.hobbys, (newValue, oldValue) {console.log(爱好改变了, newValue, oldValue);})// 不需要get函数了watch(user.address, (newValue, oldValue) {console.log(地址改变了, newValue, oldValue);})function changeHobby() {user.hobbys.push(打游戏);}function changeAge() {user.age;}function changeAddress() {user.address.city 温州市;
}
/script效果 9.6、监听配置 与vue2一样同样有两个配置immediate、deep。如果监听的是由reactive创建出来的对象的话deep失效默认深度监听。如果是由ref创建出来的对象的话则需要手动开启deep配置 代码
watch(user, (newVal, oldVal) {console.log(user的值改变了, newVal, oldVal);
}, {immediate: true, // 立即执行deep: true // 深度监听
})10、侦听器-watchEffect watchEffect和计算属性差不多不用显式的声明监视哪个属性它会自动收集函数中用到的响应式数据如果响应式数据改变了就会自动执行如果你想监听一个对象中的多个属性的话使用watchEffect会更好因为它只会监听函数中用到的属性而不是整个对象中的所有属性如果函数里面有数组类型的数据则监听不到整个数组的变化只能监听某一个下标值的变化刚加载出来的时候会执行一次相当于默认immediate: true 10.1、代码
templatebutton clickchangeAge用户年龄1/buttonbutton clickchangeAddress修改用户地址/buttonbutton clickchangeHobbys修改用户爱好/buttonh2用户名/h2{{ user.name }}h2用户年龄/h2{{ user.age }}h2爱好/h2{{ user.hobbys }}
/templatescript setupimport { ref, reactive, computed, watch, watchEffect } from vue;let user reactive({name: 张三,age: 18,hobbys: [吃饭, 睡觉, 打豆豆],address: {province: 浙江省,city: 杭州市,county: 西湖区}})/* 没有新旧值的参数 */watchEffect((aaa) {console.log(userAge可能改变了, user.age);console.log(userHobby可能改变了, user.hobbys[0]);// console.log(userHobby可能改变了, user.hobbys); 监听失效console.log(userAddress可能改变了, user.address.city);})function changeHobbys() {user.hobbys[0] 打游戏;}function changeAge() {user.age;}function changeAddress() {user.address.city 温州市;
}
/script10.2、效果 11、侦听器总结 参考官网侦听器 | Vue.js (vuejs.org)取消侦听器watch、watchEffect都会返回一个取消当前侦听器的函数调用函数即可取消侦听。watch 可以直接对双向绑定的基本类型数据进行监听。如果监听由reactive创建出来的对象则默认是深度监听ref创建出来的话需要手动开启深度监听配置。并且newValue和oldValue的值相同。如果监听对象里面的基本属性则第一个参数需要传入那个属性的一个get函数。如果监听对象里的的对象/数组则第一个参数直接使用对象.属性即可不需要封装为get函数 watchEffect 不需要指明被监听的对象函数里用到的响应式数据会自动被监听到。非常适合对象里的多个属性被监听都会执行同一段逻辑的时候。如果函数里用到数组的话则只会在函数内指明数组下标获取数据时对应下标数据发生变化才会被监听到。监听数组还是建议使用watch监听 2、深入组件
1、组件注册
1.1、全局注册
方法一
import { createApp } from vue
import App from ./App.vuecreateApp(App)
.use(ElementPlus)
.component(MyButton2, {// 组件实现。。。
})
.mount(#app)方法二
import { createApp } from vue
import App from ./App.vue
// 全局导入
import MyButton2 from /components/MyButton2;createApp(App)
.component(MyButton2, MyButton2)
.mount(#app)方法三
import { createApp } from vue
import App from ./App.vue
// 全局导入
import MyButton2 from /components/MyButton2;
import MyButton3 from /components/MyButton3;createApp(App)
.component(MyButton2, MyButton2) // 链式导入
.component(MyButton3, MyButton3)
.mount(#app)
1.2、局部注册 无需像vue2一样需要导入-注册了。直接导入即可使用 template!-- 局部导入 --MyButton/MyButton!-- 全局组件 --MyButton2/MyButton2
/templatescript setupimport { ref, reactive, nextTick } from vue;import MyButton from /components/MyButton.vue;/script2、props 在使用 script setup 的单文件组件中props 可以使用 defineProps() 宏来声明 2.1、语法
// 语法一
const prop defineProps([defaultColor])
console.log(prop.defaultColor);// 语法二带有类型校验和默认值
const prop defineProps({defaultColor: {type: String,default:red}})console.log(prop);2.2、案例
templatediv classmyButtonbutton clickchangeColor :style{backgroundColor: btnBgcColor}点我改变按钮颜色(局部导入)/button/div
/templatescript setupimport { ref } from vueconst prop defineProps({defaultColor: {type: String,default:red}})console.log(prop);// 获取父组件传入的参数const btnBgcColor ref(prop.defaultColor);function changeColor() {btnBgcColor.value randomColor();}function randomColor() {return # Math.random().toString(16).slice(2, 8).padEnd(6, 0);}/scriptstyle scoped langscss
.myButton {button {padding: 5px 10px;border-radius: 5px;}
}
/style3、事件 总共分为两种写法 在模板中直接调用使用$emit(xxxx)即可和vue2使用方法一致。在script中调用需要先在defineEmits([xxx,xxx])中声明然后下面使用这个函数的返回值进行调用。 可以对函数参数进行校验校验失败则会在控制台提示警告但不阻碍父组件函数执行 调用的时候不管是模板中使用$emit还是script中使用emit第一个参数永远是绑定在组件上的方法属性名后面所有参数为调用这个方法的参数 3.1、直接在模板中调用
!-- 子组件 --
templatediv classmyButton!-- 直接使用$emit调用预期绑定的函数即可 --button click$emit(myButtonClick)点我执行方法/button/div
/template!-- 父组件 --
templateMyButton myButtonClickhandleBtnFun/MyButton
/templatescript setupimport MyButton from /components/MyButton.vue;// 点击 MyButton 组件中的按钮将会执行这个方法function handleBtnFun() {alert(按钮被点击了);}/script3.2、在script中调用
!-- 父组件不变所以省略 --!-- 子组件 --
templatediv classmyButtonbutton clickhandleBtnFun点我执行方法/button/div
/templatescript setup// 需要先声明接受的函数/*如果这里没有声明下面直接调用那么控制台会警告[Vue warn]: Component emitted event myButtonClick1 but it is neither declared in the emits option nor as an onMyButtonClick1 prop.*/const emit defineEmits([myButtonClick])function handleBtnFun() {emit(myButtonClick)}/script3.3、defineEmits校验参数 校验成功返回true失败则返回false如果校验失败那么会在控制台打印警告信息但是并不会阻碍父组件函数执行 代码
!-- 子组件 --
templatediv classmyButtonbutton clickhandleBtnFun点我执行方法/button/div
/templatescript setup// 需要先声明接受的函数/*如果这里没有声明下面直接调用那么控制台会警告[Vue warn]: Component emitted event myButtonClick1 but it is neither declared in the emits option nor as an onMyButtonClick1 prop.*/const emit defineEmits({myButtonClick: data {console.log(data);// 判断参数是否小于 0.5 如果小于则警告但是并不影响执行父组件绑定的方法if(data 0.5) {console.warn(data 0.5);return false;}return true;}})function handleBtnFun() {emit(myButtonClick, Math.random())}/script!-- 父组件 --
template!-- 局部导入 --MyButton defaultColorpink myButtonClickhandleBtnFun/MyButton
/templatescript setupimport MyButton from /components/MyButton.vue;function handleBtnFun(data) {alert(按钮被点击了, data);}/script效果 4、组件v-model v-model其实就是歌语法糖原本需要在组件上写一个动态属性(赋值)以及绑定一个修改函数(该值)。现在只需要写一个v-model即可。 vue2中 将内部原生 input 元素的 value attribute 绑定到 value prop当原生的 input 事件触发时触发一个携带了新值的 input 自定义事件 vue3中 将内部原生 input 元素的 value attribute 绑定到 modelValue prop当原生的 input 事件触发时触发一个携带了新值的 update:modelValue 自定义事件这个modelValue的名字是可以自行修改的下面v-model的参数会讲到(用于处理组件内有多个双向绑定表单框的情况)。 4.1、案例一(通用)
子组件
templateinput typetext :valuemodelValue input$emit(update:modelValue, $event.target.value)
/template
script setup
const { modelValue } defineProps([modelValue])console.log(modelValue);/script父组件
template!-- 局部导入 --MyInput v-modelvalue/MyInput
/templatescript setupimport { ref } from vue;import MyInput from /components/MyInput.vue;const value ref();/script4.2、案例二(使用computed) 父组件不变所以省略不写 子组件
templateinput typetext v-modelvalue
/template
script setup
import { computed } from vue;const props defineProps([modelValue])
const emit defineEmits([update:modelValue])const value computed({get() {return props.modelValue},set(val) {emit(update:modelValue, val)}
})/script4.3、修改默认绑定的modelValue属性名 vue2中 默认绑定的属性名为value并且无法修改 vue3中 提供了给v-model提供参数的写法可以修改绑定的属性名v-model:xxxvalue 父组件
template!-- 将v-model 默认绑定的modelValue属性名 改为abc --MyInput v-model:abcvalue/MyInput
/templatescript setupimport { ref, reactive, nextTick } from vue;import MyInput from /components/MyInput.vue;const value ref();/script子组件
templateinput typetext :valueabc input$emit(update:abc, $event.target.value)
/template
script setup
const { abc } defineProps([abc])/script4.4、处理组件多v-model情况(使用4.3方法) 有了上面修改属性名的方法那么我们给组件中每个input绑定单独的属性名即可 父组件
template!-- 局部导入 --MyInput v-model:value1value1 v-model:value2value2/MyInput
/templatescript setupimport { ref, reactive, nextTick } from vue;import MyInput from /components/MyInput.vue;const value1 ref();const value2 ref();/script子组件
templateinput typetext :valuevalue1 input$emit(update:value1, $event.target.value)brbrinput typetext :valuevalue2 input$emit(update:value2, $event.target.value)
/template
script setup
const { value1, value2 } defineProps([value1, value2])/script效果 4.5、组件内单独处理v-model修饰符 v-model已经自带了一些修饰符像.number、.lazy、.trim等vue2是无法自定义修饰符的。在vue3中你可以在组件中接收v-model的修饰符并做出一些单独的处理不过只能针对你当前组件并不是全局的。在子组件中可以在defineProps中多接收一个参数modelModifiers来接收所有绑定在v-model上的修饰符modelModifiers是个对象如果传入修饰符xxx那么xxx对应的值则是true最好在接收modelModifiers的时候赋一个默认值为{}。下面演示将v-model的首字母转为大写的案例 父组件
template!-- 局部导入 --MyInput v-model.capitalizevalue/MyInput
/templatescript setupimport { ref, reactive, nextTick } from vue;import MyInput from /components/MyInput.vue;const value ref();/script子组件
templateinput typetext :valuemodelValue inputemitValue
/template
script setup
const emit defineEmits([update:modelValue])
const { modelValue, modelModifiers } defineProps({modelModifiers: {type: Object,default: () ({})},modelValue: {type: String,default: }
})console.log(modelModifiers, modelModifiers.capitalize); // true// input 修改事件将首字母转为大写
function emitValue(el) {let val el.target.value;if(modelModifiers.capitalize) {// 将modelValue首字母大写val val.charAt(0).toUpperCase() val.slice(1);}emit(update:modelValue, val)
}
/script效果 4.6、处理带参数(修改默认绑定属性名)并且带修饰符的情况 对于又有参数又有修饰符的 v-model 绑定生成的 prop 名将是 arg Modifiers不管是一个v-model还是多个都是上面这个规则。 父组件
template!-- 局部导入 --MyInput v-model:abc.capitalizevalue/MyInput
/templatescript setupimport { ref, reactive, nextTick } from vue;import MyInput from /components/MyInput.vue;const value ref();/script子组件
templateinput typetext :valueabc inputemitValue
/template
script setup
const emit defineEmits([update:abc])
const { abc, abcModifiers } defineProps({abcModifiers: {type: Object,default: () ({})},abc: {type: String,default: }
})console.log(abcModifiers, abcModifiers.capitalize); // true// input 修改事件将首字母转为大写
function emitValue(el) {let val el.target.value;if(abcModifiers.capitalize) {// 将modelValue首字母大写val val.charAt(0).toUpperCase() val.slice(1);}emit(update:abc, val)
}
/script效果 5、透传Attributes 透传 attribute”指的是传递给一个组件却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 class、style 和 id当一个组件以单个元素为根作渲染时透传的 attribute 会自动被添加到根元素上 5.1、举例透传 如果父组件上绑定了click事件那么就相当于在子组件的button上绑定了click事件。事件接收到的$event.target实际上指向的就是button元素 子组件
templatebutton测试按钮/button
/templatescript setup/scriptstyle scoped langscssbutton {padding: 5px 10px;border-radius: 5px;}
/style父组件
template!-- 局部导入 --MyButton classmyButton2/MyButton
/templatescript setupimport MyButton from /components/MyButton.vue;
/script实际渲染效果 5.2、举例透传合并 不仅针对class、style对于v-on绑定的事件也是如此如果在组件上绑定了click事件子组件的button上也绑定了click事件则子组件的事件(先)和父组件上绑定的事件(后)则会同时触发。 子组件
templatebutton classmyButton测试按钮/button
/templatescript setup/scriptstyle scoped langscssbutton {padding: 5px 10px;border-radius: 5px;}
/style父组件
template!-- 局部导入 --MyButton classmyButton2/MyButton
/templatescript setupimport MyButton from /components/MyButton.vue;
/script实际渲染效果 5.3、深层组件继承 如果子组件的根节点是孙子组件并且子组件中并没有使用props接收(消费)那么class、style、click等会直接透传到孙子组件上下面只演示class透传 父组件
template!-- 局部导入 --MyButton classtestClass clickhandleBtnClick/MyButton
/templatescript setupimport { ref } from vue;import MyButton from /components/MyButton.vue;function handleBtnClick(el) {// 指向 Mybutton2里的buttonconsole.log(handleBtnClick, el.target);}/script子组件
templateMyButton2 classmyButton测试按钮/MyButton2
/templatescript setup
import MyButton2 from /components/MyButton2;
/scriptstyle scoped langscssbutton {padding: 5px 10px;border-radius: 5px;}
/style孙子组件
templatebutton classmubutton2测试按钮2/button
/templatescript setupimport { ref } from vue/scriptstyle scoped langscssbutton {padding: 5px 10px;border-radius: 5px;}
/style实际渲染效果 5.4、禁用Attributes继承 如果你不想要一个组件自动地继承 attribute你可以在组件选项中设置 inheritAttrs: false从 3.3 开始你也可以直接在 script setup 中使用 defineOptions最常见的需要禁用 attribute 继承的场景就是 attribute 需要应用在根节点以外的其他元素上。通过设置 inheritAttrs 选项为 false你可以完全控制透传进来的 attribute 被如何使用这些透传进来的 attribute 可以在模板的表达式中直接用 $attrs 访问到这个 $attrs 对象包含了除组件所声明的 props 和 emits 之外的所有其他 attribute例如 classstylev-on 监听器等等有几点需要注意 和 props 有所不同透传 attributes 在 JavaScript 中保留了它们原始的大小写所以像 foo-bar 这样的一个 attribute 需要通过 $attrs[foo-bar] 来访问。像 click 这样的一个 v-on 事件监听器将在此对象下被暴露为一个函数 $attrs.onClick。 父组件
template!-- 局部导入 --MyButton classtestClass my-attraaa clickhandleBtnClick/MyButton
/templatescript setupimport { ref } from vue;import MyButton from /components/MyButton.vue;function handleBtnClick(el) {// 指向 Mybutton里的buttonconsole.log(handleBtnClick, el.target);}
/script子组件
templatediv classmybuttonspan{{ logAttrs($attrs) }}/spanbrbutton classbtn v-bind$attrs测试按钮/button/div
/templatescript setup// 阻止属性透传
defineOptions({inheritAttrs: false
})function logAttrs(attrs) {console.log(logAttrs, attrs);return attrs;
}
/scriptstyle scoped langscssbutton {padding: 5px 10px;border-radius: 5px;}
/style效果 5.5、多根节点的Attributes的继承 和单根节点组件有所不同有着多个根节点的组件没有自动 attribute 透传行为。如果 $attrs 没有被显式绑定将会抛出一个运行时警告 子组件
templatebutton classbtn测试按钮/buttonbrbutton classbtn2测试按钮/button
/templatescript setup/scriptstyle scoped langscssbutton {padding: 5px 10px;border-radius: 5px;}
/style效果 5.6、在JS中访问透传的Attributes 如果需要你可以在 script setup 中使用 useAttrs() API 来访问一个组件的所有透传 attribute需要注意的是虽然这里的 attrs 对象总是反映为最新的透传 attribute但它并不是响应式的 (考虑到性能因素)。你不能通过侦听器去监听它的变化。如果你需要响应性可以使用 prop。或者你也可以使用 onUpdated() 使得在每次更新时结合最新的 attrs 执行副作用。 子组件
templatediv classmybuttonbutton classbtn v-bind$attrs测试按钮/button/div
/templatescript setup
import { useAttrs } from vue// 阻止属性透传
defineOptions({inheritAttrs: false
})console.log(useAttrs());/scriptstyle scoped langscssbutton {padding: 5px 10px;border-radius: 5px;}
/style效果 6、插槽 插槽看vue3官方教程和vue2官方教程用法基本一致。但是vue3教程中并没有提示在模板中、JS中怎样获取父组件传过来的插槽。其实和上面透传Attributes一致 在template中使用$slots即可获取所有传过来的插槽。在JS中使用useSlots即可获取所有传过来的插槽 6.1、父组件
template!-- 局部导入 --MyButtontemplate #defaulth2default/h2/templatetemplate #slot2h2slot2/h2/templatetemplate #slot3h2slot3/h2/template/MyButton
/templatescript setupimport MyButton from /components/MyButton.vue;
/script6.2、子组件
templatediv classmybuttonspan{{ logSlots($slots) }}/spanslot namedefault/slotslot nameslot2/slotslot nameslot3/slot/div
/templatescript setup
import { useSlots } from vuefunction logSlots(slots) {console.log(logSlots, slots);return slots;
}console.log(useSlots, useSlots());/scriptstyle scoped langscssbutton {padding: 5px 10px;border-radius: 5px;}
/style6.3、效果 7、依赖注入(provide/inject) provide 和 inject 一个父组件相对于其所有的后代组件会作为依赖提供者。任何后代的组件树无论层级有多深都可以注入由父组件提供给整条链路的依赖。 provide和inject的功能我就不多介绍了vue2中也有下面主要演示api如何使用 7.1、provide写法 provide() 函数接收两个参数。 第一个参数被称为注入名可以是一个字符串或是一个 Symbol。后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide()使用不同的注入名注入不同的依赖值。第二个参数是提供的值值可以是任意类型包括响应式的状态比如一个 ref // 传入静态值
provide(test1, test1)// 传入动态值
const test2 ref(1);
provide(test2, test2);// 传入事件
function handleTest2Change() {test2.value;
}
provide(handleTest2Change, handleTest2Change);7.2、全局provide 除了在一个组件中提供依赖我们还可以在整个应用层面提供依赖在应用级别提供的数据在该应用内的所有组件中都可以注入。这在你编写插件时会特别有用因为插件一般都不会使用组件形式来提供值。 import { createApp } from vue
import App from ./App.vuecreateApp(App)
.provide(message, hello world!)
.mount(#app)7.3、inject写法与演示 inject()接收三个参数 第一个参数是接收的注入名可以是一个字符串或Symble第二个参数是默认值当接收不到的时候默认值就会生效第三个参数表示默认值应该被当作一个工厂函数 父组件
templatediv classhomeViewh2父组件/h2MyButton/MyButton/div
/templatescript setupimport { ref, provide } from vue;import MyButton from /components/MyButton.vue;// 传入静态值provide(test1, test1)// 传入动态值const test2 ref(1);provide(test2, test2);// 传入事件function handleTest2Change() {test2.value;}provide(handleTest2Change, handleTest2Change);/scriptstyle scoped langscss.homeView {border: 1px solid red;}
/style子组件
templatediv classmybuttonh2子组件/h2h3test1的值{{ test1 }}/h3h3test2的值{{ test2 }}/h3h3test3的值{{ test3 }}/h3h3全局message的值{{ message }}/h3button clickhandleTest2Change修改test2的值/buttonmy-button2/my-button2/div
/templatescript setup
import { inject } from vue
import MyButton2 from /components/MyButton2;const test1 inject(test1)const test2 inject(test2);// 由于没有提供test3所以会使用默认值test3
const test3 inject(test3, 使用默认值test3);// 全局message 因为在main.js中使用app.provide()注入了message
const message inject(message);// 调用提供者的函数修改提供者中的test2的值
const handleTest2Change inject(handleTest2Change)/scriptstyle scoped langscss.mybutton {border: 1px solid #000;margin: 10px;}button {padding: 5px 10px;border-radius: 5px;}
/style孙子组件
templatediv classmyButton2h2孙子组件/h2h2子组件/h2h3test1的值{{ test1 }}/h3h3test2的值{{ test2 }}/h3h3test3的值{{ test3 }}/h3h3全局message的值{{ message }}/h3button clickhandleTest2Change修改test2的值/button/div
/templatescript setupimport { ref, inject } from vueconst test1 inject(test1)const test2 inject(test2);// 由于没有提供test3所以会使用默认值test3
const test3 inject(test3, 使用默认值test3);// 全局message 因为在main.js中使用app.provide()注入了message
const message inject(message);// 调用提供者的函数修改提供者中的test2的值
const handleTest2Change inject(handleTest2Change)/scriptstyle scoped langscss.myButton2 {border: 1px solid pink;margin: 10px;}button {padding: 5px 10px;border-radius: 5px;}
/style效果 7.4、和响应式数据配合使用 当提供 / 注入响应式的数据时建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内使其更容易维护。有的时候我们可能需要在注入方组件中更改数据。在这种情况下我们推荐在供给方组件内声明并提供一个更改数据的方法函数像下面案例最后如果你想确保提供的数据不能被注入方的组件更改你可以使用 readonly() 来包装提供的值。以下案例将上面案例进行优化修改 父组件
templatediv classhomeViewh2父组件/h2MyButton/MyButton/div
/templatescript setupimport { ref, provide, readonly } from vue;import MyButton from /components/MyButton.vue;provide(test1, readonly(ref(a)))// 传入动态值const test2 ref(1);provide(test2, {test2: readonly(test2),handleTest2Change});// 传入事件function handleTest2Change() {test2.value;}
/scriptstyle scoped langscss.homeView {border: 1px solid red;}
/style子组件
templatediv classmybuttonh2子组件/h2h3test1的值{{ test1 }}/h3h3test2的值{{ test2 }}/h3h3test3的值{{ test3 }}/h3h3全局message的值{{ message }}/h3!-- 子组件内直接修改值是 不可以的 --button clicktest1a修改test1的值/buttonbutton clickhandleTest2Change修改test2的值/buttonmy-button2/my-button2/div
/templatescript setup
import { inject } from vue
import MyButton2 from /components/MyButton2;const test1 inject(test1)const { test2, handleTest2Change } inject(test2);// 由于没有提供test3所以会使用默认值test3
const test3 inject(test3, 使用默认值test3);// 全局message 因为在main.js中使用app.provide()注入了message
const message inject(message);/scriptstyle scoped langscss.mybutton {border: 1px solid #000;margin: 10px;}button {padding: 5px 10px;border-radius: 5px;}
/style孙子组件
templatediv classmyButton2h2孙子组件/h2h2子组件/h2h3test1的值{{ test1 }}/h3h3test2的值{{ test2 }}/h3h3test3的值{{ test3 }}/h3h3全局message的值{{ message }}/h3!-- 子组件内直接修改值是 不可以的 --button clicktest1a修改test1的值/buttonbutton clickhandleTest2Change修改test2的值/button/div
/templatescript setupimport { ref, inject } from vueconst test1 inject(test1)const { test2, handleTest2Change } inject(test2);// 由于没有提供test3所以会使用默认值test3
const test3 inject(test3, 使用默认值test3);// 全局message 因为在main.js中使用app.provide()注入了message
const message inject(message);/scriptstyle scoped langscss.myButton2 {border: 1px solid pink;margin: 10px;}button {padding: 5px 10px;border-radius: 5px;}
/style
效果 7.5、使用Symbol作注入名 如果你正在构建大型的应用包含非常多的依赖提供或者你正在编写提供给其他开发者使用的组件库建议最好使用Symbol来作为注入名以避免潜在的冲突。通常推荐在一个单独的文件中导出这些注入名 Symbol keys.js
export const test1Provide Symbol(test1);export const test2Provide Symbol(test2);父组件
// 省略htmlscript setupimport { ref, provide, readonly } from vue;import { test1Provide, test2Provide } from /utils/keys.js;import MyButton from /components/MyButton.vue;provide(test1Provide, readonly(ref(a)))// 传入动态值const test2Val ref(1);provide(test2Provide, {test2: readonly(test2Val),handleTest2Change});// 传入事件function handleTest2Change() {test2Val.value;}
/script// 省略css子组件
// 省略html...script setup
import { inject } from vue
import { test1Provide, test2Provide } from /utils/keys.js;import MyButton2 from /components/MyButton2;const test1 inject(test1Provide)const { test2, handleTest2Change } inject(test2Provide);// 由于没有提供test3所以会使用默认值test3
const test3 inject(test3, 使用默认值test3);// 全局message 因为在main.js中使用app.provide()注入了message
const message inject(message);/script// 省略css...孙子组件
// 省略html...script setup
import { ref, inject } from vue
import { test1Provide, test2Provide } from /utils/keys.js;const test1 inject(test1Provide)const { test2, handleTest2Change } inject(test2Provide);// 由于没有提供test3所以会使用默认值test3
const test3 inject(test3, 使用默认值test3);// 全局message 因为在main.js中使用app.provide()注入了message
const message inject(message);/script// 省略css...效果 8、异步组件 在大型项目中我们可能需要拆分应用为更小的块并仅在需要时再从服务器加载相关组件Vue 提供了 defineAsyncComponent 方法来实现此功能 8.1、语法 defineAsyncComponent 方法接收一个返回 Promise 的加载函数。这个 Promise 的 resolve 回调方法应该在从服务器获得组件定义时调用。你也可以调用 reject(reason) 表明加载失败 import { defineAsyncComponent } from vueconst AsyncComp defineAsyncComponent(() {return new Promise((resolve, reject) {// ...从服务器获取组件resolve(/* 获取到的组件 */)})
})
// ... 像使用其他一般组件一样使用 AsyncCompES 模块动态导入也会返回一个 Promise所以多数情况下我们会将它和 defineAsyncComponent 搭配使用最后得到的 AsyncComp 是一个外层包装过的组件仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的 props 和插槽传给内部组件所以你可以使用这个异步的包装组件无缝地替换原始组件同时实现延迟加载 import { defineAsyncComponent } from vueconst AsyncComp defineAsyncComponent(() import(./components/MyComponent.vue)
)8.2、全/局部注册 与普通组件一样异步组件可以使用 app.component() 全局注册 app.component(MyComponent, defineAsyncComponent(() import(./components/MyComponent.vue)
))也可以直接在父组件中直接定义它们 script setup
import { defineAsyncComponent } from vueconst AdminPage defineAsyncComponent(() import(./components/AdminPageComponent.vue)
)
/scripttemplateAdminPage /
/template8.3、加载与错误状态 异步操作不可避免地会涉及到加载和错误状态因此 defineAsyncComponent() 也支持在高级选项中处理这些状态如果提供了一个加载组件它将在内部组件加载时先行显示。在加载组件显示之前有一个默认的 200ms 延迟——这是因为在网络状况较好时加载完成得很快加载组件和最终组件之间的替换太快可能产生闪烁反而影响用户感受。如果提供了一个报错组件则它会在加载器函数返回的 Promise 抛错时被渲染。你还可以指定一个超时时间在请求耗时超过指定时间时也会渲染报错组件。 const AsyncComp defineAsyncComponent({// 加载函数loader: () import(./Foo.vue),// 加载异步组件时使用的组件loadingComponent: LoadingComponent,// 展示加载组件前的延迟时间默认为 200msdelay: 200,// 加载失败后展示的组件errorComponent: ErrorComponent,// 如果提供了一个 timeout 时间限制并超时了// 也会显示这里配置的报错组件默认值是Infinitytimeout: 3000
})8.4、搭配 Suspense 使用 异步组件可以搭配内置的 Suspense 组件一起使用若想了解 Suspense 和异步组件之间交互请参阅 章节。 8.5、演示
Error和Loading组件还有子组件 父组件
templatediv classhomeViewh2父组件/h2MyButton/MyButton/div
/templatescript setupimport { ref, defineAsyncComponent } from vue;import Error from /components/Error;import Loading from /components/Loading;const MyButton defineAsyncComponent({loader: () {return new Promise((resolve, reject) {setTimeout(() {// reject(111) // 使用reject后也会展示Error组件resolve(import(/components/MyButton.vue))}, 2000) // 改成大于3秒后将会展示Error组件。})},// 加载异步组件时使用的组件loadingComponent: Loading,// 展示加载组件前的延迟时间默认为 200msdelay: 200,// 加载失败后展示的组件errorComponent: Error,// 如果提供了一个 timeout 时间限制并超时了// 也会显示这里配置的报错组件默认值是Infinitytimeout: 3000})/scriptstyle scoped langscss.homeView {border: 1px solid red;}
/style加载成功效果 加载失败效果 3、逻辑复用
1、组合式函数
1.1、什么是组合式函数 在 Vue 应用的概念中“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。当构建前端应用时我们常常需要复用公共任务的逻辑。 例如为了在不同地方格式化时间我们可能会抽取一个可复用的日期格式化函数。这个函数封装了无状态的逻辑它在接收一些输入后立刻返回所期望的输出。复用无状态逻辑的库有很多比如你可能已经用过的 lodash 或是 date-fns。 1.2、鼠标跟踪器示例 当你持续想获取当前鼠标的xy坐标时需要使用监听器监听鼠标移动事件并且赋值给两个响应式变量。然后等页面销毁后也得取消监听器监听。 组件代码
templatediv classhomeViewh2鼠标X坐标{{ mousePosition.x }}/h2h2鼠标Y坐标{{ mousePosition.y }}/h2/div
/templatescript setupimport { ref, onMounted, onUnmounted } from vue;// 监听鼠标xy轴坐标const mousePosition ref({ x: 0, y: 0 });function handleMouseMove(e) {mousePosition.value {x: e.pageX,y: e.pageY};}onMounted(() {window.addEventListener(mousemove, handleMouseMove);});onUnmounted(() {window.removeEventListener(mousemove, handleMouseMove);});/script一个页面监听一次就要写这么多代码如果多个页面都需要监听那么要写很多遍。这时候我们就要考虑吧要把这个组件封装一下以便复用了我们做的只是把它移到一个外部函数中去并返回需要暴露的状态。和在组件中一样你也可以在组合式函数中使用所有的组合式 API。现在useMouse() 的功能可以在任何组件中轻易复用了。你还可以嵌套多个组合式函数一个组合式函数可以调用一个或多个其他的组合式函数。这使得我们可以像使用多个组件组合成整个应用一样用多个较小且逻辑独立的单元来组合形成复杂的逻辑。 mouse.js
import { ref, onMounted, onUnmounted } from vue;export const useMouse () {// 监听鼠标xy轴坐标const mousePosition ref({ x: 0, y: 0 });function handleMouseMove(e) {mousePosition.value {x: e.pageX,y: e.pageY};}onMounted(() {window.addEventListener(mousemove, handleMouseMove);});onUnmounted(() {window.removeEventListener(mousemove, handleMouseMove);});return mousePosition;
}组件代码
templatediv classhomeViewh2鼠标X坐标{{ mousePosition.x }}/h2h2鼠标Y坐标{{ mousePosition.y }}/h2/div
/templatescript setupimport { ref, onMounted, onUnmounted } from vue;import { useMouse } from /hooks/mouse.js;// mousePosition是一个ref对象已是响应式const mousePosition useMouse();/script1.3、优化封装 我们不妨再想想对事件的监听和当组件销毁时取消监听。也是一步经常会用到的操作我们不妨再把这两步给封装以下。 event.js
import { onMounted, onUnmounted } from vue;export const useEvent (el, eventName, handler) {onMounted(() {el.addEventListener(eventName, handler);});onUnmounted(() {el.removeEventListener(eventName, handler);});
};优化后的mouse.js
import { ref, onMounted, onUnmounted } from vue;
import { useEvent } from ./event;export const useMouse () {// 监听鼠标xy轴坐标const mousePosition ref({ x: 0, y: 0 });function handleMouseMove(e) {mousePosition.value {x: e.pageX,y: e.pageY};}useEvent(window, mousemove, handleMouseMove);return mousePosition;
}1.4、异步状态示例 useMouse() 组合式函数没有接收任何参数因此让我们再来看一个需要接收一个参数的组合式函数示例。在做异步数据请求时我们常常需要处理不同的状态加载中、加载成功和加载失败。 script setup
import { ref } from vueconst data ref(null)
const error ref(null)fetch(...).then((res) res.json()).then((json) (data.value json)).catch((err) (error.value err))
/scripttemplatediv v-iferrorOops! Error encountered: {{ error.message }}/divdiv v-else-ifdataData loaded:pre{{ data }}/pre/divdiv v-elseLoading.../div
/template如果在每个需要获取数据的组件中都要重复这种模式那就太繁琐了。让我们把它抽取成一个组合式函数 // fetch.js
import { ref } from vueexport function useFetch(url) {const data ref(null)const error ref(null)fetch(url).then((res) res.json()).then((json) (data.value json)).catch((err) (error.value err))return { data, error }
}现在我们在组件里只需要 script setup
import { useFetch } from ./fetch.jsconst { data, error } useFetch(...)
/script1.5、接收响应式状态 useFetch() 接收一个静态 URL 字符串作为输入——因此它只会执行一次 fetch 并且就此结束。如果我们想要在 URL 改变时重新 fetch 呢 为了实现这一点我们需要将响应式状态传入组合式函数并让它基于传入的状态来创建执行操作的侦听器。 举例来说useFetch() 应该能够接收一个 ref 或者接收一个 getter 函数 const url ref(/initial-url)const { data, error } useFetch(url)// 这将会重新触发 fetch
url.value /new-url// 当 props.id 改变时重新 fetch
const { data, error } useFetch(() /posts/${props.id})我们可以用 watchEffect() 和 toValue() API 来重构我们现有的实现 // fetch.js
import { ref, watchEffect, toValue } from vueexport function useFetch(url) {const data ref(null)const error ref(null)const fetchData () {// reset state before fetching..data.value nullerror.value nullfetch(toValue(url)).then((res) res.json()).then((json) (data.value json)).catch((err) (error.value err))}watchEffect(() {fetchData()})return { data, error }
}这个版本的 useFetch() 现在能接收静态 URL 字符串、ref 和 getter使其更加灵活。watch effect 会立即运行并且会跟踪 toValue(url) 期间访问的任何依赖项。如果没有跟踪到依赖项例如 url 已经是字符串则 effect 只会运行一次否则它将在跟踪到的任何依赖项更改时重新运行。 1.6、toValue介绍 toValue() 是一个在 3.3 版本中新增的 API。它的设计目的是将 ref 或 getter 规范化为值。如果参数是 ref它会返回 ref 的值如果参数是函数它会调用函数并返回其返回值。否则它会原样返回参数。注意 toValue(url) 是在 watchEffect 回调函数的内部调用的。这确保了在 toValue() 规范化期间访问的任何响应式依赖项都会被侦听器跟踪。 1.7、约定和实践
1.7.1、命名 组合式函数约定用驼峰命名法命名并以“use”作为开头。 1.7.2、输入参数 即便不依赖于 ref 或 getter 的响应性组合式函数也可以接收它们作为参数。如果你正在编写一个可能被其他开发者使用的组合式函数最好处理一下输入参数是 ref 或 getter 而非原始值的情况。可以利用 toValue() 工具函数来实现 import { toValue } from vuefunction useFeature(maybeRefOrGetter) {// 如果 maybeRefOrGetter 是一个 ref 或 getter// 将返回它的规范化值。// 否则原样返回。const value toValue(maybeRefOrGetter)
}如果你的组合式函数在输入参数是 ref 或 getter 的情况下创建了响应式 effect为了让它能够被正确追踪请确保要么使用 watch() 显式地监视 ref 或 getter要么在 watchEffect() 中调用 toValue()。 1.7.3、使用限制 组合式函数只能在 script setup 或 setup() 钩子中被调用。在这些上下文中它们也只能被同步调用。在某些情况下你也可以在像 onMounted() 这样的生命周期钩子中调用它们。这些限制很重要因为这些是 Vue 用于确定当前活跃的组件实例的上下文。访问活跃的组件实例很有必要这样才能 将生命周期钩子注册到该组件实例上将计算属性和监听器注册到该组件实例上以便在该组件被卸载时停止监听避免内存泄漏。 1.8、与其他模式的比较
1.8.1、和 Mixin 的对比 Vue 2 的用户可能会对 mixins 选项比较熟悉。它也让我们能够把组件逻辑提取到可复用的单元里。然而 mixins 有三个主要的短板 不清晰的数据来源当使用了多个 mixin 时实例上的数据属性来自哪个 mixin 变得不清晰这使追溯实现和理解组件行为变得困难。这也是我们推荐在组合式函数中使用 ref 解构模式的理由让属性的来源在消费组件时一目了然。命名空间冲突多个来自不同作者的 mixin 可能会注册相同的属性名造成命名冲突。若使用组合式函数你可以通过在解构变量时对变量进行重命名来避免相同的键名。隐式的跨 mixin 交流多个 mixin 需要依赖共享的属性名来进行相互作用这使得它们隐性地耦合在一起。而一个组合式函数的返回值可以作为另一个组合式函数的参数被传入像普通函数那样。 基于上述理由我们不再推荐在 Vue 3 中继续使用 mixin。保留该功能只是为了项目迁移的需求和照顾熟悉它的用户。 1.8.2、和无渲染组件的对比 在组件插槽一章中我们讨论过了基于作用域插槽的无渲染组件。我们甚至用它实现了一样的鼠标追踪器示例。组合式函数相对于无渲染组件的主要优势是组合式函数不会产生额外的组件实例开销。当在整个应用中使用时由无渲染组件产生的额外组件实例会带来无法忽视的性能开销。我们推荐在纯逻辑复用时使用组合式函数在需要同时复用逻辑和视图布局时使用无渲染组件。 1.8.3、和 React Hooks 的对比 如果你有 React 的开发经验你可能注意到组合式函数和自定义 React hooks 非常相似。组合式 API 的一部分灵感正来自于 React hooksVue 的组合式函数也的确在逻辑组合能力上与 React hooks 相近。然而Vue 的组合式函数是基于 Vue 细粒度的响应性系统这和 React hooks 的执行模型有本质上的不同。这一话题在组合式 API 的常见问题中有更细致的讨论。 2、自定义指令 除了 Vue 内置的一系列指令 (比如 v-model 或 v-show) 之外Vue 还允许你注册自定义的指令 (Custom Directives)。我们已经介绍了两种在 Vue 中重用代码的方式组件和组合式函数。组件是主要的构建模块而组合式函数则侧重于有状态的逻辑。另一方面自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。 提示 只有当所需功能只能通过直接的 DOM 操作来实现时才应该使用自定义指令。其他情况下应该尽可能地使用 v-bind 这样的内置指令来声明式地使用模板这样更高效也对服务端渲染更友好。 2.0、指令钩子(重要) 一个指令的定义对象可以提供几种钩子函数 (都是可选的) const myDirective {// 在绑定元素的 attribute 前// 或事件监听器应用前调用created(el, binding, vnode, prevVnode) {// 下面会介绍各个参数的细节},// 在元素被插入到 DOM 前调用beforeMount(el, binding, vnode, prevVnode) {},// 在绑定元素的父组件// 及他自己的所有子节点都挂载完成后调用mounted(el, binding, vnode, prevVnode) {},// 绑定元素的父组件更新前调用beforeUpdate(el, binding, vnode, prevVnode) {},// 在绑定元素的父组件// 及他自己的所有子节点都更新后调用updated(el, binding, vnode, prevVnode) {},// 绑定元素的父组件卸载前调用beforeUnmount(el, binding, vnode, prevVnode) {},// 绑定元素的父组件卸载后调用unmounted(el, binding, vnode, prevVnode) {}
}2.1、钩子参数 指令的钩子会传递以下几种参数 el指令绑定到的元素。这可以用于直接操作 DOM。binding一个对象包含以下属性。 value传递给指令的值。例如在 v-my-directive1 1 中值是 2。oldValue之前的值仅在 beforeUpdate 和 updated 中可用。无论值是否更改它都可用。arg传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中参数是 foo。modifiers一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中修饰符对象是 { foo: true, bar: true }。instance使用该指令的组件实例。dir指令的定义对象。 vnode代表绑定元素的底层 VNode。prevNode代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。 提示 除了 el 外其他参数都是只读的不要更改它们。若你需要在不同的钩子间共享信息推荐通过元素的 dataset attribute 实现。 2.1.1、举例 举例来说像下面这样使用指令 div v-example:foo.barbazbinding 参数会是一个这样的对象 {arg: foo,modifiers: { bar: true },value: /* baz 的值 */,oldValue: /* 上一次更新时 baz 的值 */
}这里指令的参数会基于组件的 arg 数据属性响应式地更新。 和内置指令类似自定义指令的参数也可以是动态的。举例来说 div v-example:[arg]value/div2.2、基本使用 下面是一个自定义指令的例子当一个 input 元素被 Vue 插入到 DOM 中后它会被自动聚焦。假设你还未点击页面中的其他地方那么上面这个 input 元素应该会被自动聚焦。该指令比 autofocus attribute 更有用因为它不仅仅可以在页面加载完成后生效还可以在 Vue 动态插入元素后生效。在 script setup 中任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令。在下面的例子中vFocus 即可以在模板中以 v-focus 的形式使用。 templatediv classhomeViewinput v-focus //div
/templatescript setupimport { ref } from vue;// 在模板中启用 v-focusconst vFocus {mounted: (el) el.focus()}/script2.3、不使用setup语法糖 在没有使用 script setup 的情况下自定义指令需要通过 directives 选项注册 export default {setup() {/*...*/},directives: {// 在模板中启用 v-focusfocus: {/* ... */}}
}2.4、注册全局指令
const app createApp({})// 使 v-focus 在所有组件中都可用
app.directive(focus, {/* ... */
})2.5、简化形式 对于自定义指令来说一个很常见的情况是仅仅需要在 mounted 和 updated 上实现相同的行为除此之外并不需要其他钩子这种情况下我们可以直接用一个函数来定义指令如下所示 !-- 使用 --
div v-colorcolor/divscript!-- 全局注册 --app.directive(color, (el, binding) {// 这会在 mounted 和 updated 时都调用el.style.color binding.value})
/script2.6、对象字面量 如果你的指令需要多个值你可以向它传递一个 JavaScript 对象字面量。别忘了指令也可以接收任何合法的 JavaScript 表达式。 !-- 使用 --
div v-demo{ color: white, text: hello! }/divscript!-- 全局注册 --app.directive(demo, (el, binding) {console.log(binding.value.color) // whiteconsole.log(binding.value.text) // hello!})
/script2.7、在组件上使用 当在组件上使用自定义指令时它会始终应用于组件的根节点和透传 attributes 类似。 注意 组件可能含有多个根节点。当应用到一个多根组件时指令将会被忽略且抛出一个警告。和 attribute 不同指令不能通过 v-bind$attrs 来传递给一个不同的元素。总的来说不推荐在组件上使用自定义指令。 父组件
MyComponent v-demotest /子组件
!-- MyComponent 的模板 --div !-- v-demo 指令会被应用在此处 --spanMy component content/span
/div4、内置组件 vue3中总共有5个内置组件。 Transition针对单个dom元素的动画做处理TransitionGroup针对一组dom元素的动画做处理KeepAlive针对组件做缓存处理Teleport将dom移动到指定另一个dom里Suspense处理异步组件加载显示问题。 如果需要的话参考官网Transition | Vue.js (vuejs.org) 答疑篇 1、为什么建议使用ref
1.1、想修改整个对象时
代码
templatebutton clickeditUser1点击修改用户1/buttonbutton clickeditUser2点击修改用户2/buttonbutton clickeditUser3点击修改用户3/buttonh2姓名{{ user.name }}/h2h2年龄{{ user.age }}/h2 button clickuser.age年龄1/buttonh2地址{{ user.address.street }} {{ user.address.city }} {{ user.address.state }}/h2button clickuser.address.city 北京修改城市/button/templatescript setupimport { reactive } from vue;let user reactive({name: 张三,age: 25,address: {street: 河南省,city: 安阳市,state: 林州市,}});// 想要修改整改对象的值 这么修改会失去响应式function editUser1() {user {name: 李四,age: 20,address: {street: 江苏省,city: 南京市,state: 江宁区,}}console.log(editUser1, user);}// 这么修改响应式依旧会丢失function editUser2() {user reactive({name: 李四,age: 20,address: {street: 江苏省,city: 南京市,state: 江宁区,}})console.log(editUser2, user);}// 只有一个一个修改值才可以function editUser3() {user.name 李四;user.age 20;user.address.city 南京市;user.address.state 江宁区;user.address.street 江苏省;console.log(editUser3, user);}/script效果 1.2、想给对象置空时 同上只能一个值一个值设置空值将整个对象设置为空值无效不会触发页面更新 1.3、解构时 结构后的数据会丢失响应式只有再使用toRefs才可以继续将结构后的数据转化为响应式 代码
templateh2姓名{{ name }}/h2h2年龄{{ age }}/h2 button clickage年龄1/buttonh2地址{{ address.street }} {{ address.city }} {{ address.state }}/h2button clickaddress.city 北京修改城市/buttonbrbrh2姓名2{{ name2 }}/h2h2年龄2{{ age2 }}/h2 button clickage2;年龄1/buttonh2地址{{ address2.street }} {{ address2.city }} {{ address2.state }}/h2button clickaddress2.street 河南省;address2.city 郑州市;address2.state 金水区;修改城市/button/templatescript setupimport { reactive, toRefs, isRef } from vue;const user reactive({name: 张三,age: 25,address: {street: 河南省,city: 安阳市,state: 林州市,}});const { name, age, address } user;console.log(user1, name, age, address);const user2 reactive({name2: 李四,age2: 20,address2: {street: 江苏省,city: 南京市,state: 江宁区,}});const { name2, age2, address2 } toRefs(user2);console.log(user2, name2, age2, address2);
/script效果