Vue3 极速入门🚀

454 阅读7分钟

我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!

一. 前言

image.png 技术更新很快,时代在推你进步,你却停滞不前,只会被时代淘汰。迫于内卷压力,再不学 vue3 真的感觉在跟时代划一道鸿沟。接下来让我们以最快的脚步认识vue3

二. Vue3优势和新特性

vue3相对于vue2有哪些性能提升?

源码体积的优化

  • 重写了虚拟 dom。

响应式系统的升级

  • 用 Proxy 和 Reflect 来代替 vue2 中的 Object.definepeoperty()方法来重写响应式。
  • vue3 中可以监听动态新增的属性。
  • vue3 中可以监听删除的属性。
  • vue3 中可以监听数组的索引和 length 属性。

代码编译优化

  • 使用了 Composition API 来代替 vue2 中的 Options API。
  • 组件内不需要根节点了,使用 fragment(代码片段)代替了,fragment(代码片段)不会在页面显示。
  • vue3 中标记和提升所有的静态根节点,diff 的时候只需要对比动态节点内容。

Composition API 与 Options API 对比

vue2结构是这样的,当代码量少的时候,逻辑结构看起来挺清晰的。当功能越来越多时,代码分散在不同的地方,反复横跳翻滚,显得非常臃肿,难以维护。

image.png

Vue3的思路就是根据逻辑功能,对代码进行组织划分,把同一个功能的相关代码全都放在一起,或者把它们单独拿出来放在一个函数中,从而解决上述代码臃肿的问题。 image.png

演变过程 acbef781bcbb47528e7babacccb6547a.gif

Proxy 相对于 Object.defineProperty 有哪些优点?

  • 代码的执行效率更快。
  • Proxy 可以直接监听对象而非属性。
  • Proxy 可以直接监听数组的变化。
  • Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的。
  • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改。
  • Proxy 不需要初始化的时候遍历所有属性,另外有多层属性嵌套的话,只有访问某个属性的时候,才会递归处理下一级的属性。

vue3 响应式系统的实现原理

通过 2 个响应式 API 函数的调用,一个是 reactive() , 一个是 ref()。

reactive()

  • reactive 函数是用来把普通对象创建成为响应式对象的,函数内通过执行 proxy 创建的 get、set、deleteProperty 方法来实现。
  • get 方法获取响应式数据,同时调用 track 方法去收集依赖。
  • set 方法设置响应式数据,同时调用 trigger 方法是触发响应式数据的更新。
  • deleteProperty 方法删除响应式数据,同时用 trigger 方法是触发响应式数据的更新。

ref()

  • ref 函数是用来把一般类型的数据或者普通对象创建成为响应式对象的,函数内返回的是一个对象。
  • 对象内的 get 方法获取响应式数据,同时调用 track 方法去收集依赖。
  • 对象内的 set 方法设置响应式数据,同时调用 trigger 方法是触发响应式数据的更新。
  • tarck 函数内通过 targetMap 找到 depsMap 通过 depsMap 找到 dep,最后向 dep 内添加 effect()函数。
  • trigger 函数内通过 targetMap 找到 depsMap 通过 depsMap 找到 dep,最后遍历 dep 数组,执行里面的 effect()函数来更新响应式数据。

vue2.x 与 vue3.x 生命周期对比

由下图可见:

  • beforeCreate、created由setup()代替。
  • beforeDestroy -> onBeforeUnmount
  • destroyed -> onUnmounted
  • errorCaptured -> onErrorCaptured
  • 新增# onRenderTracked()onRenderTriggered()
  • 其余的在原基础上增加onimage.png

三. Fragment

<template>
  <img alt="Vue logo" src="../assets/logo.png" />
  <div>home页面</div>
</template>

<script>
export default {
  name: 'HomeView',
}
</script>

在 template 中不再需要一个根元素包裹。实际上内部会将多个标签包含在一个Fragment虚拟元素中。

好处:减少标签层级, 减小内存占用。

四. setup函数

setup 返回的是一个对象,这个对象的属性会与组件中 data 函数返回的对象进行合并,返回的方法和 methods 合并,合并之后直接可以在模板中使用。

setup在 beforeCreate 之前执行,所以this在setup里面无效。 code.juejin.cn/pen/7145283…

<template>
  <div>学习课程:{{ message }}</div>
  <button @click="changeMessage">changeMessage</button>
</template>

<script>
import { defineComponent, ref } from 'vue'
export default defineComponent({
  name: 'HomeView',
  setup () {
    let message = ref("极速入门Vue3");
    console.log(this)
    function changeMessage () {
      message.value = "我被改变了"   //注意使用ref重新赋值要在.value上操作
    }
    return {
      message,
      changeMessage
    }
  }
})

GIF1.gif

五. ref方法

Demo

code.juejin.cn/pen/7145284…

<template>
  <p>基础类型:{{ message }}</p>
  <p>引用类型:{{ obj.name }}-{{ obj.sex }}</p>
  <button @click="changeObjName">修改名字</button>
</template>

<script>
import { defineComponent, ref } from 'vue'
export default defineComponent({
  name: "App",
  setup () {
    let message = ref("学习Vue3");
    let obj = ref({
      name: "小明",
      sex: "男"
    })
    function changeObjName () {
      obj.value.name = "小风"
    }
    return {
      message,
      obj,
      changeObjName
    }
  }
})
</script>

GIF2.gif

使用ref获取dom元素

code.juejin.cn/pen/7145284…

<template>
  <div ref="myRef">myRef元素</div>
</template>

<script>
import { defineComponent, ref, onMounted } from 'vue'
export default defineComponent({
  name: "HomeView",
  setup () {
    let myRef = ref(null);
    onMounted(() => {
      setTimeout(() => {
        myRef.value.style.color = "red"; //1秒后变颜色
      }, 1000)
    })
    return {
      myRef
    }
  }
})
</script>

GIF3.gif

六. reactive / toRefs方法

code.juejin.cn/pen/7145285…

<template>
  <p>基础类型:{{ message }}</p>
  <p>引用类型:{{ name }}-{{ sex }}</p>
  <button @click="changeObjName">修改名字</button>
</template>

<script>
import { defineComponent, reactive, toRefs } from 'vue'
export default defineComponent({
  name: "HomeView",
  setup () {
    let message = reactive("学习Vue3");
    let obj = reactive({
      name: "小明",
      sex: "男"
    });
    function changeObjName () {
      obj.name = "小风"; //reactive可以直接修
    }
    return {
      message,
      ...toRefs(obj), //拓展运算符,使用toRefs在模板上不需要通过obj.name取值
      changeObjName,
    };
  }
})
</script>

reactive参数必须是对象 (json / arr),reactive定义基本数据类型的话,我们需要在reactive中将数据包装一下。

七. 判断响应式

isRef

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

<script>
import { defineComponent, ref, isRef } from 'vue'
export default defineComponent({
  name: "HomeView",
  setup () {
    const ref1 = ref(1)
    const ref2 = 2;
    console.log(isRef(ref1))  // true
    console.log(isRef(ref2))  // false
  }
})
</script>

isReactive

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

let ref2 = reactive({name: '小浪'}) 
console.log(isReactive(ref2)) // true

isReadonly 只读

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

let ref3 = readonly({name: '小浪'}) 
console.log(isReadonly(ref3)) // true

isProxy

检查对象是否是由 reactive 或 readonly 创建的 proxy。

let ref4 = reactive({name: '小浪'}) 
console.log(isProxy(ref4)) // true 

let ref5 = readonly({name: '小浪'}) 
console.log(isProxy(ref5)) // true

八. customRef 自定义 ref

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

官方Demo实现自定义hook防抖函数【传送门code.juejin.cn/pen/7145285…

<template>
  <div>{{ message }}</div>
  <input v-model="text" />
</template>

<script>
import { defineComponent, ref, customRef } from 'vue'
function useDebouncedRef (value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get () {
        // 告诉Vue追踪数据
        track()
        return value
      },
      set (newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          // 告诉Vue更新界面
          trigger()
        }, delay)
      }
    }
  })
}
export default defineComponent({
  name: "HomeView",
  setup () {
    const message = ref("学习Vue3");
    const text = useDebouncedRef('hello');
    console.log(message)
    console.log(text)
    return {
      message,
      text
    }
  }
})
</script>

image.png

九. 浅响应式

shallowRef / shallowReactive

shallowRef只处理基本数据类型的响应式, 不进行对象的响应式处理。
shallowReactive只处理对象最外层属性的响应式(浅响应式),一旦发生改变,则更新视图。其它深层,虽然值发生了改变,但是视图不会进行更新。

什么时候使用?

  • 如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===> shallowReactive。
  • 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换 ===> shallowRef
//shallowRef
<template>
  <div>
    <h1>数值:{{ number }}</h1>
    <button @click="number++">number增加+1</button>
    <h1>年龄:{{ person.age }}</h1>
    <button @click="person.age++">年龄增加+1</button>
  </div>
</template>
 
<script>
import { shallowRef } from 'vue'
export default {
  name: "HomeView",
  setup () {
    let number = shallowRef(0); // 基础类型做了响应式处理
    let person = shallowRef({ //引用类型不做响应式处理
      age: 18
    })
    return {
      number,
      person
    }
  }
};
</script>

GIF5.gif

//shallowReactive
<template>
  <div>
    <h1>姓名:{{ name }}</h1>
    <h2>年龄:{{ age }}</h2>
    <h3>喜欢的水果:{{ likeFood.fruits.apple }}</h3>
    <button @click="name += '仔'">修改姓名</button>
    <button @click="age++">修改年龄</button>
    <button @click="likeFood.fruits.apple += '香'">修改水果</button>
  </div>
</template>
 
<script>
import { toRefs, shallowReactive } from 'vue'
export default {
  name: "HomeView",
  setup () {
    let person = shallowReactive({    // 只将第一层数据做了响应式处理 
      name: '小明',
      age: 18,
      likeFood: {
        fruits: {
          apple: '草莓'               // 深层次的数据将会是一个普通的对象
        }
      }
    })
    // 将数据返回出去
    return {
      ...toRefs(person)
    }
  }
};
</script>

GIF4.gif

shallowReadonly

创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)。通俗点就是只有第一层是只读,深层为可更改。

//shallowReadonly
<template>
  <h2>姓名:{{ name }}</h2>
  <h2>年龄:{{ age }}</h2>
  <h2>薪水:{{ job.j1.salary }}</h2>
  <button @click="name += '~'">修改姓名</button>
  <button @click="age++">增长年龄</button>
  <button @click="job.j1.salary++">增长薪水</button>
</template>

<script>
import { reactive, toRefs, shallowReadonly } from 'vue'
export default {
  name: 'HomeView',
  setup () {
    // 数据
    let person = reactive({
      name: '张三',
      age: 18,
      job: {
        j1: {
          salary: 20
        }
      }
    })
    person = shallowReadonly(person)
    return {
      ...toRefs(person)
    }
  },
}
</script>

GIF6.gif

十. toRaw / markRaw

toRaw

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

const foo = {};
const reactiveFoo = reactive(foo);
console.log(toRaw(reactiveFoo) === foo) // true
<script>
import { reactive, toRaw, isReactive } from 'vue'
export default {
  name: 'HomeView',
  setup () {
    const person = reactive({
      name: '小明',
      age: 18,
      likeFood: {
        fruits: {
          apple: '草莓'
        }
      }
    });
    const person2 = toRaw(person);
    console.log(person)
    console.log(person2)

    console.log(isReactive(person))
    console.log(isReactive(person2))
    return {

    }
  },
}
</script>

image.png

markRaw

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

<script>
import { reactive, markRaw, isReactive } from 'vue'
export default {
  name: 'HomeView',
  setup () {
    let person = {
      name: "小明",
      age: 18
    };
    const markRawObj = markRaw(person); //标记person
    const reactiveObj = reactive(markRawObj); //尝试转为响应式
    console.log(isReactive(reactiveObj)) // false,说明未转成响应式
    console.log(reactiveObj)

    const obj = reactive({
      name: "小风",
      age: 19
    })
    console.log(obj)
    return {};
  }
}
</script>

image.png

十一. computed 计算属性

接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。 code.juejin.cn/pen/7145286…

<template>
  <div>姓名:{{ name }}</div>
  <div>年龄:{{ age }}</div>
  <div>合并信息:{{ getInfo }}</div>
</template>

<script>
import { ref, computed } from "vue";
export default {
  name: 'HomeView',
  setup () {
    let name = ref('小明')
    let age = ref(21)

    //计算属性
    let getInfo = computed(() => {
      return `${name.value}${age.value}`
    })
    getInfo.value += "尝试新增点东西"; //不允许修改,这里是非法的,修改不成功的
    return {
      name,
      age,
      getInfo,
    }
  }
}
</script>

image.png

或者,接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。

const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value) // 0

十二. watch 监听

watch用法与vue2.x的差不多,watch(data,handler,object)。

  • data:可以是返回值的getter函数,也可以是 ref。
  • hander: 回调函数
  • object: 可选配置项{ immediate : true, deep : true } //立即执行和深度监测
<template>
  <ul>
    <li
      v-for="(item, index) in courseList"
      :key="index"
      @click="selectCourseFun(index)"
    >
      {{ item }}
    </li>
  </ul>
  <div>默认课程:{{ defaultCourse }}</div>
  <div>选中的课程:{{ selectCourse }}</div>
</template>

<script>
import { ref, reactive, toRefs, watch } from "vue";
export default {
  name: 'HomeView',
  setup () {
    const defaultCourse = ref("javaScript进阶");
    const data = reactive({
      courseList: ["vue3初识", "Node.js进阶", "css世界"],
      selectCourse: "",
      selectCourseFun: (index) => {
        data.selectCourse = data.courseList[index];
        defaultCourse.value = data.courseList[index];
      }
    })
    //监听单个
    // watch(defaultCourse, (newValue, oldValue) => {
    //   console.log(`new--->${newValue}`);
    //   console.log(`old--->${oldValue}`);
    // })
    
    //监听多个
    watch([defaultCourse, () => data.selectCourse], (newValue, oldValue) => {
      console.log(`new--->${newValue}`);
      console.log(`old--->${oldValue}`);
    })
    return {
      defaultCourse,
      ...toRefs(data)
    }
  }
}
</script>

GIF7.gif

十三. watchEffect

立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

  • 不需要手动传入依赖
  • 每次初始化时会执行一次回调函数来自动获取依赖
  • 无法获取到原值,只能得到变化后的值
<template>
  <input type="text" v-model="name" />
</template>

<script>
import { reactive, toRefs, watchEffect } from "vue";
export default {
  name: 'HomeView',
  setup () {
    let data = reactive({
      name: "小明",
      sex: "男",
    });
    watchEffect(() => {
      console.log(data.name); //需要监听哪个就写哪个
    });
    return {
      ...toRefs(data),
    };
  }
}
</script>

GIF2.gif

十四. 注意事项

Vue.prototype 替换为 config.globalProperties

//vue2.x
//main.js
Vue.prototype.$test = "测试" 

//页面xx.vue
let value = this.$test;
//vu3.x
//main.js
const app = createApp({}); 
app.config.globalProperties.$test = "测试"

//页面使用 
<template> 
   <h1>{{ $test }}</h1> //直接使用 
</template> 
import {  getCurrentInstance } from "vue"; 
export default { 
   setup(){ 
     const { appContext } = getCurrentInstance(); 
     console.log(appContext.config.globalProperties.$test)  //可以通过这样获取 
     } 
  }

废弃 v-bind:title.sync 写法

//父组件
<ChildComponent :title.sync="pageTitle" />  //vue2.x写法
<ChildComponent v-model:title="pageTitle" /> //vue3.x写法

//子组件
<template>
  <h1>{{ title }}</h1>
  <button @click="changeTitle('newTitle')">Change</button>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
  props: {
    title: String
  },
  setup () {
    function changeTitle (val) {
      this.$emit("update:title", val)
    }
    return {
      changeTitle
    }
  }
})
</script>

废弃 filter 过滤器

在 3.x 中,过滤器已移除,且不再支持。取而代之的是,我们建议用方法调用或计算属性来替换它们。

//父页组件
<template>
  <child v-model:name="name" v-model:value="value"></child>
</template>
<script>
import { defineComponent, reactive, toRefs } from 'vue'
import child from "@/components/child";
export default defineComponent({
  components: {
    child
  },
  setup () {
    let params = reactive({
      name: "测试过滤功能",
      value: 12.0124525721
    })
    return {
      ...toRefs(params)
    }
  }
})
</script>
//子组件
<template>
  <h1>{{ name }}</h1>
  <h2>{{ newValue }}</h2>
</template>
<script>
import { defineComponent, computed } from "vue";
export default defineComponent({
  props: {
    name: String,
    value: Number,
  },
  setup (props) {
    let newValue = computed(() => {
      return Number(props.value.toFixed(2))
    })
    return {
      newValue
    }
  }
})
</script>

使用全局方法实现过滤器

//main.js
const app = createApp(App);
app.config.globalProperties.$filters = { 
    exactTwoDecimal(val){ 
        return Number(val.toFixed(2)) 
    }
}

//页面
<template> 
   <h2>{{ $filters.exactTwoDecimal(value)}}</h2> 
</template>