Vue3 基础

110 阅读10分钟

一、皮毛

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-bindv-onv-forv-ifv-modelv-showv-slot
  • 侦听器:watchwatchEffectwatchPostEffect
  • 组件: 生命周期、ref、数据传递、事件传递、模板传递、内置组件
  • 逻辑复用:自定义指令directiveHook
  • 工程:工具链、路由、状态管理、测试、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,并配置.babelrcbabel.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默认为undefinedBoolean类型的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写法,defineEmit写法,非setup写法,参数接收,emit写法,defineEmit写法,非setup写法,参数接收,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()的函数运行