一路繁花似锦绣前程
失败的越多,成功才越有价值

导航

 

十、Vue3的组件化(二)

1、组件的拆分和嵌套
* 对组件进行拆分,拆分成一个个小的组件
* 再将这些组件组合嵌套在一起,最终形成我们的应用程序
2、组件的css作用域(vue-loader)
<!-- 如果子组件是多个根节点,则子组件的根节点上不会有父组件的data-v- -->
<aaa data-v-aaa>
    <div data-v-aaa></div>
    <bbb data-v-bbb data-v-aaa>
        <div data-v-aaa data-v-bbb></div>
        <ccc data-v-ccc data-v-aaa data-v-bbb>
            <div data-v-aaa data-v-ccc></div>
        </ccc>
    </bbb>
</aaa>

<style scoped>
    div div {}
    /*div div[data-v-aaa] {}*/

    div /deep/ div {}
    /*div[data-v-aaa] div {}*/
</style>
3、组件的通信props和$emit
  • 概念
* 父子组件通信:
      - 父传子:通过props属性
      - 子传父:通过$emit触发事件
* 非父子组件通信方式
      - provide和inject
      - mitt全局事件总线
  • props用法
<script>
    export default {
        // props: ["name", "gender", "age"],
        props: {
            name: String,
            gender: [String, Number],
            age: {
                // type可选值:String,Number,Boolean,Array,Object,Date,Function,Symbol
                type: Number,
                required: true,
                /**
                 * 默认值为引用类型,如果default为对象,则会出现多个引用指向同一个对象的问题。
                 * 所以default必须为一个工厂函数,返回一个对象
                 */
                default: 19,
                validator(value) {
                    return !!value
                }
            }
        }
    }
</script>
  • props命名
HTML中标签的属性名大小写不敏感,浏览器会把大写字符解释为小写字符。
所以模板中的属性名(prop)需要使用短横线分隔命名(组件名也是这样)
  • 非prop的attribute
* 非prop的attribute:当我们传递给一个组件某个属性,但是该属性并没有定义对应的props或者emits时,
  就称之为非prop的attribute
* attribute继承:当组件有单个根节点时,非prop的attribute将自动添加到根节点的attribute中
<!--当组件有多个根节点时,如果有非prop的attribute(不会添加到根节点的attribute中),会报警告-->
<template>
    <div>黄婷婷</div>
    <!--  消除警告方式一:显示绑定  -->
    <div :name="$attrs.name">黄婷婷</div>
</template>

<script>
    export default {
        // 消除警告方式二:禁用attribute继承
        // inheritAttrs: false,
        mounted() {
            // 通过$attrs来访问所有的非prop的attribute
            console.log(this.$attrs)
        }
    }
</script>
  • 自定义事件
<!--父组件-->
<template>
    <div>{{name}}:{{age}}</div>
    <child @emit-handler="emitHandler"></child>
</template>

<script>
    import child from "./components/child"

    export default {
        components: {child},
        data() {
            return {
                name: "",
                age: 0
            }
        },
        methods: {
            emitHandler(name, age) {
                this.name = name
                this.age = age
            }
        }
    }
</script>
<!--子组件-->
<template>
    <button @click="$emit('emitHandler','黄婷婷',18)">事件</button>
</template>

<script>
    export default {
        // 数组写法
        emits: ['emitHandler']
        // 对象写法
        /*emits: {
            // 值可以是null
            emitHandler: (name, age) => {
                return true
            }
        }*/
    }
</script>
  • provide和inject基本使用
<!--父组件-->
<template>
    <son></son>
    <button @click="age++">按钮</button>
</template>

<script>
    import son from "./views/son"
    import {computed} from "vue"

    export default {
        components: {son},
        provide() {
            return {
                name: "丁彩燕",
                // provide可以是对象,this要指向当前组件实例,则provide必须是一个函数
                // computed返回值是一个ref对象,将age变成一个响应式的数据
                age: computed(() => {
                    return this.age
                })
            }
        },
        data() {
            return {
                age: 18
            }
        }
    }
</script>
<!--子孙组件-->
<template>
    <div>{{name}}</div>
    <!--ref对象的值通过.value获取-->
    <div>{{age.value}}</div>
</template>

<script>
    export default {
        inject: ["name", "age"]
    }
</script>
  • 全局事件总线mitt库
# Vue3从实例中移除了$on、$off、$once方法,希望继续使用全局事件总线,要安装第三方的mitt库
npm i -S mitt
// 工具
import mitt from "mitt";

const emitter = mitt()

export default emitter
<!-- 发出事件 -->
<template>
    <button @click="btnHandler">按钮</button>
</template>

<script>
    import emitter from "../utils/eventBus";

    export default {
        methods: {
            btnHandler() {
                emitter.emit("onBtnHandler", {name: "黄婷婷", age: 18})
            }
        }
    }
</script>
<!-- 监听事件 -->
<template>
    <div>{{person.name}}:{{person.age}}</div>
</template>

<script>
    import emitter from "../utils/eventBus";

    export default {
        data() {
            return {
                person: {}
            }
        },
        mounted() {
            emitter.on("onBtnHandler", arg => {
                this.person = arg
            })
            /*emitter.on("*", (type, arg) => {
                console.log(type) // onBtnHandler
                this.person = arg
            })*/
        }
    }
</script>
  • mitt的事件取消
// 取消emitter中所有的监听
emitter.all.clear()

// 定义一个函数
function onFoo() {}
emitter.on("foo", onFoo)// 监听
emitter.off("foo", onFoo)// 取消监听
4、插槽slot
  • 基本使用
<!-- 提供插槽的组件 -->
<template>
    <div>
        <button>返回</button>
        <!-- 当有多个插槽时,每个插槽都会显示对应插入的内容 -->
        <slot></slot>
        <slot>
            <!-- 插槽可赋默认内容 -->
            <span>张婧仪</span>
        </slot>
        <!-- 提供具名插槽。未具名插槽,相当于具名default插槽 -->
        <slot name="right"></slot>
        <!-- 动态插槽名 -->
        <slot :name="name"></slot>
    </div>
</template>

<script>
    export default {
        props: {
            name: {
                type: String,
                required: true
            }
        }
    }
</script>
<!-- 使用插槽组件 -->
<template>
    <first :name="name">
        <!-- 插槽的根节点可以有多个 -->
        <span>黄婷婷</span>
        <span>孟美岐</span>
        <!-- 使用具名插槽,不可以使用多个相同的具名插槽 -->
        <!--<template v-slot:right>
            <button>菜单</button>
        </template>-->
        <!-- 具名插槽缩写 -->
        <template #right>
            <button>菜单</button>
        </template>
        <!-- 使用动态插槽名,v-bind:default优先级较低 -->
        <template v-slot:[name]>
            <span>张婧仪</span>
        </template>
    </first>
</template>

<script>
    import First from "./components/First";

    export default {
        components: {First},
        data() {
            return {
                name: "icon"
            }
        }
    }
</script>
  • 渲染作用域
- 父级模板里的所有内容都是在父级作用域中编译的
- 子模板里的所有内容都是在子作用域中编译的
  • 作用域插槽
<!-- 提供插槽的组件 -->
<template>
    <table>
        <!-- 属性优先级:name > v-for > v-bind -->
        <slot name="default" v-for="li in persons" 
              v-bind="li" :key="li.name"></slot>
    </table>
</template>

<script>
    export default {
        props: {
            persons: {
                type: Array,
                required: true
            }
        }
    }
</script>
<!-- 使用插槽组件 -->
<template>
    <first :persons="persons">
        <!-- 简写:v-slot="" 或 #="" -->
        <template v-slot:default="row">
            <tr>
                <td>{{row.name}}</td>
                <td>{{row.age}}</td>
            </tr>
        </template>
    </first>
</template>

<script>
    import First from "./components/First";

    export default {
        components: {First},
        data() {
            return {
                persons: [
                    {name: "黄婷婷", age: 18},
                    {name: "孟美岐", age: 19},
                    {name: "鞠婧祎", age: 20}
                ]
            }
        }
    }
</script>
  • 独占默认插槽的细节
<template>
    <first :persons="persons" v-slot:default="row">
        <!-- 当使用其他的插槽时,独占默认插槽的<template>不能省 -->
        <tr>
            <td>{{row.name}}</td>
            <td>{{row.age}}</td>
        </tr>
    </first>
</template>

十一、Vue3的组件化(三)

1、动态组件component的使用
<template>
    <button @click="comp='first'">first</button>
    <button @click="comp='second'">second</button>
    <component :is="comp"
               :person="{name:'黄婷婷',age:18}"
               @btn-click="btnClick"
    ></component>
</template>

<script>
    import First from "./components/First";
    import Second from "./components/Second";

    export default {
        // component组件的is属性的值,为已注册的组件名(局部 > 全局)
        components: {First, Second},
        data() {
            return {
                comp: 'first'
            }
        },
        methods: {
            btnClick(ev) {
                console.log(ev)
            }
        }
    }
</script>
2、keep-alive
<template>
    <button @click="comp='first'">first</button>
    <button @click="comp='second'">second</button>
    <keep-alive :include="['first']">
        <component :is="comp"></component>
    </keep-alive>
</template>

<script>
    /**
     * 1、keep-alive有一些属性:
     *     - include(string|RegExp|array):只有名称匹配的组件会被缓存
     *     - exclude(string|RegExp|array):任何名称匹配的组件都不缓存
     *     - max(string|RegExp|array):最多可以缓存多少组件实例,
     *       一旦达到这个数字,那么缓存组件中最近没有被访问的实例会被销毁
     * 2、include和exclude prop允许组件有条件地缓存
     *     - 二者都可以用逗号分隔字符串、正则表达式或一个数组来表示
     *     - 匹配首先检查组件自身的name选项
     */
    import First from "./components/First";
    import Second from "./components/Second";

    export default {
        components: {First, Second},
        data() {
            return {
                comp: 'first'
            }
        }
    }
</script>
<template>
    <div>
        <div>first</div>
        <button @click="count++">按钮</button>
        {{count}}
    </div>
</template>

<script>
    export default {
        name: 'first',
        data() {
            return {
                count: 0
            }
        }
    }
</script>
3、异步组件和代码分包
  • webpack的代码分包
// 通过import函数导入的模块,后续webpack对其进行打包的时候就会进行分包的操作
import('./utils/index').then(res => {
    console.log(res.sum(1, 2))
})
  • 异步组件
<script>
    import {defineAsyncComponent} from "vue"
    import First from "./components/First";
    // 1、函数写法
    // const Second = defineAsyncComponent(() => import('./components/Second'))
    // 2、对象写法
    const Second = defineAsyncComponent({
        loader: () => import('./components/Second'),
        // loadingComponent,// 组件加载中显示的组件
        // errorComponent,// 组件加载失败显示的组件
        // 在显示loadingComponent组件之前,等待多长时间
        delay: 2000,
        /**
         * @param error:错误信息
         * @param retry:函数,调用retry尝试重新加载
         * @param fail:函数,指示加载程序结束退出
         * @param attempts:记录尝试的次数
         */
        onError: function (error, retry, fail, attempts) {
        }
    })

    export default {
        components: {First, Second}
    }
</script>
  • 异步组件和Suspense
<!--
suspense是一个内置的全局组件,该组件有两个插槽
    - default:如果default可以显示,那么显示default的内容
    - fallback:如果default无法显示,那么会显示fallback插槽的内容
-->
<suspense>
    <template #default>
        <异步组件></异步组件>
    </template>
    <template #fallback>
        <loading></loading>
    </template>
</suspense>
4、$refs的使用
<template>
    <!-- 绑定到一个元素上 -->
    <div ref="div">黄婷婷</div>
    <!-- 绑定到一个组件实例上 -->
    <first ref="first"></first>
    <button @click="btnHandler">按钮</button>
</template>

<script>
    import First from "./components/First";

    export default {
        components: {First},
        methods: {
            btnHandler() {
                // 获取元素对象
                console.log(this.$refs.div)
                // 获取组件实例的Proxy对象
                console.log(this.$refs.first)
                // 操作属性
                this.$refs.first.name = '姜贞羽'
                // 调用函数
                this.$refs.first.eventHandler()
            }
        }
    }
</script>
5、$parent、$root、$el
* 通过组件实例的$parent来访问父组件
* 通过组件实例的$root来访问根组件
* 通过组件实例的$el来访问组件的根元素(测试发现如果是多个根元素$el为#text节点)
* 注意:在Vue3中已经移除了$children的属性
6、生命周期
  • 生命周期
<template>
    <div ref="div">{{name}}</div>
    <button @click="name='黄婷婷'">美女</button>
</template>

<script>
    export default {
        data() {
            return {
                name: '孟美岐'
            }
        },
        beforeCreate() {
            // beforeCreate undefined undefined
            console.log('beforeCreate', this.name, this.$refs.div?.innerText)
        },
        created() {
            // created 孟美岐 undefined
            console.log('created', this.name, this.$refs.div?.innerText)
        },
        beforeMount() {
            // beforeMount 孟美岐 undefined
            console.log('beforeMount', this.name, this.$refs.div?.innerText)
        },
        mounted() {
            // mounted 孟美岐 孟美岐
            console.log('mounted', this.name, this.$refs.div?.innerText)
        },
        // 视图发生更新才会调用
        beforeUpdate() {
            // beforeUpdate 黄婷婷 孟美岐
            console.log('beforeUpdate', this.name, this.$refs.div?.innerText)
        },
        // 视图发生更新才会调用
        updated() {
            // updated 黄婷婷 黄婷婷
            console.log('updated', this.name, this.$refs.div?.innerText)
        },
        beforeUnmount() {
            // beforeUnmount 黄婷婷 黄婷婷
            console.log('beforeUnmount', this.name, this.$refs.div?.innerText)
        },
        unmounted() {
            // unmounted 黄婷婷 undefined
            console.log('unmounted', this.name, this.$refs.div?.innerText)
        }
    }
</script>
  • 缓存组件的生命周期
<script>
    export default {
        name: 'first',
        // 该组件必须已缓存<keep-alive>才有的生命周期
        activated() {
            console.log('activated')
        },
        // 该组件必须已缓存<keep-alive>才有的生命周期
        deactivated() {
            console.log('deactivated')
        }
    }
</script>
7、组件的v-model
<template>
    <first v-model="name" v-model:age="age"></first>
    <div>{{name}}</div>
    <div>{{age}}</div>
</template>

<script>
    import First from "./components/First";

    export default {
        components: {First},
        data() {
            return {
                name: '黄婷婷',
                age: 18
            }
        }
    }
</script>
<!--First.vue-->
<template>
    <input type="text" v-model="modelValueHandler">
    <input type="text" v-model="ageHandler">
</template>

<script>
    export default {
        /**
         * 1、v-model:modelValue=""简写v-model=""
         * 2、emits命名固定:'update:对应的props名'
         */
        props: ['modelValue', 'age'],
        emits: ['update:modelValue', 'update:age'],
        computed: {
            modelValueHandler: {
                set(val) {
                    this.$emit('update:modelValue', val)
                },
                get() {
                    return this.modelValue
                }
            },
            ageHandler: {
                set(val) {
                    this.$emit('update:age', val)
                },
                get() {
                    return this.age
                }
            },
        }
    }
</script>

十二、Vue3过渡&动画实现

1、过渡动画
  • 基本使用
<template>
    <button @click="show=!show">按钮</button>
    <transition name="fade">
        <div v-if="show">黄婷婷</div>
    </transition>
</template>

<script>
    export default {
        data() {
            return {
                show: true
            }
        }
    }
</script>

<style scoped>
    .fade-enter-from, .fade-leave-to {
        opacity: 0;
    }

    .fade-enter-to, .fade-leave-from {
        opacity: 1;
    }

    .fade-enter-active, .fade-leave-active {
        transition: all 1s ease;
    }
</style>
  • 概念
* 当插入或删除包含在transition组件中的元素时,vue将会做以下处理
      - 自动嗅探目标元素是否应用了css过渡或者动画,如果有,那么在恰当的时机添加/删除css类名
      - 如果transition组件提供了javascript钩子函数,这些钩子函数将在恰当的时机被调用
      - 如果没有找到javascript钩子并且也没有检测到css过渡/动画,
        DOM插入、删除操作将会立即执行
* 我们会发现上面提到了很多个class,事实上vue就是帮助我们在这些class之间来回切换完成的动画
      - v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,
        在元素被插入之后的下一帧移除
      - v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,
        在元素被插入之前生效,在过渡/动画完成之后移除。
        这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数
      - v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效
       (与此同时v-ente-from被移除),在过渡/动画完成之后移除
      - v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除
      - v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,
        在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。
        这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数
      - v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效
        (与此同时v-leave-from被删除),在过渡/动画完成之后移除
* class的name命名规则如下:
      - 如果我们使用的是一个没有name的transition,name所有的class是以v-作为默认前缀
      - 如果我们添加了一个name属性,比如<transition name="fade">,
        那么所有的class会以fade-开头
2、帧动画
  • 基本使用
<template>
    <button @click="show=!show">按钮</button>
    <transition name="fade">
        <div v-if="show">黄婷婷</div>
    </transition>
</template>

<script>
    export default {
        data() {
            return {
                show: true
            }
        }
    }
</script>

<style scoped>
    .fade-enter-active {
        animation: fade 1s ease;
    }

    .fade-leave-active {
        animation: fade 1s ease reverse;
    }

    @keyframes fade {
        0% {
            opacity: 0;
        }
        100% {
            opacity: 1;
        }
    }
</style>
  • 过渡动画和帧动画同时使用
<!-- 
* 可以设置type属性为animation或者transition来明确的告知vue监听的类型
      - animationstart:帧动画开始
      - animationend:帧动画结束
      - transitionstart:过渡动画开始
      - transitionend:过渡动画结束
 -->
<transition type="transition"></transition>
  • 明确指定动画的时间
<!-- 过渡/动画未完成,class移除的话,过渡/动画会直接失效 -->
<!-- Number -->
<transition :duration="1000"></transition>
<!-- Object -->
<transition :duration="{enter:800,leave:1000}"></transition>
3、动画模式mode
  • mode
<!--
* 默认:动画同时进行
* in-out:enter执行完才执行leave
* out-in:leave执行完才执行enter
 -->
<transition mode="out-in"></transition>
  • appear
<!-- 组件是否初始化即执行动画,默认false -->
<transition appear></transition>
4、animate.css
  • 配合属性name使用
# 安装
npm i -S animate.css
// 入口文件引入
import "animate.css"
<template>
    <button @click="show=!show">按钮</button>
    <!-- 动画不适用于行内元素(inline) -->
    <transition name="wobble">
        <div class="container" v-if="show">黄婷婷</div>
    </transition>
</template>

<script>
    export default {
        data() {
            return {
                show: true
            }
        }
    }
</script>

<style scoped>
    .container {
        display: inline-block;
    }

    .wobble-enter-active {
        /*官网找动画关键帧*/
        animation: wobble 1s ease;
    }

    .wobble-leave-active {
        animation: wobble 1s ease reverse;
    }
</style>
  • 配合属性enter-active-class或leave-active-class使用
<template>
    <button @click="show=!show">按钮</button>
    <!--
    * 自定义过渡类名
        - enter-from-class
        - enter-active-class
        - enter-to-class
        - leave-from-class
        - leave-active-class
        - leave-to-class
     -->
    <transition enter-active-class="animate__animated animate__backInUp"
                leave-active-class="animate__animated animate__backInUp animate__reverse">
        <div class="container" v-if="show">黄婷婷</div>
    </transition>
</template>

<script>
    export default {
        data() {
            return {
                show: true
            }
        }
    }
</script>

<style scoped>
    .container {
        display: inline-block;
    }

    .animate__reverse {
        animation-direction: reverse;
    }
</style>
5、gsap
  • transition动画钩子函数
<transition @before-enter=""
            @enter=""
            @after-enter=""
            @enter-cancelled=""
            @before-leave=""
            @leave=""
            @after-leave=""
            @leave-cancelled=""
            :css="false">
</transition>
  • gsap基本使用
# 安装
npm i -S gsap
<template>
    <button @click="show=!show">按钮</button>
    <!-- 在使用js动画的时候,css属性通常设为false(提高性能) -->
    <transition @enter="enter"
                @leave="leave"
                :css="false">
        <div class="container" v-if="show">黄婷婷</div>
    </transition>
</template>

<script>
    import gsap from "gsap"

    export default {
        data() {
            return {
                show: true
            }
        },
        methods: {
            enter(el, done) {
                gsap.from(el, {
                    scale: 0,
                    x: 200,
                    onComplete: done
                })
            },
            leave(el, done) {
                gsap.to(el, {
                    scale: 0,
                    x: 200,
                    onComplete: done
                })
            },
        }
    }
</script>

<style scoped>
    .container {
        display: inline-block;
    }
</style>
  • gsap实现数字翻牌器
<template>
    <button @click="changeNumber=changeNumber?0:100">按钮</button>
    <div>{{showNumber}}</div>
</template>

<script>
    import gsap from "gsap"

    export default {
        data() {
            return {
                changeNumber: 0,
                showNumber: 0
            }
        },
        watch: {
            changeNumber(val) {
                gsap.to(this, {duration: 1, showNumber: val})
            }
        }
    }
</script>
6、列表的过渡
# 安装
npm i -S lodash
<template>
    <button @click="addNum">添加数字</button>
    <button @click="removeNum">删除数字</button>
    <button @click="shuffleNum">数字洗牌</button>
    <!-- tag="p"表示包裹的元素,不指定则不包裹元素 -->
    <transition-group tag="p" name="why">
        <!-- 需要提供唯一的key -->
        <span v-for="item in numbers" :key="item" class="item">{{item}}</span>
    </transition-group>
</template>

<script>
    import _ from "lodash"

    export default {
        data() {
            return {
                numbers: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
                numCounter: 10
            }
        },
        methods: {
            addNum() {
                this.numbers.splice(this.randomIndex(), 0, this.numCounter++)
            },
            removeNum() {
                this.numbers.splice(this.randomIndex(), 1)
            },
            shuffleNum() {
                this.numbers = _.shuffle(this.numbers)
            },
            randomIndex() {
                return Math.floor(Math.random() * this.numbers.length)
            }
        }
    }
</script>

<style scoped>
    .item {
        display: inline-block;
        margin-right: 10px;
    }

    .why-enter-from, .why-leave-to {
        opacity: 0;
        transform: translateY(30px);
    }

    .why-enter-active, .why-leave-active {
        transition: all 1s ease;
    }

    /*元素脱离文档流*/
    .why-leave-active {
        position: absolute;
    }

    /*元素改变位置的过程中应用*/
    .why-move {
        transition: transform 1s ease;
    }
</style>
7、列表的交错过渡
<template>
    <input type="text" v-model="keyword">
    <transition-group tag="ul" :css="false"
                      @before-enter="beforeEnter"
                      @enter="enter"
                      @leave="leave">
        <li v-for="(item,index) in showNames" :key="item" :data-index="index">{{item}}</li>
    </transition-group>
</template>

<script>
    import gsap from "gsap"

    export default {
        data() {
            return {
                names: ["abc", "cba", "nba", "why", "lilei", "hmm", "kobe", "james"],
                keyword: ""
            }
        },
        computed: {
            showNames() {
                return this.names.filter(item => item.indexOf(this.keyword) !== -1)
            }
        },
        methods: {
            beforeEnter(el) {
                el.style.opacity = 0
                el.style.height = "0em"
            },
            enter(el, done) {
                gsap.to(el, {
                    opacity: 1,
                    height: "1.5em",
                    delay: el.dataset.index * 0.5,
                    onComplete: done
                })
            },
            leave(el, done) {
                gsap.to(el, {
                    opacity: 0,
                    height: "0em",
                    delay: el.dataset.index * 0.5,
                    onComplete: done
                })
            }
        }
    }
</script>

十三、composition api(一)

1、mixin基本使用
<template>
    <button @click="clickHandler">按钮</button>
    <div>{{message}}</div>
</template>

<script>
    export default {
        /**
         * 1、组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取
         * 2、在vue2和vue3中都支持的一种方式就是使用mixin来完成
         *       - mixin提供了一种非常灵活的方式,来分发vue组件中的可复用功能
         *       - 一个mixin对象可以包含任何组件选项
         *       - 当组件使用mixin对象时,所有mixin对象的选项将被混合进入该组件本身的选项中
         */
        mixins: [{
            data() {
                return {
                    message: ""
                }
            },
            mounted() {
                this.message = "黄婷婷"
            },
            methods: {
                clickHandler() {
                    this.message = "孟美岐"
                }
            }
        }]
    }
</script>
2、mixin的合并规则
* 如果是data函数的返回值对象
    - 返回值对象默认情况下会进行合并
    - 如果data返回值对象的属性发生了冲突,那么会保留组件自身的数据
* 如果生命周期钩子函数
    - 生命周期的钩子函数会被合并到数组中,都会被调用
* 值为对象的选项,例如methods、components、directives将被合并为同一个对象
    - 比如都有methods选项,并且都定义了方法,那么它们都会生效
    - 但是如果对象的key相同,那么会取组件对象的键值对
3、mixin全局混入
import {createApp} from 'vue'
import App from './App.vue'

const app = createApp(App)

app.mixin({
    data() {
        return {
            message: ""
        }
    },
    mounted() {
        this.message = "黄婷婷"
    },
    methods: {
        clickHandler() {
            this.message = "孟美岐"
        }
    }
})

app.mount('#app')
4、extends
<!--First.vue-->
<template>
    <!-- template不会继承 -->
</template>

<script>
    export default {
        data() {
            return {
                message: ""
            }
        },
        mounted() {
            this.message = "黄婷婷"
        },
        methods: {
            clickHandler() {
                this.message = "孟美岐"
            }
        }
    }
</script>
<!--App.vue-->
<template>
    <button @click="clickHandler">按钮</button>
    <div>{{message}}</div>
</template>

<script>
    import First from "./components/First";

    export default {
        extends: First
    }
</script>
5、setup参数和返回值
<template>
    <div>{{name}}</div>
    <div>{{age}}</div>
</template>

<script>
    export default {
        props: {
            name: {
                type: String,
                required: true
            }
        },
        /**
         * 1、setup函数有哪些参数
         *    - props:父组件传递过来的属性,setup中不可以使用this,
         *      props作为参数传递到setup函数中
         *    - context:包含三个属性
         *        + attrs:所有的非prop的attribute
         *        + slots:父组件传递过来的插槽(一般在render函数中使用)
         *        + emit:组件内部需要发出事件时会用到emit
         * 2、setup函数有什么样的返回值
         *    - setup的返回值可以在模板template中使用(优先级高于data)
         */
        setup(props, context) {
            return {
                age: 18
            }
        }
    }
</script>
6、setup函数源码阅读
* setup()是在解析其它组件选项之前被调用的,所以setup()内部的this的行为与其它选项
  中的this完全不同。(packages/runtime-core/src/renderer.ts:526)
7、reactive api
<template>
    <button @click="clickHandler">按钮</button>
    <div>{{state.age}}</div>
</template>

<script>
    import {reactive} from "vue"

    export default {
        setup(props, context) {
            let state = reactive({
                age: 18
            })
            const clickHandler = () => {
                state.age++
            }
            return {
                state,
                clickHandler
            }
        }
    }
</script>
8、ref api
<template>
    <button @click="clickHandler">按钮</button>
    <!-- template模板中使用ref对象,它会自动进行解包 -->
    <div>{{age}}</div>
</template>

<script>
    import {ref} from "vue"

    export default {
        setup(props, context) {
            /**
             * 1、reactive()要求传入的是一个对象或数组,
             *   传入基本类型(String、Number、Boolean)会报警告
             * 2、ref()会返回一个可变的响应式对象,
             *   内部的值是在ref的value属性中被维护的
             */
            let age = ref(18)
            const clickHandler = () => {
                age.value++
            }
            return {
                age,
                clickHandler
            }
        }
    }
</script>
9、ref浅层解包
* template模板中直接使用ref对象,会自动解包
* 当ref对象外层包裹一层普通对象,则不会自动解包
* 当ref对象外层包裹的是reactive对象,也会自动解包
10、readonly
<script>
    import {reactive, ref, readonly} from "vue"

    export default {
        setup(props, context) {
            // 1、readonly会返回原生对象的只读代理(proxy对象,set()被劫持)
            const readonlyInfo = readonly({name: "黄婷婷"})
            // readonlyInfo.name = "孟美岐" // 无法修改

            // 2、reactive对象
            const reactiveInfo = reactive({name: "黄婷婷"})
            // readonlyReactiveInfo一般传给子组件,父组件修改reactiveInfo值
            const readonlyReactiveInfo = readonly(reactiveInfo)

            // 3、ref对象
            const age = ref(18)
            const readonlyAge = readonly(age)

            return {}
        }
    }
</script>

十四、composition api(二)

1、reactive判断的api
* isProxy
    - 检查对象是否是由reactive或readonly创建的proxy
* isReactive
    - 检查对象是否是由reactive创建的响应式代理
    - 如果该代理是readonly建的,但包裹了由reactive创建的另一个代理,它也会返回true
* isReadonly
    - 检查对象是否是由readonly创建的只读代理
* toRaw
    - 返回reactive或readonly代理的原始对象(不建议保留对原始对象的持久引用。请谨慎使用)
* shallowReactive
    - 创建一个响应式代理,它跟踪其自身property的响应性,但不执行嵌套对象的深层响应式转换
      (深层还是原生对象)
* shallowReadonly
    - 创建一个proxy,使其自身的property为只读,但不执行嵌套对象的深度只读转换
      (深层还是可读、可写的)
2、toRefs和toRef
<template>
    <div>{{name}}:{{age}}</div>
    <button @click="clickHandler">按钮</button>
</template>

<script>
    import {reactive, toRefs, toRef} from "vue"

    export default {
        setup(props, context) {
            const info = reactive({name: "黄婷婷", age: 18});
            // 1、toRefs将reactive对象中的所有属性都转成ref,建立连接
            const {name} = toRefs(info)
            // 2、toRef对其中一个属性进行转换ref,建立连接
            const age = toRef(info, "age")

            const clickHandler = () => {
                /**
                 * 相当于已经在age.value和info.age之间建立了连接,
                 * 任何一个修改都会引起另外一个变化
                 */
                age.value++
            }

            return {
                name,
                age,
                clickHandler
            }
        }
    }
</script>
3、ref其他的api
<template>
    <div>{{info}}</div>
    <button @click="clickHandler">按钮</button>
</template>

<script>
    import {shallowRef, triggerRef} from "vue"

    export default {
        setup() {
            /**
             * 1、unref
             *     - 如果参数是一个ref,则返回内部值,否则返回参数本身
             *     - 这是val = isRef(val) ? val.value : val的语法糖函数
             * 2、isRef
             *     - 判断值是否是一个ref对象
             * 3、shallowRef
             *     - 创建一个浅层的ref对象
             * 4、triggerRef
             *     - 手动触发和shallowRef相关联的副作用
             */
            const info = shallowRef({name: "黄婷婷"});
            const clickHandler = () => {
                info.value.name = "孟美岐"
                triggerRef(info)
            }

            return {
                info,
                clickHandler
            }
        }
    }
</script>
4、customRef
<template>
    <input type="text" v-model="info">
    <div>{{info}}</div>
</template>

<script>
    import {customRef} from "vue"

    function debounceRef(value) {
        let timer = null
        // 创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制
        return customRef((track, trigger) => {
            return {
                get() {
                    track() // 收集依赖
                    return value
                },
                set(newValue) {
                    clearTimeout(timer)
                    timer = setTimeout(() => {
                        value = newValue
                        trigger() // 更新视图
                    }, 1000)
                }
            }
        })
    }

    export default {
        setup() {
            const info = debounceRef("黄婷婷");

            return {
                info
            }
        }
    }
</script>
5、computed
<template>
    <div>{{fullName}}</div>
    <button @click="clickHandler">按钮</button>
</template>

<script>
    import {computed, ref} from "vue"

    export default {
        setup() {
            const firstName = ref("黄婷婷")
            const lastName = ref("孟美岐")

            // 1、getter函数(返回值是ref对象)
            // const fullName = computed(() => `${firstName.value} ${lastName.value}`)
            // 2、getter、setter对象
            let fullName = computed({
                get() {
                    return `${firstName.value} ${lastName.value}`
                },
                set(newValue) {
                    const names = newValue.split(" ");
                    firstName.value = names[0]
                    lastName.value = names[1]
                }
            })

            function clickHandler() {
                fullName.value = "姜贞羽 鞠婧祎"
            }

            return {
                fullName,
                clickHandler
            }
        }
    }
</script>
6、watchEffect
<template>
    <div>{{age}}</div>
    <button @click="age++">按钮</button>
    <button @click="stopWatch">停止侦听</button>
</template>

<script>
    import {watchEffect, ref} from "vue"

    export default {
        setup() {
            const age = ref(18)

            // 1、会立即执行一次,用以收集依赖
            const stopWatch = watchEffect(onInvalidate => {
                // 2、当副作用即将重新执行或者侦听器被停止时会执行该函数传入的回调函数
                onInvalidate(() => {
                    console.log("取消网络请求,清除上一次的副作用")
                })
                console.log(age.value)
            })

            return {
                age,
                stopWatch
            }
        }
    }
</script>
7、setup引用元素和watchEffect执行时机
<template>
    <!-- ref属性会做特殊处理,无需加冒号(:ref)即可绑定 -->
    <div ref="div">黄婷婷</div>
</template>

<script>
    import {watchEffect, ref} from "vue"

    export default {
        setup() {
            const div = ref(null)

            watchEffect(() => {
                console.log(div.value)
            }, {
                /**
                 * pre(默认值):元素挂载之前执行
                 * post:元素挂载之后执行
                 * sync:
                 */
                flush: "post"
            })

            return {
                div
            }
        }
    }
</script>
8、watch
<template>
    <input type="text" v-model="name">
    <input type="text" v-model="friend.age">
    <div>{{name}}:{{friend.age}}</div>
</template>

<script>
    import {watch, toRefs, reactive} from "vue"

    export default {
        setup() {
            const info = reactive({name: "黄婷婷", friend: {age: 18}})
            const {name, friend} = toRefs(info)

            // 1、getter函数
            /*watch(() => info.name, (newValue, oldValue) => {
                console.log(newValue, oldValue)
            })*/

            // 2、reactive对象
            // 2、1newValue、oldValue是proxy对象(默认是深度侦听)
            /*watch(info, (newValue, oldValue) => {
                console.log(newValue, oldValue)
            })*/

            // 2、2newValue、oldValue是值本身(默认不是深度侦听)
            watch(() => ({...info}), (newValue, oldValue) => {
                console.log(newValue, oldValue)
            }, {
                deep: true,
                immediate: false
            })

            // 3、ref对象
            /*watch(name, (newValue, oldValue) => {
                console.log(newValue, oldValue)
            })*/

            // 4、数组
            /*watch([() => ({...info}), name], ([newInfo, newName], [oldInfo, olbName]) => {
                console.log(newInfo, newName, oldInfo, olbName)
            })*/
            // (packages/runtime-core/src/apiWatch.ts:170)

            return {
                name,
                friend
            }
        }
    }
</script>

十五、composition api(三)

1、生命周期钩子
<template>
    <input type="text" v-model="name">
    <div>{{name}}</div>
</template>

<script>
    import {ref, onMounted, onUpdated, onUnmounted} from "vue"

    export default {
        setup() {
            const name = ref("黄婷婷")

            /**
             * 1、可注册多个生命周期
             * 2、因为setup是围绕beforeCreate和created生命周期钩子运行的,所以不需要显示
             *   地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在setup函数中编写
             */
            onMounted(() => {
                console.log("onMounted1")
            })
            onMounted(() => {
                console.log("onMounted2")
            })
            onUpdated(() => {
                console.log("onUpdated")
            })
            onUnmounted(() => {
                console.log("onUnmounted")
            })

            return {
                name
            }
        }
    }
</script>
2、provide和inject
<!-- 父组件 -->
<template>
    <button @click="age++">按钮</button>
    <first></first>
</template>

<script>
    import {ref, provide, readonly} from "vue"
    import First from "./components/First"

    export default {
        components: {First},
        setup() {
            // 开发中能用ref的尽量用ref。表单可以用reactive
            const age = ref(18)

            // 为了符合单向数据流规范,参数2一般都会readonly
            provide("age", readonly(age))

            return {
                age
            }
        }
    }
</script>
<!-- 子孙组件 -->
<template>
    <div>{{age}}</div>
</template>

<script>
    import {inject} from "vue"

    export default {
        setup() {
            // 参数2为默认值
            const age = inject("age", "");

            return {
                age
            }
        }
    }
</script>
3、useCounter案例
<!-- App.vue -->
<template>
    <div>{{counter}}</div>
    <div>{{doubleCounter}}</div>
    <button @click="increment">按钮</button>
    <button @click="decrement">按钮</button>
</template>

<script>
    // 提取的hooks函数命名方式一般以use开头
    import useCounter from "./hooks/useCounter"

    export default {
        setup() {
            const {counter, doubleCounter, increment, decrement} = useCounter()

            return {
                counter,
                doubleCounter,
                increment,
                decrement,
            }
        }
    }
</script>
// useCounter.js
import {ref, computed} from "vue"

export default function () {
    const counter = ref(0)
    const doubleCounter = computed(() => counter.value * 2)

    const increment = () => counter.value++
    const decrement = () => counter.value--

    return {
        counter,
        doubleCounter,
        increment,
        decrement,
    }
}
4、useTitle案例
<!-- App.vue -->
<template>
    <div></div>
</template>

<script>
    import useTitle from "./hooks/useTitle"

    export default {
        setup() {
            const titleRef = useTitle("黄婷婷")
            setTimeout(() => {
                titleRef.value = "孟美岐"
            }, 3000)

            return {}
        }
    }
</script>
// useTitle.js
import {ref, watch} from "vue"

export default function (title = "姜贞羽") {
    const titleRef = ref(title)

    watch(titleRef, (newValue) => {
        document.title = newValue
    }, {
        immediate: true
    })

    return titleRef
}
5、useScrollPosition案例
<!-- App.vue -->
<template>
    <div class="content"></div>
    <div class="scroll">
        <div>scrollX:{{scrollX}}</div>
        <div>scrollY:{{scrollY}}</div>
    </div>
</template>

<script>
    import useScrollPosition from "./hooks/useScrollPosition"

    export default {
        setup() {
            const {scrollX, scrollY} = useScrollPosition();

            return {scrollX, scrollY}
        }
    }
</script>

<style scoped>
    .content {
        width: 3000px;
        height: 5000px;
    }

    .scroll {
        position: fixed;
        right: 30px;
        bottom: 30px;
    }
</style>
// useScrollPosition.js
import {ref} from "vue"

export default function () {
    const scrollX = ref(0)
    const scrollY = ref(0)

    document.addEventListener("scroll", () => {
        scrollX.value = window.scrollX
        scrollY.value = window.scrollY
    })

    return {
        scrollX,
        scrollY
    }
}
6、useMousePosition案例
<!-- App.vue -->
<template>
    <div class="mouse">
        <div>mouseX:{{mouseX}}</div>
        <div>mouseY:{{mouseY}}</div>
    </div>
</template>

<script>
    import useMousePosition from "./hooks/useMousePosition"

    export default {
        setup() {
            const {mouseX, mouseY} = useMousePosition();

            return {mouseX, mouseY}
        }
    }
</script>

<style scoped>
    .mouse {
        position: fixed;
        right: 30px;
        bottom: 30px;
    }
</style>
// useMousePosition.js
import {ref} from "vue"

export default function () {
    const mouseX = ref(0)
    const mouseY = ref(0)

    window.addEventListener("mousemove", (event) => {
        mouseX.value = event.pageX
        mouseY.value = event.pageY
    })

    return {
        mouseX,
        mouseY
    }
}
7、useLocalStorage案例
<!-- App.vue -->
<template>
    <div>{{person}}</div>
    <button @click="clickHandler">按钮</button>
</template>

<script>
    import useLocalStorage from "./hooks/useLocalStorage"

    export default {
        setup() {
            const person = useLocalStorage("person", {name: "黄婷婷", age: 18});
            const clickHandler = () => {
                person.value = {name: "孟美岐", age: 19}
            }

            return {
                person,
                clickHandler
            }
        }
    }
</script>
import {ref, watch} from "vue"

export default function (key, value) {
    const data = ref(value)

    if (value) {
        window.localStorage.setItem(key, JSON.stringify(value))
    } else {
        data.value = JSON.parse(window.localStorage.getItem(key))
    }

    watch(data, (newValue) => {
        window.localStorage.setItem(key, JSON.stringify(newValue))
    })

    return data
}
8、setup顶层编写方式
<!-- 父组件 -->
<template>
    <first :person="person" @click-handler="clickHandler"></first>
</template>

<script setup>
    import First from "./components/First"
    import {ref} from "vue"

    let person = ref({name: "黄婷婷", age: 18})

    const clickHandler = ($event) => {
        person.value = $event
    }
</script>
<!-- 子组件 -->
<template>
    <div>{{person}}</div>
    <button @click="clickHandler">按钮</button>
</template>

<script setup>
    import {defineProps, defineEmits} from "vue"

    const props = defineProps({
        person: {
            type: Object,
            required: true
        }
    })
    const emits = defineEmits(["clickHandler"]);

    const clickHandler = () => {
        console.log(props.person)
        emits("clickHandler", {name: "孟美岐", age: 19})
    }
</script>
9、render和h函数
<!-- 父组件 -->
<template>
    <div>黄婷婷</div>
</template>

<script>
    import {h} from "vue"

    export default {
        /**
         * 1、template会编译为render函数(template优先级高于render函数)
         * 2、认识h函数
         *     - 参数1(type):{String | Object | Function}:比如:"div"。
         *       HTML标签名、组件、异步组件、函数式组件
         *     - 参数2(props?):{Object}:比如:{}。
         *       attribute、prop、事件
         *     - 参数3(children?):{String | Array | Object}:
         *       比如:["黄婷婷",h("div","孟美岐"),h(MyComponent,{nameProp:"姜贞羽"})]。
         * 3、注意事项
         *     - 如果没有props,那么通常可以将children作为第二个参数传入
         *     - 如果会产生歧义,可以将null作为第二个参数传入,将children作为第三个参数传入
         */
        render() {
            return h("div", {title: "孟美岐"}, "孟美岐")
        }
    }
</script>
10、render函数实现计数器
<script>
    import {h, ref} from "vue"

    export default {
        setup() {
            const counter = ref(0)

            // setup返回的函数就是render函数(setup返回render优先级高于template)
            return () => {
                return h("div", null, [
                    h("div", null, counter.value),
                    h("button", {
                        onclick: () => counter.value++
                    }, "+1"),
                    h("button", {
                        onclick: () => counter.value--
                    }, "-1")
                ])
            }
        }
    }
</script>
11、render组件和插槽的使用
<!-- App.vue -->
<script>
    import {h} from "vue"
    import First from "./components/First"

    export default {
        render() {
            return h(First, null, {
                default: scope => h("div", null, scope.name)
            })
        }
    }
</script>
<!-- First.vue -->
<script>
    import {h} from "vue"

    export default {
        render() {
            return h("div", null, [
                this.$slots.default ? this.$slots.default({name: "黄婷婷"}) : h("span", null, "孟美岐")
            ])
        }
    }
</script>
12、render函数jsx写法
  • 安装
# 测试结果不用安装,所以也不用配置
npm i @vue/babel-plugin-jsx -D
  • 配置(babel.config.js)
module.exports = {
    plugins: [
        "@vue/babel-plugin-jsx"
    ]
}
  • jsx计数器案例
<script>
    export default {
        data() {
            return {
                counter: 0
            }
        },
        render() {
            const increment=()=>this.counter++
            const decrement=()=>this.counter--

            // 单括号语法,只能有一个根节点
            return (<div>
                <div>{this.counter}</div>
                <button onclick={increment}>+1</button>
                <button onclick={decrement}>-1</button>
            </div>)
        }
    }
</script>
  • jsx组件和插槽的使用
<!-- App.vue -->
<script>
    import First from "./components/First"

    export default {
        components:{First},
        render() {
            // 1、组件要以短横线命名,则必须注册
            // 2、{{}}:外层是单括号语法,内层是对象
            return (<first>
                {{default: scope => <div>{scope.name}</div>}}
            </first>)
        }
    }
</script>
<!-- First.vue -->
<script>
    export default {
        render() {
            return (<div>
                {this.$slots.default ? this.$slots.default({name: "黄婷婷"}) : <span>孟美岐</span>}
            </div>)
        }
    }
</script>
posted on 2022-04-13 17:22  一路繁花似锦绣前程  阅读(362)  评论(0编辑  收藏  举报