阅读 317

Vue3.0 项目实战中踩坑总结

生命周期钩子

我们可以直接看生命周期图来认识都有哪些生命周期钩子(图片来自公众号《程序员成长指北》):

image.png

全部生命周期钩子如图所示:

640_12

我们可以看到beforeCreatecreatedsetup替换了。其次,钩子命名都增加了on; Vue3.x还新增用于调试的钩子函数onRenderTriggeredonRenderTricked

下面我们简单使用几个钩子, 方便大家学习如何使用,Vue3.x中的钩子是需要从vue中导入的:

<template>
    <div>{{num}}</div>
</template>
<script>
    import {
        ref,
        defineComponent,
        onBeforeMount,
        onMounted,
        onBeforeUpdate,
        onUpdated,
        onBeforeUnmount,
        onUnmounted,
        onErrorCaptured,
        onRenderTracked,
        onRenderTriggered
    } from "vue";

    export default defineComponent({
        // beforeCreate和created是vue2的
        beforeCreate() {
            console.log("------beforeCreate-----");
        },
        created() {
            console.log("------created-----");
        },
        setup() {
            console.log("------setup-----");

            // vue3.x生命周期写在setup中
            onBeforeMount(() => {
                console.log("------onBeforeMount-----");
            });
            onMounted(() => {
                console.log("------onMounted-----");
            });
            onUpdated(() => {
                console.log('updated!')
            })
            onUnmounted(() => {
                console.log('unmounted!')
            })
            const num = ref(0)
            setInterval(() => {
                num.value++;
            }, 1000)
            // 调试哪些数据发生了变化
            onRenderTriggered((event) => {
                console.log("------onRenderTriggered-----", event);
            })

            return {
                num
            }
        },
    });
</script>
复制代码

我们通过setInterval来改变数据,可以看到onUpdated函数和onRenderTriggered函数都被触发了!

image-20210708192547192

setup

执行顺序

export default defineComponent ({
    beforeCreate() {
        console.log("----beforeCreate----");
    },
    created() {
        console.log("----created----");
    },
    setup() {
        console.log("----setup----");
    },
})

结果是:
setup
beforeCreate
created
复制代码

warning 由于在执行setup 时尚未创建组件实例,因此在 setup 选项中没有 this

setup 参数

setup接受两个参数:

  1. props: 组件传入的属性/参数
  2. context

setup中接受的props是响应式的,由于是响应式的, 所以不可以使用ES6解构,解构会消除它的响应式。

错误代码示例, 这段代码会让props不再支持响应式:

export default defineComponent ({
    setup(props, context) {
        const { name } = props
        console.log(name)
    },
})
复制代码

如果要使用结构,则需要使用官方的toRefs,这个我们后续介绍。

setup第二个参数contextsetup中不能访问Vue2中最常用的this对象,所以context中就提供了this中最常用的三个属性:attrsslotemit,分别对应Vue2.x中的 $attr属性、slot插槽 和$emit发射事件,并且这几个属性都是自动同步最新的值,所以我们每次使用拿到的都是最新值。

reactive、ref与toRefs

在vue2.x中, 定义双向绑定的数据都是在data中, 但是Vue3 要使用reactiveref来进行双向绑定数据定义。

那么refreactive他们有什么区别呢?

<template>
    <div>{{obj.name}}-{{obj.count}}</div>
    <div>{{basetype}}</div>
    <div>{{baseTypeReactive}}</div>
    <div>{{objreactive.count}}</div>
</template>
<script>
    import {
        reactive,
        ref
    } from 'vue';
    export default {
        setup() {
            const obj = ref({
                count: 1,
                name: "张三"
            })
            const basetype = ref(2)
            setTimeout(() => {
                obj.value.count = obj.value.count + 1
                obj.value.name = "李四"
                basetype.value += 1
            }, 1000)

            const baseTypeReactive = reactive(6)
            const objreactive = reactive({
                count: 10
            })

            return {
                obj,
                basetype,
                baseTypeReactive,
                objreactive
            }
        },
    }
</script>
复制代码

reactive

  • reactive 是 Vue3 中提供的实现响应式数据的方法。
  • 在 Vue2 中响应式数据是通过 defineProperty 来实现的,在 Vue3 中响应式数据是通过 ES6 的 Proxy 来实现的。
  • reactive 参数必须是对象 (json / arr),不能代理基本类型,例如字符串、数字、boolean等。
  • 本质: 就是将传入的数据包装成一个Proxy对象
  • 如果给 reactive 传递了其它对象(如Date对象)
    • 默认情况下,修改对象无法实现界面的数据绑定更新。
    • 如果需要更新,需要进行重新赋值。(即不允许直接操作数据,需要放个新的数据来替代原数据)

reactive 使用基本类型参数

基本类型(数字、字符串、布尔值)在 reactive 中无法被创建成 proxy 对象,也就无法实现监听,无法实现响应式。

<template>
    <div>
        <p>{{msg}}</p>
        <button @click="c">button</button>
    </div>
</template>

<script>
    import {
        reactive
    } from 'vue'
    export default {
        name: 'App',
        setup() {
            let msg = reactive(0)

            function c() {
                console.log(msg);
                msg++;
            }
            return {
                msg,
                c
            };
        }
    }
</script>
复制代码

08utpj.png

点击 button ,我们期望的结果是数字从 0 变成 1,然而实际上界面上的数字并没有发生任何改变。

查看控制台,它的输出是这样的(我点了 3 次)

08uN1s.png

出现提示

value cannot be made reactive: 0

而输出的值确实发生了变化,只不过这种变化并没有反馈到界面上,也就是说并没有实现双向数据绑定。当然,如果是 ref 的话,就不存在这样的问题。而如果要使用 reactive ,我们需要将参数从 基本类型 转化为 对象。

<template>
    <div>
        <p>{{msg.num}}</p>
        <button @click="c">button</button>
    </div>
</template>

<script>
    import {
        reactive
    } from 'vue'
    export default {
        name: 'App',
        setup() {
            let msg = reactive({
                num: 0
            })

            function c() {
                console.log(msg);
                msg.num++;
            }
            return {
                msg,
                c
            };
        }
    }
</script>
复制代码

将参数替换成了对象 {num: 0},此时,点击按钮界面就会产生改变(我点了 3 次)。

08u8AS.png

在控制台打印消息

08uGtg.png

可以看到,msg 成功被创建成了 proxy 对象,他通过劫持对象的 getset 方法实现了对象的双向数据绑定。

深层的、对象内部的变化也能被察觉到(注意下面代码中的 inner

<template>
<div>
  <p>{{msg.num.inner}}</p>
  <button @click="c">button</button>
</div>
</template>

<script>
import { reactive } from 'vue'
export default {
  name: 'App',
  setup() {
    let msg = reactive({
      num: {
        inner: 0
      }
    })
    function c() {
      console.log(msg);
      msg.num.inner ++;
    }
    return {
      msg,
      c
    };
  }
}
</script>
复制代码

08uJhQ.png

数组变化当然也可以监听

<template>
    <div>
        <p>{{msg}}</p>
        <button @click="c">button</button>
    </div>
</template>

<script>
    import {
        reactive
    } from 'vue'
    export default {
        name: 'App',
        setup() {
            let msg = reactive([1, 2, 3])

            function c() {
                console.log(msg);
                msg[0] += 1;
                msg[1] = 5;
            }
            return {
                msg,
                c
            };
        }
    }
</script>
复制代码

08uUcn.png

对象数组也可

<template>
    <div>
        <p>{{msg}}</p>
        <button @click="push">push</button>
        <button @click="change">change</button>
    </div>
</template>

<script>
    import {
        reactive
    } from 'vue'
    export default {
        name: 'App',
        setup() {
            let msg = reactive([{
                name: 'lilei',
                age: 12
            }])

            function change() {
                console.log(msg);
                msg[0].age += 1;
            }

            function push() {
                console.log(msg);
                msg.push({
                    name: 'zhaodafa',
                    age: 22
                })
            }
            return {
                msg,
                change,
                push
            };
        }
    }
</script>
复制代码

特殊情况:reactive 中监听 Date 日期格式数据

如果参数不是数组、对象,而是稍微奇怪一点的数据类型,例如说 Date ,那么麻烦又来了。

<template>
    <div>
        <p>{{msg}}</p>
        <button @click="c">button</button>
    </div>
</template>

<script>
    import {
        reactive
    } from 'vue'
    export default {
        name: 'App',
        setup() {
            let msg = reactive(new Date())

            function c() {
                console.log(msg);
                msg.setDate(msg.getDate() + 1);
                console.log(msg);
            }
            return {
                msg,
                c
            };
        }
    }
</script>
复制代码

08uaXq.png

08uwn0.png

这里我先打印了 msg 两次,可以看到,点击一次 button ,msg 的数据是存在变化的,但界面并未发生变化,同时我们发现在控制台里,msg 并未被识别成 proxy

就算我们把 Date 放在对象里,如下:

<template>
    <div>
        <p>{{msg.date}}</p>
        <button @click="c">button</button>
    </div>
</template>

<script>
    import {
        reactive
    } from 'vue'
    export default {
        name: 'App',
        setup() {
            let msg = reactive({
                date: new Date()
            });

            function c() {
                console.log(msg);
                msg.date.setDate(msg.date.getDate() + 1);
                console.log(msg);
            }
            return {
                msg,
                c
            };
        }
    }
</script>
复制代码

也仍然不起效果。

08u0BV.png

08uB7T.png

显然,对于这种数据类型,我们需要做特殊处理。

处理方式就是重新赋值(而不是直接修改原来的值)。

<template>
    <div>
        <p>{{msg.date}}</p>
        <button @click="c">button</button>
    </div>
</template>

<script>
    import {
        reactive
    } from 'vue'
    export default {
        name: 'App',
        setup() {
            let msg = reactive({
                date: new Date()
            });

            function c() {
                console.log(msg);
                msg.date.setDate((msg.date.getDate() + 1));
                msg.date = new Date(msg.date);
                console.log(msg);
            }
            return {
                msg,
                c
            };
        }
    }
</script>
复制代码

这里我采用了拷贝的方案重新赋值了 msg.date,界面成功发生了变化(日期 + 1)。

08urAU.png

ref

ref可以监听复杂对象也可以监听基础数据类型,如下:

<template>
    <!-- 直接取值,无需xxx.value -->
    <div>{{obj.name}}-{{obj.count}}</div>
    <div>{{basetype}}</div>
    <div>{{date}}</div>
</template>
<script>
    import {
        ref
    } from 'vue';
    export default {
        setup() {
            const obj = ref({
                count: 1,
                name: "张三"
            })
            const basetype = ref(2)
            const date = ref(new Date())
            setTimeout(() => {
                obj.value.count = obj.value.count + 1
                obj.value.name = "李四"
                basetype.value += 1
                date.value.setDate((date.value.getDate() + 1)); // 此处也可直接修改Date类型,不需要重新赋值
                // date.setDate((date.value.getDate() + 1));
                // date = new Date(date);
            }, 1000)


            return {
                obj,
                basetype,
                date
            }
        },
    }
</script>
复制代码

ref 监听Date类型也可直接修改Date类型,不需要重新拷贝赋值

但是要注意ref监听的对象在setup方法中需要使用xxx.value来赋值和取值;在页面上可以直接取值

解构方法:toRefs

页面是通过user.name,user.age写感觉很繁琐,我们能不能直接将user中的属性解构出来使用呢?答案是不能直接对user进行结构, 这样会消除它的响应式, 上面我们已经说过了。上面我们说props不能使用ES6直接解构

解决办法就是使用toRefs

toRefs用于将一个reactive对象转化为属性全部为ref对象的普通对象。具体使用方式如下:

<template>
    <div class="homePage">
        <p>第 {{ year }} 年</p>
        <p>姓名: {{ nickname }}</p>
        <p>年龄: {{ age }}</p>
    </div>
</template>

<script>
    import {
        defineComponent,
        reactive,
        ref,
        toRefs
    } from "vue";
    export default defineComponent({
        setup() {
            const year = ref(0);
            const user = reactive({
                nickname: "xiaofan",
                age: 26,
                gender: "女"
            });
            setInterval(() => {
                year.value++
                user.age++
            }, 1000)
            return {
                year,
                // 使用reRefs
                ...toRefs(user)
            }
        },
    });
</script>
复制代码

watch 与 watchEffect

watch 函数用来侦听特定的数据源,并在回调函数中执行副作用。默认情况是惰性的,也就是说仅在侦听的源数据变更时才执行回调。

watch(source, callback, [options])
复制代码

参数说明:

  • source:可以支持string,Object,Function,Array; 用于指定要侦听的响应式变量
  • callback: 执行的回调函数
  • options:支持deep、immediate 和 flush 选项。

侦听reactive定义的数据

<template>
    <div>{{nickname}}</div>
</template>
<script>
    import {
        defineComponent,
        ref,
        reactive,
        toRefs,
        watch
    } from "vue";
    export default defineComponent({
        setup() {
            const state = reactive({
                nickname: "xiaofan",
                age: 20
            });

            setTimeout(() => {
                state.age++
            }, 1000)

            // 修改age值时会触发 watch的回调
            watch(
                () => state.age,
                (curAge, preAge) => {
                    console.log("新值:", curAge, "老值:", preAge);
                }
            );

            return {
                ...toRefs(state)
            }
        },
    });
</script>
复制代码

侦听ref定义的数据

const year = ref(0)

setTimeout(() =>{
    year.value ++ 
},1000)

watch(year, (newVal, oldVal) =>{
    console.log("新值:", newVal, "老值:", oldVal);
})
复制代码

侦听多个数据

上面两个例子中,我们分别使用了两个watch, 当我们需要侦听多个数据源时, 可以进行合并, 同时侦听多个数据:

watch([() => state.age, year], ([curAge, preAge], [newVal, oldVal]) => {
    console.log("新值:", curAge, "老值:", preAge);
    console.log("新值:", newVal, "老值:", oldVal);
});
复制代码

侦听复杂的嵌套对象

我们实际开发中,复杂数据随处可见, 比如:

const state = reactive({
    room: {
    id: 100,
    attrs: {
        size: "140平方米",
        type:"三室两厅"
    },
    },
});
watch(() => state.room, (newType, oldType) => {
    console.log("新值:", newType, "老值:", oldType);
}, {deep:true});
复制代码

在复杂数据访问中,如果不使用第三个参数deep:true, 是无法监听到数据变化的。

前面我们提到,默认情况下,watch是惰性的, 那什么情况下不是惰性的, 可以立即执行回调函数呢?其实使用也很简单, 给第三个参数中设置immediate: true即可。

stop 停止监听

我们在组件中创建的watch监听,会在组件被销毁时自动停止。如果在组件销毁之前我们想要停止掉某个监听, 可以调用watch()函数的返回值,操作如下:

const stopWatchRoom = watch(() => state.room, (newType, oldType) => {
    console.log("新值:", newType, "老值:", oldType);
}, {deep:true});

setTimeout(()=>{
    // 停止监听
    stopWatchRoom()
}, 3000)
复制代码

还有一个监听函数watchEffect,在我看来watch已经能满足监听的需求,为什么还要有watchEffect呢?虽然我没有get到它的必要性,但是还是要介绍一下watchEffect,首先看看它的使用和watch究竟有何不同。

import { defineComponent, ref, reactive, toRefs, watchEffect } from "vue";
export default defineComponent({
  setup() {
    const state = reactive({ nickname: "xiaofan", age: 20 });
    let year = ref(0)

    setInterval(() =>{
        state.age++
        year.value++
    },1000)

    watchEffect(() => {
        console.log(state);
        console.log(year);
      }
    );

    return {
        ...toRefs(state)
    }
  },
});
复制代码

执行结果首先打印一次stateyear值;然后每隔一秒,打印stateyear值。

从上面的代码可以看出, 并没有像watch一样需要先传入依赖,watchEffect会自动收集依赖, 只要指定一个回调函数。在组件初始化时, 会先执行一次来收集依赖, 然后当收集到的依赖中数据发生变化时, 就会再次执行回调函数。

所以总结对比如下:

  1. watchEffect 不需要手动传入依赖
  2. watchEffect 会先执行一次用来自动收集依赖
  3. watchEffect 无法获取到变化前的值, 只能获取变化后的值

Tips: 如果定义一个非响应式的值, watch和watchEffect是无法监听到值的变化的!!!

自定义 Hooks

在vue2 中可以抽出mixin 来实现共有逻辑功能,(但是其弊端在此处就不赘述了), vue3中可以将其封装成一个hook, 我们约定这些「自定义 Hook」以 use 作为前缀,和普通的函数加以区分。

useCount.js 实现:

import {
    ref,
    computed
} from "vue";


export default function useCount(initValue = 1) {
    const count = ref(initValue);

    const increase = (delta) => {
        if (typeof delta !== "undefined") {
            count.value += delta;
        } else {
            count.value += 1;
        }
    };
    const multiple = computed(() => count.value * 2)

    const decrease = (delta) => {
        if (typeof delta !== "undefined") {
            count.value -= delta;
        } else {
            count.value -= 1;
        }
    };

    return {
        count,
        multiple,
        increase,
        decrease,
    };
}
复制代码

接下来看一下在组件中使用useCount这个 hook:

<template>
    <div>
        <p>count: {{ count }}</p>
        <p>倍数: {{ multiple }}</p>
        <div>
            <button @click="increase()">加1</button>
            <button @click="decrease()">减一</button>
        </div>
    </div>

</template>
<script>
    import {
        defineComponent
    } from 'vue';
    import useCount from "../hooks/useCount";
    export default defineComponent({
        setup() {
            const {
                count,
                multiple,
                increase,
                decrease
            } = useCount(10);
            return {
                count,
                multiple,
                increase,
                decrease,
            };
        },
    });
</script>
复制代码

props父子组件传参

父子组建通过props传递参数,如 <test :name="basetype"></test> 中的name

// 父组件
<template>
    <div>
        父组件 basetype:{{basetype}}
        <button @click="addNum">addNum</button>
    </div>

    <test :name="basetype"></test>
</template>
<script>
import {
  ref, defineComponent, getCurrentInstance, readonly
} from 'vue';
import test from './test.vue'
export default defineComponent({
  components: { test },
  setup() {
    const basetype = ref(2)

    function addNum() {
      basetype.value += 1
    }

    return {
      addNum,
      basetype,
    }
  },
})
</script>
复制代码
// 子组件
<template>
    <div style="border: 1px solid red;">
        <h2>子组件 name from basetype:{{name}}</h2>
    </div>
</template>
<script>
import { defineComponent, watch } from 'vue'
export default defineComponent({
  props: {
    name: {
      type: Number
    }
  },
  setup(props) {
    watch(props, (val, oldVal) => {
      console.log('子组件watch: ' + JSON.stringify(val))
    })
    return {
      // props传递过来的值直接在template中使用,不需要return
      // name: props.name  // 此处不可以写;写了之后就会认为是return { name: 2 };不会是响应式的了
    }
  },
})
</script>
复制代码
image-20210711214827114

image-20210711221229769

Teleport

Teleport 就是将组件的位置传送绑定到定义的Dom位置,像是哆啦A梦中的「任意门」,之前Element UI的dialog和select展开之后的组件默认不是绑定在父组件的dom元素上,而是在body下的全局元素。

我们的Teleport其实就是类似于这样的功能,但是它是支持想绑在哪里就绑在哪里的,先看一个小例子:

<template>
    <div>父组件</div>
    <button @click="show = !show">changeShow</button>
    <modal v-if="show">
        <div>子组件 slot</div>
        <div slot="footer">子组件 footer slot</div>
    </modal>
</template>
<script setup>
import { ref, defineComponent } from 'vue';
import modal from './test.vue'
let show = ref(false)
</script>
复制代码
// teleport 组件
<template>
    <teleport to="body">  // 这里确定了绑定元素到body里
        <div :class="$style.modal">
            Modal
            <div class="modal_content">
                <slot></slot>
            </div>
            <div class="modal_footer">
                <slot name="footer"></slot>
            </div>
        </div>
    </teleport>
</template>

<style lang='scss' module>
.modal {
    border: 1px solid red;
}
</style>
复制代码

image-20210711223326811

我们再换一个地方绑定,将上面的body修改为<teleport to="#modaldom">, 下图可看到绑定的位置发生了变化

image-20210711224318848

Suspense

Suspense是Vue 3新增的内置标签,针对异步组件采取的降级渲染效果,小的层面可以处理请求数据未返回时候组件的渲染Loading,个人觉得在骨架屏方面也可以使用。

异步组件一般有以下的情况:

  • 页面加载之前显示加载动画
  • 显示占位符内容
  • 处理延迟加载的图像和文件
  • 骨架屏

以前在Vue 2中,我们必须使用v-if 加上一个loading标识位来检查我们的数据是否已加载并显示内容,但是现在,可以使用随Vue3的Suspense了,下面看代码:

<template>
    <Suspense>
        <template #default>
            <asyncCom></asyncCom>
        </template>
        <template #fallback>
            <div>Loading...</div>
        </template>
    </Suspense>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue';
const asyncCom = defineAsyncComponent(() => import("./test.vue"));
</script>
复制代码
<template>
    <div>
        <ul>
            <li v-for="item in jsonData" :key="item.name">{{ item.name }} - {{ item.age }}</li>
        </ul>
    </div>
</template>

<script setup>
import { ref } from "vue";
function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        {
          name: "张三",
          age: 15,
        },
        {
          name: "李四",
          age: 17,
        },
      ]);
    }, 1500);
  });
}

ref: jsonData = await fetchData();
</script>
复制代码

效果如下:

异步组建加载前:

image-20210711230750117

异步组件加载后:

image-20210711230807293

Vue从上到下执行子组件的setup里的全部语句,执行完同步语句(包括await语句)之后,父组件就认为子组件加载完成,在这之前,子组件setup状态始终未pending,所以父组件显示降级内容(Loading...),等子组件setup的状态变成resolved或者rejected,父组件就显示默认内容。

拓展阅读:

Object.defineProperty vs Proxy

Vue2.x的时候就经常遇到一个问题,数据更新了啊,为何页面不更新呢?什么时候用$set更新,什么时候用$forceUpdate强制更新,你是否也一度陷入困境。后来的学习过程中开始接触源码,才知道一切的根源都是 Object.defineProperty

这里简单对比一下Object.defineProperty 与Proxy

  1. Object.defineProperty只能劫持对象的属性, 而Proxy是直接代理对象由于Object.defineProperty只能劫持对象属性,需要遍历对象的每一个属性,如果属性值也是对象,就需要递归进行深度遍历。但是Proxy直接代理对象, 不需要遍历操作
  2. Object.defineProperty对新增属性需要手动进行Observe,因为Object.defineProperty劫持的是对象的属性,所以新增属性时,需要重新遍历对象, 对其新增属性再次使用Object.defineProperty进行劫持。也就是Vue2.x中给数组和对象新增属性时,需要使用$set才能保证新增的属性也是响应式的, $set内部也是通过调用Object.defineProperty去处理的。

参考博文:

[1] vue3中reactive注意点(系列四) www.cnblogs.com/fsg6/p/1448…

[2]Vue 3新引入的Suspense组件介绍 www.jianshu.com/p/4bc2dfba1…

文章分类
前端
文章标签