基础渲染
响应式ref
能在改变时触发更新的状态被称作是响应式的。我们可以使用 Vue 的 reactive()
API 来或者是 ref()
API 声明响应式状态。
1 2 3 4 5 6
| import { ref } from 'vue'
const message = ref('Hello World!')
console.log(message.value) message.value = 'Changed'
|
之后在渲染部分,我们使用双花括号来将 js 中的逻辑设置到页面上进行展示渲染
1 2 3 4 5 6 7 8 9 10 11 12 13
| <script setup> import { ref } from 'vue'
// 组件逻辑 // 此处声明一些响应式状态 const message = ref('Hello World!') console.log(message.value) // "Hello World!" message.value = 'Changed' </script>
<template> <h1>{{message}}</h1> </template>
|
注意我们在模板中访问的 message
ref 时不需要使用 .value
:它会被自动解包,让使用更简单。
我们可以简单地理解,想要在JS变量修改的同时也在页面上立即看到变化,我们就需要通过ref()
来响应式声明一些组件状态(也可以理解为变量)
(感觉像是 React 的 useState()
属性绑定
在 Vue 中,双大括号只能用于文本插值
为了给 HTML 的属性绑定一个动态值,需要使用 v-bind
指令:
1
| <div v-bind:id="dynamicId"></div>
|
例如上面的代码,就是将HTML中的id属性,与js变量dynamicId进行绑定
上面的语法我们可以简写
1
| <div :id="dynamicId"></div>
|
由此我们可以进行发散,属于HTML元素的任何其他的属性也类似:
例如下面的代码我们就将一个动态的class与标题进行绑定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <script setup> import { ref } from 'vue'
const titleClass = ref('title') </script>
<template> <h1 :class="titleClass">Make me red</h1> </template>
<style> .title { color: red; } </style>
|
我们先是声明了一个组件状态 titleClass = title,之后通过 v-bind 将 h1 的 class 属性动态地与这个状态绑定
最终 vue 响应式渲染的结果就是:
1
| <h1 class="title">Make me red</h1>
|
事件监听
这也是很常用的一种语法,例如点击按钮时执行某些逻辑,在Vue中我们用v-on
指令监听DOM事件
1
| <button v-on:click="increment">{{ count }}</button>
|
同样地,也可以进行简写,我们使用@
1
| <button @click="increment">{{ count }}</button>
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| <script setup> import { ref } from 'vue'
const count = ref(0)
function increment() { count.value++ } </script>
<template> <button @click="increment">Count is: {{ count }}</button> </template>
|
表单绑定
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <script setup> import { ref } from 'vue'
const text = ref('')
function onInput(e) { text.value = e.target.value } </script>
<template> <input :value="text" @input="onInput" placeholder="Type here"> <p>{{ text }}</p> </template>
|
这个组件的效果就是在输入框里输入文字,下面的段落会同步更新显示内容
实际上这里就是将 v-bind 和 v-on 组合使用:
- @input接收原生DOM事件,输入框中最终输入的值,会被作为 onInput 函数的入参
- 之后我们在 onInput 中更新 text 的值
上面的逻辑可以使用 v-model 来等价进行替换
1 2 3 4 5 6 7 8 9 10
| <script setup> import { ref } from 'vue'
const text = ref('') </script>
<template> <input v-model="text" placeholder="Type here"> <p>{{ text }}</p> </template>
|
条件渲染
使用 v-if
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <script setup> import { ref } from 'vue'
const awesome = ref(true)
function toggle() { awesome.value = !awesome.value } </script>
<template> <button @click="toggle">Toggle</button> <h1 v-if="awesome">Vue is awesome!</h1> <h1 v-else>Oh no 😢</h1> </template>
|
遍历数组列表渲染
使用 v-for 指令来遍历数组列表进行渲染,这里我们以无序数组为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <script setop> const todos = ref([ { id: id++, text: 'Learn HTML' }, { id: id++, text: 'Learn JavaScript' }, { id: id++, text: 'Learn Vue' } ]) </script>
<template> <ul> <li v-for="todo in todos" :key="todo.id"> {{ todo.text }} </li> </ul> </template>
|
这里就表示遍历 todos 数组的每一个元素,每一个元素通过局部变量 todo 来访问
我们还给每个 todo 对象设置了唯一的 id
,并且将它作为特殊的 key 属性绑定到每个 <li>
key
使得 Vue 能够精确地移动每个 <li>
,以匹配对应的对象在数组中的位置。
现在我们通过 vue 的基础指令实现了一个 todo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <script setup> import { ref } from 'vue'
// 给每个 todo 对象一个唯一的 id let id = 0
const newTodo = ref('') const todos = ref([ { id: id++, text: 'Learn HTML' }, { id: id++, text: 'Learn JavaScript' }, { id: id++, text: 'Learn Vue' } ])
function addTodo() { // 数组新增对象 todos.value.push({ id: id++, text: newTodo.value }) newTodo.value = ''
}
function removeTodo(todo) { //用id进行过滤 todos.value = todos.value.filter(ogTodo => ogTodo.id !== todo.id) } </script>
<template> <form @submit.prevent="addTodo"> <input v-model="newTodo" required placeholder="new todo"> <button>Add Todo</button> </form> <ul> <li v-for="todo in todos" :key="todo.id"> {{ todo.text }} <button @click="removeTodo(todo)">X</button> </li> </ul> </template>
|
基于状态计算
我们在上一步的基础上,给每一个列表元素加了一个字段,并将其通过 v-model 与 vue 状态进行绑定
1 2 3 4 5 6 7
| <script setup> const todos = ref([ { id: id++, text: 'Learn HTML', done: true }, { id: id++, text: 'Learn JavaScript', done: true }, { id: id++, text: 'Learn Vue', done: false } ]) </script>
|
1 2 3 4
| <li v-for="todo in todos"> <input type="checkbox" v-model="todo.done"> ... </li>
|
现在我们想要实现隐藏所有 done=true
的元素,也就是基于状态渲染不同的列表项
我们可以使用 compute()
API,在其中计算真正需要遍历的列表,传给 v-for 进行遍历
1 2 3 4 5 6 7 8
| const filteredTodos = computed(() => { if(!hideCompleted.value){ return todos.value } return todos.value.filter(todo => !todo.done); })
|
之后传给 v-for
1 2
| - <li v-for="todo in todos"> + <li v-for="todo in filteredTodos">
|
生命周期和模板引用
上述的操作都是vue完成了DOM更新,有的时候我们可能需要手动操作DOM进行更新,这个时候就需要进行模板引用
我们通过在模板中DOM新增 ref
这个属性来实现
1
| <p ref="pElementRef">hello</p>
|
对应的我们需要在 script 中声明这个引用
1
| const pElementRef = ref(null)
|
这里使用 null 和 React 中的 useRef 差不多,都是在组件初始化的时候 DOM 还未生成,所以要用 null
说到初始化生命周期,vue暴露了一些生命周期的钩子,开发者可以直接可以注册一些逻辑 用于组件的指定生命周期
下面就是在组件初始化的时候修改段落的文本内容
1 2 3 4 5 6 7 8 9 10 11 12 13
| <script setup> import { ref, onMounted } from 'vue'
const pElementRef = ref(null)
onMounted(() => { pElementRef.value.textContent = 'mounted!' }) </script>
<template> <p ref="pElementRef">Hello</p> </template>
|
状态监听器
由于我们上面实现的效果都是响应式的,有的时候我们希望自定义一些逻辑,当状态变化时执行这些逻辑,我们可以用watch()
1 2 3 4 5 6 7
| import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newCount) => { console.log(`new count is: ${newCount}`) })
|
watch()
可以直接侦听一个 ref(也就是组件状态),并且只要 count
的值改变就会触发回调
下面的逻辑就是当点击按钮触发组件状态 todoId 变更的时候,我们重新调用 fetchData() 逻辑,请求新的数据用于展示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <script setup> import { ref, watch } from 'vue'
const todoId = ref(1) const todoData = ref(null)
async function fetchData() { todoData.value = null const res = await fetch( `https://jsonplaceholder.typicode.com/todos/${todoId.value}` ) todoData.value = await res.json() } watch(todoId, () => { fetchData(); })
fetchData() </script>
<template> <p>Todo id: {{ todoId }}</p> <button @click="todoId++" :disabled="!todoData">Fetch next todo</button> <p v-if="!todoData">Loading...</p> <pre v-else>{{ todoData }}</pre> </template>
|
多组件开发
引入子组件渲染
这部分逻辑和 React 差不多
向子组件传递数据
父子组件数据流向通过 props 进行传递
这部分声明使用逻辑和 React 略有不同,但也有一些相似之处
首先我们在子组件中通过 defineProps()
编译时宏定义需要接收的 props
1 2 3
| const props = defineProps({ msg: String })
|
一旦声明,msg
prop 就可以在子组件的模板中使用。它也可以通过 defineProps()
所返回的对象在 JS 中访问
之后我们可以类似地像 React 那样传递 props 到子组件中。但是实际上是通过 v-ind 语法(类似 HTML 的属性)
1 2 3 4 5 6 7 8 9 10
| <script setup> import { ref } from 'vue' import ChildComp from './ChildComp.vue'
const greeting = ref('Hello from parent') </script>
<template> <ChildComp :msg="greeting" /> </template>
|
向父组件传播事件
很常见的一个场景就是在子组件中执行了某项操作,父组件需要修改对应的状态
这个时候我们可以通过 emit()
API 来实现
在子组件中通过 emit()
定义需要暴露的事件。emit()
的第一个参数是事件的名称。后面的所有参数都将传递给事件监听器
1 2 3 4 5 6 7
| <script setup>
const emit = defineEmits(['response'])
emit('response', 'hello from child') </script>
|
在父组件上,通过 v-on 监听子组件暴露的事件
1
| <ChildComp @response="(msg) => childMsg = msg" />
|
向子组件传递模板片段
在引入子组件的时候如果不通过单标签闭合的方式而是用双标签的形式,就会开启slot
1 2 3 4
| <ChildComp> This is some slot content! </ChildComp>
|
在子组件中,可以使用 <slot>
元素作为插槽出口 (slot outlet) 渲染父组件中的插槽内容 (slot content):
子组件的slot如果也是双标签的话,可以在里面设置兜底内容(父组件没有传递任何slot内容时展示)
1
| <slot>Fallback content</slot>
|