vue3 基础(一):可复用 & 组合

1,095 阅读13分钟

vue3 变更

main.js

vue2:

image.png

vue3:

image.png

vue3 新特性

  • 更好的 ts 支持:源码全部用 ts 重写
  • tree-shaking
    • 打包后体积更小(没有用到的代码在打包时会删除)
  • fragments 支持多个根节点

image.png

  • teleport 传送门

image.png

image.png

  • custom renderer 自定义渲染器(小程序、iOS等等平台)- canvas
  • composition api

image.png

一、组合式 API

介绍

组合式 API:将同一个逻辑关注点的相关代码收集到一起。(为了不引入全新的概念,采用独立的函数来创建和监听响应式的状态等)

组合 API 基础

setup 基础

setup 选项是一个接收 propscontext函数

在组件创建之前执行(发生在 datacomputedmethods 被解析之前)。

setup 返回一个对象,该对象的 property 以及 传递给 setupprops 参数中的 property 都可以在模板中访问到

<template>
    <div>{{ collectionName }}: {{ readersNumber }} {{ book.title }}</div>
</template>

<script>
    import { ref, reactive } from 'vue'

    export default {
        props: {
            collectionName: String
        },
        setup(props) {
            const readersNumber = ref(0)
            const book = reactive({ title: 'Vue 3 Guide' })

            // 暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板
            // 必须 return 才能访问
            return {
                readersNumber,
                book
            }
        }
    }
</script>

在 setup 中你应该避免使用 this,因为它不会找到组件实例。setup 的调用发生在 datacomputedmethods 被解析之前,所以它们无法在 setup 中被获取。

响应式对象

ref

在 Vue 3.0 中,我们可以通过一个新的 ref 函数 使任何响应式变量在任何地方起作用。换句话说,ref 为我们的值创建了一个响应式引用

ref 接收参数 并 将其包裹在一个带有 value property 的对象中返回,然后可以使用 .value 访问或更改响应式变量的值

<template>
    <!-- 响应式对象:模板自动解构(模板中不需要写 `.value`) -->
    <div>count:{{ count }}</div>
</template>

<script>
    import { ref, onMounted } from 'vue'

    export default {
        setup(props) {
            const count = ref(0)
            
            onMounted(() => {
                console.log(count); // { value: 0 }
                console.log(counter.value); // 0
                
                counter.value++;
                console.log(counter.value); // 1
            })

            return {
                count
            }
        }
    }
</script>

ref.png

setup 返回的 refs 在模板中访问时是被自动浅解包的,因此不应在模板中使用 .value

reactive

<template>
    <div>count:{{ count }}</div>
    <div>user-name:{{ user.name }}</div>
    <div>user-age:{{ user.age }}</div>
</template>

<script>
    import { ref, reactive, onMounted } from 'vue'

    export default {
        setup(props) {
            const count = ref(0)
            const user = reactive({
                name: 'una',
                age: 23
            })
            
            onMounted(() => {
                console.log(count); // { value: 0 }
                console.log(user); // { name: 'una', age: 23 }
                
                // 心智负担
                console.log(count.value); // 0
                count.value++;
                console.log(count.value); // 1
                
                // user-> reactive (proxy) 本质上是一个代理对象
                console.log(user.age); // 23 不需要 `.value`
            })

            // 暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板
            return {
                count,
                user
            }
        }
    }
</script>

image.png

ref vs reactive

为什么要用 ref ?因为数字、布尔值、字符串等值类型改变不容易监听,使用 ref 包裹一下,变为引用类型,保持 JavaScript 中不同数据类型的行为统一

ref 也可以包裹引用类型,在底层的时候会自动把他转为 reactive,但使用时还是要用 .value

所以推荐:使用值类型ref 包裹,引用类型reactive 代理。

readonly

从控制台的显示来看,readonly 是只读的 reactive

<template>
    <div>user-name:{{ useronly.name }}</div>
    <div>user-age:{{ useronly.age }}</div>
</template>

<script>
    import { readonly, onMounted } from 'vue'

    export default {
        setup(props) {
            const useronly = readonly({
                name: 'una',
                age: 23
            })
            
            onMounted(() => {
                console.log(useronly);
                useronly.age = 20; // 失败,只读类型不能修改
            })

            // 暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板
            return {
                useronly
            }
        }
    }
</script>

readonly.png

使用场景

  • props
  • provide/inject (后面会介绍)

计算属性 computed

需要给 computed 函数传递一个参数,它是一个类似 getter 的回调函数输出的是一个只读的响应式引用。我们需要像 ref 一样使用 .value

<template>
    <div>count:{{ count }}</div>
    <div>count * 2:{{ double }}</div>
</template>

<script>
    import { ref, computed, onMounted } from 'vue'

    export default {
        setup() {
            const count = ref(1)
            
            const double = computed(() => count.value * 2)
            
            onMounted(() => {
                console.log(double);
                console.log(double.value); // 2
            })

            return {
                count,
                double
            }
        }
    }
</script>

computed.png

侦听

watch

<template>
    <div>count: {{ count }}</div>
    <div>double : {{ double }}</div>
    <div>num : {{ num }}</div>
    <div>user - age: {{ user.age }}</div>
    <div>user - name: {{ user.name }}</div>

    <button @click="handleNumAdd">num add</button>
    <button @click="handleUserAgeAdd">user age add</button>
    <button @click="handleCountAdd">count add </button>
</template>

<script>
import { ref, reactive, computed, watch } from "vue";
export default {
    setup() {
        const count = ref(0)
        const num = ref(2)
        const user = reactive({
            name: 'una',
            age: 23
        })

        const double = computed(() => count.value * 2)

        const handleCountAdd = () => {
            console.log('count.value++');
            count.value++;
        };

        const handleNumAdd = () => {
            console.log('num.value++');
            num.value++;
        };

        const handleUserAgeAdd = () => {
            console.log('user.age++');
            user.age++;
        };

        // ref
        watch(num, (newVal, oldVal) => {
            console.log("watch - num ");
            console.log(newVal, oldVal);
        });

        // reactive
        // user.age
        watch(
            () => user.age, // 如果观察的是一个 对象里面的某个key 的话, 需要用一个函数来 return
            (newVal, oldVal) => {
                console.log("user age", newVal, oldVal);
            },
            {
                immediate: true, // 立即执行
            }
        );

        // 侦听多个
        watch([count, double], (val) => {
            console.log('侦听多个', val);
        });
        
        // watch([count, double], (newVal, oldVal) => {
        //     console.log('侦听多个', newVal, oldVal);
        // });

        return {
            count,
            num,
            user,
            double,
            handleCountAdd,
            handleNumAdd,
            handleUserAgeAdd
        }
    }

};

watch.png

若侦听多个数据且返回新旧值的话,会返回2个数组,分别存放相应的新值和旧值:

侦听多个数据且返回新旧值.png

watchEffect

特点:

  • 立即执行
  • 不需指定具体的响应式对象,只要里面依赖的值变化就执行
  • 没有新旧值
  • 返回一个 stop 函数,用于 手动停止侦听
  • 组件销毁时,watchEffect 自动销毁
<template>
    <div>count: {{ count }}</div>
    <div>double : {{ double }}</div>
    <div>num : {{ num }}</div>
    <div>user - age: {{ user.age }}</div>
    <div>user - name: {{ user.name }}</div>
    
    <button @click="handleNumAdd">num add</button>
    <button @click="handleUserAgeAdd">user age add</button>
    <button @click="handleCountAdd">count add </button>
    <button @click="stopWatchEffect">stop-watch-effect</button>
</template>

<script>
import { ref, reactive, computed, watchEffect } from "vue";
export default {
    setup(props) {
        const count = ref(0)
        const num = ref(2)
        const user = reactive({
            name: 'una',
            age: 23
        })

        const double = computed(() => count.value * 2)

        const handleCountAdd = () => {
            console.log('count.value++');
            count.value++;
        };

        const handleNumAdd = () => {
            console.log('num.value++');
            num.value++;
        };

        const handleUserAgeAdd = () => {
            console.log('user.age++');
            user.age++;
        };

        // 不需指定具体的响应式对象,只要里面依赖的值变化就执行
        // 没有新旧值
        watchEffect(() => {
            console.log("watch - effect - 自动停止");
            // getter
            console.log(num.value);
            console.log(user.age);
        });

        // 手动停止,使用 watchEffect 返回的 stop 函数
        const stop = watchEffect(() => {
            console.log("watch - effect - 手动停止");
            console.log(count.value);
            console.log(double.value);
        });
        const stopWatchEffect = () => {
            stop();
        };

        return {
            count,
            num,
            user,
            double,
            handleCountAdd,
            handleNumAdd,
            handleUserAgeAdd,
            stopWatchEffect
        }
    }
};
</script>

image.png

watch VS watchEffect

  • watchEffect 立即执行;watch 不立即执行,当 watch 设置 immediate: true 才会立即执行。
  • watch 需要设置要侦听的响应式对象;watchEffect 不需指定具体的响应式对象,只要里面依赖的值变化就执行。
  • watchEffect 没有新旧值;watch 有新旧值。
  • watchEffect 返回一个 stop 函数,用于 手动停止侦听;而 watch 只能在组件销毁时,自动销毁。

Setup

组合式 API 入口(可以实际使用它的地方): setup

参数

使用 setup 函数时,它将接收两个参数:

  • props
  • context

props

setup 函数中的 props 是响应式的,当传入新的 prop 时,它将被更新。

但是,因为 props 是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性。如果需要解构 prop,可以在 setup 函数中使用 toRefs 函数来完成此操作。toRefs 将会为 prop 创建一个 ref 响应式引用

import { toRefs } from 'vue'

export default {
    setup(props) {
        const { title } = toRefs(props)
        console.log(title.value)
    }
}

如果 title 是可选的 prop,则传入的 props 中可能没有 title 。在这种情况下,toRefs 将不会为 title 创建一个 ref,你需要使用 toRef 替代它

import { toRef } from 'vue'

export default {
    setup(props) {
        const title = toRef(props, 'title')
        console.log(title.value)
    }
}

context

context 是一个普通的 JavaScript 对象(非响应式,可解构),它暴露组件的三个 property

  • attrs
  • slots
  • emit
export default {
    setup(props, context) {
        // Attribute (非响应式对象)
        console.log(context.attrs)

        // 插槽 (非响应式对象)
        console.log(context.slots)

        // 触发事件 (方法)
        console.log(context.emit)
    }
}

// 解构
export default {
    setup(props, { attrs, slots, emit }) {
        ...
    }
}

attrsslots 是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.xslots.x 的方式引用 property。

请注意,与 props 不同,attrsslots 是非响应式的。如果你打算根据 attrsslots 更改应用副作用,那么应该在 onUpdated 生命周期钩子中执行此操作。

// 待补充

使用渲染函数

setup 还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态.

import { h, ref, reactive } from 'vue'

export default {
  setup() {
    const readersNumber = ref(0)
    const book = reactive({ title: 'Vue 3 Guide' })
    // 请注意这里我们需要显式调用 ref 的 value
    return () => h('div', [readersNumber.value, book.title])
  }
}

使用 this

setup() 内部,this 不是该活跃实例的引用,因为 setup() 是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同。这使得 setup() 在和其它选项式 API 一起使用时可能会导致混淆。因此,最好从头到尾使用同一种 API 进行开发

生命周期钩子 ???

你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。支持多个,谁先注册谁先执行

下表包含如何在 setup () 内部调用生命周期钩子:

选项式 APIHook inside setup
beforeCreateNot needed
createdNot needed
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated

errorCaptured 错误捕获,只要报错就走这里。

因为 setup 是围绕 beforeCreatecreated 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。

这些函数接受一个回调函数,当钩子被组件调用时将会被执行:

export default {
    setup() {
        // mounted
        onMounted(() => {
            console.log('Component is mounted!')
        })
    }
}

Provide / Inject

我们也可以在组合式 API 中使用 provide/inject。两者都只能在 setup() 调用

  • provide:
    • name (<String> 类型)
    • value
  • inject:
    • inject 的 property 的 name
    • 默认值 (可选):当 provide 该 property 为 undefined 时使用

vue2:

// ./src/App.vue
export default {
    components: {
        Foo
    },
    provide: {
        location: 'North Pole'
    }
}

// ./src/components/Foo.vue
export default {
    inject: ['location']
}

vue3:

// ./src/App.vue
import { provide } from "vue";
export default {
    components: {
        Foo
    },
    setup() {
        provide("location", "North Pole");
        // 当不传值的时候,如果 inject 有默认值就用默认值
        // provide("location");
    },
};

// ./src/components/Foo.vue
import { inject } from 'vue'
export default {
    setup() {
        // const userLocation = inject('location');
        
        // 设置默认值
        const userLocation = inject('location', 'The Universe');

        return {
            userLocation
        }
    }
}

响应性

添加响应性

为了增加 provide 值和 inject 值之间的响应性,我们可以在设置 provide 值时使用 refreactive

import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'

export default {
  setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    provide('location', location)
    provide('geolocation', geolocation)
  }
}

修改响应式 property

当使用响应式 provide / inject 值时,建议尽可能将对响应式 property 的所有修改限制在 定义 provide 的组件 内部

import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'

export default {
  components: {
    MyMarker
  },
  setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    provide('location', location)
    provide('geolocation', geolocation)
    
    const updateLocation = () => {
      location.value = 'South Pole'
    }

    return {
      location,
      updateLocation
    }
  },
}

然而,有时我们需要在注入数据的组件内部更新 inject 的数据。在这种情况下,我们建议 provide 一个方法来负责改变响应式 property

import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'

export default {
  components: {
    MyMarker
  },
  setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    const updateLocation = () => {
      location.value = 'South Pole'
    }

    provide('location', location)
    provide('geolocation', geolocation)
    provide('updateLocation', updateLocation) // 把修改的方法暴露出来
  }
}
import { inject } from 'vue'

export default {
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')
    const updateUserLocation = inject('updateLocation')

    return {
      userLocation,
      userGeolocation,
      updateUserLocation
    }
  }
}
</script>

如果要确保通过 provide 传递的数据不会被 inject 的组件更改,我们建议对提供者的 property 使用 readonly(只读)

import { provide, reactive, readonly, ref } from 'vue'

export default {
  setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    const updateLocation = () => {
      location.value = 'South Pole'
    }

    provide('location', readonly(location)) // 只读
    provide('geolocation', readonly(geolocation)) // 只读
    provide('updateLocation', updateLocation)
  }
}

模板引用(refs)

在使用组合式 API 时,响应式引用和模板引用的概念是统一的。为了获得对模板内元素或组件实例的引用,我们可以像往常一样声明 ref 并从 setup() 返回。

在虚拟 DOM 补丁算法中,如果 VNode 的 ref 键对应于渲染上下文中的 ref,则 VNode 的相应元素或组件实例将被分配给该 ref 的值。这是在虚拟 DOM 挂载/打补丁过程中执行的,因此模板引用只会在初始渲染之后获得赋值。

// ./src/components/Foo.vue
<template>
    <input type="text" ref="input" />
</template>

import { ref } from "vue";
export default {
    setup() {
        // refs
        const input = ref(null); // 1. 变量名要与 ref 设置的名称一致
        // 2. DOM 元素将在初始渲染后分配给 ref,挂载后才能获取到值
        onMounted(() => {
            console.log(input.value);
        });

        return {
            input
        };
    },
};

作为模板使用的 ref 的行为与任何其他 ref 一样:它们是响应式的,可以传递到 (或从中返回) 复合函数中。

JSX 中的用法

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

    return () =>
      h('div', {
        ref: root
      })

    // with JSX
    return () => <div ref={root} />
  }
}

v-for 中的用法

组合式 API 模板引用在 v-for 内部使用时没有特殊处理。相反,请使用函数引用执行自定义处理:

<template>
    <!-- 通过一个函数,动态挂载 ref -->
    <div v-for="(item, i) in list" :ref="el => { if (el) divs[i] = el }">
        {{ item }}
    </div>
</template>

<script>
import { ref, reactive, onBeforeUpdate } from 'vue'
export default {
    setup() {
        const list = reactive([1, 2, 3])
        const divs = ref([]) // refs 数组

        // 确保在每次更新之前重置ref
        onBeforeUpdate(() => {
            divs.value = []
        })

        return {
            list,
            divs
        }
    }
}
</script>

侦听模板引用

watch()watchEffect() 在 DOM 挂载或更新之前运行,所以当侦听器运行时,模板引用还未被更新

<template>
  <div ref="root">This is a root element</div>
</template>

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

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

      watchEffect(() => {
        // 在 DOM 更新之前运行,因此,模板引用还没有持有对元素的引用。
        console.log(root.value) // => null
      })

      return {
        root
      }
    }
  }
</script>

使用模板引用的侦听器应该用 flush: 'post' 选项来定义,这将在 DOM 更新后运行,确保模板引用与 DOM 保持同步,并引用正确的元素。

<template>
  <div ref="root">This is a root element</div>
</template>

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

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

      watchEffect(() => {
        console.log(root.value) // => <div></div>
      }, 
      {
        flush: 'post'
      })

      return {
        root
      }
    }
  }
</script>

二、 Mixin 混入

与 vue2 基本一致。

基础

Mixin 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个 mixin 对象可以包含任意组件选项。当组件使用 mixin 对象时,所有 mixin 对象的选项将被“混合”进入该组件本身的选项

// define a mixin object
const myMixin = {
  created() {
    this.hello()
  },
  methods: {
    hello() {
      console.log('hello from mixin!')
    }
  }
}

// define an app that uses this mixin
const app = Vue.createApp({
  mixins: [myMixin]
})

app.mount('#mixins-basic') // => "hello from mixin!"

选项合并

当组件和 mixin 对象含有同名选项时,这些选项将以恰当的方式进行“合并”。

比如,每个 mixin 可以拥有自己的 data 函数。每个 data 函数都会被调用,并将返回结果合并。在数据的 property 发生冲突时,会以组件自身的数据为优先

const myMixin = {
  data() {
    return {
      message: 'hello',
      foo: 'abc'
    }
  }
}

const app = Vue.createApp({
  mixins: [myMixin],
  data() {
    return {
      message: 'goodbye',
      bar: 'def'
    }
  },
  created() {
    console.log(this.$data) // => { message: "goodbye", foo: "abc", bar: "def" }
  }
})

同名钩子函数将合并为一个数组,因此都将被调用。另外,mixin 对象的钩子将在组件自身钩子之前调用

const myMixin = {
  created() {
    console.log('mixin 对象的钩子被调用')
  }
}

const app = Vue.createApp({
  mixins: [myMixin],
  created() {
    console.log('组件钩子被调用')
  }
})

// => "mixin 对象的钩子被调用"
// => "组件钩子被调用"

值为对象的选项,例如 methodscomponentsdirectives,将被合并为同一个对象两个对象键名冲突时,取组件对象的键值对

const myMixin = {
  methods: {
    foo() {
      console.log('foo')
    },
    conflicting() {
      console.log('from mixin')
    }
  }
}

const app = Vue.createApp({
  mixins: [myMixin],
  methods: {
    bar() {
      console.log('bar')
    },
    conflicting() {
      console.log('from self')
    }
  }
})

const vm = app.mount('#mixins-basic')

vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"

全局 mixin

Mixin 也可以进行全局注册。使用时格外小心!一旦使用全局 mixin,它将影响每一个之后创建的组件 (例如,每个子组件)。

const app = Vue.createApp({
  myOption: 'hello!'
})

// 为自定义的选项 'myOption' 注入一个处理器。
app.mixin({
  created() {
    const myOption = this.$options.myOption
    if (myOption) {
      console.log(myOption)
    }
  }
})

// 将myOption也添加到子组件
app.component('test-component', {
  myOption: 'hello from component!'
})

app.mount('#mixins-global')

// => "hello!"
// => "hello from component!"

大多数情况下,只应当应用于自定义选项,不推荐使用全局 mixin。推荐将其作为插件发布,以避免重复应用 mixin

自定义选项合并策略

自定义选项在合并时,默认策略为简单地覆盖已有值。如果想让某个自定义选项以自定义逻辑进行合并,可以在 app.config.optionMergeStrategies 中添加一个函数:

const app = Vue.createApp({})

app.config.optionMergeStrategies.customOption = (toVal, fromVal) => {
  // return mergedVal
}

合并策略接收在父实例子实例上定义的该选项的值,分别作为第一个和第二个参数。让我们来检查一下使用 mixin 时,这些参数有哪些:

const app = Vue.createApp({
  custom: 'hello!'
})

app.config.optionMergeStrategies.custom = (toVal, fromVal) => {
  console.log(fromVal, toVal)
  // 父实例,子实例 => "goodbye!", undefined
  // 父实例,子实例 => "hello", "goodbye!"
  return fromVal || toVal
}

app.mixin({
  custom: 'goodbye!',
  created() {
    console.log(this.$options.custom) // => "hello!"
  }
})

如你所见,在控制台中,我们先从 mixin 打印 toValfromVal,然后从 app 打印。如果存在,我们返回 fromValthis.$options.custom 控制台打印的为hello!。如果我们尝试将策略更改为始终从子实例返回值,控制台打印的为 goodbye!

不足

在 Vue 2 中,mixin 是将部分组件逻辑抽象成可重用块的主要工具。但是,他们有几个问题:

  • Mixin 很容易发生冲突:因为每个 mixin 的 property 都被合并到同一个组件中,所以为了避免 property 名冲突,你仍然需要了解其他每个特性。
  • 可重用性是有限的:我们不能向 mixin 传递任何参数来改变它的逻辑,这降低了它们在抽象逻辑方面的灵活性。

为了解决这些问题,我们添加了一种通过逻辑关注点组织代码的新方法:组合式 API。

动机与目的

  • 更好的代码组织与逻辑复用

    • 代码组织:基于功能而不是基于选项

    vue2 分散:

    image.png

    vue3 集合:

    image.png

    或者分出去作为一个函数,用的时候再导进来

    image.png

    image.png

    • 逻辑复用:本身就是函数,天然支持复用 vue2 mixin:

    image.png

    image.png

    mixin 的弊端:

    • 命名冲突
    • 来源不清晰

    vue3:

    image.png

    image.png

    也可以使用 toRefs 方法:

    image.png

    响应式丢失问题:解构有可能会导致返回给 tempalte 的是普通对象。

    vueuse

  • 更好的类型推导:ts

三、自定义指令

全局指令:

const app = Vue.createApp({})
// 注册一个全局自定义指令 `v-focus`
app.directive('focus', {
  // 当被绑定的元素挂载到 DOM 中时……
  mounted(el) {
    // 聚焦元素
    el.focus()
  }
})

局部指令:

directives: {
  focus: {
    // 指令的定义
    mounted(el) {
      el.focus()
    }
  }
}

然后你可以在模板中任何元素上使用新的 v-focus attribute:

<input v-focus />

钩子函数(改动)

一个指令定义对象可以提供如下几个钩子函数 (均为可选):

  • created:在绑定元素的 attribute 或事件监听器被应用之前调用。在指令需要附加须要在普通的 v-on 事件监听器前调用的事件监听器时,这很有用。
  • beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用。
  • mounted:在绑定元素的父组件被挂载后调用。
  • beforeUpdate:在更新包含组件的 VNode 之前调用。
  • updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用。
  • beforeUnmount:在卸载绑定元素的父组件之前调用
  • unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次。

钩子函数参数

与 vue2 相同

函数简写

在前面的例子中,你可能想在 mountedupdated 时触发相同行为,而不关心其他的钩子函数。那么你可以通过将这个回调函数传递给指令来实现:

app.directive('pin', (el, binding) => {
  el.style.position = 'fixed'
  const s = binding.arg || 'top'
  el.style[s] = binding.value + 'px'
})

对象字面量

如果指令需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法的 JavaScript 表达式。

<div v-demo="{ color: 'white', text: 'hello!' }"></div>

app.directive('demo', (el, binding) => {
  console.log(binding.value.color) // => "white"
  console.log(binding.value.text) // => "hello!"
})

非 prop 的 attribute 类似,当在组件中使用时,自定义指令总是会被应用在组件的根节点上

但和 attribute 不同,指令不会通过 v-bind="$attrs" 被传入另一个元素。

<my-component v-demo="test"></my-component>

app.component('my-component', {
  template: `
    <div> // v-demo 指令将会被应用在这里
      <span>My component content</span>
    </div>
  `
})

有了片段支持以后,组件可能会有多个根节点。当被应用在一个多根节点的组件上时,指令会被忽略,并且会抛出一个警告

四、Teleport

有时组件模板的一部分逻辑上属于该组件,而从技术角度来看,最好将模板的这一部分移动到 DOM 中 Vue app 之外的其他位置

一个常见的场景是创建一个包含全屏模式的组件。在大多数情况下,你希望模态框的逻辑存在于组件中,但是模态框的快速定位就很难通过 CSS 来解决,或者需要更改组件组合。

Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件。

让我们修改 modal-button 以使用 <teleport>,并告诉 Vue “Teleport 这个 HTML 到该‘body’标签”。

app.component('modal-button', {
    template: `
        <button @click="modalOpen = true">
            Open full screen modal! (With teleport!)
        </button>

        <!-- 将模态框内容渲染为 body 标签的子级 -->
        <teleport to="body">
            <div v-if="modalOpen" class="modal">
                <div>
                    I'm a teleported modal! 
                    (My parent is "body")
                    <button @click="modalOpen = false"> Close </button>
                </div>
            </div>
        </teleport>
    `,
    data() {
        return { 
            modalOpen: false
        }
    }
})

一旦我们单击按钮打开模态框,Vue 将正确地将模态框内容渲染为 body 标签的子级

与 Vue components 一起使用

如果 <teleport> 包含 Vue 组件,则它仍将是 <teleport> 父组件的逻辑子组件:

const app = Vue.createApp({
  template: `
    <h1>Root instance</h1>
    <parent-component />
  `
})

app.component('parent-component', {
  template: `
    <h2>This is a parent component</h2>
    <teleport to="#endofbody">
      <child-component name="John" />
    </teleport>
  `
})

app.component('child-component', {
  props: ['name'],
  template: `
    <div>Hello, {{ name }}</div>
  `
})

在这种情况下,即使在不同的地方渲染 child-component,它仍将是 parent-component 的子级,并将从中接收 name prop

这也意味着来自父组件的注入按预期工作,并且子组件将嵌套在 Vue Devtools 中的父组件之下,而不是放在实际内容移动到的位置。

在同一目标上使用多个 teleport

一个常见的用例场景是一个可重用的 <Modal> 组件,它可能同时有多个实例处于活动状态。对于这种情况,多个 <teleport> 组件可以将其内容挂载到同一个目标元素。顺序将是一个简单的追加——稍后挂载将位于目标元素中较早的挂载之后。

<teleport to="#modals">
  <div>A</div>
</teleport>
<teleport to="#modals">
  <div>B</div>
</teleport>

<!-- result-->
<div id="modals">
  <div>A</div>
  <div>B</div>
</div>

五、插件

插件是自包含的代码,通常向 Vue 添加全局级功能。它可以是公开 install() 方法的 object,也可以是 function

插件的功能范围没有严格的限制——一般有下面几种:

  • 添加全局方法或者 property。如:vue-custom-element
  • 添加全局资源:指令/过滤器/过渡等。如:vue-touch
  • 通过全局 mixin 来添加一些组件选项。如:vue-router
  • 添加全局实例方法,通过把它们添加到 config.globalProperties 上实现。
  • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router

编写插件

为了更好地理解如何创建自己的 Vue.js 版插件,我们将创建一个非常简化的插件版本。

每当这个插件被添加到应用程序中时,如果它是一个对象,就会调用 install 方法。如果它是一个 function,则函数本身将被调用。在这两种情况下——它都会收到两个参数:由 Vue 的 createApp 生成的 app 对象用户传入的选项

让我们从设置插件对象开始。建议在单独的文件中创建它并将其导出,以保持包含的逻辑和分离的逻辑。

// plugins/i18n.js
export default {
  install: (app, options) => {
    // Plugin code goes here
  }
}

我们想要一个函数来翻译整个应用程序可用的键,因此我们将使用 app.config.globalProperties 暴露它。

该函数将接收一个 key 字符串,我们将使用它在用户提供的选项中查找转换后的字符串。

// plugins/i18n.js
export default {
  install: (app, options) => {
    app.config.globalProperties.$translate = key => {
      return key.split('.').reduce((o, i) => {
        if (o) return o[i]
      }, options)
    }
  }
}

我们假设用户使用插件时,将在 options 参数中传递一个包含翻译后的键的对象。我们的 $translate 函数将使用诸如 greetings.hello 之类的字符串,查看用户提供的配置内部并返回转换后的值-在这种情况下为 Bonjour!

greetings: {
  hello: 'Bonjour!'
}

插件还允许我们使用 inject 为插件的用户提供功能或 attribute。例如,我们可以允许应用程序访问 options 参数以能够使用翻译对象。

// plugins/i18n.js
export default {
  install: (app, options) => {
    app.config.globalProperties.$translate = key => {
      return key.split('.').reduce((o, i) => {
        if (o) return o[i]
      }, options)
    }

    app.provide('i18n', options)
  }
}

插件用户现在可以将 inject[i18n] 注入到他们的组件并访问该对象。

另外,由于我们可以访问 app 对象,因此插件可以使用所有其他功能,例如使用 mixindirective

// plugins/i18n.js
export default {
  install: (app, options) => {
    app.config.globalProperties.$translate = (key) => {
      return key.split('.')
        .reduce((o, i) => { if (o) return o[i] }, options)
    }

    app.provide('i18n', options)

    app.directive('my-directive', {
      mounted (el, binding, vnode, oldVnode) {
        // some logic ...
      }
      ...
    })

    app.mixin({
      created() {
        // some logic ...
      }
      ...
    })
  }
}

使用插件

在使用 createApp() 初始化 Vue 应用程序后,你可以通过调用 use() 方法将插件添加到你的应用程序中。

我们将使用在编写插件部分中创建的 i18nPlugin 进行演示。

use() 方法有两个参数。第一个是要安装的插件,在这种情况下为 i18nPlugin。

它还会自动阻止你多次使用同一插件,因此在同一插件上多次调用只会安装一次该插件

第二个参数是可选的,并且取决于每个特定的插件。在演示 i18nPlugin 的情况下,它是带有转换后的字符串的对象。

import { createApp } from 'vue'
import Root from './App.vue'
import i18nPlugin from './plugins/i18n'

const app = createApp(Root)
const i18nStrings = {
  greetings: {
    hi: 'Hallo!'
  }
}

app.use(i18nPlugin, i18nStrings)
app.mount('#app')

其他

segmentfault.com/q/101000003…

v3.cn.vuejs.org/api/compute…

www.jianshu.com/p/f97c029b1…

blog.csdn.net/qq_17794813…

reactive 直接赋值失去响应式:blog.csdn.net/qq_43750656…

vue-script-setup官方资料:github.com/vuejs/rfcs/…

vue-script-setup:github.com/zhixinpeng/…

创建项目: blog.csdn.net/hyk521/arti…