Vue3 学习笔记(一)模板语法、响应式基础

安装

npm init vue@latest
这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。
前提:node版本需要在15以上,选择Yes和no时候 按空格切换选择

创建实例

<div id="app">
  <button @click="count++">{{ count }}</button>
</div>

import { createApp } from 'vue'

const app = createApp({
  data() {
    return {
      count: 0
    }
  }
})

app.mount('#app')

模板语法

原始html ,使用v-html指令(请仅在内容安全可信时再使用 v-html,并且永远不要使用用户提供的 HTML 内容,避免出现xss漏洞)
布尔类型:<button :disabled="isButtonDisabled">Button</button>
当 isButtonDisabled 为真值或一个空字符串 (即 <button disabled="">) 时,元素会包含这个 disabled attribute。而当其为其他假值时 attribute 将被忽略。(这里经过尝试 设置为null或者undefined 则disabed为false,不会显示)

缩写(常用)

<!-- 完整语法 -->
<a v-on:click="doSomething"> ... </a>
<!-- 缩写 -->
<a @click="doSomething"> ... </a>
<!-- 完整语法 -->
<a v-bind:href="url"> ... </a>
<!-- 缩写 -->
<a :href="url"> ... </a>

调用函数
可以在绑定的表达式中使用一个组件暴露的方法

<span :title="toTitleDate(date)">
  {{ formatDate(date) }}
</span>

注意:绑定在表达式中的方法在组件每次更新时都会被重新调用,因此不应该产生任何副作用,比如改变数据或触发异步操作。

JavaScript 表达式
模板表达式都被放在沙盒中,只能访问一个受限的全局变量列表,如 Math 和 Date。你不应该在模板表达式中试图访问用户定义的全局变量。
其中受限的全局变量列表如下:
const GLOBALS_WHITE_LISTED = 'Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,' + 'decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,' + 'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt'

没有显式包含在列表中的全局对象将不能在模板内表达式中访问,例如用户附加在 window 上的属性。然而,你也可以自行在 app.config.globalProperties 上显式地添加它们,供所有的 Vue 表达式使用。
如:app.config.globalProperties.msg = 'hello'

动态参数 (New)
<a v-on:[eventName]="doSomething"> ... </a>
在这个示例中,当 eventName 的值为 "focus" 时,v-on:[eventName] 将等价于 v-on:focus
动态参数表达式约定:要避免使用大写字符来命名键名,因为浏览器会把 attribute 名全部强制转为小写。
注意事项:
动态参数中表达式的值应当是一个字符串,或者是 null。特殊值 null 意为显式移除该绑定。其他非字符串的值会触发警告。
如果你需要传入一个复杂的动态参数,我们推荐使用计算属性替换复杂的表达式,切记不用字符串拼接形式

修饰符
修饰符是以点开头的特殊后缀,表明指令需要以一些特殊的方式被绑定。例如 .prevent 修饰符会告知 v-on 指令对触发的事件调用 event.preventDefault():
<form @submit.prevent="onSubmit">...</form>

响应式基础

响应式对象其实是 JavaScript Proxy,其行为表现与一般对象相似。不同之处在于 Vue 能够跟踪对响应式对象属性的访问与更改操作。

import { reactive } from 'vue'

const obj = reactive({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

function mutateDeeply() {
  // 以下都会按照期望工作
  obj.nested.count++
  obj.arr.push('baz')
}

响应式 reactive()
reactive() 返回的是一个原始对象的 Proxy,它和原始对象是不相等
对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身 (代理对象本身)
reactive对象中属性值为引用类型的,当访问该属性时,得到也是一个Proxy代理对象(深层次响应性)
限制:仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效
当我们将响应式对象的属性赋值或解构至本地变量时,或是将该属性传入一个函数时,我们会失去响应性
如:

const state = reactive({
  count: 0,
  person: {
    name: 'zhang san',
    age: 20
  }
})

// n 是一个局部变量,同 state.count 失去响应性连接
let n = state.count
// 不影响原始的 state
n++

// count 也和 state.count 失去了响应性连接
//person 指向 state中person属性的代理对象
let { count, person } = state
// 不会影响原始的 state
count++

//person 为引用 所以会改变state中person对象的age值
person.age = 100

//{"count":0,"person":{"name":"zhang san","age":100}}
console.log("now state==>",JSON.stringify(state))
//如果直接赋值给person新的值, 则引用丢失,但并不会影响state

用 ref() 定义响应式变量
ref() 方法来允许我们创建可以使用任何值类型的响应式 ref
ref() 将传入参数的值包装为一个带 .value 属性的 ref 对象
ref 的 .value 属性也是响应式的。同时,当值为对象类型时,会用 reactive() 自动转换它的 .value。

ref 被传递给函数或是从一般对象上被解构时,不会丢失响应性(经常用于将逻辑提取到组合函数中)

const obj = {
  foo: ref(1),
  bar: ref(2)
}

// 该函数接收一个 ref
// 需要通过 .value 取值
// 但它会保持响应性
callSomeFunction(obj.foo)

// 仍然是响应式的
const { foo, bar } = obj

ref 在模板中的解包
当 ref 在模板中作为顶层属性被访问时,它们会被自动解包,所以不需要使用 .value(适用于顶层属性)

<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">
    {{ count }} <!-- 无需 .value -->
  </button>
</template>

ref在响应式对象中的解包
当一个 ref 被嵌套在一个响应式对象中,作为属性被访问或更改时,它会自动解包

const count = ref(0)
const state = reactive({
  count
})

console.log(state.count) // 0

state.count = 1
console.log(count.value) // 1

数组和集合类型的 ref 解包
当 ref 作为响应式数组或像 Map 这种原生集合类型的元素被访问时,不会进行解包。

const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)

最佳实践
使用 Vue 的响应式系统的最佳实践是仅使用你声明对象的代理版本

响应式函数API工具

isRef():检查某个值是否为 ref
unref():如果参数是 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 计算的一个语法糖。
toRef():基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。(结构解析响应不会丢失)

const state = reactive({
  foo: 1,
  bar: 2
})
const fooRef = toRef(state, 'foo')
//更改该 ref 会更新源属性
fooRef.value++
console.log(state.foo) // 2  ref会自动解包

toRefs():将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

const stateAsRefs = toRefs(state)
// 这个 ref 和源属性已经“链接上了”
state.foo++
console.log(stateAsRefs.foo.value) // 2

isProxy():检查一个对象是否是由 reactive()、readonly()、shallowReactive() 或 shallowReadonly() 创建的代理。
isReactive():检查一个对象是否是由 reactive() 或 shallowReactive() 创建的代理。

响应性语法糖(实验性功能
$ref() 方法是一个编译时的宏命令

<script setup>
let count = $ref(0)

console.log(count)

function increment() {
  count++
}
</script>

<template>
  <button @click="increment">{{ count }}</button>
</template>

代码会编译成

import { ref } from 'vue'

let count = ref(0)

console.log(count.value)

function increment() {
  count.value++
}

每一个会返回 ref 的响应式 API 都有一个相对应的、以 $ 为前缀的宏函数。包括以下这些 API:

ref -> $ref
computed -> $computed
shallowRef -> $shallowRef
customRef -> $customRef
toRef -> $toRef

响应性语法糖目前默认是关闭状态,需要你显式选择启用。此外,以下列出的所有配置都需要 vue@^3.2.25。

vite脚手架启用

// vite.config.js
export default {
  plugins: [
    vue({
      reactivityTransform: true
    })
  ]
}

或组件中显式引用

import { $ref } from 'vue/macros'
let count = $ref(0)

DOM 更新时机
Vue 将缓冲它们直到更新周期的 “下个时机” 以确保无论你进行了多少次声明更改,每个组件都只需要更新一次。
若要等待一个状态改变后的 DOM 更新完成,你可以使用 nextTick() 这个全局 API

computed 计算属性
其返回值为一个计算属性 ref
如:

<script setup>
import { ref, computed } from 'vue'

const numbers = ref([1, 2, 3, 4, 5])

//常用 获取数组中的偶数
const evenNumbers = computed(() => {
  return numbers.value.filter((n) => n % 2 === 0)
})

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
  // getter 
  get() {
    return firstName.value + ' ' + lastName.value
  },
  // setter
  set(newValue) {
    // 注意:我们这里使用的是解构赋值语法
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})
</script>

计算属性和方法
计算属性将基于它们的响应依赖关系缓存。计算属性只会在相关响应式依赖发生改变时重新求值。
如果你不希望有缓存,请用 method 来替代

最佳实践
不要在计算函数中做异步请求或者更改 DOM
避免直接修改计算属性值(值是只读的)

侦听器(watch)
watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组

需要注意的地方:watch ref 基本类型 其数值发生变化 watch能侦测到 且newValue, oldValue不同,若是数组、对象等引用数据类型,只有将整个替换才会侦测到,如果替换里面的属性等部分内容,需要加deep:true强制转成深层侦听器,这样才能检测到。

举例两种最常用的用法:

// 单个 ref
const x = ref(0)
watch(x, (newX) => {
  console.log(`x is ${newX}`)
})

//侦测响应式对象
const obj = reactive({ count: 0 })
// 提供一个 getter 函数
watch(
  () => obj.count,
  (count) => {
    console.log(`count is: ${count}`)
  }
)
// 这种写法错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
  console.log(`count is: ${count}`)
})

//特殊情况举例:
const obj = ref({ count: 0 })
watch(obj, (newValue, oldValue) => {
})
setTimeout(() => {
  obj.value.count++ //这种情况检测不到,需要将整个对象替换才行 如:obj.value = { count: obj.value.count + 1 } 或者 设置deep:true就能检测到
   //如果将定义改成 const obj = reactive({ count: 0 }) obj.count++ 也是能检测到的,这里需要注意下 
}, 5000);

深层次侦测器
直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发

const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // 在嵌套的属性变更时触发
  // 注意:`newValue` 此处和 `oldValue` 是相等的
})

一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调,或显式地加上 deep 选项,强制转成深层侦听器

watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // 注意:`newValue` 此处和 `oldValue` 是相等的
    // *除非* state.someObject 被整个替换了
  },
  { deep: true }
)

注意事项:深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。

watchEffect

会立即执行一遍回调,然后在相关状态更改时重新执行回调,如:

const url = ref('https://...')
const data = ref(null)

watchEffect(async () => {
  const response = await fetch(url.value)
  data.value = await response.json()
})

注意:watchEffect 仅会在其同步执行期间,才追踪依赖

侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。

import { watchEffect } from 'vue'

// 它会自动停止
watchEffect(() => {})

// ...这个则不会!
setTimeout(() => {
  const unwatch = watchEffect(() => {})
  // ...当该侦听器不再需要时
  unwatch()
}, 100)

Class和Style绑定

绑定 HTML class

<div
  class="static"
  :class="{ active: isActive, 'text-danger': hasError }"  //对象形式 最常用
></div>
const isActive = ref(true)
const hasError = ref(false)

<div :class="classObject"></div> <!---绑定的是对象-->
const classObject = reactive({
  active: true,
  'text-danger': false
})

使用计算属性 (也很常用)

const isActive = ref(true)
const error = ref(null)
const classObject = computed(() => ({
  active: isActive.value && !error.value,
  'text-danger': error.value && error.value.type === 'fatal'
}))

使用数组

<div :class="[isActive ? activeClass : '', errorClass]"></div>
const activeClass = ref('active')
const errorClass = ref('text-danger')

在数组中使用对象语法

<div :class="[{ active: isActive }, errorClass]"></div>

在组件中使用
对于只有一个根元素的组件,当你使用了 class attribute 时,这些 class 会被添加到根元素上,并与该元素上已有的 class 合并。
如果你的组件有多个根元素,你将需要指定哪个根元素来接收这个 class。你可以通过组件的 $attrs 属性来实现指定:如

<MyComponent class="baz" />
<!-- MyComponent 模板使用 $attrs 时 -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>

内联样式
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
或者绑定成对象

<div :style="styleObject"></div>
const styleObject = reactive({
  color: 'red',
  fontSize: '13px'
})

使用数组绑定多个内联样式(包含多个样式对象的数组)
<div :style="[baseStyles, overridingStyles]"></div>

条件渲染和列表渲染

v-if、v-else、v-else-if(常用)

<div v-if="type === 'A'">
  A
</div>
<div v-else-if="type === 'B'">
  B
</div>
<div v-else-if="type === 'C'">
  C
</div>
<div v-else>
  Not A/B/C
</div>

在template中使用v-if,是不可见的包装器元素,最后渲染的结果并不会包含这个 <template> 元素

<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
</template>

v-show 不支持在 <template> 元素上使用,也不能和 v-else 搭配使用。

v-if VS v-show
v-if 是“真正”的条件渲染,因为它会确保在切换过程中,条件块内的事件监听器和子组件适当地被销毁和重建。
v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。

v-for 中遍历对象

const myObject = reactive({
  title: 'How to do lists in Vue',
  author: 'Jane Doe',
  publishedAt: '2016-04-10'
})
<li v-for="(value, key, index) in myObject">
  {{ index }}. {{ key }}: {{ value }}
</li>

在遍历对象时,会按 Object.keys() 的结果遍历,但是不能保证它在不同 JavaScript 引擎下的结果都一致。
注意事项:不要使用对象或数组之类的非基本类型值作为 v-for 的 key。请用字符串或数值类型的值。

在 v-for 里使用值范围
v-for 可以直接接受一个整数值。在这种用例中,会将该模板基于 1...n 的取值范围重复多次。
<span v-for="n in 10">{{ n }}</span>(注意此处 n 的初值是从 1 开始而非 0)

与模板上的 v-if 类似,你也可以在 <template> 标签上使用 v-for 来渲染一个包含多个元素的块。

在组件中使用v-for

  <ul>
    <todo-item
      v-for="(todo, index) in todos"
      :key="todo.id"
      :title="todo.title"
      @remove="todos.splice(index, 1)"
    ></todo-item>
  </ul>
<!--子组件 使用$emit触发父组件的自定义事件-->
app.component('todo-item', {
  template: `
    <li>
      {{ title }}
      <button @click="$emit('remove')">Remove</button>
    </li>
  `,
  props: ['title'],
  emits: ['remove'] 
})

v-if 与 v-for 一起使用
不推荐同时使用 v-if 和 v-for。当 v-if 与 v-for 一起使用时,v-if 具有比 v-for 更高的优先级
当它们处于同一节点,v-if 的优先级比 v-for 更高,这意味着 v-if 将没有权限访问 v-for 里的变量
可以把 v-for 移动到 <template> 标签中来修正:

<template v-for="todo in todos" :key="todo.name">
  <li v-if="!todo.isComplete">
    {{ todo.name }}
  </li>
</template>

数组的更新检测
Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:
push()、pop()、shift()、unshift()、splice()、sort()、reverse() (最后两个会变更原数组)
替换数组(如:filter()、concat() 和 slice()它们不会变更原始数组,而总是返回一个新数组。当使用非变更方法时,可以用新数组替换旧数组)
example1.items = example1.items.filter(item => item.message.match(/Foo/))

模板引用

可以使用 ref attribute 为子组件或 HTML 元素指定引用 ID

<script setup>
import { ref, onMounted } from 'vue'

// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)

onMounted(() => {
  input.value.focus()
})
</script>

<template>
  <input ref="input" />
</template>

只可以在组件挂载后才能访问模板引用
如果你需要侦听一个模板引用 ref 的变化,确保考虑到其值为 null 的情况

watchEffect(() => {
  if (input.value) {
    input.value.focus()
  } else {
    // 此时还未挂载,或此元素已经被卸载(例如通过 v-if 控制)
  }
})

在选项api中的使用
<base-input ref="usernameInput"></base-input>
访问时:this.$refs.usernameInput.focusInput() //其中focusInput为mounted的方法
$refs 只会在组件渲染完成之后生效。这仅作为一个用于直接操作子元素的“逃生舱”——你应该避免在模板或计算属性中访问 $refs

v-for 中的模板引用(ref是数组)

<script setup>
import { ref, onMounted } from 'vue'

const list = ref([
  /* ... */
])

const itemRefs = ref([]) //得到的是一个数组,不能保证与源数组顺序一致

onMounted(() => console.log(itemRefs.value))
</script>

<template>
  <ul>
    <li v-for="item in list" ref="itemRefs">
      {{ item }}
    </li>
  </ul>
</template>

组件中ref
可以使用ref拿到子组件实例,但是默认在<script setup>定义的内容都是私有的,如需暴露给父组件,需要使用defineExpose 定义,不过大部分情况下,我们都使用emit和props完成组件交互操作

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容