一、皮毛
2.组合式API写法中,定义state状态值的方式不同
// Vue
<script setup> // 使用setup声明script中的写法式组合式的写法
import { ref } from 'vue'; // 类似于React中的import {useState} from 'react';
const count = ref(0); // 类似于React中的 const [count, setCount] = useState(0);
const increment = () => {
count.vaule++; // 类似于React中 setCount(count++);
}
</script>
<template>
<button @click="increment">{{count}}</button>
</template>
// React
import {useState} from 'react';
const Demo = (props) =>{
const [count, setCount] = useState(0);
const increment = () => {
setCount(count++);
}
return <button onClick={increment}>{count}</button>
}
3.在Vue组合式API写法中,生命周期 运行内容式通过回调传递进去的:
// vue
<script>
import {ref, onMounted} from 'vue';
const count = ref(0);
onMounted(() => {
console.log(`mounted, ${count.value}`); // 这里值得注意的是在Vue的script中读取state变量count是使用的count.value,不是count,这一点是和React完全不同的一点(react在代码中直接使用count变量,而setCount)。
});
</script>
//React
// class组件中直接使用 componentDidMount(){},其中放置组件mounted 时的回调函数内容
// function组件搭配useEffect(() =>{}, []),给useEfect第二个参数传递空数组,模拟组件mounted的效果。第一个参数中传递的是需要在mounted是回调运行的函数内容。
前言
- 模板语法与响应式:
{{}}、ref、reactive、computed- 指令:
v-bind、v-on、v-for、v-if、v-model、v-show、v-slot- 侦听器:
watch、watchEffect、watchPostEffect- 组件: 生命周期、ref、数据传递、事件传递、模板传递、内置组件
- 逻辑复用:自定义指令
directive、Hook- 工程:工具链、路由、状态管理、测试、SSR
- 业务:生产部署、性能、安全、无障碍
- TS
可以把上面Vue的基础知识分为三个部分来梳理记忆:
- template相关(
写在<template>中的:模板语法{{}},指令,自定义指令; 渲染函数与jsx) - script相关(
写在<script>中的:响应式变量(ref,reactive,computed),生命周期函数,侦听器,hook) - 组件数据传递相关(
由上至下传递数据:props,attrs,provide - inject;由下至上传递数据:emit触发事件;上下均有(且能传递组件模版):slot) - 实际项目(
工程(vite,工具链,路由,状态管理,单测,ssr),业务(生产部署,性能,安全,无障碍),ts)
一、template相关
1、模板语法(template相关)
模板语法有两种个地方涉及到,<template>、jsx。传统vue文件书写模板是在<template>中,但是可以通过给vue工程加装插件,实现在vue工程中也能书写jsx。但是vue对书写在<template>中的模板具有更好的编译时优化。
<template>模板
// vue 采用双大括号的方式,并且在模板html标签属性中使用 ""的方式引入js变量或函数
<template>
<button @click="() => {increment(2)}">{{coount}}</button>
</template>
// React 模板中无论标签属性,还是标签内容,均使用{}引入js变量或函数
return <button onClick={ () =>{increment(2)} }>{count}</button>
// vue 中使用jsx语法书写方式需要添加jsx插件
// 如果没有添加不能书写jsx,可以使用v-html
// 但不推荐v-html,一是XSS漏洞,二是vue并不是一个基于字符串的模板引擎
<script setup>
const arr = [1,2,3];
// 报错,vue不能直接写jsx
// const raw = arr.map(item => <span style="color: red">{{item}}</span>)
const raw = arr.map(item => `<span style="color: red">${item}</span>`);
</script>
<template>
<p>this is : {{raw}} </p> // 会将raw直接渲染为string
<p v-html="raw"/> // 会将raw渲染为dom
</template>
渲染函数 && jsx
Vue中的jsx 和 React中的jsx是有区别的:
- vue的jsx props可以直接使用
class, for,而react的需要使用className, htmlfor- 向下传递的模板有区别(
vue通过slot传递)
在vue中书写jsx需要配置插件@vue/babel-plugin-jsx,并配置.babelrc或babel.config.js
module.exports = {
plugins: [
"@vue/babel-plugin-jsx"
]
}
在vue中让Typescript使用jsx的类型定义,需要在ts.config.json中配置:
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "vue"
}
}
或者在对应文件顶部添加/* @jsxImportSource vue */
在vite构建项目中,可以只引入@vitejs/plugin-vue-jsx,并在vite.cconfig.ts中注册vueJsx()插件即可。
(此时无需单独引入@vue/babel-plugin-jsx,并配置)
2、指令(template相关)
v-bind, v-on, v-if, v-show, v-for, v-model,v-slot
v-bind(缩写:)
:变量
// vue 中模板属性绑定js变量,需使用 v-bind,事件需使用 v-on
<template>
<button
:id="btnId"
@click="increment"
v-bind="attArr"
:title="totitle('dd')"
:[attvarible]="####" // 动态参数,变量统一用小写,attvariable只能是字符串且,且没有空格、引号等html非法attribute
>####</button>
</template>
<script setup>
import {ref} from 'vue';
const attvarible = ref('href');
const btnId = 'btn#321';
const attArr = {
href: 'http://#####',
class: 'wrapper'
}
const increment = () => {}
const toTitle = (data) => `title: ${data}`
</script>
// react 没有这么多花里胡哨的指令,组件模板的属性统一使用{}绑定,对于多属性,可以使用es解构写法
import {useState} from 'react';
const Demo = (props) => {
const btnId = 'btn#321';
const attArr = {
href: 'http://#####',
class: 'wrapper'
}
const increment = () => {}
const toTitle = (data) => `title: ${data}`;
return <button id={btnId} onClick={increment} title={totitle()} {...attArr}>###</button>
}
同名简写 :id(版本> vue3.4)
<template>
<button :id>点击</button>
</template>
<script setup>
const id = "dash";
</script>
调用函数 :function()
<template>
// 每一次组件更新都会给id绑定值
// 所以这里的getId()函数内部不要去做修改数据以及触发异步操作
//(即不要产生副作用,导致组件触发更新)
<button :id="getId()">点击</button>
</template>
:class :style
// vue绑定 class类名 或 style样式值 的方式与 React 差异不大,差异也基本是模板语法不同导致的。
// vue
// 字符串写法
<button :class="active 'text-danger'">{{count}}</button>
// 对象写法
<button :class="{active: isActive, 'text-danger': isDanger}">{{count}}</button>
// 数组写法
<button :class="[isActive ? 'active' : '', isDanger ? 'text-danger' : '']">{{count}}</button>
// 数组、对象可以互相嵌套,总之,:class后面的值会经过计算,成为最终值
// 静态 :class 和动态的 :class可以共存
<button class="btnWrap" :class="{active: isActive, text-danger: isDanger}">{{count}}</button>
vue绑定 内联样式(style)的方式与上面相同,差异的地方就是 class 是绑定类名,而style是绑定具体的css样式值。
向下传递样式(在组件内使用样式)
当创建了一个<MyComponent />的Vue组件,向<MyComponent /> 绑定的class, style值会向下传递:
// 如果 MyComponent 是单根元素组件
// <MyComponent /> 组件内部
<template>
<p :class="active text-danger">{{words}}</p>
</template>
// 使用<MyCompoent /> 时
<template>
<MyComponent :class="dingxu" /></MyComponent>
</template>
// 最终渲染
<p :class="active, text-danger dingxu">{{words}}</p>
// 如果 MyComponent 是多个根元素组件
// <MyComponent /> 组件内部
<template>
<p :class="active text-danger $attrs.class">{{words}}</p>
<button @click="increment">{{count}}</button>
</template>
// 使用<MyCompoent /> 时
<template>
<MyComponent :class="dingxu" /></MyComponent>
</template>
// 最终渲染 (通过$attrs.class透传)
<p :class="active text-danger dingxu">{{words}}</p>
<button @click="increment">{{count}}</button>
// vue 中事件修饰符 Modifier
<button @click.prevent="increment">{{count}}</button>
// react 中使用事件修饰符
<button onClick={ (e) =>{e.preventDefault(); increment();} }>{count}</button>
1. 挂载应用实例的方式不同
// vue 常规挂载根组件
import {createApp} from 'vue';
import App from './App';
const app = createApp(App);
app.mount('#app');
// vue 根模板
<div id="app">
<button>{{count}}</button>
</div>
import {creatApp} from 'vue';
const app = createApp({
data() {
return {
count: 0
}
}
});
app.mount('#app');
// vue 多应用实例挂载
const app1 = createApp(APP1);
const app2 = createApp(APP2);
const app3 = createApp(APP3);
app1.mount('#app1');
app2.mount('#app2');
app3.mount('#app3');
// vue 全局配置 与注册全局组件
app.config.handleError = (err) => {
// 错误处理
};
app.component('TodoTab', TodoTab);
//react
3.响应式的写法不同
// vue: ref()、reactive(),vue建议以 ref为主要使用方式
// ref 方式同react中的useState,前面已有论述,这里不赘述
// ref 写法还是有一些逻辑表现不一致的地方,需要注意一下
<script setup>
import {ref} from 'vue';
const count = ref(0);
const raw = { id: ref(0) };
const { id } = raw;
const increment = () => {
id.value++;
}
</script>
<template>
<p>{{count + 1}}</p> // 正确打印为 1
<p>{{id}}</p> // 正确打印为0 (为顶级属性,会解包)
<p>{{raw.id + 1}}</p> // 非正常打印,值为 [object object]1 (不为顶级属性,不解包)
<p>{{raw.id}}</p> // 正确打印为 0 (不为顶级属性,但为{{}}最终值,会解包)
<button @click="increment">{{raw.id}}--{{id}}</button> //变化同步
</template>
// reactive 设置响应式状态变量的另一种方式,但有一定的局限性——只能包含对象({}、Map、Set、数组)
// reactive({}) 中是对传入对象的一个代理,代理对象为响应式,而原对象并非是
// 注意reactive({}) 中包含ref()值,此时会有一些解包的细节问题
// 代理对象为响应式,原对象不是响应式
<script setup>
import {reactive} from 'vue';
const raw = {id: 0};
const state = reactive(raw);
const incrementRaw = () => {
raw.id++;
}
const incrementState = () => {
state.id++;
}
</script>
<template>
<p>{{raw === state}}</p> // false, 代理对象和原对象不相等
<button @click="incrementRaw">{{raw.id}} -- {{state.id}}</button> //值不变,原对象不是响应式
<button @click="incrementState">{{raw.id}} -- {{state.id}}</button> //值变,原对象和代理对象变化同步
</template>
// reactive({})中包含ref()值时,ref()解包的逻辑不一致处
<script setup>
import {ref, reactive} from 'vue';
const count = ref(0);
const raw = {count};
const arr = [ref(0)];
const state = reactive(raw);
const stateArr = reactive(arr);
const increment = () => {
// state.count.value++; //报错,因为直接 reactive 中对象中的ref()值直接解包了
state.count++; // reactive 中对象中的ref()值直接解包
}
const incrementArr = () => {
// state[0]++; // 报错,因为reactive中数组、Map中的ref()值不解包
stateArr[0].value++; // reactive中数组、Map中的ref()值不解包
}
</script>
// Vue响应式写法提炼 (吐槽一句,Vue中ref()各种解包操作,真的很乱,没有React优雅、逻辑统一)
基本逻辑:ref(),reactive()的基本使用方法不赘述,注意在<tempalte>中调用状态变量时不要使用++、--操作符。
在<script></script>中ref()解包规律:
1. 普通的ref(), 顶级的ref(), 读/改 使用 count.value形式(假设 count = ref(0) )。
2. 位于 普通对象{}, reactive([])中的ref()变量,读/改时 使用 .value 形式,同上。
3. 位于 reactive({})中的ref()变量, 读/改时 直接使用变量值,不用 .value,.value会报错 (这就是所谓的解包)
( 假设 state = reactive({count: ref(0)}), state.count + 1 )
在<template></template>中ref()、reactive()的解包规律:
1. 普通ref(),顶级ref(),reactive({})变量,读/改时 直接使用变量值,不用.value, .value会报错
2. 位于 普通对象{}, reactive([])中的ref()变量,读时 直接使用变量值、使用 .value 形式都行。
改时 使用 .value 形式。
4. 计算属性、样式绑定、条件渲染、列表渲染、监听事件、表单输入绑定、生命周期、侦听器、模板引用、组件基础
(1)计算属性 computed
computed(), 基于传入的getter()函数的依赖源进行响应式缓存。
// 默认传入的时getter()
<script setup>
import {ref, computed} from 'vue';
const author = ref({
name: "dingding",
books:['iron', 'spider', 'greenlighten']
});
const hasBooks = () => {
return author.value.books.length > 0 ? 'yes' : 'no';
}
const computedHasBooks = computed(() => {
return author.value.books.length > 0 ? 'yes' : 'no';
});
const increment = () => {
console.log(computedHasBooks.value);// 此时定义的computedHasBooks相当于是一个ref()变量
// 这又是一个我觉得vue写起来没有react舒服的地方之一
// computedHasBooks的初心是想要限定一个变量在其依赖发生变化时,才进行重运算,其他发生变化时导致的组件重渲染并不会导致该变量的重新计算
// computed()定义的变量相当于一个ref()变量
// computedHasBooks.value++是报错的,因为computedHasBooks.value是只读的
}
</script>
<template>
<buton @click="increment">{{computedHasBooks}}</buton>
</template>
hasBooks()和computedHasBooks()区别就在于:当组件因为其他原因发生重渲染时,author.value.books并没有发生变化,但hasBooks()还是会重新进行一次运算,而computedHasBooks()不会进行 重运算。
此时定义的computedHasBooks相当于一个ref()变量。
<script setup>
// 当然 你可以通过传入setter()来使computed()定义的变量可以改变
// 逻辑上 computed()定义变量的改变 应取决于它的依赖源的变化
import {computed} from 'vue';
const computedHasBooks = computed({
get() {
return author.value.books.length > 0 ? 1 : 0;
},
set(newVal) {
return newVal; // 这样写没有意义,因为computedHasBooks get的时候,是去计算author.value.books.length
}
});
</script>
// 关于computed()定义的变量,还有一些注意事项:
computed()中的getter函数中,应该返回一个纯粹的值,依赖于其他项变化而变化的结果值
computed()中getter函数 不要引起副作用(异步请求、修改dom操作)
computed()定义的变量不要直接修改,因为只读。可以传入setter函数来修改依赖源,从而达到修改getter的结果
<script setup>
import {ref, computed} from 'vue';
const firstName = ref('ding');
const lastName = ref('xu');
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(newVal) {
[firstName.value, lastName.value] = newVal.split(' ');
}
})
const increment = () => {
fullName.value = 'David Ding';
}
</script>
(3)条件渲染 v-if
v-if, v-else-if, v-else 指令
1、真实的组件的销毁与重建
2、惰性(如果其后绑定的标志位 初始值为false,则什么也不做)
3、v-else必须依附一个v-if,不然会被忽略掉
4、支持在<template v-if="type"></template>的写法,以控制整组模板的渲染与否
5、v-if 与 v-for同时使用在同一个元素上,v-if的优先级更高(不建议同时使用)
v-show
1、仅切换 display属性值
2、不支持在<template>上使用
(4)列表渲染 v-for
// v-for 的使用类似于 react中使用map函数遍历数组生成jsx元素
1. <button v-for="item in items">{{item.message}}</button>
item of items
(item, index) in items
(value, key) in object
2. <button v-for="item in items" :key="item.id">{{item.message}}</button>
最好bind key值,key值为基本类型值,且互不相同(这点与react相同)
3. <MyComponent v-for="item in items" :item="item" :key="item.id">{{item.message}}</MyComponent>
在组件上时用v-for不会传递item数据给组件,需要单独传递
4. <button v-for="item in items" :key="item.id" v-if="item.visible">{{item.message}}</button>
报错,v-if优先级高于v-for,v-if先运算,作用域内并没有item
5. <button v-for="{message} in items" :key="item.id" v-if="item.visible">{{message}}</button>
item支持解构写法
6. <button v-for="value in items">{{value}}</button>
此时items为对象 items = {a: '##', b: '###'},支持v-for写法
({a , b}, key) in items
7. <button v-for="n in 10">{{n}}</button>
v-for里面使用范围值,会生成1,2,3,4,5,6,7,8,9,10 <button> (注意:此处从0开始而非1开始)
(5)监听事件 v-on
Vue中事件的绑定方式类似于React,上面已经有描述。
// 直接绑定 js运算语句运行
<button @click="count++">{{count}}</button>
// 绑定函数
<button @click="increment">{{count}}</button>
// 传递 event事件对象
<button @click="increment(ag1, ag2, $event)">{{count}}</button>
<button @click="(event) => { increment(ag1,ag2, event) }">{{count}}</button>
// vue中支持相对快捷的 修饰符, 来对事件函数进行一定的限定
1. 事件修饰符
<button @click.prevent="increment">{{count}}</button>
.stop
.prevent
.self
.capture
.once
.passive
2. 按键修饰符
<button @keyup.enter="increment">{{count}}</button>
.enter
.tab
.delete (捕获“Delete”和“Backspace”两个按键)
.esc
.space
.up
.down
.left
.right
系统修饰符
<button @keyup.alt.enter="increment">{{count}}</button>
<button @keyup.ctrl="increment">{{count}}</button>
.ctrl
.alt
.shift
.meta
.exact修饰符
<button @click.ctrl.exact="increment">{{count}}</button>
3. 鼠标修饰符
<button @click.right="increment">{{count}}</button>
.left
.right
.middle
(6)表单输入绑定 v-model
前端框架,不论是vue也好还是react也好,都有其响应式 状态变量的写法,其实是将Javascript变量与 DOM元素关联起来,也就是所谓的 数据驱动UI。那么如何将 DOM变量同Javascript变量绑定起来,实现 UI驱动数据改变 呢?
这里可以把DOM变量简单的理解为 用户通过网页的实时输入——各类表单项输入值。
// 简单的绑定方式就是通过event监听获取表单项的value变化,然后将value值传递给 JS变量
const userInp = ref('');
<input type="string" @change="(e) => {useInp = e.target.value}" />
// vue 对于这一过程提供了一个简单的v-model指令,来快捷的完成这一过程
const useInp = ref('');
<input type="string" v-model="useInp" />
// 各类表单项
// input string 绑定value字符串
<input v-model="message" />
// input textarea 绑定value字符串
<p style="white-space: pre-line;">{{ message }}</p>
<textarea v-model="message" />
// input checkbox 绑定 value 布尔值
<input type="checkbox" id="checkbox" v-model="checked" /> // true / false
<label for="checkbox">{{ checked }}</label>
//true-value 和 false-value 是 Vue 特有的 attributes,仅支持和 v-model 配套使用。
<input type="checkbox" v-model="toggle" :true-value="trueVal" :false-value="falseVal" />
// input radio 绑定 value值
<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>
<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>
// select 绑定value值
<select v-model="selected"> // A / B / C
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<select v-model="selected">
<option v-for="option in options" :key="option.value" :value="option.value">
{{ option.text }}
</option>
</select>
// select multiple 绑定多选数组
<select v-model="selected" multiple> // ['A'] / ['A', 'B', 'C']
<option>A</option>
<option>B</option>
<option>C</option>
</select>
// 修饰符
// .lazy
默认情况下,v-model 会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy 修饰符来改为在每次 change 事件后更新数据:
<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />
// .number
如果你想让用户输入自动转换为数字,你可以在 v-model 后添加 .number 修饰符来管理输入:
<input v-model.number="age" />
如果该值无法被 parseFloat() 处理,那么将返回原始值。
number 修饰符会在输入框有 type="number" 时自动启用。
// .trim
如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model 后添加 .trim 修饰符:
<input v-model.trim="msg" />
上面描述了在 表单 中通过 v-model 实现 UI驱动数据变化 的方式。那么在自定义组件中呢?
(7)生命周期beforeCreate,created,beforeMount,mounted,beforeUpdate,updated,beforeUnmount,unmounted
beforeCreate
created
beforeMount
mounted
beforeUpdate
updated
beforeUnmount
unmounted
// onMounted() 不应该写在 setTimeout()里面去
<script setup>
import {onMounted} from 'vue';
setTimeout(() => {
onMounted(() => {}); // 异步注册时当前组件实例已丢失。写法无效,onMounted()中的函数不会运行
}, 1000)
</script>
(8)侦听器 watch, watchEffect, watchPostEffect
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.indexOf('?') > -1) {
answer.value = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error
}
}
})
// vue 中的侦听器 和 react中的 effect钩子十分类似,但也有一些使用上的区别
// watch 监听变量的变化,然后决定副作用函数是否运行 <===> useEffect(() =>{}, []), []中监听事件变化
// 不同之处:
// watch监听的变量,可以是基本类型变量,可以是数组,可以是对象 <===> react useEffect中统一写在数组 []
// 对于 watch(obj.count, () =>{}) 的写法是不允许的 <===> useEffect(()=>{}, [obj.count])是允许的
// const obj = ref({id: 0}), watch(obj, ()=>{})是监听不到obj的变化,
// 而是watch(obj.value, ()=>{}), 或者 watch(obj, () => {}, {deep: true})强制深层次监听obj对象
// watch(obj.value.id, ()=>{}), 不允许,同上
// watch(obj, ()=>{}, {immediate: true}); 会在在创建侦听器时,立即执行一遍回调,这样写就进一步像effect钩子了
// watch(obj, ()=>{}, {immediate: true})的写法等同于 watchEffect(() ={})
// watchEffect() 会自动追踪其内所依赖的变量(类似于computed()),然后运行副作用。
// watchEffect()做不到watch()能够精确的追踪依赖变量变化(个人更喜欢watch, watchEffect()不就是奇怪版的useEffect()嘛)
// 当状态变量同时触发侦听器回调、Vue组件更新时,谁先运行?
默认:先运行侦听回调,再运行Vue组件更新。
故:再侦听器中访问到的DOM时Vue更新之前的状态。
// watch(obj, ()=>{}, {flush: 'post'}); 强制watch()内的副作用回调函数在组件更新完成后执行。
// watch(obj, ()=>{}, {flush: 'post'})的写法等同于 watchPostEffect()
// 这一点与React有点不同,useEffEct()原本是组件UI被浏览器渲染之后再执行,Vue中需要添加{flush: 'post'}
// 侦听器的停止:在vue中同步创建的侦听器会自动停止。
// 如果在异步函数中创建侦听器,则不会自动停止: setTimeout(() => { watch() }, 100); (尽量不要这样做)
// 手动停止:const unwatch = watch(obj, () =>{}); unwatch(); // 执行unwatch()停止侦听器
// useEffect(() => {}, []),再回调函数里面return 需要销毁的内容即可
<script setup>
import {ref, reactive, watch, watchEffect, watchPostEffect} from 'vue';
</script>
(9)模板引用 ref
模板引用ref就是在 父组件中通过ref获取子组件的全部属性、方法的方式。
<script setup>
import {ref, onMounted} from 'vue';
const myBtn = ref(null);
onMounted(() => {
myBtn.value.showMessage();
})
</script>
<template>
<MyButton ref="myBtn" />
</template>
// MyButton
<script setup>
const showMessage = () => { alert('hhhhh, ref')};
defineExpose({
showMessage,
})
</script>
<template>
<button>ref</button>
</template>
// 注意,这里的如果MyButton是setup的,那么父组件是访问不到MyButton内部的任何数据,这时需要通过在 MyButton内设置defaineExport()来导出外部可以访问的内部数据
// 如果MyButton仅仅是选项式的组件,那么不用defineExpose()
// 但通过ref取子组件的属性、方法都是不推荐的,会让代码数据流混乱。父子之间数据流的传递应该规范的通过props和emit来传递。
模板引用ref还可以结合v-bind动态的绑定函数,函数第一个参数为ref所引用的元素:
<script setup>
</script>
<template>
<button :ref="(el) => {/*可以直接对el dom元素进行一系列操作*/}"
</template>
模板引用ref还可以在v-for中使用,此时ref中保存的就是 元素数组。
<script setup>
import {ref} from 'vue';
const tempRef = ref([]);
const items = [1,2,3];
onMounted(() => {
console.log(tempRef.value.map(ite => console.log(ite)));
})
</script>
<template>
<ul>
<li v-for="item in items" ref="tempRef"}>{{item}}</li>
</ul>
</template>
(10)组件基础
属性传递、事件触发、模板传递、动态组件
// props属性传递
// 子中:defineProps(['***'])、 const { **** } = useAttrs();
// 传递的是js变量,而不能传递jsx
<script>
import { useAttrs } from "vue";
const prop = defineProps(["title"]);
const props = useAttrs();
</script>
// 函数传递
// 1. 做为普通的JS Function,通过属性传递函数,在子中调用函数,改变父中变量
// 2. 通过emit触发自定义事件
//父中 <MyBtn @enlarge-text = " **** " />
//子中,在合适的时机触发enlarge-text事件,
// 直接使用$emit()触发: $emit('enlarge-text')
// 使用defineEmits(['***'])定义emit函数,然后在合适的地方触发:
const emit = defineEmit('enlarge-text');
<button @click="emit('***')" />
// 模板传递
// 到这里,还没有接触到如何在Vue中传递jsx,现在想要在父子之间传递模板,需要通过slot的方式
// 子中:
<template>
<div>
<slot name="top" />
<slot name="bottom" />
</div>
</template>
// 父中
<template>
<div>
<p>**********</p>
<template #top>
<MyButton />
</template>
<template #bottom>
<MyList />
</template>
</div>
</template>
// 动态组件 :is <Keep-Alive></Keep-Alive>
三、深入组件
1、注册
// 全局注册
import { createApp } from "vue";
import ComponentA from "...";
import ComponentB from "...";
const app = createApp();
app
.component("ComponentA", ComponentA)
.component("ComponentB", ComponentB);
全局注册问题:全局注册的组件不能被 摇树算法 自动去掉没有使用的组件;全局组件依赖不明确。
2、Props
定义、小驼峰、单向传递、校验、布尔转换
// props定义
<script setup>
const props = defineProps(['title']);
const con = () => { console.log(props.title); }
</script>
<script>
export default {
props: ['title'],
setup(props) { console.log(props.title); } // setup() 接收 props 作为第一个参数
}
</script>
<script setup>
const props = defineProps({
title: String,
age: Number,
})
</script>
<script>
export default {
props: {
title: String,
age: Number
}
}
</script>
// 小驼峰
小驼峰命名, html上 小写加中划线
// 单向数据流
父组件的Props变化会传递到子组件相应值响应,同React
不要在子组件修改props属性值。报错 props.title = ding; ×
属性值为对象,可以修改对象内属性值,但不推荐,可能造成性能影响。props.title.content = "ding"; ×
// 校验
<script setup>
const props = defineProps({
title: null; // 传入null、undefined则不做任何校验
title2: Number,
title3: {
type: [Number, String],
required: true, // 必传
default: 12, // 默认值,也可用函数default(prop),函数的返回值就是默认值
validator(prop) {
return [1, 12, 15].includes(rowProp)
}
}
});
</script>
1. props默认为undefined,Boolean类型的prop默认值为false
2. 如果传入的props解析为undefined(不管是传了还是没传),均为default设置的值
// 布尔转换
被设置为Boolean类型的属性,不传递默认为false,传递了为true
//myBtn定义 中
<script setup>
const props = definedProps({
disable: Boolean
})
</script>
// 使用myBtn
<myBtn disable /> // disable解析为true
<myBtn /> // disable解析为false
然后同时出现允许多种属性时,会有怪异之处(String位于前面 [String, Boolean])
<script setup>
const props = defineProps({
disable: [Boolean, Number], // 解析为true
disable2: [Number, Boolean], // 解析为true
disable3: [Boolean, String], // 解析为true
disable4: [String, Boolean], // 解析为空字符串 disable = ""
});
</script>
3、透传Attributes
组件数据向下传递二——Attributes。
定义、透传、非响应
我简单的将组件间数据传递分为向下 与 向上
向下为: props、attrs、slot、provide
向上为: emit
// attrs定义:
从父组件传递过来的(非props、非emit)属性
// attrs透传:
单根节点组件:默认透传到根节点
多根节点组件:不进行默认透传,如果不用$attrs进行承接,则vue会抛出warning
// 禁止attrs默认透传到根节点方式
(例如将属性透传给内部元素,而非根节点,见下例)
<template>
<div class="btnWrap">
<button v-bind="$attrs">按钮</button>
</div>
</template>
<script setup>
defineOptions({
inheritAttrs: false // 禁止attrs默认透传到根节点
})
</script>
// attrs承接
<tempate></tempate> 中使用 $attrs ($attrs['pre-commit']、$attrs.onClick)
<script setup></script> 中使用 const attrs = useAttrs(); ( 需import { useAttrs } from 'vue'; )
<script></script> 中使用 setup(props, ctx) {ctx.attrs}
// attrs承接的属性为非响应式,不能通过watch监听到属性变化(通过useAttr()承接下来的attrs)
通过$attrs.***的属性是响应式的
4、slot
默认插槽、具名插槽、动态插槽、插槽作用域、作用域插槽
// 默认插槽
// 父
<MyBtn>
<p>#####</p> // 渲染到MyBtn的默认插槽上
</MyBtn>
<MyBtn>
<template #default></template> // 渲染到MyBtn的默认插槽上
</MyBtn>
// 子组件内 <MyBtn></MyBtn>
<template>
<slot></slot>
</template>
<template>
<slot name="default"></slot>
</template>
// 具名插槽
// 父
<MyBtn>
<template #header></template>
</MyBtn>
// 子
<template>
<slot name="header"></slot>
</template>
// 动态插槽
<MyBtn>
<template #[changableSlot]></template>
</MyBtn>
// 插槽作用域
插槽传入的组件 能够访问父组件中的数据,但不能访问子组件中的数据
// 作用域插槽
// 当插槽既需要父组件的数据, 也需要子组件的数据时,通过 #header="childProps"的方式获得子组件的数据
// 父
<MyBtn>
<template #header="childProps"> // childProps可采用结构写法, {title, age}
{{ childProps.title }} - {{ childProps.age }}
</template>
</MyBtn>
//子
<template>
<slot name="header" title="hhahahh" age="23"></slot>
</template>
// 默认插槽 可采用v-bind的方式直接绑定数据
// 父
<MyBtn>
<p v-bind="childProps">hahahahha</p>
</MyBtn>
单出现多个具名插槽时,要指定#default,不然会编译报错
<MyBtn>
<p #default="defaultProps">hahahahah</p>
<template #header="headerProps"></template>
</MyBtn>
5、注入依赖(provide、inject)
props,attrs是父子组件间传递数据的方式,但是如果某个参数,需要从一个组件传递到很深层次的组件内部怎么办(从第一代传递到第20代组件)?如果通过props逐层传递,那么这将是噩梦的开始。
这个时候就需要 注入依赖,在上层组件 provide 数据,在下层组件 inject数据。
(”组件内大喇叭,全局大喇叭“)
provide普通变量、ref变量、readonly()变量,全局provide,inject注入,inject注入默认值
// 提供, provide数据
// 组件内 (在后代组件中都可以通过inject来使用)
<script setup>
import { provide, ref } from 'vue';
const count = ref(0);
provide('message', 'hello'); // 名称为message,值为hello
provide('count', count); // 名称为count,值为响应式ref值count(不是count.value)。注入进去的会是该 ref 对象,而不会自动解包为其内部的值
provide('haha', readonly(count)); // 名称为count,值为只读, 不能在inject方改变provide过来的依赖值 (provide的数据改变中最好不要在inject的地方发生更改,可以使用readeonly()来强制在这一点。如果一定要在下层更改数据可以在provide数据的同时,也provide数据更改的function)
</script>
// 全局
<script setup>
import { createApp } from 'vue'
const app = createApp();
app.provide('count', count)
</script>
// 注入依赖
<script setup>
import { inject } from 'vue'
const count = inject('count') // 直接注入上层组件、全局 的provide变量使用
</script>
// 注入的都是上层或全局 provide 的变量,如果没有provide变量,则报错
// 为了避免报错,可以给inject的变量定义默认值
<script setup>
import { inject } from 'vue'
const message = inject('message', 'hello')
</script>
// 可以使用Symbol作为provide数据的变量名
6、事件
emit('click')与原生事件同名情况
自下向上传递改变
// 子组件
<template>
<button @click="$emit('btn-click', ag1, ag2, ag3)">点击</button>
</template>
// 父组件
<MyButton @btn-click="format" />
<MyButton @btn-click.once="format" />
// 在script中定义emit事件的方法
<script setup>
const emit = defineEmit(['btn-click', 'self-click']);
emit("btn-click");
</script>
<script>
export default {
emit(['btn-click', 'self-click']), // 定义
setup(props, ctx) {
const emit = ctx.emit;
emit('btn-click'); // 触发
}
}
</script>
// emit校验
<script setup>
const emit = defineEmit({
submit({ userName, pwd }){
if(userName && pwd) {
return true; // return true表示自定义事件有意义
} else {
return false; // return false 表示自定义事件没有意义
}
}
})
</script>
// 自定义事件 如果emit事件名与原生事件重名,则在上层组件监听到 自定义事件,不会监听原生事件
// 子
<template>
<button @click="$emit('click', ag1, ag2)"
</template>
//父
<template>
<MyButton @click="btnClick" />
</template>
7、异步组件
definedAsyncComponent()
<template>
<asyncComp />
</template>
<script setup>
import { defineAsyncComponent } from 'vue';
const asyncComp = defineAsyncComponent(() => {
import "../components/MyComponent.vue";
})
</script>
<template>
<asyncComp />
</template>
<script setup>
import { defineAsyncComponent } from 'vue';
const asyncComp = defineAsyncComponent({
loader: () => import '../components/MyComponent.vue',
loadingComponent: loadingComp.
errorComponent: errorComp,
delay: 200,
timeout: 3000
})
</script>
搭配 <Suspense /> 使用
四、逻辑复用
1、组合式函数(钩子函数 Hook)
一段JS代码,能够使用vue、pinia等库的 API封装常用逻辑,并返回相应的可复用方法、状态数据。
类似于lodash、date-fns就是无状态复用逻辑,Vue / React中的Hook思想其是就是有状态的复用逻辑。
传入ref、getter参数时,Hook内部处理细节
- 传入了
ref、getter类型的参数,并且在Hook中监听参数变化,运行逻辑函数时,需要在watch函数中正确的监听ref、getter变量,或者使用toValue()函数,来保证外层传递的ref、getter变量同Hook内部的变化同步。 return {}。返回的值一般是普通对象包裹的ref变量,以保持Hook提供出去的值能够和外层使用的值响应同步。
四、实际项目
1、工程
1.1、路由
从前端的视角来看路由:浏览器的地址发生变化,浏览器展示不同的内容。
用户点击网页链接地址,浏览器根据这个链接地址去到对应的服务器拉取该文件路径下的html文件并渲染,这个html文件内部有引用其他css文件、js文件处理一些样式以及交互的东西,来丰富这个html。
对于古早的前端路由方式,就是用户点击不同的网页地址,链接到的不同的html文件,用户想要看到不同的页面,背后是浏览器在拉取不同的html静态资源文件来渲染(这是对服务器渲染的简陋认知)。反观之,正确的浏览器地址栏变化,会对应上某台服务器的文件路径上的一个html文件。这种方式有好有坏。当浏览器网页追求应用级别的更加友好的交互体验时,这种方式是达不到要求的,因为每切换一次以访问新的页面,就需要浏览器向服务器发起新的html静态资源的请求,不说网络状态差的情况,单是这么一个拉取文件的过程,就相对耗时,就是一个相对较长的等待过程,达不到即刷即用的效果。于是浏览器在HTML5规则中推出了 当在URL地址后添加#(hash符),浏览器不会在#之后的地址发生变化时向服务器请求静态资源。搭配HTML5规则中推出的几个history API,用于可以在Javascript中监听到URL中#之后值的变化(当然还有其他细节。。)。这样网站开发者就可以在某个html文件中通过新推的几个API监听拦截到用户的url变化,使用javascript在当前的网页生成不同的html元素进行渲染,完成用户切换不同"页面"的需求,并且在浏览器的地址栏出现不同的地址变化,并且浏览器没有去请求新的html静态元素,提高交互反应速度。这就是单页面应用(SPA)、客户端路由的基础概念。
对于不同的前端框架(vue、react),他们都基于这个原理提供了相应的路由库——vue-router, react-router。
vue-router
客户端路由,可以把它理解为"假的路由",它是一套浏览器内部的规则,现代前端框架基于这个规则,提供了对应的库,使用这个库的api能力,就可以更加便捷的去编写SPA应用。
路由器(vue)
路由器实例由createRouter生成,并作为插件注册进入vue工程的根vue对象中,使你注册到该路由器的url地址生效。
// router.js
import { createWebHashHistory, createRouter } from 'vue-router';
import Login from '@/views/Login.vue';
import Home from '@/views/Home.vue';
const routes = [
{path: '/login', component: Login },
{path: '/home', component: Home }
];
const router = createRouter({
history: createWebHashHistory(),
routes
});
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './rouer.js';
const app = createApp(App);
app.use(router).mount('#app');
然后在vue工程中的对应template中内置<router-view>,即可在浏览器地址栏推送不同的注册在路由器router中的URL时,在<router-view>划定的区域,渲染对应的component字段下的组件。
// app.use(router)做了什么事情?
1. 全局注册router-link、router-view组件(这样在使用的时候就不用显式import引用)
2. 启用$router, $route(这样就可以在vue选项式组件中通过this.$router访问路由器实例,this.$route访问当前路由对象)
3. 启用useRouter, useRoute(这样就可以在vue组合式组件中通过router = useRouter()访问路由器实例,通过route = useRoute()访问当前路由对象)
4. 推送初始路由(触发路由器解析初始路由)。
// 基础
1.<router-view>同router的联系?
a. 在实际项目中,位于根元素<App>中的<router-view>会渲染注册到router中的一级路由;
而二级路由(一级路由的children字段中的路由信息)的component将会渲染到一级路由<router-view>组件内部再次书写<router-view>的位置。
b. 在实际项目中,二级路由书写带 / ,则表示当前路由将被视为根路径,即URL不需要带上父级路径片段;
如果不带 / ,则URL需要带上父级路径片段。
3. router.push({
name: "home", // "/:pathMatch(.*)*"
params: {id: "22", age: "23"},
query: route.query,
hash: route.hash
})
4. 路由组件传参,可以通过$route.params等方式向组件传递参数,
也可以:当 `props` 设置为 `true` 时,`route.params` 将被设置为组件的 props。
const routes = [ { path: '/user/:id', component: User, props: true } ]
在User组件中可以通过props直接访问路由传递过来的id参数
5. 不同的历史记录模式:Hash模式、HTML5模式(就是history模式,也可以应用在SPA中,但是需要服务端nginx的配置:在服务器上添加一个简单的回退路由。如果 URL 不匹配任何静态资源,它应提供与你的应用程序中的 `index.html` 相同的页面。)、Memory模式(一般不推荐)。
路由进阶(vue)
导航守卫顺序依次为:
导航触发 --> beforeRouteLeave(在失活的组件中), beforeEach, beforeRouteUpdate(在重用组件中), beforeEnter --> 解析异步路由组件 --> beforeRouteEnter, beforeResolve --> 导航确认 --> afterEach --> 渲染DOM --> beforeRouteEnter中传入next()的函数运行