VueCLI需要在4.3.1以上才可以支持Vue3
npm update -g @vue/cli
vue -V
@vue/cli 4.4.6
vue create vue3-learning
vue add vue-next # 添加 vue3 插件升级为 vue3
在创建项目时选择手动添加配置,选择vue-router和Vuex,这样创建完的项目各个插件也都会升级为支持Vue3的版本
{
"dependencies": {
"core-js": "^3.6.5",
"vue": "^3.0.0-beta.1",
"vue-router": "^4.0.0-alpha.6",
"vuex": "^4.0.0-alpha.1"
}
}
import {createApp} from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
createApp(App)
.use(router)
.use(store)
.mount('#app');
import {createRouter, createWebHistory} from 'vue-router';
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
});
export default router;
创建路由的方式与以前有所变化,路由模式除了Hash模式(createWebHashHistory
)和History模式(createWebHistory
),还多了带缓存的History路由(createMemoryHistory
)
使用路由跳转的API也有所变化:
import {useRouter} from 'vue-router';
export default {
setup() {
const router = useRouter();
const goHome = () => router.push('/');
return {goHome}
}
}
关于router的具体变化,后面再单独学习
import {createStore} from 'vuex';
export default createStore({
state: {
count: 0
},
mutations: {
changeCount(state, isAdd) {
state.count = isAdd ? state.count + 1 : state.count - 1;
}
},
actions: {},
modules: {}
});
使用:
import {useStore} from 'vuex';
export default {
setup() {
const store = useStore();
const storeState = store.state;
return {storeState}
}
}
可以看出来,Vuex和vue-router,API都有了一些变化,与React Hooks的API很类似,但是基本原理没有太大变化
setup
setup
函数是新的Composition API的入口点
Props初始化后就会调用setup
函数,在beforeCreate
钩子前被调用
setup
返回的对象的属性将被合并到组件模板的渲染上下文,也可以返回一个渲染函数
接受props
作为第一个参数,使用的时候,需要首先声明props
:
export default {
props: {
name: String,
},
setup(props) {
console.log(props.name)
},
}
props
是响应式的,前提是不对props
进行解构,解构后会失去响应性
setup
第二个参数是上下文对象,从2.x中的this
选择性地暴露出一些属性,例如attrs
/slots
/emit
等,可以解构取值不会失去响应性
this
的用法this
在setup
中不可用
reactive
const obj = reactive({ count: 0 })
返回一个普通对象的响应式代理,响应式转换是深层的,会影响对象内部嵌套的属性,基于ES2015的Proxy实现,返回的代理对象不等于原始对象,避免使用原始对象
经过试验,Vue3中可以通过修改数组下标来响应式的更改数组成员的值了
ref
ref
的引入是为了以变量形式传递响应式的值而不再依赖访问this
:
const count = ref(0)
接受一个参数,返回一个响应式可改变的ref
对象,ref
对象拥有一个指向内部值的单一属性.value
ref
主要目的是保证基本类型值的响应式,如果传入的参数不是基本类型,会调用reative
方法进行深层响应式转换
ref
使用时:
ref
的返回值setup
中返回应用在模板中时,会自动解构,不需要书写.value
ref
作为reactive
对象的属性被修改或访问时,也会自动解构,不需要书写.value
Array
、Map
等原声集合类中范围ref
时,不会自动解构,需要使用.value
获取值reactive
VS ref
使用ref
和reactive
的区别可以通过如何撰写编撰的JavaScript逻辑比较
// 风格 1: 将变量分离
let x = 0
let y = 0
function updatePosition(e) {
x = e.pageX
y = e.pageY
}
// --- 与下面的相比较 ---
// 风格 2: 单个对象
const pos = {
x: 0,
y: 0,
}
function updatePosition(e) {
pos.x = e.pageX
pos.y = e.pageY
}
使用ref
就是将将风格(1)转换为使用ref
,让基础类型值也具有响应性,使用reactive
和风格(2)一致
只使用reactive
的问题是,使用组合函数的时候必须始终保持对这个组合函数返回对象的引用以保持响应性,这个对象不能够被解构或者展开
// 组合函数:
function useMousePosition() {
const pos = reactive({
x: 0,
y: 0,
})
// ...
return pos
}
// 消费者组件
export default {
setup() {
// 这里会丢失响应性!
const { x, y } = useMousePosition()
return {
x,
y,
}
// 这里会丢失响应性!
return {
...useMousePosition(),
}
// 这是保持响应性的唯一办法!
// 你必须返回 `pos` 本身,并按 `pos.x` 和 `pos.y` 的方式在模板中引用 x 和 y。
return {
pos: useMousePosition(),
}
},
}
解决方法时使用toRefs
将响应式对象的每个对象都转换为响应的ref
:
function useMousePosition() {
const pos = reactive({
x: 0,
y: 0,
})
// ...
return toRefs(pos)
}
// x & y 现在是 ref 形式了!
const { x, y } = useMousePosition()
目前阶段可以从下面两种风格二选其一:
(1)如果在普通的JavaScript中声明基础变量类型与对象变量时一样区别使用ref
和reacitve
,也就是说如果声明响应式的基础类型使用ref
,如果声明响应式对象变量使用reactive
(2)全部使用reactive
,然后在组合函数返回对象时使用toRefs
目前(2020.08.01)官方对ref
和reactive
的最佳实践还没有建议,自己选择更适合自己的风格使用,我会选择风格1使用。
computed
const count = ref(1)
const plusOne = computed(() => count.value + 1)
传入一个getter
函数,也可以传入一个拥有get
和set
函数的对象
readonly
const original = reactive({ count: 0 })
const copy = readonly(original)
传入一个对象(普通或者响应式对象)或ref
,返回原始对象的深层的制度代理
watchEffect
与React的
useEffect
非常类似
const count = ref(0)
watchEffect(() => console.log(count.value))
立即执行传入的函数,并响应式追踪依赖
在setup
中或生命周期钩子中被调用是,会被链接到组件的生命周期,在组件写在时自动停止
返回值是一个函数,可以手动来停止真挺
传入的函数中,可以接受onInvalidate
函数作为入参,用来注册清理失效时的回调,在下面的情况中:
watchEffect((onInvalidate) => {
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// id 改变时 或 停止侦听时
// 取消之前的异步操作
token.cancel()
})
})
watchEffect
会在组件初始运行时同步打印出来,在监听状态变化后,会在组件更新后执行副作用
在watchEffect
中访问DOM,需要在onMounted
钩子中进行
可以通过传递第二个参数的flush
属性来改变副作用函数的执行时机:
post
,默认,在组件更新后执行sync
,同步运行pre
,在组件更新前执行在第二个参数中传入onTrack
和onTrigger
来调试
watchEffect(
() => {
/* 副作用的内容 */
},
{
onTrigger(e) {
debugger
},
}
)
onTrack
在依赖被追踪时被调用,onTrigger
在依赖变更导致副作用被触发时调用
仅在开发模式下生效
watch
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
// 侦听多个数据源
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
与2.X的watch
完全相同,与watchEffect
相比的不同点:
生命周期钩子函数只能在setup
期间同步使用,在组件卸载时,生命周期内部创建的侦听器和计算状态也会被自动删除
与Vue2.x相比,beforeCreated
和created
被删除了,对应的逻辑在setup
内部完成,其他的生命周期钩子都改为了onXxx
的形式(beforeDestoryed
改为了onBeforeUnmount
,destroyed
改为了onUnmounted
)
两个新增的调试钩子函数onRenderTracked
和onRenderTriggered
:
export default {
onRenderTriggered(e) {
debugger
// 检查哪个依赖性导致组件重新渲染
},
}
目前(2020.07.29)在Demo尝试调用这两个钩子函数,没有生效,不知道是Bug还是我调用的姿势不对
使用provide
和inject
实现依赖注入,与2.x版本中基本一致,只能在setup
中使用
import { provide, inject } from 'vue'
const ThemeSymbol = Symbol()
const Ancestor = {
setup() {
provide(ThemeSymbol, ref('dark'))
},
}
const Descendent = {
setup() {
const theme = inject(ThemeSymbol, ref('light') /* optional default value */)
return {
theme,
}
},
}
使用ref
传值可以保证provided
和injected
之间值的响应性
Vue2.x中的ref
原本是用于获取DOM的, Vue3中ref
不仅可以响应化数据,也可以实现获取DOM的功能
在setup
中声明一个ref
并返回,在模板中声明ref
并且值与返回的ref
相同,这时在渲染初始化后(onMounted
)就可以获取分配的DOM或组件实例
在v-for
中使用时,需要使用3.0新增的函数形的ref
,为ref
赋值:
{{ item }}
isRef
判断值是否是ref
对象
isProxy
判断一个对象是否是由reactive
或者readonly
创建的代理
判断一个对象是否是由reactive
创建的代理。
如果这个代理是由readonly
创建的,但是又被reactive
创建的另一个代理包裹了一层,那么同样也会返回true
isReadonly
判断一个对象是否是由readonly
创建的代理。
unref
用来快速返回ref
的值,如果参数是ref
,返回它的value
,否则返回参数本身。它是val = isRef(val) ? ref.value : ref
的语法糖
toRef
为reactive
对象的属性创建一个ref
,这个ref
可以被传递并且保持响应性
const state = reactive({
foo: 1,
bar: 2,
})
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
当需要将一个Prop中的属性作为ref
传给组合逻辑函数时,可以使用toRef
export default {
setup(props) {
useSomeFeature(toRef(props, 'foo'))
},
}
目前还没有发现这种情况的实际场景
toRefs
把一个响应式对象转换为普通对象,该普通对象的每个属性都是一个ref
,与原来的响应式对象一一对应
const state = reactive({
foo: 1,
bar: 2,
})
const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型如下:
{
foo: Ref,
bar: Ref
}
*/
// ref 对象 与 原属性的引用是 "链接" 上的
state.foo++
console.log(stateAsRefs.foo) // 2
stateAsRefs.foo.value++
console.log(state.foo) // 3
当从一个组合逻辑中返回响应式对象时,用toRefs
是很有效的,它可以让消费组件可以解构或者扩展返回的对象,而不是去响应性
function useFeatureX() {
const state = reactive({
foo: 1,
bar: 2,
})
// 对 state 的逻辑操作
// 返回时将属性都转为 ref
return toRefs(state)
}
export default {
setup() {
// 可以解构,不会丢失响应性
const { foo, bar } = useFeatureX()
return {
foo,
bar,
}
},
}
customRef
用来自定义ref
,可以显示依赖追踪和触发响应,接受一个函数,函数的两个参数是用于追踪的track
和触发响应式的trigger
,返回一个带有get
和set
属性的对象
可以使用自定义ref
来实现带防抖功能的v-model
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
},
}
})
}
export default {
setup() {
return {
text: useDebouncedRef('hello'),
}
},
}
个人还不知道有什么好的应用场景,实际上上面的例子不通过
custromRef
来实现,可能灵活度还更大
markRaw
显示标记一个对象永远不会转换为响应式dialing,返回这个对象本身
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false
// 如果被 markRaw 标记了,即使在响应式对象中作属性,也依然不是响应式的
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false
它的作用是用来覆盖reactive
为默认深层的特性,主要用来提升性能,比如一些值复杂的第三方类库的实例或者Vue组件对象,或者一个元素数量庞大,但是数据不可变,跳过Proxy也可以提升性能
这种标识只停留在根级别,markRaw
对象的属性如果被reactive
处理,仍然会返回一个响应式对象,并且导致原始值与Proxy值不同
const foo = markRaw({
nested: {},
})
const bar = reactive({
// 尽管 `foo` 己经被标记为 raw 了, 但 foo.nested 并没有
nested: foo.nested,
})
console.log(foo.nested === bar.nested) // false
shallowReactive
只为某个对象的私有(第一层)属性创建浅层次的响应式代理,不会对深层属性做深层次、递归地响应式代理
onst state = shallowReactive({
foo: 1,
nested: {
bar: 2,
},
})
// 变更 state 的自有属性是响应式的
state.foo++
// ...但不会深层代理
isReactive(state.nested) // false
state.nested.bar++ // 非响应式
shallowReadonly
与shallowReactive
类似,只为对象的私有(第一层)属性创建浅层的只读响应代理
shallowRef
创建一个ref
,将会追踪它的.value
更改操作,但是不会对变更后的.value
做响应式代理转换
const foo = shallowRef({})
// 更改对操作会触发响应
foo.value = {}
// 但上面新赋的这个对象并不会变为响应式对象
isReactive(foo.value) // false
注意,如果每次都为foo.value
重新赋值,那么仍然会触发响应式改动。上面说的“不会变为响应式对象”指的是更改value
的某个属性不会触发响应式改动
toRaw
返回reactive
或者readonly
方法转换为响应式代理的普通对象。用于临时读取,访问不会被代理、跟踪,写入时不会触发更改。不建议一致持有原始对象的引用。
组合式API的初衷就是为了实现更有组织的代码,实现更灵活的逻辑提取与复用,在代码中会出现更多的、零碎的函数模块,在不同的位置、不同的组件间进行重复调用
它可以避免Vue2.x时代逻辑复用的几种主要形式(Mixin/HOS/SLOT)的弊端,带来了比较明显的好处:
但是它在提到了代码质量的上限的同时,降低了下线,setup
中会出现大量面条式的代码,避免这种糟糕情况的关键就是,将逻辑更合理的划分为单独的函数,将setup
作为一个入口,在其中进行不同组合函数的调用。
Vue3的基于函数的组合式API受到了React Hooks的启发,在很多思维模型方面与React Hooks很类似,提供了同等级别的逻辑组合能力,但是也有着比较明显的不同,组合式API的setup
函数只会被调用一次,也就意味着使用组合式API时:
useCallback
(React Hooks需要用useCallback
进行性能优化)useEffecr
和useMemo
并传入依赖数组以捕获过时的变量,Vue的自动以来可以确保侦听器和计算值总是准确无误的(React Hooks需要手动记录依赖)