对比Vue2总结Vue3新特性(2022年最全,2.5w字!)

简介

有没有小伙伴跟笔者一样vue3项目做了好几个了,但是一直没有去总结vue3的新特性呢?

今天笔者通过对比vue2来总结vue3新特性,希望可以让你们在回顾vue2知识点的时候还能学习vue3新的知识。相信你认真看完一定会有收获。

新插件

正所谓工欲善其事,必先利其器。在讲解vue3新特性之前,笔者先来介绍几个插件。这样会大大提高我们的开发效率和体验。

Volar

使用vscode开发vue2项目的小伙伴肯定都认识Vetur这个神级插件。但是在vue3中这个插件就显得捉襟见肘了,比如vue3的多片段这个插件就会报错。

这个时候就需要使用VolarVolar可以理解为Vue3版本的Vetur,代码高亮,语法提示,基本上Vetur有的它都有。

对比Vue2总结Vue3新特性(2022年最全,2.5w字!)_第1张图片

Vue 3 Snippets

vue2中我们一直使用Vue 2 Snippets,在vue3我们推荐使用Vue 3 Snippets,因为它支持vue3的同时完全向前兼容vue2,所以小伙伴们赶快去升级吧。

对比Vue2总结Vue3新特性(2022年最全,2.5w字!)_第2张图片

Vue.js devtools beta

vue2版本的chrome devtools不再支持vue3vue3我们需要单独下载Vue.js devtools beta。(下载devtools是需要梯子的哦,如果没有可以联系笔者)。

在下载Vue.js devtools beta之前,我们需要先卸载vue2版本的Vue.js devtools,不然会有警告。

对比Vue2总结Vue3新特性(2022年最全,2.5w字!)_第3张图片

兼容性

vue3固然好用但是我们还是不能盲目追求新东西,在使用vue3开发之前我们需清楚的知道它的兼容性。

vue2 不支持 IE8 及以下版本,因为 Vue 使用了 IE8 无法模拟的 ECMAScript 5 特性。但它支持所有兼容 ECMAScript 5 的浏览器。

vue3 不支持 IE11 及以下版本。

响应性 API

vue2中,我们只要定义在data()方法中的数据就是响应式数据。或者使用Vue.observable()方法来定义响应式数据。

还可以使用this.$set( target, propertyName/index, value )Vue.set( target, propertyName/index, value )来给对象或数组添加响应式属性。使用this.$delete( target, propertyName/index)Vue.delete( target, propertyName/index)来给对象或数组删除响应式属性。

vue2中使用Vue.observable()方法。

const state = Vue.observable({ count: 0 })

但在vue3中主要是使用refreactive来定义响应式数据。由于vue3使用的是proxy进行响应式监听,所以新增、删除属性也都是响应式的,也就不需要使用上面的set delete了。

ref和isRef

接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。

一般用来定义基本类型的响应式数据。注意这里说的是一般,并不是说ref就不能定义引用类型的响应式数据。

使用ref定义的响应式数据在setup函数中使用需要加上.value,但在模板中可以直接使用。

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

<template>
  <h3>count1</h3>
  <div>count1: {{ count1 }}</div>
  <button @click="plus">plus</button>
  <button @click="decrease">decrease</button>
  
  <div>user1: {{ user1.name }}</div>
  <button @click="updateUser1Name">update user1 name</button>
</template>

<script>
import { defineComponent, ref, isRef } from "vue";
export default defineComponent({
  setup() {
    const count1 = ref(0);

    const plus = () => {
      count1.value++;
    };
    const decrease = () => {
      count1.value--;
    };
    
    const user1 = ref({ name: "randy1" });
    const updateUser1Name = () => {
      // ref定义的变量需要使用.value修改
      user1.value.name += "!";
    };
    
    console.log(isRef(count1)); // true

    return {
      count1,
      plus,
      decrease,
      user1,
      updateUser1Name
    };
  },
});
</script>

ref除了定义响应式数据还可以定义模板引用,类似vue2this.$refs这个后面笔者在讲模板引用的时候会细说。

<template> 
  <div ref="root">This is a root element</div>
</template>

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

  export default {
    setup() {
      // 创建
      const root = ref(null)

      onMounted(() => {
        // 获取子组件
        console.log(root.value) // 
This is a root element
}) return { root } } } </script>

shallowRef

创建一个跟踪自身 .value 变化的 ref,但不会使其值也变成响应式的。

这句话怎么理解呢?就是我们使用shallowRef创建出来的数据不是响应式的,也就是我们的修改页面并不会重新渲染。但是我们直接修改数据.value是会响应式的。

下面我们来看例子。

const sRef1 = shallowRef(0);
console.log("shallowRef:", sRef1.value); // 0

// 假设点击页面按钮,触发该方法
const changeShallowRef1 = () => {
  // 直接修改value,页面会同步修改,也就是会响应式
  sRef1.value++;
};

const sRef2 = shallowRef({ name: "demi1" });

// 假设点击页面按钮,触发该方法
const changeShallowRef2 = () => {
  // 不直接修改value,而是修改属性值
  sRef2.value.name = "randy";
  sRef2.value.address = { city: "汨罗" }; // 添加新属性
  // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式
  console.log(sRef2.value); // {address: {city: '汨罗'}, name: "randy"}
  
  // 但是我们直接重新赋值,页面会立马重新渲染
  sRef2.value = {name: "randy", address: {city: '汨罗'}}
};

// 假设点击页面按钮,触发该方法
const changeShallowRef3 = () => {
  // 但是我们直接重新赋值,页面会立马重新渲染
  sRef2.value = {name: "randy", address: {city: '汨罗'}}
};

通过上面的例子我们可以发现,当响应式数据是基本数据类型的时候refshallowRef没有差别。但是如果数据是引用数据类型的话ref的数据是响应式的而shallowRef不是,shallowRef需要给value重新赋值才会触发响应式。

reactive和isReactive

reactive用来定义引用类型的响应式数据。注意,不能用来定义基本数据类型的响应式数据,不然会报错。

reactive定义的对象是不能直接使用es6语法解构的,不然就会失去它的响应式,如果硬要解构需要使用toRefs()方法。

isReactive用来检查对象是否是由 reactive 创建的响应式代理。

<template>
  <div>
    <h3>user2</h3>
    <div>user2: {{ user2.name }}</div>
    <button @click="updateUser2Name">update user2 name</button>

    <h3>user3</h3>
    <div>user3 name: {{ name }} user3 age: {{ age }}</div>
    <button @click="updateUser3Name">update user3 name</button>

    <h3>count2</h3>
    <div>count2: {{ count2 }}</div>
    <button @click="plus2">plus2</button>
    <button @click="decrease2">decrease2</button>
  </div>
</template>

<script>
import { defineComponent, reactive, toRefs, isReactive } from "vue";
export default defineComponent({
  setup() {
    const _user = { name: "randy2" }
    const user2 = reactive(_user);
    const updateUser2Name = () => {
      // reactive定义的变量可以直接修改
      user2.name += "!";
      
      // 原始对象的修改并不会响应式,也就是页面并不会重新渲染
      // _user.name += "!";
      // 代理对象被改变的时候,原始对象会被修改
      // console.log(_user);
    };
    
    // 使用toRefs可以响应式解构出来,在模板能直接使用啦。
    const user3 = reactive({ name: "randy3", age: 24 });
    const updateUser3Name = () => {
      user3.name += "!";
    };

    // 使用reactive定义基本数据类型会报错
    const count2 = reactive(0);

    const plus2 = () => {
      count2.value++;
    };
    const decrease2 = () => {
      count2.value--;
    };
    
    // 检查对象是否是由 reactive 创建的响应式代理。
    console.log(isReactive(user2)); // true
    console.log(isReactive(count2)); // false

    return {
      user2,
      updateUser2Name,
      // ...user3, // 直接解构不会有响应式
      ...toRefs(user3),
      updateUser3Name,
      count2,
      plus2,
      decrease2,
    };
  },
});
</script>

reactive 将解包所有深层的 refs,同时维持 ref 的响应性。

怎么理解这句话呢,就是使用reactive定义响应式对象,里面的属性是ref定义的话可以直接赋值而不需要再.value,并且数据的修改是响应式的。

const count = ref(1)
// 可以直接定义,而不是{count: count.value}
const obj = reactive({ count })

// 这种写法也是支持的
// const obj = reactive({})
// obj.count = count

// ref 会被解包
console.log(obj.count === count.value) // true

// 它会更新 `obj.count`
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2

// 它也会更新 `count` ref
obj.count++
console.log(obj.count) // 3
console.log(count.value) // 3

shallowReactive

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

并且与 reactive不同,任何使用 refproperty不会被代理自动解包。

简单理解就是响应式只会在第一层,不会深层响应式。类似于浅拷贝。

const user1 = shallowReactive({
  name: "demi1",
  address: { city: "汨罗", count: 10 },
});

// 假设点击页面按钮,触发该方法
const changeUser1 = () => {
  // 响应式,页面会发生变化
  user1.name = "demi1 !!!";
};

// 假设点击页面按钮,触发该方法
const changeUser2 = () => {
  // 非响应式,也就是页面不会发生变化
  user1.address.city = "岳阳";
  user1.address.count++;
  // 这里数据虽然改变了,但是页面不会更新,也就是说不会响应式
  console.log(user1); // {address: {city: '岳阳', count: 11}, name: "demi1 !!!"}
};
console.log(isReactive(user1)); // true
console.log(isReactive(user1.address)); // false

readonly和isReadonly

接受一个对象 (响应式或纯对象) 或 ref数据 并返回原始对象的只读代理。只读代理是深层的:任何被访问的嵌套 property 也是只读的。

怎么理解这句话呢,就是说只要是对象不管是普通对象还是reactive定义的对象或者是ref定义的数据,定义成readonly后就不能被修改了。

这里需要特别注意,是readonly返回的对象变成只读,源对象不会受到影响,所以修改源对象还是可以的。

isReadonly用来检查对象是否是由 readonly 创建的只读代理。

// ref定义的数据会被限制,不能被修改
let name1 = ref("readonly randy");
// readOnlyName1才是只读的
let readOnlyName1 = readonly(name1);
const changeName1 = () => {
  readOnlyName1.value += "!";
  // 这里直接修改源对象还是可以的
  // name1.value += "!";
};

// 基本数据类型数据会无效,能被修改
let readOnlyName2 = readonly("readonly randy");
readOnlyName2 = "randy";
console.log(readOnlyName2); // randy

// reactive定义的对象会被限制,不能被修改
const reactiveUser1 = reactive({ name: "readonly randy" });
let readonlyUser1 = readonly(reactiveUser1);
const changeUserName1 = () => {
  readonlyUser1.name += "!";
  // 这里直接修改源对象还是可以的
  // reactiveUser1.name += "!";
};

// 普通对象也会被限制,不能被修改
let readonlyUser2 = readonly({ name: "readonly randy" });
readonlyUser2.name = "randy";
console.log(readonlyUser2.name); // readonly randy

console.log(isReadonly(readOnlyName1)); // true
console.log(isReadonly(readOnlyName2)); // false
console.log(isReadonly(readonlyUser1)); // true
console.log(isReadonly(readonlyUser2)); // true

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

const raw = {
  count: ref(123)
}

// 这里就类似const copy = reactive(raw)
const copy = readonly(raw)

console.log(raw.count.value) // 123
console.log(copy.count) // 123

shallowReadonly

浅只读,创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)。

并且与 readonly不同,任何使用 refproperty不会被代理自动解包。

简单理解就是只读的限制只会在第一层,不会深层只读。类似于浅拷贝。

const user2 = shallowReadonly(
  reactive({
    name: "demi2",
    address: { city: "汨罗2", count: 10 },
  })
);
console.log(isReadonly(user2)); // true
console.log(isReadonly(user2.address)); // false
const changeUser2 = () => {
  // 响应式,页面会同步修改
  user2.address.city = "岳阳";
  user2.address.count++;
  
  // 非响应式,也就是页面不会重新渲染该值
  user2.name = "demi1 !!!";
};

isProxy

检查对象是否是由 reactivereadonly 创建的 proxy

// ref定义的所以返回false
let name1 = ref("readonly randy");
// ref是对象,所以返回true
let readOnlyName1 = readonly(name1);

// readonly失败返回false
let readOnlyName2 = readonly("readonly randy");

const reactiveUser1 = reactive({ name: "readonly randy" });
let readonlyUser1 = readonly(reactiveUser1);

let readonlyUser2 = readonly({ name: "readonly randy" });

// 检查对象是否是由 reactive 或 readonly 创建的 proxy。
console.log(isProxy(name1)); // false
console.log(isProxy(readOnlyName1)); // true
console.log(isProxy(readOnlyName2)); // false
console.log(isProxy(reactiveUser1)); // true
console.log(isProxy(readonlyUser2)); // true

上面的例子有些小伙伴看了会比较懵逼,为什么readonly有些是true有些又是false呢?其实你弄懂了readonly就大概会清楚了。readonly是不能处理基本数据类型的,所以readonly不成功就会返回false

toRaw

返回 reactivereadonly代理的原始对象。这是一个“逃生舱”,可用于临时读取数据而无需承担代理访问/跟踪的开销,也可用于写入数据而避免触发更改。建议保留对原始对象的持久引用。请谨慎使用。

const foo = {}
const reactiveFoo = reactive(foo)

console.log(toRaw(reactiveFoo) === foo) // true

markRaw

标记一个对象,使其永远不会转换为 proxy。返回对象本身。

因为不会被proxy,也就是说不会响应式,相当于一个普通值。

const info = markRaw({ sex: "male" });
console.log(isReactive(reactive(info))); // false

const user3 = reactive({ name: "randy", info });
const changeUser3 = () => {
  user3.info.sex = "female";
  // 这里数据虽然变了,但是页面并不会重新渲染,也就是说不会响应式
  console.log(user3); // {info: {sex: 'female'}, name: "randy"}
};

unref

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

const user1 = ref({ name: "randy1" });
console.log("unref: ", unref(user1), user1.value);

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

当你要将 propref 传递给复合函数时,toRef 很有用:

export default {
  setup(props) {
    useSomeFeature(toRef(props, 'foo'))
  }
}

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

toRefs

将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 propertyref

const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型:

{
  foo: Ref,
  bar: Ref
}
*/

// ref 和原始 property 已经“链接”起来了
state.foo++
console.log(stateAsRefs.foo.value) // 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
    }
  }
}

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

customRef

创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 tracktrigger 函数作为参数,并且应该返回一个带有 getset 的对象。

这个在我们自定义响应式的时候非常有用。比如我们在获取、设置值的时候做些特殊处理。

这个在vue2中是没办法直接修改响应值的实现的,但是在vue3可以。

下面是一个延迟响应式的例子。

<template>
  <div class="customref">
    <div>{{ text }}</div>
    <input v-model="text" />
  </div>
</template>
<script>
import { defineComponent, customRef } from "vue";

export default defineComponent({
  setup() {
    const useDebouncedRef = (value, delay = 2000) => {
      let timeout;
      return customRef((track, trigger) => {
        return {
          get() {
            track();
            return value;
          },
          set(newValue) {
            clearTimeout(timeout);
            timeout = setTimeout(() => {
              value = newValue;
              trigger();
            }, delay);
          },
        };
      });
    };
    return {
      text: useDebouncedRef("randy"),
    };
  },
});
</script>

triggerRef

手动执行与 shallowRef 关联的任何作用 (effect)。

const shallow = shallowRef({
  greet: 'Hello, world'
})

// 第一次运行时输出"Hello, world"
watchEffect(() => {
  console.log(shallow.value.greet)
})

// 这不会触发作用 (effect),因为 ref 是浅层的
shallow.value.greet = 'Hello, universe'

// 触发watchEffect 输出"Hello, universe"
triggerRef(shallow)

组合式 API

为了让相关代码更紧凑vue3提出了组合式api,组合式api能将同一个逻辑关注点相关代码收集在一起。 组合式api的入口就是setup方法。

setup

用官方语言说,setup是一个组件选项,在组件被创建之前props 被解析之后执行。它是组合式 API 的入口。

setup的写法有两种,可以跟vue2一样直接导出也可以导出defineComponent对象。若要对传递给 setup() 的参数进行类型推断,你需要使用 defineComponent。

// 直接export
export default {
  setup(){}
}

import { defineComponent } from 'vue'
// 导出defineComponent对象
export default defineComponent({
  setup(){}
})

执行时机

从生命周期的角度来看,它会在beforeCreate之前执行。也就是创建组件会依次执行setupbeforeCreatecreate

this指向

setup 中你应该避免使用 this,因为它不会找到组件实例。setup 的调用发生在 data property、computed property 或 methods 被解析之前,所以它们无法在 setup 中被获取。

参数

setup 选项是一个接收 propscontext 的函数。

props

setup 函数中的第一个参数是 propsprops就是我们父组件给子组件传递的参数。

正如在一个标准组件中所期望的那样,setup 函数中的 props 是响应式的,当传入新的 prop 时,它将被更新。

import { defineComponent } from 'vue'

export default defineComponent({
  props: {
    title: String
  },
  setup(props) {
    console.log(props.title)
  }
})

因为 props 是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性。

如果需要解构请使用toRefs方法。

import { defineComponent, toRefs } from 'vue'

export default defineComponent({
  props: {
    title: String
  },
  setup(props) {
    const { title } = toRefs(props)
    console.log(title.value)
  }
})

如果 title 是可选的 prop,则传入的 props 中可能没有 title 。在这种情况下,toRefs 将不会为 title 创建一个 ref 。你需要使用 toRef 替代它:

import { defineComponent, toRef } from 'vue'

export default defineComponent({
  props: {
    title: String
  },
  setup(props) {
    const title = toRef(props, 'title')
    console.log(title.value)
  }
})
context

context 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context 使用 ES6 解构。

import { defineComponent } from 'vue'

export default defineComponent({
  setup(props, context) {
    // Attribute (非响应式对象,等同于 $attrs)
    console.log(context.attrs)

    // 插槽 (非响应式对象,等同于 $slots)
    console.log(context.slots)

    // 触发事件 (方法,等同于 $emit)
    console.log(context.emit)

    // 暴露公共 property (函数)
    console.log(context.expose)
  }
})

attrsslots 是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.xslots.x 的方式引用 property。请注意,与 props 不同,attrsslotsproperty响应式的。如果你打算根据 attrsslots 的更改应用副作用,那么应该在 onBeforeUpdate 生命周期钩子中执行此操作。

这里我们重点说下expose的使用。

假如我们想在父组件中直接调用子组件的方法该怎么做呢?我们就可以在子组件中使用expose把属性或方法暴露出去。在父组件我们就可以通过子组件的ref直接调用了。

使用的时候需要注意:

  1. 当组件没定义expose暴露内容的时候,通过ref获取到的就是组件自身的内容,也就是setup函数return的内容。

  2. 当定义了expose暴露内容的时候,通过ref获取到的就是组件expose暴露内容,并且setup函数return的内容会失效,也就是会被覆盖。

// 子组件
<template>
  <div>
    <h2>child</h2>
  </div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
  setup(props, { expose }) {
    const childSay = () => {
      console.log("childSay");
    };
    const sex = 'male'

    // 如果定义了会覆盖return中的内容
    // expose({
    //   sex
    // });
    
    return {
      childSay,
      childSay
      
    }
  },
});
</script>

// 父组件
<template>
  <div>
    <LifeChild ref="childRef"/>
  </div>
</template>
<script>
import {
  defineComponent,
  onMounted,
  ref,
} from "vue";

import LifeChild from "@/components/LifeChild";

export default defineComponent({
  components: {
    LifeChild,
  },
  setup() {
    const childRef = ref(null);

    onMounted(() => {
      // 使用子组件暴露的属性和方法
      childRef.value.childSay(); // childSay
      console.log(childRef.value.sex); // male
    });

    return {
      childRef,
    };
  },

});
</script>

返回值

setup 返回的所有内容都暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。所以我们在模板中需要使用到的数据都需要通过setup方法return出来。

上面的话怎么理解呢?就是我们在模板,或者vue2选项式写法的计算属性、方法、生命周期钩子等等中使用的数据都需要在setup方法中通过return返回出来。

结合模板使用

如果 setup 返回一个对象,那么该对象的 property 以及传递给 setupprops 参数中的 property 就都可以在模板中访问到。

<template>
  <div>{{ collectionName }}: {{ number }} {{ user.name }}</div>
</template>

<script>
  import { ref, reactive, defineComponent } from 'vue'

  export default defineComponent({
    props: {
      // 这个属性能直接在模板中使用
      collectionName: String
    },
    setup(props) {
      // 在setup函数中需要通过props获取。
      console.log(props.collectionName)
      
      // 定义响应式数据
      const number = ref(0)
      const user = reactive({ name: 'randy' })

      // 暴露给 template
      return {
        number,
        user
      }
    }
  })
</script>

这里我们通过refreactive创建了响应式数据,具体差别后面会再细说。

结合渲染函数使用

setup 还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态。

import { h, ref, reactive } from 'vue'

export default {
  setup() {
    const number = ref(0)
    const user = reactive({ name: 'randy' })
    // 请注意这里我们需要显式使用 ref 的 value
    return () => h('div', [number.value, user.name])
  }
}

返回一个渲染函数将阻止我们返回任何其它的东西。我们可以通过我们上面介绍的 expose 来解决这个问题,给它传递一个对象,其中定义的 property 将可以被外部组件实例访问。

单文件setup

要使用这个语法,需要将 setup attribute 添加到

你可能感兴趣的:(vue,vue.js,vue,前端,前端框架,javascript)