Vue3语法变更总结


1.生命周期的变更

  • 销毁的概念改为卸载,更名:

    • beforeDestroy改名为beforeUnmounted
    • destroyed改名为unmounted
  • vue2中的两次判断改为一次(如图)。

    image-20220810114820457

  • Vue3提供的**Composition API**形式对比:

    • beforeCreate => setup()
    • created => setup()
    • beforeMount => onBeforeMount
    • mounted => onMounted
    • beforeUpdate => onBeforeUpdate
    • updated => onUpdated
    • beforeUnmounted => onBeforeUnmounted
    • unmounted => onUnmounted
  • 组合式API的vue2生命钩子的优先级:

    • 下图打印结果可以看到:setup中的组合式API优先级更高

2. setup函数

2.1 setup的两个注意点:

  • setup执行时机

    • 在beforeCreated之前执行一次,并且this是undefined。
  • setup接收两个参数

    • 1.**props**:值为对象。包含:组件外部传递进来,且在组件内部声明接收了的属性。

      • 当子组件传了,父组件却没接收/接收少了,都会控制台警告。接收多了则会拿到undefined。
      1. **context**:上下文对象

        • **attrs**:值为对象。包含:组件外部传递过来但没有在props配置中生命的属性,相当于this.$attrs。

        • **slots**:收到的插槽内容,相当于this.$slots。

        • **emit**:分发自定义事件的函数,相当于this.$emit。

2.2 setup语法糖

什么时候使用setup()

image-20220818121239514


2.3 其他补充

1. vue3中插槽的使用变更:

  • 在vue3中使用插槽尽量用v-slot
  • vue2中template里使用插槽可以使用:1. name="xxx" 2. v-slot="xxx" 。但由于vue3的兼容问题,使用第一种方法依旧显示的是默认插槽,自定义插槽名会失效。

2. vue3中props接收数据:

vue2中props接收数据

  • 如果不用props,在VC的实例对象中也能拿到,通过this.$attrs;但如果使用props后,attrs中的数据就会消失(attrs就像捡漏)。

3. Ref

  • 作用:定义一个响应式的数据。

  • 语法const xxx = ref(initValue)

    • 创建一个包含响应式数据的引用对象(reference对象)
    • 模板中使用引用对象不用后面加.value,而setup中使用该值时需要加.value
  • 接收的数据

    • 可以是:基本类型、对象类型。
    • ref处理 基本类型: 响应式依旧使用是defineProperty ,以set & get数据劫持的方式。
    • 而处理 对象类型时使用proxy实现的(proxy是封装在reactive函数中的,ref内部会自动通过reactive转为代理对象)。
    • 补充:使用ref数据要 .value 是因为Object.defineProperty本身无法作用于基本数据类型,所以通过对象做了一层转化
  • 使用ref获取节点

  • **注意:如果要在setup中拿到DOM元素,声明的变量名必须与元素中写的ref一致**。

    <template>
      <div id="countBox" ref="countupRef">
        <span class="countSpan"></span>
      </div>
    </template>
    
  • 在**onMounted**中可以拿到节点:

      setup() {
          // 这里的变量名countupRef必须与ref写的一样:
        const countupRef = ref(null);
        console.log("ref", countupRef);
    
        onMounted(() => {
          // 拿到ref节点
          console.log(countupRef);
        });
    
        return {
          countupRef
        };
      },
    

4. reactive

  • 作用:定义一个对象/数组类型的响应式数据(基本类型别用,要用ref函数)。

  • 语法const 代理对象 = reactive(源对象); 接收一个对象或数组,返回一个**代理对象**(Proxy的实例对象,简称proxy对象)

    • 绿色框里的就是源对象,粉色的是代理对象:

    image.png

  • reactive定义的响应式数据是”深层次的“。

  • 内部基于ES6的Proxy实现,通过代理对象操作源对象内部的数据。

5. ref对比reactive总结

  • 从定义数据据角度对比

    • ref定义:基本类型数据
    • reactive定义:对象/数组类型数据
    • 注意:ref也可以定义对象/数组类型数据,它内部会自动通过reactive转为代理对象
  • 从原理角度对比

    • ref:通过Object.defineProperty()getset实现响应式(数据劫持)。
    • reactive:通过使用Proxy实现响应式(数据劫持),并通过Reflect操作源对象内部的数据。
  • 从使用角度对比

    • ref定义的数据:操作数据需要.value,读取数据时模板会直接读取不用.value
    • reactive定义的数据:操作数据与读取数据均不需要.value

6. Vue2&Vue3响应式原理对比

6.1 Vue2的响应式

  • 实现原理

    • 对象类型:通过Object.defineProperty() 对属性的读取、修改进行拦截(数据劫持)。
    • 数组类型:通过重写更新数组的方法实现拦截
      • 比如:调用push方法,其实调用的是vue2二次封装后的push。
      • 这个push里做了两件事:
        • 1.帮助正常调用push方法。
        • 2.帮助更新界面。
  • 存在的问题

    • 新增属性 or 删除属性,界面不会自动更新
    • 直接通过下标修改数组,界面也不会自动更新。
    // 修改对象:
    //     添加属性:
    this.$set(target, 'name', value);
    Vue.set(target, 'name', value);
    // 删除属性:
    this.$delete(target, 'name', value);
    Vue.delete(target, 'name', value);
    
    // 修改数组:
    this.xxx.splice(0, 1, '');
    
    image.png
  • Object.defineProperty:

    • 读取和修改有getset方法,但是添加和删除是捕获不到的。
    • configurable配置项:能否使用delete删除属性,默认值为false。

6.2 Vue3的响应式

6.2.1实现原理:

// 模拟Vue3中实现响应式:
const p = new Proxy(person,  {
    // 读取某个属性时调用此方法:接收两个参数:源对象&当前属性名
    get(target, propName) {
        console.log(`${propName}属性被读取~`);
        return target[pcropName];    
    },
    // 修改/增加某个属性时调用此方法:接收两个参数:比get多收到一个value值。
    set(target, propName, value) {
        console.log(`${propName}属性被修改,准备更新界面`);
        target[propName] = value;    
    },
    // 删除某个属性时调用此方法:
    deleteProperty(target, propName) {
        console.log(`${propName}属性被修改,准备更新界面`);
        // delete target[propName]  最好是把真实结果给他返回:
        return delete target[propName]   
    }
})

绿色框在真正修改源数据:

image.png

6.2.2. 响应式中Reflect的应用:

  • ES6,**Windows内置**。

  • 增删改查用法:reflect.get、reflect.set、 reflect.delete。

  • reflect里也有defineProperty方法,且有返回值(若不返回的话就不知道是否操作成功,因为它不报错。返回值是布尔值)。

与Object.definProperty对比

  • 在做封装的时候,Object.definProperty捕获错误只能通过try catch。若使用reflect就可以避免。https://www.bilibili.com/video/BV1Zy4y1K7SH?p=147&t=672.1
  • 模Vue3中实现响应式(使用reflect改进后):
    • 用到了proxy代理对象,及reflect反射对象。
// 模拟Vue3中实现响应式:
const p = new Proxy(person,  {
    // 读取某个属性时调用此方法:接收两个参数:源对象&当前属性名
    get(target, propName) {
        console.log(`${propName}属性被读取~`);
        return Reflect.get(target, propName);    
    },
    // 修改/增加某个属性时调用此方法:接收两个参数:比get多收到一个value值。
    set(target, propName, value) {
        console.log(`${propName}属性被修改,准备更新界面`);
        Reflect.set(target, propName, value);    
    },
    // 删除某个属性时调用此方法:
    deleteProperty(target, propName) {
        console.log(`${propName}属性被修改,准备更新界面`);
        // delete target[propName]  最好是把真是结果给他返回:
        return Reflect.deleteProperty(target, propName); 
    }
})

7. toRef & toRefs

1. toRef

  • 作用:创建个ref对象,其value的值指向另一个对象的某个属性。

  • 应用:要将响应式对象中的某个

  • 参数:1.目标对象;2.属性名。

    • 举例
    const obj = {
        a: 10,
        b: 'start'
    }
    
    const obj2 = toRef(obj)
    
    return {
        obj,
        // 返回的格式:
        ...toRef(obj)
    }
    
  • 和ref的区别:

对比

let person = reactive({
    name: '',
    hobby: {
        food: ''    
    }
})

// 这里拿到的只是一个属性值:
const name = person.name;
console.log('name', name)

// 使用toRef:
const name2 = toRef(person, 'name');
// 此时name不再是一个值,而是包在ref对象里的属性。
console.log('name2', name2)

如果要在setup函数return中:

注意:这里return中ref也可以但ref生成的是新的数据,但使用toRef数据依旧是保持联系的。这是两者的区别

return {
    name: toRef(person, 'name');
    food: toRef(person.hobby, 'food')
}

当setup中的数据,return一个对象类型数据后,模板中每次使用都需要对象.xxxx。当在return时就写好对象.xxxx的话,会出现一种情况:数据不再是响应式。

举例验证一下原因

相当于:最后一行name是另外新定义的,和p.name没任何关联,所以响应式会没了。

image.png

2. toRefs

  • 作用:需要使用多个toRef时,可以用toRefs简写。
  • 使用:只需给一个参数。无需重复给多个属性写toRef。
  • 语法:toRefs(obj)

注意

  • return的时候不能直接就用,return的是对象,而toRefs返回的也是对象,直接写会报错。
  • 按照这种格式(…obj):
return {
    // 这样数据的格式才正确:
    ...toRefs(person)
}

8. computed、watch与watchEffect

1. computed

  • 与vue2中computed配置功能一致。
  • 写法:
import { computed } from 'vue';

...
    // vue3中computed与vue2语法一样:
    let personObj = computed({
      name: 'a',
      age: 22
    });

完整:

image.png

2. watch函数

  • 与vue2中配置功能一致。
  • 两个小坑:
    • 监视reactive定义的响应式数据时:oldValue无法正确获取;强制开启了深度监视(deep配置失效)
    • 监视reactive定义的响应式数据中的某个属性时:deep配置有效

2.1 vue3中watch的变化:

  • watch能传递3个参数

    • 监视谁:字符串/数组。
    • 监视的回调:函数。
    • 配置项:对象。
  • 监视reactive所定义的数据时,默认开启deep深度监视。且修改无效。

  • immediate配置项的使用:

    • 情况1&2中,配置有效。
  • deep配置项的使用:

    • 当监视的数据由reactive直接管理时deep默认开启
    • 当嵌套层级较深,则需手动配置开启。
  • 获取不到oldValue的情况:可以总结为当是复杂数据类型时就获取不到。

2.2 六种使用情况:

1. 监视一个ref定义的数据 基本数据类型。immediate可用;deep无需配置

2. 监视多个ref定义的数据 基本数据类型。immediate可用;deep无需配置

3. 监视reactive定义的全部数据 deep配置无效;oldValue无法获取

4. 监视一个reactive定义的数据中的属性 监视的属性要写为函数形式;oldValue可获取

5. 监视多个reactive定义的数据中的属性 监视的属性要写在数组里且每项都是函数;oldValue可获取

6. 特殊的情况:监视reactive定义的数据中的对象属性 对象嵌套对象所以deep配置有效;oldValue无法获取

全部代码示例:

const num = ref(0);
const msg = ref('hello');
let obj = reactive({
  name: '',
  age: 30,
  hobby: {
    food: 'anything'
  }
});

// 情况1. 当监视一个ref定义的数据时:
watch(
  num,
  (newVal, oldVal) => {
    console.log('', newVal, oldVal);
  },
  { immediate: false }
);

// 情况2. 当监视多个ref定义的数据时:
watch(
  [num, msg],
  (newVal, oldVal) => {
    console.log('', newVal, oldVal);
  },
  { immediate: false }
);

// 情况3. 当监视reactive定义的全部数据时:
// 注意:
//    1.此时无法拿到正确的oldValue。
//    2.默认开启deep;且修改无效。
watch(
  obj.age,
  (newVal, oldVal) => {
    console.log('', newVal, oldVal);
  },
  { deep: false }
);

// 情况4. 当监视一个reactive定义的数据中的属性时:
// 注意:当监视这种数据时,watch的第1个形参需要是一个函数,函数里要有返回值,监视谁就返回谁。
// 这时oldValue正确。
watch(
  () => {
    return obj.age;
  },
  (newVal, oldVal) => {
    console.log('', newVal, oldVal);
  },
  { deep: false }
);

// 情况5. 当监视多个reactive定义的数据中的属性时:
// 注意:watch的第1个形参是一个数组,监视几个数据里面就包几个函数。
// 这时oldValue正确。
watch(
  [
    () => {
      return obj.name;
    },
    () => {
      return obj.age;
    }
  ],
  (newVal, oldVal) => {
    console.log('', newVal, oldVal);
  },
  { deep: false }
);

// 特殊的情况6. 当监视reactive定义的数据中的对象属性时:
// 注意:当监视这种数据时,watch的第1个形参是函数。
// oldValue无法获取。
watch(
  () => {
    return obj.hobby;
  },
  (newVal, oldVal) => {
    console.log('', newVal, oldVal);
  },
  { deep: false }
);

注意:

  • 使用watch监视基本数据类型时,别用.value,这样拿到的是一个属性真正的值而非属性。

  • 对象类型(ref定义时):要用.value才会被监测到。因为此时的value是由reactive函数生成的proxy实例对象

    • 除了上面的.value,还有一个办法:加配置项 deep: true;

3. watchEffect函数

  • watchEffect也是一个监视函数,但它不用指明监视目标。默认开启immediate: true

  • 参数:监视的回调

  • 作用:观察监视的回调中用到了谁,智能的选择监视谁。可以监视深层级数据

  • 使用场景:不用再多次写watch函数然后按照不同数据的语法写。watchEffect会自动识别并监视。

  • watchEffect有点像computed,具体**区别**:

  • 1.**computed注重计算出来的**(回调函数的返回值),所以必须要有返回值。执行时机:初始化时;所依赖的数据发生变化时。

    1. watchEffect要的是过程(使用了谁),不用返回值。watchEffect也是所依赖的数据发生变化时重新执行一次。

9. hook

  • 本质是函数,把setup中使用的Composition API进行了封装。类似于vue2中的mixin。

  • 自定义hook的优势:复用代码,让setup中的逻辑更清楚。

  • 把该功能相关的数据、方法、生命周期钩子全都抽离出去。

    • 新建hook文件后,将以上内容都放在个函数里。
  • ps:hook文件取名一般为useXxx。

使用示例

  • 实现一个点击鼠标拿到坐标的功能:(完整代码在vue3-practice)

    • 改为hook前
    <script setup>
    import { reactive } from '@vue/reactivity';
    import { onBeforeUnmount, onMounted } from '@vue/runtime-core';
    
    const position = reactive({
      x: 0,
      y: 0
    });
    
    // // 组件挂载后执行
    // onMounted(() => {
    //   // 这种写法的问题:组件卸载前销毁该事件时无法操作:
    //   window.addEventListener('click', function (event) {
    //     console.log('FROM UseHook position: ', event.pageX, event.pageY);
    //     // 数据为响应式:
    //     position.x = event.pageX;
    //     position.y = event.pageY;
    //   });
    // });
    
    // 基于以上问题,改为这样(将点击事件的回调拎出去):
    onMounted(() => {
      // 事件回调写进来:
      window.addEventListener('click', savePosition);
    });
    
    function savePosition(event) {
      console.log('FROM UseHook position: ', event.pageX, event.pageY);
      // 数据为响应式:
      position.x = event.pageX;
      position.y = event.pageY;
    }
    
    // 需要注意的是,当组件被卸载时该事件也该失效,所以要在卸载前另外处理下:
    onBeforeUnmount(() => {
      // 这里需要两个参数:1.移除的事件;2.移除的事件所对应的回调。
      window.removeEventListener('click', savePosition);
    });
    
    
    </script>
    
    • hook写法: 以下代码全都可以抽出去
image.png

usePosition文件中:

import { reactive, onMounted, onBeforeUnmount } from 'vue';

// 默认暴露可以不用起名,汇依据使用时起的名字。
export default function () {
  const position = reactive({
    x: 0,
    y: 0
  });

  function savePosition(event) {
    position.x = event.pageX;
    position.y = event.pageY;
    console.log('FROM UseHook position: ', position.x, position.y);
  }

  onMounted(() => {
    window.addEventListener('click', savePosition);
  });

  onBeforeUnmount(() => {
    window.removeEventListener('click', savePosition);
  });

  // 是函数,所以需要给返回值:
  // 将组件中需要的数据传出去:

  return position;
}

10. 其它Composition API

1. shallowReactive和shallowRef

  • shallowReactive:只处理对象中第一层的响应式,里层的不管(浅响应式)。

  • shallowRef:

    • 只处理基本类型,和ref的使用效果无区别。
    • 当用来处理对象类型时,不再去借助reactive处理,而是直接不管了。
  • 使用时机

2. readOnly和shallowReadOnly

  • readOnly:让响应式数据变为只读状态(深只读)。
  • shallowReadOnly:让响应式数据变为只读状态(浅只读,除第一层只读外,里层的还可以修改)。
  • 应用场景:让数据不被修改。

3. toRaw和markRaw

raw:原始的

  • toRaw

    • 作用:将reactive生成的响应式数据转为普通对象(ref不行)。
    • 使用场景:用于读取响应式对象对应的普通对象,使其所有操作都不会引起页面更新(数据会变但不响应式)。
  • markRaw

    • 作用:标记一个对象,使其之后不能再成为响应式对象。
    • 应用场景:(应用场景比toRaw广)
      • 1.不应该设为响应式的值,比如复杂的第三方类库等。
      • 2.当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。

4. customRef

  • 作用:创建一个自定义的ref,并跟踪其依赖项和更新触发进行显示控制。

使用示例:(完整代码在vue3-practice的useCustomRef中)

import { customRef } from 'vue';

// 使用customRef自定义一个ref:
function myRef(initVal, delay) {
  // customRef接收两个参数:
  return customRef((track, trigger) => {
    // 下面的返回是customRef规定的:
    return {
      get() {
        console.log('正在读value:', initVal);
        // 在return之前通知vue追踪数据
        track();
        return initVal;
      },
      set(newVal) {
        console.log('正在修改value:', newVal);
        // // 只有把新值赋回给初始值的变量才能动态的变化起来:
        // initVal = newVal;
        // // 通知vue重新解析模板:所以这里可以自定义的使用值。比如规定3秒后再执行。
        // trigger();

        // 这离开了定时器会出现一个状况:当输入速度太快,等到了页面刷新时,部分被积压的输入被刷掉,页面上能看到输入在回退。
        // 解决办法:先定义一个timer定时器,每次进来执行前先清除定时器。(参考js防抖的原理)
        clearTimeout();
        setTimeout(() => {
          // 只有把新值赋回给初始值的变量才能动态的变化起来:
          initVal = newVal;
          // 通知vue重新解析模板:所以这里可以自定义的使用值。比如规定3秒后再执行。
          trigger();
        }, delay);
      }
    };
  });
}

// 给myRef两个实参:1.initVal初始值;2.delay延迟执行的时间。
let num = myRef(0, 500);
console.log('myRef自定义的数据:', num);
  • 可以优化的地方:延迟执行的时间是写死的,可以写为动态改变的。(已改

    • 实现:多传递一个变量(比如delay)。
  • 实参写在调用myRef时的()中。


11. provide & inject

  • provide和inject(注入)是一种组件间通信的方式。

  • 作用:实现祖孙组件间的通信。

  • 使用:父组件使用provide提供数据,后代组件通过inject接收使用数据。

    • provide两个参数:1.给传递的数据起的名字;2.实参。
    • inject通过传递的参数名拿到真实数据。(如下代码块)
  • 语法

// 父组件中:
let num = ref(0);
provide('xxx', num)

// 后代组件中接收:
const xxx = inject('xxx');
  • 注意:所有后代组件都可以使用inject拿到数据,但一般用在祖孙之间传递。

12. 几个用于判断响应式数据的API

几个常用的用来判断的API:

  1. isRef:检查一个值是否是一个ref对象。
  2. isReactive:检查一个对象是否是由reactive创建的响应式代理。
  3. isReadonly:检查一个对象是否由readonly创建的只读代理。
  4. isProxy:检查一个对象是否由reactive或者readonly方法创建的代理。

使用场景:判断一个数据是否经过某些api的修饰。

需要注意的一点

  • isProxy判断一个由~所定义的数据时,它也会返回true。
  • 这是因为readonly属性不会改变原数据的形式。依旧是proxy代理,而非转为普通object对象。
let obj1 = reactive({
    a: 1,
    b: 2
})
let obj2 = readonly(obj)
// 打印为true:
console.log(isProxy(obj2));

13. 新的组件

1. Fragment

Fragment:碎片

  • vue2中:组件必须有个根标签。
  • vue3中:无需根标签,内部已经将多个标签包在一个Fragment虚拟元素中。
  • 优势:减少标签层级嵌套,减小内存占用。

2. Teleport

teleport:瞬间移动;传送;

  • 作用:将一段HTML结构移动到指定位置。
  • to属性用来指定位置。
<teleport to="body/或任意其他位置">
    <div></div>
<teleport>

3. Suspense (试验阶段)

Suspense:悬念

  • 作用:异步引入组件。

  • 静态 & 动态(异步)引入组件的对比

    • 写法:

      // 静态引入:
      import child from './文件路径';
      
      import { defineAsnycComponent } from 'vue';
      // 动态(异步)引入组件:
      // import在以前用作一个关键词,在这里用作一个函数。
      const child = defineAsnycComponent(()=> import('./文件路径')))
      
    • 静态引入:当网速慢的时候,造成几个组件都不显示都在等着。

    • 动态引入

  • Suspense使用:Suspense组件标签中内置了两个插槽:

    • 1.要放入展示的组件;
    • 2.组件未加载时loading状态的内容。
    • 注意插槽的名字不能换
<template>
  <div class="app">
  <h3>我是App组件</h3>
  <Suspense>
    <template v-slot:default>
      <Child/>
    </template>
    
    <template v-slot:fallback>
      <h3>加载中.....</h3>
    </template>
  </Suspense>
  </div>
</template>

PS:当组件用了异步引入时,setup可以是async函数。

image.png

14. vue3的其它调整

1. 全局API的转移:

image.png

2. 其它:

  • data始终应被声明为函数。
  • 过渡类名的更改:

image.png

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

  • 移除v-on.native修饰符

    • 父组件中绑定事件
 <my-component
   v-on:close="handleComponentEvent"
   v-on:click="handleNativeClickEvent"
 />
    • 子组件中声明自定义事件
 <script>
   export default {
     emits: ['close']
   }
 </script>
  • 移除过滤器(filter)

过滤器虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 “只是 JavaScript” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。


文章作者: polariis
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 polariis !
评论
  目录