新年第1篇两手抓—开发Vue3得心应手与应对狂轰乱炸

633 阅读21分钟

好多天没认真打磨一篇图文并茂、知识面广的技术blog,哈哈,因为最近看不少电子书耽误很多时间,但既然进入变化多端的前端领域,提高技术是唯一出路,这篇文章花了不少于十天时间的打磨才成型,从基础到原理再到实践都有体现,我相信阅读完肯定对大家有帮助,希望大家给与点赞支持,也可通过评论区进行互动,欢迎指正

如果想精神和物质都能得到满足,请花上一杯咖啡的时间阅读它,😝。

特别强调,此篇包含有必用知识点:

  • vue3常用API【肯定要常记在心才能正常开发】
  • ref和reactive的区别【开发过程中必须要懂,务必踩坑】
  • watch和watchEffect的区别【开发过程中必须要懂,务必踩坑】
  • 响应式原理 【知其然必知其所以然,融会贯通】
  • diff算法【面试逃得了吗?】
  • vue3和vue2的区别点【懂了开发可顺手拈来】
  • vite和webpack比较【vite的好处,你觉得面试不会被问到吗?】

当然,除了以上,还包含很多扩展性的知识点,欢迎往下阅读。

总结此篇,是边实践边输出,肯定还得边思考,脑手并用,才能技术基础知识扎实。总之,以官网为抓手,以别人blog为参考,以实践为验证,以自己思考为输出。懂得这些在业务面前可以灵活做出选择。

插曲:如果想使用TS+Vue3开发,推荐阅读《Typescript入门与实践》这边书,很详细。

vue3相关API

1. 常用响应式API

ref和reactive

ref

  • 语法:
    const/let/var xxx = ref(initValue) const定义的也能改变

    有时我们可能需要为 ref 的内部值指定复杂类型。

const foo = ref<string | number>('foo') // foo 的类型:Ref<string | number>
foo.value = 123 // ok!
  • 可接受的数据类型:基本类型、引用类型

  • 作用:把参数加工成一个响应式对象,全称为reference对象

  • 核心原理:如果ref使用的是基本类型响应式依赖Object.defineProperty()的get()和set();如果ref使用的是引用类型,底层ref会借助reactive生成proxy变成响应式

  • 具体实践:

  1. ref使用的是基本类型:
js中ref需要用.value去获取值

let refBaseType = ref(undefined)
console.log(refBaseType) //输出如下图

image.png

refBaseType.value = 'i am ref of base type'
console.log(refBaseType) //输出如下图

image.png

// template中ref不需要用.value去获取值

<div>值呢:{{refBaseType}}</div>
  1. ref使用的是引用类型
// 对象
let refReferenceType = ref({})
console.log(refReferenceType) //输出如下图,可以看出使用了Proxy,其实是借助reactive的proxy

image.png

refReferenceType.value = { count: 1 }
console.log(refReferenceType)//输出如下图,可以看出使用了Proxy,其实是借助reactive的proxy

image.png

// template中ref直接去对象中的属性值,不会自动添加.value

<div>值呢:{{refReferenceType.value.count}}</div>
// 数组
let refBaseType = ref([])
refBaseType.value = [1, 2, 3, 4, 5]
console.log(refBaseType.value) // Proxy {0: 1, 1: 2, 2: 3, 3: 4, 4: 5}

reactive

  • 语法:const xxx = reactive(initValue) const定义的也能改变

  • 可接受的数据类型:引用类型(Object|Array|Map|Set|WeakMap|WeakSet)

  • 作用:把参数加工成一个代理对象,全称为Proxy对象

  • 核心原理:基于Es6的Proxy实现,通过Reflect反射代理操作源对象,相比于reactive定义的浅层次响应式数据对象,reactive定义的是更深层次的响应式数据对象

  • 具体实践:

//数组
let a = reactive([1,2,3])
console.log(a)  //Proxy {0: 1, 1: 2, 2: 3}

let b = ref([1,2,3])
console.log(b)   //RefImpl{}

//这样定义数组也可以
let a = reactive({
    arr: []
})
a.arr = [1, 2, 3]

<div>值呢:{{a}} {{b}}</div> 
// <div>值呢:[1,2,3] [1,2,3]</div>
//Map
var temp = new Map()
temp.set('a', 1)
var tempRef = reactive(temp)  
console.log(tempRef) // Proxy {'a' => 1}

ref和reactive的区别

  1. ref和reactive都可以做响应式

  2. 可接受的数据类型

    ref:基本类型、引用类型

    reactive:引用类型

  3. 复制整个对象(数组)

    ref:如果定义的是引用类型底层会借助reactive形成proxy代理对象,可以直接复制整个对象。

    reactive:不能一次性修改整个对象

    注:直接替换整个对象(数组)而不是修改对象(数组)中的某一个属性,使用ref(reactive不能一次性修改整个对象(数组))。比如 请求list数据,需要将数据整体赋值个响应对象,只能用ref。

//ts写法
const resList = {a:1}
var temp = {
    type: number,
}
var tempRef: Ref<any> = ref(temp)  //这里temp是一个对象,却用的是ref

temp = resList
tempRef.value = temp 
//js写法
const resList = {a:1}
var temp = {}
var tempRef = ref(temp)  //这里temp是一个对象,却用的是ref
temp = resList
tempRef.value = temp
console.log(tempRef)  //结果如下图

image.png

const resList = {a:1}
var temp = {}
var tempRef = reactive(temp)  //这里使用reactive,但结果不是响应式
tempRef = resList
console.log(tempRef) // {a:1}
  1. 都有对应判断类型的函数

    isRef:判断数据是否是ref

    isReactive:判断数据是否是reactive

  2. 当将ref分配给 reactive property 时,ref 将被自动解包(reactive 将解包所有深层的 refs`,同时维持 ref 的响应性)

const count = ref(1)
const obj = reactive({})

obj.count = count

console.log(obj.count) // 1
console.log(obj.count === count.value) // true

ref和reactive建议使用

1. 基本类型都用ref来定义,引用类型都有reactive

let a = ref('hhy')

let b = reactive({
    arr: [1, 2, 3]
})

2. 都用reactive来定义,把所有变量放在一对象里,然后用toRefs进行解构导出到页面使用,下面还会讲到

let state = reactive({
  name: 'hhy',
  arr: [],
  obj: {}
})
console.log(state.arr)
console.log(state.obj)
//导出到页面使用
const {name, arr, obj } = toRefs(state) // toRefs解决对象解构赋值后引用丢失问题
return {name, arr, obj}


或者
const dataAsRefs = toRefs(state)
return { ...dataAsRefs };
  1. 直接替换整个对象(数组)而不是修改对象(数组)中的某一个属性,使用ref

根据实际情况结合它们的特定灵活使用,ref和reactive结合使用最佳。比如变量模板中使用变量特别多,可以用reactive都集中在一个对象中。

isRef unref toRef toRefs

  • isRef:检查值是否为一个 ref 对象。

  • unref:如果参数是一个 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 的语法糖函数

function useFoo(x: number | Ref<number>) {
  const unwrapped = unref(x) // unwrapped 现在一定是数字类型
}
  • toRef:用来为源响应式对象上的某个 property 新创建一个 ref。然后,ref 可以被传递,它会保持对其源 property 的响应式连接。
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

console.log(fooRef) // ObjectRefImpl{} 
console.log(state) //Proxy {foo: 3, bar: 2}
使用场景:**当你要将 prop 的 ref 传递给复合函数时,`toRef` 很有用**:
export default {
  setup(props) {
    useSomeFeature(toRef(props, 'foo'))
  }
}

即使源 property 不存在,toRef 也会返回一个可用的 ref。这使得它在使用可选 prop 时特别有用,可选 prop 并不会被 toRefs处理。

  • toRefs:将响应式对象转换为普通对象,其中结果对象的**每个 property **都是指向原始对象相应 property 的 ref
const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型:
{
  foo: Ref<number>,
  bar: Ref<number>
}
*/

// ref 和原始 property 已经“链接”起来了
state.foo++
console.log(stateAsRefs.foo.value) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

console.log(stateAsRefs) // {foo: ObjectRefImpl, bar: ObjectRefImpl}
console.log(state) //Proxy {foo: 3, bar: 2}

使用场景:当从组合式函数返回响应式对象时,toRefs 非常有用,这样消费组件就可以在不丢失响应性的情况下对返回的对象进行解构/展开:

function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2
  })
  // 操作 state 的逻辑
  // 返回时转换为ref
  return toRefs(state)
}
setup() {
    // 可以在不失去响应性的情况下解构
    const { foo, bar } = useFeatureX()
    return {
      foo,
      bar
    }
}

toRefstoRef的区别

  • 作用

toRefs 只会为源对象中包含的 property 生成 ref。
如果要为特定的 property 创建 ref,则应当使用 toRef

  • 使用场景

toRef当要将 prop 的 ref 传递给复合函数时,toRef 很有用 toRefs当从组合式函数返回响应式对象时,toRefs 非常有用,这样消费组件就可以在不丢失响应性的情况下对返回的对象进行解构/展开

watchwatchEffect

watch

  • 语法:watch( source, cb, [options] )

    • source:可以是表达式或函数,用于指定监听的依赖对象
    • cb:依赖对象变化后执行的回调函数
    • options:可参数,可以配置的属性有 immediate(立即触发回调函数)、deep(深度监听)
  • 停止监听

const person = reactive({name: '', age: 10})
// 监听多个参数
const stop1 = watch([() => person.name, () => person.age], ([curName, curAge], [prevName, prevAge]) => {
 console.log(curName, curAge, '----', prevName, prevAge)
 setTimeout(() => {
  stop1()
 }, 5000)
})
  • 具体实践:
  1. 监听ref响应
const name = ref('hhy')
watch(name, (curVal, prevVal) => {
    console.log(curVal, prevVal) //输出值随着name变化而变化
})
  1. 监听reactive响应
let person = reactive({
    name:'hhy',
    age: 18,
    child:{
        son: 1
    }
})

setTimeout(()=>{
    person.child.son = 3
}, 1500)

watch(
() => person.child,
(val, oval) =>{
    console.log(val,oval);  //Proxy {son: 3}   Proxy {son: 3}  
    // 也算是监听到了 但显然修改前和修改后的值都变成了修改后的
},{
    deep: true
})

使用侦听器来比较一个数组或对象的值,这些值是响应式的,要求它有一个由值构成的副本。

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

watch(
  () => [...numbers],
  (numbers, prevNumbers) => {
    console.log(numbers, prevNumbers)
  }
)
numbers.push(5) // logs: [1,2,3,4,5] [1,2,3,4]

尝试检查深度嵌套对象或数组中的 property 变化时,仍然需要 deep 选项设置为 true。

const state = reactive({ 
  id: 1,
  attributes: { 
    name: '',
  }
})

watch(
  () => state,
  (state, prevState) => {
  // 这里没有执行
    console.log('not deep', state.attributes.name, prevState.attributes.name)
  }
)

watch(
  () => state,
  (state, prevState) => {
    console.log('deep', state.attributes.name, prevState.attributes.name)
  },
  { deep: true }
)

state.attributes.name = 'Alex' // 日志: "deep" "Alex" "Alex"

watchEffect

  • 语法:watchEffect(cb, options)
    • cb 函数体内有访问响应式数据的函数(可以是普调函数也可以是异步函数);
    • options 选项配置 flush 定义 cb的执行时机;

cb 会在执行 watchEffect 函数的时候进行首次执行

接受 onInvalidate 函数作为cb的参数:首次执行不会调用 onInvalidate函数;之后的每次cb调用都会执行oninvalidate函数; 如果cb内部会执行一些异步的操作,可以使用onInvalidate函数;实现对之前异步操作的忽略行为。

onInvalidate(fn)传入的回调会在 watchEffect 从新运行或者 watchEffect 中止的时候执行。

如果监听属性也需要在组件中使用(例如:当与模板引用一起),默认在组件更新执行副作用。

  • 取消监听
const person = reactive({name: '', age: 10})

const stop = watchEffect(() => {
    console.log(person.name) 
    setTimeout(() => {
       stop()
    }, 5000)
})

// stop()  // 这样也可以
  • 具体实践:
...
watchEffect(()=>{
    console.log(person.child.son); //先后输出 1  3  
    //能输出修改前后修改后的值
})
watchEffect(async (onInvalidate) =>{ 
    onInvalidate(() => {
        /* */
    }) 
}, { flush: "pre" | "post" | "sync"})   // 默认pre

zhuanlan.zhihu.com/p/425546659

watch 与 watchEffect 的区别

  • 相同的行为
  1. 都可以监听属性的行为

  2. watch 与 watchEffect 在手动停止侦听清除副作用 (watch将 onInvalidate 作为第三个参数传递给回调)、刷新时机(默认组件更新前刷新副作用)和调试方面有相同的行为。

  • 不同的行为
  1. 写法不一样:具体看上面👆🏻

  2. 收集依赖

    watchEffect不需要指定监听的属性,在组件初始化的时候会执行一次用以收集依赖(与computed同理),只要回调中引用了响应式的属性,而后收集到的依赖发生变化,这个回调才会执行;

    watch一开始就指定了依赖,依赖不发生变更都会执行

  3. 啥时候执行

    watchEffect当回调中引用了响应式的属性变更时,回调就会执行,另外在组件初始化的时候会执行一次; 【自动默认开启了immediate:true

    watch只要监听指定的属性而做出变更,依赖不发生变更都会执行,不过必须是侦听源发生变化时被调用,它是惰性的。【惰性的执行副作用默认是惰性的,不过开启immediate:true也可变成非惰性】

  4. 访问被侦听状态的先前值和当前值

    • 如果是ref 响应类型,watch可以同时拿到被侦听状态的先前值和当前值;watchEffect是先后输出。

    • 如果是reactive响应属性,watch监听不能获取之前数据的值,只能获取当前值;watchEffect能先后输出新旧值。

const name = ref(3)
watch(name, (curVal, prevVal) => {
    console.log(curVal, prevVal)  //4 3
})

watchEffect(()=>{
    console.log(name); //先后输出 3 4
})
function add(){
    name.value++
}
watch(
() => person.child,
(val, oval) =>{
    console.log(val,oval);  //Proxy {son: 3}   Proxy {son: 3}  
    // 也算是监听到了 但显然修改前和修改后的值都变成了修改后的
},{
    deep: true
})

watchEffect(()=>{
    console.log(person.child.son); //先后输出 1 3
})
  1. watch需要侦听特定的数据源,watchEffect不需要特意声明要侦听的属性,受函数里面的响应式属性影响

    watchEffect有点像computed,都是里面的值发生了改变就调用一次,但是呢computed要写返回值,而watchEffect不用写返回值

  2. watch可以侦听多个数据的变化,用一个侦听器承载(注意多个同步更改只会触发一次侦听器

  3. watchEffect可解决某些side effect,借助onInvalidate进行优化,异步操作放在这里更合适

    watchEffect可解决某些side effect,比如用根据页码limit去查询用户详情,然后监听这个limit,当limit改变的时候就会去发起一次请求,这个watch可以做到,但是在请求数据过程中,limit发生多次变化,那么就会发起多次请求,且最后一次返回的数据会覆盖之前返回的所有用户详情。这不仅会导致资源浪费,还无法保证watch回调执行的顺序。使用watchEffect可以解决

<template>
    <div>pageSize:{{pageSize}}</div>
    <div>limit:{{limit}}</div>
    <div>list:{{list}}</div> //当点击停止是获取最后一次值
    <button @click="add">点击加</button>
</template>
setup(props, {emit}){
    watchEffect(() =>{
    // userId多次变化,watchEffect会执行,所以取消异步请求
      const apiCall = asyncMethod(userId)
      onInvalidate(()=>{
        apiCall.cancel()
      })
    })

    let pageSize = ref(1);
    let limit = ref(20);
    let list = ref(null);

    function fetchData(pageSize, limit){
        return new Promise(resolve => {
            // 模拟异步请求数据
            setTimeout(() => {
                resolve(`查询第 ${pageSize} 页, 每页数量 ${limit}`)
            }, 3000)
        })
    }
    watchEffect(async (onInvalidate) => {
        console.log(111) //limit变化一直执行
        let cancel = false;
        onInvalidate(() => {  //没有stop先执行回调 在执行副作用
            console.log(444) //limit变化一直执行  
            cancel = true; // 有stop 执行会停在这里 副作用不在执行
        })
        console.log(222) //limit变化一直执行
        let result = await fetchData(pageSize.value, limit.value)
        // // 已经失效就不更新,limit多次变化,watchEffect会执行,所以取消异步请求
        if(cancel) return ;
        // 会得到最后的值,不会多次请求
        list.value = result;
        console.log(333) // limit停止变化,最后执行
    })
    // 执行顺序 444 111 222
    function add(){
        limit.value ++
    }
    return {
        add,
        pageSize,
        limit,
        list
    }
}

没有stop先执行回调 在执行副作用,有stop 执行会停在onInvalidate函数中副作用不再执行

  1. watchEffect在setup或者生命周期里注册的话,在组件取消挂载的时候会自动的停止掉。watchEffect 最好放在 setup 选项里面。

2. 常用组合式API (生命周期可在这里面体现)

setup

  • 语法:setup(props, {emit,attr,slot, expose})

  • 只能访问 propsattrsslotsemit

执行 setup 时,组件实例尚未被创建。因此,你只能访问:propsattrsslotsemit

换句话说,你将无法访问选项: datacomputedmethodsrefs (模板 ref)

  • 使用渲染函数
setup() {
    const readersNumber = ref(0)
    const book = reactive({ title: 'Vue 3 Guide' })
    // 请注意这里我们需要显式使用 ref 的 value
    return () => h('div', [readersNumber.value, book.title])
}
  • props 是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性。如果需要解构 prop,可以在 setup 函数中使用toRefs 函数来完成此操作
setup(props) {
  const { title } = toRefs(props)
  console.log(title.value)
}
  • 如果 title 是可选的 prop,则传入的 props 中可能没有 title 。在这种情况下,toRefs 将不会为 title 创建一个 ref 。需要使用 toRef 替代它
setup(props) {
  const title = toRef(props, 'title')
  console.log(title.value)
}

Provide / Inject【逻辑用法同Vue2】

生命周期钩子 【见后文】

3. 其他的响应式API,不常用但需要了解

响应性基础API( readonly shallowReadonly shallowReactive isProxy isReactive toRaw markRaw

  • readonly:接受一个对象 (响应式或纯对象) 或 ref 并返回原始对象的只读代理。只读代理是深层的:任何被访问的嵌套 property 也是只读的
const original = reactive({ count: 0 })
const copy = readonly(original) //只读代理
// 变更 original 会触发依赖于副本的侦听器
original.count++
// 变更副本将失败并导致警告
copy.count++ // 警告!

警告: image.png

readonly与 reactive 一样,如果任何 property 使用了 ref,当它通过代理访问时,则被自动解

const raw = {
  count: ref(123) //property 使用了 `ref`
}
const copy = readonly(raw)
console.log(raw.count.value) // 123
console.log(copy.count) // 123    自动解包
  • shallowReadonly:创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(暴露原始值)。

    与 reactive不同,任何使用 ref的 property 都不会被代理自动解包。

const state = shallowReadonly({
  foo: 1,
  nested: {
    bar: 2
  },
  count: ref(123)
})

// 改变 state 本身的 property 将失败
state.foo++
// 但适用于嵌套对象,深层的不是只读的
isReadonly(state.nested) // false
state.nested.bar++ // 适用

console.log(state.count.value) //123  不会被代理自动解包,所以需要加上value

shallowReadonlyreadonly 的区别:

shallowReadonly却只是浅层只读(第一层只读,其余层可以进行修改) readonly是接收了一个响应式数据然后重新赋值,返回的数据就不允许修改(深层只读),>shallowReadonly却只是浅层只读(第一层只读,其余层可以进行修改)

  • shallowReactive:创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (暴露原始值)。

    与 reactive不同,任何使用 ref的 property 都不会被代理自动解包。

const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2
  },
  count: ref(123)
})

// 改变 state 本身的性质是响应式的
state.foo++
// ...但是不转换嵌套对象
isReactive(state.nested) // false
state.nested.bar++ // 非响应式

console.log(state.count.value) //123  不会被代理自动解包,所以需要加上value
  • isProxy:检查对象是否是由 reactive 或 readonly 创建的 proxy。
  • isReactive:检查对象是否是由 reactive 创建的响应式代理。
const state = reactive({
      name: 'John'
})
console.log(isReactive(state)) // -> true

如果该代理是 readonly创建的,但包裹了由reactive创建的另一个代理,它也会返回 true

// 从普通对象创建的只读 proxy
const plain = readonly({
  name: 'Mary'
})
console.log(isReactive(plain)) // -> false

// 从响应式 proxy 创建的只读 proxy
const state = reactive({
   name: 'John'
})
const stateCopy = readonly(state)
console.log(isReactive(stateCopy)) // -> true
  • isReadonly:检查对象是否是由 readonly创建的只读代理。
  • toRaw:返回 reactive或 readonly代理的原始对象。这是一个“逃生舱”,可用于临时读取数据而无需承担代理访问/跟踪的开销,也可用于写入数据而避免触发更改。建议保留对原始对象的持久引用。请谨慎使用
const foo = {}
const reactiveFoo = reactive(foo)
console.log(toRaw(reactiveFoo) === foo) // true
  • markRaw:标记一个对象,使其永远不会转换为 proxy。返回对象本身。
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

// 嵌套在其他响应式对象中时也可以使用
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false

toRaw和 `markRaw的区别:

toRaw其实就是将一个由reactive或 readonly(如果是ref定义的话,包括ref定义的对象,是没有效果的)生成的响应式对象转为普通对象
如果在后续操作中对数据添加的数据为响应式数据,会重新变成响应式数据,当然要是将数据进行markRaw操作后就不会变为响应式。

readonlymarkRaw的区别:

readonly是根本没办法改,但markRaw是不会变成响应式(页面上看不到变化),但是数据还会发生改变。

<template>
    <div>当前姓名:{{person.name}}</div>
    <div>当前年龄:{{person.age}}</div>
    <div>当前儿子数量:{{person.child.son}}K</div>
    <div v-if="person.wife">女朋友:{{person.wife}}</div>
    <div v-if="person.job">工作{{person.job}}</div>
    <button @click="person.name+='!'">点击加!</button>
    <button @click="addAges">点击加一</button>
    <button @click="addChild">点击儿子加一</button>
    <button @click="add">添加女朋友</button>
    <button @click="addAge">添加女朋友年龄</button>
    <button @click="addRef">添加工作</button>
</template>

 setup(props, {emit}){
    let person = reactive({
        name:'hhy',
        age: 18,
        child:{
            son: 1
        }
    })
    function addAges(){
        person.age++
        console.log(person)
    }
    function addChild(){
        let fullName=toRaw(person) // 变成不可代理
        fullName.child.son++
        console.log(fullName)
    }
    function addRef(){
    //添加了ref元素,又变成可代理属性
        let job = ref('前端开发')
        person.job = job
        console.log(person)//Proxy{name: 'hhy', age: 19, child: {…}, job: RefImpl}
    }
    function changeRef(){
        person.job = '开发'
        console.log(person.job) //开发
    }
    function add(){
        let wife = {sex:'女',age: 18}
        person.wife = markRaw(wife)
    }
    function addAge(){
        person.wife.age++
        console.log(person.wife.age)
    }
    return {
      person,
      add,
      addAge,
      addAges,
      addChild,
      addRef
    }
}

Refs(shallowRef triggerRef customRef

创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换

  • shallowRef:创建一个跟踪自身 .value 变化的 ref,但不会使其值也变成响应式的。
const foo = shallowRef({a:1})
// 改变 ref 的值是响应式的
foo.value = {}

console.log(foo.value) //{}
console.log(foo) //RefImpl {}

// 但是这个值不会被转换。
isReactive(foo.value) // false

shallowReactive 和 shallowRef的区别:

shallowReactive浅层次的响应式,它就是只把第一层的数据变为响应式,深层的数据不会变为响应式;
shallowRef如果定义的是基本类型的数据,那么它和ref是一样的不会有什么改变;
shallowRef如果定义的是对象类型的数据,那么它就不会进行响应式,之前我们说过如果ref定义的是对象,那么它会自动调用reactive变为Proxy,但是要是用到的是shallowRef那么就不会调用reactive去进行响应式。

shallowReactive:只处理对象最外层属性的响应式(浅响应式)。 shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理。

  • triggerRef:手动执行与 shallowRef关联的任何作用 (effect)。
const shallow = shallowRef({
  greet: 'Hello, world'
})
// 第一次运行时记录一次 "Hello, world"
watchEffect(() => {
  console.log(shallow.value.greet)
})
// 这不会触发作用 (effect),因为 ref 是浅层的
shallow.value.greet = 'Hello, universe'
// 记录 "Hello, universe"
triggerRef(shallow)
  • customRef:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 track 和 trigger 函数作为参数,并且应该返回一个带有 get 和 set 的对象。

使用自定义 ref 通过 v-model 实现 debounce (一定时间内只执行一次)的示例:

<input v-model="text" />

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')
    }
  }
}

watchPostEffect 和 watchSyncEffect

  • watchPostEffectwatchEffect 的别名,带有 flush: 'post' 选项。
  • watchSyncEffectwatchEffect 的别名,带有 flush: 'sync' 选项。

Effect作用域API(effectScopegetCurrentScopeonScopeDispose

Effect 作用域是一个高阶的 API,主要服务于库作者,不额外总结,请看官网

4. 其他的组合式API,不常用但需要了解

getCurrentInstance

  • getCurrentInstance 支持访问内部组件实例。只暴露给高阶使用场景,典型的比如在库中。 强烈反对在应用的代码中使用 getCurrentInstance。请不要把它当作在组合式 API 中获取 this 的替代方案来使用。

vue3与vue2的区别

vue3的template支持不用根标签包裹

减少便签层级,减少内存占用。

<template>
12
</template>
能在页面中渲染,不报错

router写法不一样

在vue3中,定义了一个vue-router然后引入的useRoute,useRouter 相当于vue2的 this.$routethis.$router, 但是其他之前vue2的操作都可以进行。

比如进行跳转不是用this.$router.push('/about'),而是useRouter().push('/about')

import {useRouter,useRoute} from "vue-router";
setup(){
  const router= useRouter()
  const route= useRoute()
  function jump(){
    router.push('/about')
  }
  onMounted(()=>{
    let path = route.path
    let query = route.query
  })
  return{
    jump
  }
}

全局API写法不一样

2.x 全局 API(Vue3.x 实例 API (app)
Vue.config.xxxxapp.config.xxxx
Vue.config.productionTip移除
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use
Vue.prototypeapp.config.globalProperties

钩子函数写法不一样

选项式 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated

生命周期原理不一样,借用图片:

image.png

image.png

在vue2中,我们是先new Vue(),然后执行beforeCreatecreated接着问你有没有vm.$mount(el),有,才继续执行,但是在vue3中,它是先全部准备好后然后再进行函数。

其实在vue3中生命周期没有多大的改变,只是改变了改变了销毁前,和销毁,让它更加语义化了 beforeDestroy改名为beforeUnmount,destroyed改名为unmounted

移除vue2的一些写法

  • 移除keyCode作为 v-on 的修饰符,同时也不再支持config.keyCodes

  • 移除v-on.native修饰符

  • 移除过滤器(filter

vue3新增Teleport标签

teleport 提供了一种有趣的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件。

其实就是可以不考虑你写在什么位置,你可以定义teleport在任意标签里进行定位等(常见操作为模态框),除了body外,还可以写css选择器(id,class

// 在同一目标上使用多个 teleport
<teleport to="#modals">
  <div>A</div>
</teleport>
<teleport to="#modals">
  <div>B</div>
</teleport>

<!-- result-->
<div id="modals">
  <div>A</div>
  <div>B</div>
</div>

具体可参考:teleport

vue3 watch可以监听多个属性

响应式原理不一样

可参考精品文章:# ue3 的响应式和以前有什么区别,Proxy 无敌?

diff算法不一样

可精读我的另一篇:react+vue2+vue3 diff算法分析及比较

静态标记

diff算法不比较静态标签

事件缓存

静态提升

TS+vue3+vite注意几点

  1. 定义全局属性
import { ComponentCustomProperties } from 'vue';
import { Store } from 'vuex';

declare module '@vue/runtime-core' {
  // declare your own store states
  interface State {
    user: any;
  }

  // provide typings for `this.$store`
  interface ComponentCustomProperties {
    $store: Store<State>;
    t: any;
  }
}
  1. vite.config.js、tsconfig配置
resolve: {
    alias: {
      '@': resolve(__dirname, './src'),
      '@topology': resolve(__dirname, '../topology.js/packages'),
    }
  },
{
  "compilerOptions": {
    "baseUrl": ".",
    "types": ["vite/client"],
    "paths": {
      "@/*": ["src/*"],
      "@topology/*": ["../topology.js/packages/*"]
    },
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
  1. shims-vue.d.ts 中申明 defineComponent
// 申明在.vue文件中使用,vue3.0+ts出现不提示@/components/xxx.vue的现象,需要加此来解决
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

vite和webpack比较

vite的优点:

  • 无需打包,快速的冷服务器启动
  • 即时热模块更换(HMR,热更新)
  • 真正的按需编译

webpack是一开始是入口文件,然后分析路由,然后模块,最后进行打包,然后告诉你,服务器准备好了(默认8080)

image.png

然而vite是什么,它一开始是先告诉你服务器准备完成,然后等你发送HTTP请求,然后是入口文件,Dynamic import(动态导入)code split point(代码分割)

image.png

结束撒花

对了有手痒的想实战项目的话可以去看一看大佬的🎉🎉Vue 3 + Element Plus + Vite 2 的后台管理系统开源啦🎉🎉

想深入源码分析可阅读:

【Vue3源码分析】响应式原理
【Vue源码分析】Refs
【Vue3源码分析】computed