创建项目
- node@16(可用 nvm 管理 node 版本,方便 vue2 vue3 切换)
- npm init vue@latest
全局 API 应用实例
- main.js
import { createApp } from 'vue'
const app = createApp({})
复制代码
- Vue.prototype 替换为 app.config.globalProperties
// src/main.js
const app = createApp(App);
app.config.globalProperties.msg = 'hello'
// 子组件拿取
<script setup>
const { msg } = getCurrentInstance().appContext.config.globalProperties;
// hello
console.log('globalProperties---msg', msg);
</script>
复制代码
- app.component 、app.directive、 app.use
app.component('button-counter', {
data: () => ({
count: 0,
}),
template: '<button @click="count++">Clicked {{ count }} times.</button>',
});
app.directive('focus', {
mounted: (el) => el.focus(),
});
复制代码
模板指令
- v-model
<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" />
<!-- 是以下的简写: -->
<ChildComponent
:title="pageTitle"
@update:title="pageTitle = $event"
:content="pageContent"
@update:content="pageContent = $event"
/>
复制代码
- key
<!-- Vue 2.x -->
<div v-if="condition" key="a">Yes</div>
<div v-else key="a">No</div>
<!-- Vue 3.x (推荐方案:移除 key) -->
<div v-if="condition">Yes</div>
<div v-else>No</div>
<!-- Vue 2.x -->
<template v-for="item in list">
<div v-if="item.isVisible" :key="item.id">...</div>
<span v-else :key="item.id">...</span>
</template>
<!-- Vue 3.x -->
<template v-for="item in list" :key="item.id">
<div v-if="item.isVisible">...</div>
<span v-else>...</span>
</template>
复制代码
- v-if 与 v-for 的优先级对比
2.x 版本中在一个元素上同时使用 v-if 和 v-for 时,v-for 会优先作用。
3.x 版本中 v-if 总是优先于 v-for 生效。
复制代码
setup
- 顶层的绑定会被暴露给模板
<template>
<button @click="log">{{ msg }}</button>
<MyComponent />
</template>
<script setup>
// import 导入的内容也会以同样的方式暴露
import { capitalize } from './helpers'
import MyComponent from './MyComponent.vue'
// 变量
const msg = 'Hello!'
// 函数
function log() {
console.log(msg)
}
</script>
复制代码
- 顶层 await(目前推荐如下示例使用)
<template>
<h3>app</h3>
<p>user:{{user}}</p>
</template>
<script setup>
import { ref } from 'vue'
const user = ref(null);
const fetchUserId = () => {
// 模拟ajax
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("123")
}, 1500);
})
}
const fetchUser = async () => {
// 模拟接口相互依赖
const userId = await fetchUserId()
const userInfo = await Promise.resolve({ userId, name: "bwf" })
user.value = userInfo
}
fetchUser()
console.log('script---end');
</script>
复制代码
实现数据响应式的 API ref + reactive
ref 和 reactive 是 Vue3 中用来实现数据响应式的 API ref 定义基本数据类型或引用数据类型,reactive 只能定义引用数据类型
reactive
- 其底层是通过 ES6 的 Proxy 来实现数据响应式,相对于 Vue2 的 Object.defineProperty,具有能监听普通对象的增删操作,数组的下标修改、数组的 length 修改、数组的增删改
/**
* 模拟vue3 Proxy代理 实现对于对象的拦截
* target: 被代理的对象 data
* key: 操作的属性名
* value: 值
*/
const p = new Proxy(data, {
//读取属性会执行的回调
get(target, key, receiver) {
console.log('get', target, key);
return Reflect.get(target, key, receiver);
},
//修改或添加属性会执行的回调
set(target, key, value, receiver) {
console.log('set', target, key, value);
return Reflect.set(target, key, value, receiver);
},
//删除属性会执行的回调
deleteProperty(target, key) {
console.log('deleteProperty', target, key);
return delete target[key];
},
});
复制代码
- reactive 的示例
const paginationConfig = reactive({
pageNum: 1,
pageSize: 10,
});
const onChange = () => {
paginationConfig.pageNum = 2;
paginationConfig.pageSize = 20;
};
<a-pagination v-model:current="paginationConfig.pageNum"></a-pagination>;
复制代码
- reactive 不能定义基本数据类型
// 源码
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
if (!shared.isObject(target)) {
{
console.warn(`value cannot be made reactive: ${String(target)}`);
}
return target;
}
// some code
}
复制代码
- reactive 实现深层嵌套的响应式
const obj = {
a: {
count: 1,
},
};
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
console.log('这里是get');
// 判断如果是个对象在包装一次,实现深层嵌套的响应式
if (typeof target[key] === 'object') {
return reactive(target[key]);
}
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log('这里是set');
return Reflect.set(target, key, value, receiver);
},
});
}
const proxy = reactive(obj);
复制代码
- 以下是 reactive 定义的变量响应式失去的几个情况
1、解构 props 对象,丢失地址会导致失去响应式
<template>
<h2>App</h2>
<p>state.userInfo:{{state.userInfo}}</p>
<button @click="changeUserInfo">change userInfo</button>
</template>
<script setup>
import { reactive, ref, isReadonly } from 'vue'
const state = reactive({ id: "112", userInfo: { name: "bwf", list: [{ id: "11" }, { id: "22" }] } })
let { id, userInfo } = state
const changeUserInfo = (data) => {
// 以下操作不会响应式
// id = 999
// userInfo = { name: "bbbb" }
// userInfo = reactive({ name: "bbbb" })
// 这种操作会影响式, 没有丢失 userInfo 地址
userInfo.name = 'xxxx'
}
</script>
复制代码
2、 给 reactive 响应式对象直接赋值,会导致该对象失去响应式(我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失)
let userInfo = reactive({ name: 'bwf', list: [{ id: '11' }, { id: '22' }] });
const changeVue = () => {
// 强制地替换 会失去响应式
// userInfo = { name: "xxx", list: [{ id: "11" }] }
// 这样也会失去响应式
// userInfo = reactive({ name: "xxx", list: [{ id: "11" }] })
};
复制代码
3、 解决 reactive 定义的变量,解构丢失属性地址 或 直接赋值 导致响应式丢失的方法
-
- 把响应式数据统统挂载到 一个名叫 state 的对象上,类似 vue2 中的 data 选项的做法
<template>
<h2>App</h2>
<p>state.userInfo:{{state.userInfo}}</p>
<button @click="changeUserInfo({ name: 'xxx', list: [{ id: '33' }] })">change userInfo</button>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({ userInfo: { name: "bwf", list: [{ id: "11" }, { id: "22" }] } })
const changeUserInfo = (data) => {
// 可以响应式
state.userInfo = data
}
</script>
复制代码
-
- 将此对象改成用 ref 定义
<template>
<h2>App</h2>
<p>userInfo:{{userInfo}}</p>
<button @click="changeUserInfo({ name: 'xxx', list: [{ id: '33' }] })">change userInfo</button>
</template
<script setup>
import { reactive, ref, isReadonly } from 'vue'
const userInfo = ref({ name: "bwf", list: [{ id: "11" }, { id: "22" }] })
const changeUserInfo = (data) => {
// 可以响应式
userInfo.value = data
}
</script>
复制代码
-
- 不要直接赋值,对于普通对象,可以直接改变它的属性 或 采用 Object.assign 可以响应式;对于数组类型的,可以 push 等方法响应式
<template>
<h2>App</h2>
<p>userInfo:{{userInfo}}</p>
<button @click="changeUserInfo">change userInfo</button>
</template>
<script setup>
import { reactive, ref, isReadonly } from 'vue'
const userInfo = reactive({ name: "bwf", list: [{ id: "11" }, { id: "22" }] })
const changeUserInfo = (data) => {
// 以下方式都可以响应式
userInfo.name = "lalala"
userInfo.list = [{ id: "33" }]
Object.assign(userInfo, { name: "lalala" })
}
</script>
复制代码
ref
- ref() 将传入参数的值包装为一个带 .value 属性的 ref 对象, ref 的 .value 属性也是响应式的
// 源码
class RefImpl {
constructor(value, __v_isShallow) {
this.__v_isShallow = __v_isShallow;
this.dep = undefined;
this.__v_isRef = true;
this._rawValue = __v_isShallow ? value : toRaw(value);
this._value = __v_isShallow ? value : toReactive(value);
}
get value() {
// 取值的时候依赖收集
trackRefValue(this);
return this._value;
}
set value(newVal) {
const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
newVal = useDirectValue ? newVal : toRaw(newVal);
if (shared.hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = useDirectValue ? newVal : toReactive(newVal);
triggerRefValue(this, newVal);
}
}
}
const toReactive = (value) => (shared.isObject(value) ? reactive(value) : value);
复制代码
- ref 定义对象类型时,底层走的还是 reactive()的逻辑
- ref 定义基本类型时,走的还是 getter setter
- 使用 ref 定义基本数据类型时,在脚本里使用时,需要加.value 后缀,然而在模板里不需要
- ref 被传递给函数或是从一般对象上被解构时,不会丢失响应性:
- ref 使用示例
const user = ref({ name: "bwf" })
let isShow = ref(false)
const onChange = () => {
// 以下两种更新都可以响应式
user.value = { name: "xxxx", id: 999 }
isShow.value = true
}
<p>user.name:{{user.name}}</p>
<a-modal v-model:visible="isShow"></a-modal>
复制代码
ref 和 reactive 定义数组对比
// ref定义
const tableData = ref([]);
const getTableData = async () => {
const { data } = await getTableDataApi(); // 模拟接口获取表格数据
tableData.value = data; // 修改
};
// reactive定义
const tableData = reactive([]);
const getTableData = async () => {
const { data } = await getTableDataApi(); // 模拟接口获取表格数据
// tableData = data // 修改,错误示例,这样赋值会使tableData失去响应式
tableData.push(...data); // 先使用...将data解构,再使用push方法
// tableData = reactive(data) // 赋值前再包一层reactive 也可以实现 tableData 的响应式
};
复制代码
ref vs reactive
1.ref 用于定义基本类型和引用类型,reactive 仅用于定义引用类型 2.reactive 只能用于定义引用数据类型的原因在于内部是通过 ES6 的 Proxy 实现响应式的,而 Proxy 不适用于基本数据类型 3.ref 定义对象时,底层会通过 reactive 转换成具有深层次的响应式对象,所以 ref 本质上是 reactive 的再封装 4.在脚本里使用 ref 定义的数据时,记得加.value 后缀 5.在定义数组时,建议使用 ref,从而可避免 reactive 定义时值修改导致的响应式丢失问题 6.把响应式数据统统挂载到 一个名叫 state 的对象上,类似 vue2 中的 data 选项的做法,这样的话直接 reactive 定义变量就行 7. 不论是 ref 还是 reactive 定义的变量,解构赋值后都不可以改变引用地址,否则会失去响应式
<template>
<h2>App</h2>
<p>obj.foo:{{obj.foo}}</p>
<p>obj:{{obj}}</p>
<button @click="changeUserInfo">change userInfo</button>
</template>
<script setup>
import { reactive, ref, isReadonly } from 'vue'
const obj = ref({ foo: 12, info: { name: "bwf" } })
let { foo, info } = obj.value
const changeUserInfo = (data) => {
// 以下 不能响应式
// info = { name: "xxxxxxxx" }
// 以下 可以响应式
// obj.value.foo = 999999999
// obj.value.info = { name: "xxx" }
info.name = "lll"
}
</script>
复制代码
computed watch watchEffect
- computed computed() 方法默认接收一个 getter 函数,返回值为一个计算属性 ref(ComputedRefImpl )。可以通过 publishedBooksMessage.value 访问计算结果。计算属性 ref 也会在模板中自动解包,因此在模板表达式中引用时无需添加 .value。
<template>
<h2>App</h2>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
<br>
<button @click="changeBooks">change books</button>
</template>
<script setup>
import { reactive, computed, isRef } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 2 ? 'Yes' : 'No'
})
// true true
console.log(isRef(publishedBooksMessage), isReadonly(publishedBooksMessage));
const changeBooks = ()=>{
author.books.length = 0
}
</script>
复制代码
- watch watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组 注意,你不能直接侦听响应式对象的属性值,例如:
<template>
<h3>app</h3>
<p>x:{{x}}</p>
<p>user:{{user}}</p>
<button @click="++x">change x</button>
<button @click="user.name='xxx'">change user.name</button>
<button @click="changeUser">change user</button>
</template>
<script setup>
import { reactive, ref, watch } from 'vue'
const x = ref(0)
const user = reactive({ name: "bwf", list: [{ name: "xx" }, { name: "ll" }] })
const changeUser = () => {
user.list.push({ name: "add" })
// user.list = [{ name: "add" }]
}
// watch 一个 ref
watch(x, (newVal) => {
console.log('x', newVal)
})
// 注意,你不能直接侦听响应式对象的属性值,例如:
watch(user.name, (newVal) => {
// 以下不会执行
console.log('user.name1', newVal)
})
// 只有改变 user.name 才会触发
watch(() => user.name, (newVal) => {
console.log('user.name2', newVal)
})
// 在嵌套的属性变更时触发 (user里面的name 或者 list发生任何改变都会触发)
watch(user, (newVal) => {
console.log('user', newVal)
})
watch(() => user.list, (newVal) => {
// 在不配置deep:true的情况下
// 会触发
// user.list = [{ name: "add" }]
// 不会触发
// user.list.push({ name: "add" })
// 配置deep:true(当用于大型数据结构时,开销很大) 以下都会触发
// user.list = [{ name: "add" }]
// user.list.push({ name: "add" })
console.log('user.list', newVal)
}, { deep: true })
</script>
复制代码
即时回调的侦听器
watch(
source,
(newValue, oldValue) => {
// 立即执行,当 `source` 改变时会再次执行
},
{ immediate: true }
);
复制代码
- watchEffect
<template>
<h3>app</h3>
<p>pid:{{pid}}</p>
<button @click="changePid">change pid</button>
</template>
<script setup>
import { ref, watchEffect } from 'vue'
const id = ref('1')
const pid = ref('0000')
const data = ref(null)
// watchEffect 回调会立即执行
// pid id任何一个发生改变时都会触发
watchEffect(async () => {
console.log('watchEffect---start');
const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${pid.value}/${id.value}`)
data.value = await response.json()
})
const changePid = () => {
pid.value = "99"
}
</script>
复制代码
如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post' 选项, 后置刷新的 watchEffect() 有个更方便的别名 watchPostEffect()
要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数:
const unwatch = watchEffect(() => {});
// ...当该侦听器不再需要时
unwatch();
复制代码
生命周期钩子 onMounted 和 onUnmounted
<script setup>
import {onMounted} from 'vue' onMounted(() => {console.log(`the component is now mounted.`)})
</script>
复制代码
nextTick
<template>
<button id="counter" @click="increment">{{ count }}</button>
</template>
<script setup>
import { ref, nextTick } from 'vue'
const count = ref(0)
async function increment() {
count.value++
// DOM 还未更新
console.log(document.getElementById('counter').textContent) // 0
await nextTick()
// DOM 此时已经更新
console.log(document.getElementById('counter').textContent) // 1
}
</script>
复制代码
访问 DOM 元素 + 子组件上实例
<template>
<input ref="input" />
<Child ref="child" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
// 声明一个 ref 来存放该元素的引用, 必须和模板里的 ref 同名
const input = ref(null)
const child = ref(null)
onMounted(() => {
input.value.focus()
// child.value 是 <Child /> 组件的实例
})
</script>
复制代码
defineProps、defineEmits、defineExpose
- 都是只能在
<script setup>中使用的编译器宏,不需要导入 - 单向流
- 使用了
<script setup>的组件是默认私有的:一个父组件无法访问到一个使用了<script setup> 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露
// 父组件
<template>
<h3>app</h3>
<p>count:{{count}}</p>
<p>user:{{user}}</p>
<button @click="getChild">获取子实例的属性或方法</button>
<Child :count="count" :user="user" @changeCount="changeCount" @changeUser="changeUser" ref="childRef"></Child>
</template>
<script setup>
import { reactive, ref } from 'vue'
import Child from './components/Child.vue'
const count = ref(0)
const user = reactive({ name: "bwf", list: [{ id: "11" }] })
const childRef = ref(null)
const changeCount = (params) => {
// 以下会响应式
count.value = params
}
const changeUser = (params) => {
// 这样会丢失响应式
// user = params
// 以下 会响应式
Object.assign(user, params)
}
const getChild = ()=>{
// childMethod
childRef.value.childMethod()
}
</script>
// 子组件
<template>
<h3>Child</h3>
<p>count:{{count}}</p>
<p>countNew:{{countNew}}</p>
<p>user:{{user}}</p>
<button @click="changeCount">change count</button>
<button @click="changeUser">change user</button>
</template>
<script setup>
import { isReadonly, reactive, ref } from "vue";
const props = defineProps({
count: Number,
user: {
type: Object,
}
})
const emits = defineEmits(['changeCount', 'changeUser'])
// 只是将 props.count 作为初始值,这样做就使 count 和后续更新无关了
const countNew = ref(props.count)
const changeCount = () => {
emits('changeCount', 3)
// emits('changeCount', ++countNew.value)
}
const changeUser = () => {
emits('changeUser', { name: "xxxx", id: "999" })
}
const childMethod = () => {
console.log('childMethod');
}
defineExpose({childMethod})
</script>
复制代码
组合式函数
“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。
- 和 Mixin 的对比
Vue 2 的用户可能会对 mixins 选项比较熟悉。它也让我们能够把组件逻辑提取到可复用的单元里。然而 mixins 有三个主要的短板:
1.不清晰的数据来源:当使用了多个 mixin 时,实例上的数据属性来自哪个 mixin 变得不清晰,这使追溯实现和理解组件行为变得困难。这也是我们推荐在组合式函数中使用 ref + 解构模式的理由:让属性的来源在消费组件时一目了然。
2.命名空间冲突:多个来自不同作者的 mixin 可能会注册相同的属性名,造成命名冲突。若使用组合式函数,你可以通过在解构变量时对变量进行重命名来避免相同的键名。
3.隐式的跨 mixin 交流:多个 mixin 需要依赖共享的属性名来进行相互作用,这使得它们隐性地耦合在一起。而一个组合式函数的返回值可以作为另一个组合式函数的参数被传入,像普通函数那样。
基于上述理由,我们不再推荐在 Vue 3 中继续使用 mixin。保留该功能只是为了项目迁移的需求和照顾熟悉它的用户。
复制代码
- 应用示例
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double Count: {{ counter.doubleCount }}</p>
<button @click="counter.increment">Increment</button>
<button @click="counter.decrement">Decrement</button>
</div>
</template>
<script setup>
import { isRef } from 'vue';
import useCounter from './composables/useCounter'
const counter = useCounter(0)
// true
console.log('counter', isRef(counter.count));
</script>
// composables/useCounter
import { ref, computed } from 'vue'
// 按照惯例,组合式函数名以“use”开头
export default function useCounter(initialValue) {
// 用于初始化计数器的值
const count = ref(initialValue)
function increment() {
count.value++
}
function decrement() {
count.value--
}
const doubleCount = computed(() => count.value * 2)
// 返回值 count doubleCount 是两个ref, ref 则可以维持这一响应性连接。
return {
count,
increment,
decrement,
doubleCount
}
}
作者:悦悦妍妍
链接:juejin.cn/post/722228…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。