Vue3对比Vue2

12/6/2021 vue响应式

# Vue3

# vue2存在的一些问题

在讨论为什么学习Vue3之前,我们先来看看Vue2上存在哪些问题

# 逻辑杂糅

写过vue2的朋友应该都知道,只要页面逻辑一多,我们就会在data()、methods、computed、watch这几个地方来回跳,一个业务逻辑会分散到不同的地方不便于维护和开发

export default{
	data(){
        return{
            //功能1
            //功能2
            //...
        }
    },
    methods:{
        //功能1
        //功能2
        //...
    },
    computed:{
        //功能1
        //功能2
        //...
    },
    watch:{
        //功能1
        //功能2
        //...
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

vue2的解决方法Mixin(混入)

//mixin.js
export default {
  created() {
    this.hello()
  },
  methods: {
    hello() {
      console.log('hello from mixin!')
    }
  }
}

//import myMixin from './mixin.js'
const app = Vue.createApp({
  mixins: [myMixin]
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这种混入的方式虽然造成了逻辑的抽离,但是我们关于混入后this上的数据一无所知,只有去抽离出的部分查看才能得知,这无疑也是加大了工作量。同时混入还存在命名冲突问题,尤其是抽离公共组件后。

# 缺少ts支持

由于vue2不是由TypeScript进行开发的,所以说天生缺少对TypeScript的支持。在vue2中使用了flow来进行类型检查。

# 响应式依靠defineproperty

简单来说Object.defineproperty只能劫持到对象的属性,无法监听整个对象。每次需要遍历整个对象。具体怎么搞的我不清楚,有兴趣自己查。

# Vue3的解决方案及优化措施

首先Vue3是ts写的并且目前兼容vue2的用法

# composition API组合式API

<template>
    
<template/>
export default{
    setup(props,context){ //context:{attrs,slot,emit}
        const data = ref(data)
        const obj = reactive(obj)
        //返回给模板
        return {
            //data...
        }
    }
}
//渲染函数
export default{
    setup() {
        const counter = ref(0)
        function add(){
            counter.value++;
        }
        return ()=>h('div',[h('button',{onClick:add},"Add 1"),counter.value])
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

我们不再需要把功能拆分写在data、methods、computed、watch中,我们可以把功能写在一个函数(hook)中,形如:

//a.js
export default function useCount(num){
    const counter = ref(num)
    function add(){
        counter.value++;
    }
    return {
        counter,
        add
    }
}
//app.vue
import useCount from 'a.js'
export default{
    setup(){
        const {counter,add} = useCount(5);//实现了对功能的拆分
        //其他功能
        return {
            counter,add
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

我们习惯把hook前面加上use来进行命名

# Proxy实现响应式

我们都知道在vue3中我们要监听数据的变化,都是通过ref(),和**reactive()**两个函数来构造响应式数据。

响应式的本质就是:将定义响应式数据更新,并把和该数据所有有关的方法重新调用一遍,并把试图更新的过程。

简单看下基础原理:

  • track() 保存代码
  • effect() 需要运行代码
  • trigger() 运行所有存储的代码
let price = 1,quantity = 5,total;
let dep = new Set(); 
let effect = ()=>{total=price * quantity}
function track(){dep.add(effect)}
function trigger(){dep.forEach(effect=>effect())}
1
2
3
4
5

引入depsMap(一个depsMap对应一个响应式对象),一个dep对应一个响应式对象上的属性

const depsMap = new Map();
function track(key){
    let dep = depsMap.get(key);
    if(!dep){
        depsMap.set(key,(dep = new Set()));
    }
    dep.add(effect);
}
function trigger(key){
    let dep = depsMap.get(key);
    if(dep){
        dep.forEach(effect=>effect())
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

targetMap(weekMap,key是object。存储所有响应式对象)

const targetMap = new WeakMap();
function track(target,key){
    let depsMap = targetMap.get(target);
    if(!depsMap){
        targetMap.set(target,(depsMap = new Map()));
    }
    let dep = depsMap.get(key);
    if(!dep){
        depsMap.set(key,(dep = new Set()));
    }
    dep.add(effect);
}
function trigger(target,key){
    let depsMap = targetMap.get(target);
    if(!depsMap){return}
    let dep = depsMap.get(key);
    if(dep){
        dep.forEach(effect=>effect())
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

test

let product = {price:5,quantity:2}
let total = 0;
let effect = ()=>{
    total = product.price*product.quantity;
}
track(product,'quantity');
effect();
/*
> total //10
> product.quantity = 3
> trigger(product,'quantity')
> total //15
*/
1
2
3
4
5
6
7
8
9
10
11
12
13

question:如何自动执行?

# 响应式引擎

当我们运行effect,如果访问(get)了产品的属性,我们想track去保存effect,当我们(set)去改变产品的属性时,我们想用trigger去运行保存的effect

# 怎样去拦截GET,SET?(Proxy、reactive)
  • vue2 使用了ES5 的Object.defineProperty()
  • vue3 使用ES6 的Reflect 和Proxy
console.log(Reflect.get(Product,'quantity'))
1
function reactive(target){
    const handler = {
        get(target,key,receiver){
            let res = Reflect.get(target,key,receiver)//receiver能保证this指向正确
            track(target,key);
            return res;
        },
        set(target,key,value,receiver){
            let oldValue = target[key];
            let res = Reflect.set(target,key,value,receiver);
            if(oldValue!=res){
                trigger(target,key);
            }
             return res
        }
    }
    return new Proxy(target,handler)
}
//你可以把ref理解成这样
function ref(intiaValue){
    return reactive({value:intiaValue})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

test

let product = reactive({price:5,quantity:2})
let total = 0;
let effect = ()=>{
    total = product.price*product.quantity;
}
effect();
console.log(total);
product.quantity = 3;
console.log(total)
1
2
3
4
5
6
7
8
9

在vue3中ref、reactive都是递归监听的,reactive无法代理基本类型通常我们使用ref代理基础类型,reactive代理对象

# 渲染优化

Vue的渲染主要分为三个阶段:(这之前会有个编译阶段把template转化成render function

  • 渲染阶段 render function=>virtual DOM node
  • 挂载阶段 virtual DOM node=>build webpage
  • patch阶段 oldVnode与newVnode比较更新webpage(diff

Vue template Explorer (opens new window)进去可以看到优化前后变化

# 静态提升hoistStatic

vue2.x中无论元素是否参与更新,每次都会重新创建,再渲染

vue3中对于不更新的元素,会做静态提升,只会创建一次,渲染时直接复用即可

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "我是静态", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _hoisted_1,
    _createVNode("p", null, _toDisplayString(_ctx.change), 1 /* TEXT */)
  ]))
}
1
2
3
4
5
6
7
8
9
10

# 事件监听缓存cacheHandlers

缓存绑定事件

<div>
    <button @click="onClick">按钮</button>
</div>
1
2
3

之前

import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("button", { onClick: _ctx.onClick }, "按钮", 8 /* PROPS */, ["onClick"])
  ]))
}
//8表示静态标记中的动态属性,我们没必要去对比,因为事件是一样的
1
2
3
4
5
6
7
8

之后

import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("button", {
      onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick && _ctx.onClick(...args)))
    }, "按钮")
  ]))
}
1
2
3
4
5
6
7
8
9

# Vue3特性

# 生命周期

image-20210728174505380

如你所见生命周期发生了一些变化,现在这些onXxx都是通过vue来导入

# Props与Provide/inject

# Props

通过setup的第一个参数可以拿到props的内容,通过第二个参数拿到emit来对其的值进行修改

Props是父组件向子组件传递的参数,是一个单向下绑定,也就是说只能父组件来修改props的内容,不能反过来更新状态。不过我们可以通过触发自定义事件的方式来进行更新。

//son.vue
<template>
    <div>子组件{{msg}}</div>
    <button @click="$emit('change')">方式一改变msg</button>
	<button @click="change">方式二改变msg</button>
</template>
<script>
export default{
    props:['msg'],
    setup(props,{emit}){
        const change =()=>{
            emit('change')
        }
        return {
            change
        }
    }
}
//father.vue
<template>
    <div>父组件{{msg}}</div>
  	<Son @change="chmsg" :msg="msg" />
</template>
<script>
import Son from './components/son.vue'
import {ref} from 'vue'
export default{
   components:{
    Son
  },
  setup(){
    let msg = ref(1);
    const chmsg = ()=>{
      msg.value =3;
    }
    return{
      msg,
      chmsg
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

需要注意的是,props只传递一层

# Provide/inject

只能在setup()期间调用,对子孙组件都有影响。类似react中context

import {provide} from 'vue'
provide('theme',value)//value可以是响应式
import { inject } from 'vue'
const theme = inject('theme',defaultValue)
1
2
3
4

# computed/watch/watchEffect

# watch

watch(source, callback, [options])options支持:

  • deep 用于多层嵌套监听
  • immediate 立刻执行一次回调
  • flush 控制回调执行时机
import {watch} from 'vue'
export default{
  setup(){
	const state = reactive({ nickname: "xiaofan", age: 20 });
    setInterval(() => {
    state.age++;
    }, 1000);

    watch(
    state.age,
    (curAge, preAge) => {
    console.log("新值:", curAge, "老值:", preAge);
    }
    );
    return{
    ...toRefs(state)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

注意source只能是getter/effect function,ref,reactive obj,或者他们组成的数组

结束监听可以掉用watch的返回值

# watchEffect

watchEffect(callback)

组件初始化后执行一次,自动收集依赖,拿不到旧值

watchEffect(()=>{
            console.log(state.age);
})
1
2
3

# computed

computed(getter function/obj(get&set))

const count = ref(1)
//const plusOne = computed(() => count.value + 1)
const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  }
})
console.log(plusOne.value) // 2
1
2
3
4
5
6
7
8
9

# 各种Ref

# isRef

isRef(val)判断是否是ref

总是返回值,如果是ref则返回内部值

# toRefs

toRefs(reactiveObj)把reactiveObj转化为一个ref组成的对象,并且存在着链接关系(你改变我也改变)

const state = reactive({
  foo: 1,
  bar: 2
})
const stateAsRefs = toRefs(state)
/*
Type of stateAsRefs:
{
  foo: Ref<number>,
  bar: Ref<number>
}
*/
1
2
3
4
5
6
7
8
9
10
11
12

# customRef

能控制追踪和响应,自定义ref,和我们上面写的那个reactive有点像,

因为setup不能是异步的,我们可以用这个来完成异步操作

type CustomRefFactory<T> = (
  track: () => void,//追踪
  trigger: () => void//更新
) => {
  get: () => T
  set: (value: T) => void
}
1
2
3
4
5
6
7

防抖

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

请求,注意不能在get中请求

function fetchRef(value) {
  return customRef((track, trigger) => {
    let Data;
    function getData() {
      fetch(value)
        .then((res) => res.json())
        .then((data) => {
          Data = data;
          trigger();
        }).catch((err) => {
          console.log(err);
        });
    }
    getData();
    return {
      get() {
        track();
        return Data;
      },
      set(newValue) {
        value = newValue;
        getData();
      }
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# setup语法糖

<script setup>
import Son from './components/son.vue'
import test from './components/test.vue'
import {ref,provide,defineProps, defineEmit,useContext} from 'vue'

const emit = defineEmit(['child-click'])
const ctx = useContext()
const props = defineProps({
  msg: String
})
let msg = ref(1);
const chmsg = ()=>{
  console.log(1);
  msg.value =3;
}
provide('theme','dark')
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

加上setup属性后,我们可以发现。组件不需要注册了,setup()没有了,甚至导出也不用写了

我们定义的变量和属性他会全部给我们暴露出去

props和emit使用define的形式定义,最近不用导入了,已经是个全局API,所以实验阶段变化还是存在

可以直接使用await,现阶段不太推荐。

# Vue3生态

# Vue Router

不再赘述

# Vuex

不再赘述

# Vite

推荐使用Vite作为你的构建工具,速度快

Vite文档 (opens new window)

Last Updated: 12/6/2021, 3:30:27 PM