vue2 基础(二):组件化基础

171 阅读11分钟

一、组件注册

组件是可复用的 Vue 实例,且带有一个名字。我们可以在一个通过 new Vue 创建的 Vue 根实例中,把这个组件作为自定义元素来使用:

  • 全局注册:Vue.component
    • 全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生
  • 局部注册:components 引入
    • 局部注册的组件在其子组件中不可用

注意点:

  • 必须有一个 root element,template 也只能有一个根 div
  • data 必须是一个函数

因为组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,例如 data、computed、watch、methods 以及生命周期钩子等。仅有的例外是像 el 这样根实例特有的选项。

<div id="app">
    {{msg}}

    <!-- 可复用 -->
    <Foo></Foo>
    <Foo></Foo>
    <Bar></Bar>
</div>
/* 局部注册 */
const Foo = {
    template: `
        <div>
            foo
            {{count}}
            <button @click="count++">click</button>
            <Bar></Bar>
        </div>
    `,

    // data 必须是一个函数
    data() {
        return {
            count: 1,
        };
    },

    methods: {},
};

/* 全局注册 */
// 必须在根 Vue 实例 (通过 new Vue) 创建之前发生
const Bar = {
    template: `<div>Bar</div>`,
};
Vue.component("Bar", Bar);

const app = new Vue({
    el: "#app",
    // 局部注册 -> 引入你要用到的组件
    components: {
        Foo,
    },
    data: {
        msg: "hello ",
    },
});

面试点:为什么 data 必须是一个函数。

data 是一个对象,对象是引用类型,若改动了 第一个引用的 Foo 组件 内的 data,其他引用的 Foo 组件 的 data 也会被改变

data 变成一个函数,让他每次都创建一个新的对象。这样每个组件对应的 data 对象都是独立的,就不会有这种问题了。

基础组件的自动化全局注册

cn.vuejs.org/v2/guide/co…

二、生命周期 ???

new Vue() -> 初始化事件 & 生命周期 -> beforeCreate -> 初始化依赖注入 & 响应性 -> created -> 判断是否有 “el” 选项 (有则继续判断是否有 “template” 选项,无则调用 app.$mount(el))-> 判断是否有 “template” 选项 (有则编译模板至渲染函数,无则编译 el 的 innerHTML 至模板)-> beforeMount -> 创建 app.$el 并添加至 el -> mounted -> 当数据发生变化,虚拟 DOM 重新渲染和更新 之前是 beforeUpdate,之后是 updated -> 当调用了 app.$destroy -> beforeDestroy -> 拆卸 观察器(watch)、子组件、事件监听器 -> destroyed

  • beforeCreate 创建前
    • 初始化事件 & 生命周期之后触发
  • created 创建
    • 初始化依赖注入 & 响应性之后触发
  • beforeMount 挂载前
    • $el 处理
    • 子组件创建挂载
  • mounted 挂载
    • el 赋值
    • 所有子组件挂载完毕
  • beforeUpdate 更新前
    • 数据发生改变,但此时视图尚未更新
  • updated 视图更新
    • 视图更新之后
  • beforeDestroy 销毁前
    • 调用 app.$destroy
  • destroyed 销毁
    • 清除 watch、清空子组件、处理事件监听之后

segmentfault.com/a/119000001…

vue 生命周期

<div id="app">
    {{msg}}

    <!-- v-if 销毁与重建 -->
    <Foo v-if="isShow"></Foo>
</div>
// hook 钩子
const Foo = {
    template: `
        <div>Foo
            {{count}}
            <button @click="count++">click</button>
        </div>
    `,
    data() {
        return {
            count: 1,
        };
    },
    // 创建
    beforeCreate() {
        console.log("before - create");
        console.log(this.$el); // undefined
        console.log(this.$data); // undefined
    },
    created() {
        console.log("created");
        console.log(this.$el); // undefined
        console.log(this.$data); // ok
    },

    // 挂载
    beforeMount() {
        console.log("before- mount");

        // $el 开始处理。此时 $el 还没处理完,不能在这里获取
        console.log(this.$el); // 虚拟 DOM
        console.log(this.$data); // undefined
    },
    mounted() {
        console.log("mounted");
        
        // $el 处理完成,可以获取
        console.log(this.$el); // 真实 DOM
        
        // children 若有子组件,则此时所有子组件才完成挂载
    },

    // 更新
    beforeUpdate() {
        // 点击按钮,数据发生改变,但此时视图尚未更新,获取到的是更新前的数据
        console.log(this.$el);
        console.log("before - update");
    },

    updated() {
        // 视图更新,获取到的是更新后的数据
        console.log("updated");
        console.log(this.$el);
    },

    // 销毁
    beforeDestroy() {
        console.log("before - destroy");
    },
    destroyed() {
        // 清除 watch、清空子组件、处理事件监听之后
        console.log("destroyed");
    },
};

const app = new Vue({
    el: "#app",
    components: {
        Foo,
    },
    data: {
        msg: "hello ",
        isShow: true,
    },
});

生命周期钩子的 this 上下文指向调用它的 Vue 实例

不要在选项 property 或回调上使用箭头函数,比如 created: () => console.log(this.a)vm.$watch('a', newValue => this.myMethod())

因为箭头函数并没有 thisthis 会作为变量一直向上级词法作用域查找,直至找到为止,经常导致 Uncaught TypeError: Cannot read property of undefinedUncaught TypeError: this.myMethod is not a function 之类的错误。

多个组件调用顺序(面试点 )

<div id="app">
    {{msg}}

    <Foo v-if="isShow"></Foo>
</div>
const Bar = {
    template: `<div>Bar</div>`,
    beforeCreate() {
        console.log("bar - before - create");
    },
    created() {
        console.log("bar -created");
    },
    beforeMount() {
        console.log("bar -before- mount");
    },
    mounted() {
        console.log("bar -mounted");
    },
    beforeUpdate() {
        console.log("bar -before - update");
    },
    updated() {
        console.log("bar -updated");
    },
    beforeDestroy() {
        console.log("bar - before - destroy");
    },
    destroyed() {
        console.log("bar -destroyed");
    },
};

// hook 钩子
const Foo = {
    components: {
        Bar,
    },
    template: `
        <div>Foo
            {{count}}
            <button @click="count++">click</button>
            <Bar></Bar>
        </div>
    `,
    data() {
        return {
            count: 1,
        };
    },
    beforeCreate() {
        console.log("before - create");
    },
    created() {
        console.log("created");
    },
    beforeMount() {
        console.log("before- mount");
    },
    mounted() {
        console.log("mounted");
    },
    beforeUpdate() {
        console.log("before - update");
    },
    updated() {
        console.log("updated");
    },
    beforeDestroy() {
        console.log("before - destroy");
    },
    destroyed() {
        console.log("destroyed");
    },
};

const app = new Vue({
    el: "#app",
    components: {
        Foo,
    },
    data: {
        msg: "hello ",
        isShow: true,
    },
});

调用顺序:

  • before - create
  • created
  • before- mount
    • bar - before - create
    • bar -created
    • bar -before- mount
    • bar -mounted
  • mounted

注意:父组件挂载之前要先完成其子组件的创建与挂载

三、组件接收数据 props 父 -> 子

prop:能够在组件实例中访问这个值,就像访问 data 中的值一样。

prop 命名:HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名

  • 数组

    props: ["title"]
    
    • 不能设置更多属性
  • 对象

    props: {
        type: {
            default: "info", // 默认值
            validator(val) { // 验证
                return ['success', 'warning', 'info', 'error'].indexOf(value) !== -1
            },
            type: String, // 类型检查
            required: true // 必填项
        },
        // 对象或数组的默认值必须从一个工厂函数获取
        userInfo: {
            type: Object,
            // 对象或数组默认值必须从一个工厂函数获取
            // default: () => {}
            default: () => {
                return { message: 'hello' }
            }
        }
    },
    
    • 可设置参数类型默认值,还能设置 validator 进行数据验证

    • prop在组件实例创建之前进行验证,所以实例的 property (如 datacomputed 等) 在 defaultvalidator 函数中是不可用的

    • prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告。

    • 类型检查的 type 可以是以下类型:

      • StringNumberBooleanArrayObjectDateFunctionSymbol
      • 自定义构造函数,通过 instanceof 来进行检查确认
      function Person (firstName, lastName) {
          this.firstName = firstName
          this.lastName = lastName
      }
      
      Vue.component('blog-post', {
          props: {
              author: Person // 判断 author 的值是否是通过 new Person 创建的
          }
      })
      

一个 prop 被注册之后,可以把数据作为一个自定义属性传递进组件:

<Foo title="a"></Foo>

<Foo :title="msg"></Foo>

使用:

<div id="app">
    {{msg}}
    <Foo title="a"></Foo>
</div>
const Foo = {
    /* 组件传入参数*/

    // 数组,不能设置更多
    // title -> 响应式对象
    // props: ["title"],

    // 对象
    props: {
        title: {
            default: "456", // 默认值
            validator(val) { // 验证
                console.log(val);
                // 组件可以接受的参数
                return val === "a" || val === "b";
            },
            type: String, // 类型检查
        },
    },

    computed: {
        titlePlus() {
            return this.title + "plus";
        },
    },
    template: `
        <div>Foo
            {{title}}
            {{titlePlus}}
            <button @click="getProps">get props</button>
        </div>
    `,
    methods: {
        getProps() {
            // console.log(this.$props); // {title: 'a'}
            // 直接挂载在实例上
            // console.log(this.title) // 'a'

            // 单向数据流,组件传入参数不可修改
            // props 是不可以被修改的
            this.$props.title = "bbbbbbbbbb"; // 报错:props 不能被修改
        },
    },
};

const app = new Vue({
    el: "#app",
    components: {
        Foo,
    },
    data: {
        msg: "hello ",
    },
});

单向数据流

防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解

若子组件能修改父组件传来的数据,相当于父组件的数据就被修改了。若有多个子组件,以及子组件的子组件,当某一个子组件修改了来自父组件的数据,父组件也被修改了,但是你不知道具体是哪个子组件修改的父组件。为了项目的可维护性考虑,于是 vue 采用了单向数据流,从源头解决此种问题。

Prop 的 属性

一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 prop 定义的 attribute。

组件可以接受任意的 attribute,而这些 attribute 会被添加到这个组件的根元素上。

  • 对于绝大多数 attribute 来说,从外部提供给组件的值会替换掉组件内部设置好的值。所以如果传入 type="text" 就会替换掉 type="date" 并把它破坏。
  • class 和 style 会被合并,从而得到最终的值。

禁用 Attribute 继承

不希望组件的根元素继承 attribute,可以在组件的选项中设置 inheritAttrs: false不会影响 style 和 class 的绑定)。

Vue.component('my-component', {
    inheritAttrs: false,
    // ...
})

这尤其适合配合实例的 $attrs 使用,包含了传递给一个组件的 attribute 名和 attribute 值,例如:

<base-input
    label="Username:"
    v-model="username"
    required
    placeholder="Enter your username"
></base-input>
<!-- 此时的 $attrs 包括属性:required、placeholder -->
<!-- $attrs: { required: true, placeholder: 'Enter your username' } -->

有了 inheritAttrs: false$attrs,你就可以手动决定这些 attribute 会被赋予哪个元素

Vue.component('base-input', {
    inheritAttrs: false,
    props: ['label', 'value'],
    template: `
        <label>
          {{ label }}
          <input
            v-bind="$attrs"
            :value="value"
            @input="$emit('input', $event.target.value)"
          >
        </label>
    `
})

四、自定义事件 子 -> 父

组件接收数据 $emit

Vue 实例提供了一个自定义事件的系统,父级组件可以像处理 native DOM 事件一样通过 v-on 监听子组件实例的任意事件。

同时子组件可以通过调用内建的 $emit 方法并传入事件名称来触发一个事件。

  • $emit
    • 第一个参数为事件名称
    • 后续参数为要传给父组件的值。多个参数用 , 隔开
    • 模板中,可以通过 $event 访问到 被抛出的值
    • 方法中,被抛出的值 作为 参数 传入方法

不同于组件和 prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。如果触发一个 camelCase 名字的事件,则监听这个名字的 kebab-case 版本是不会有任何效果的。

v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 v-on:myEvent 将会变成 v-on:myevent——导致 myEvent 不可能被监听到。

因此,我们推荐你始终使用 kebab-case 的事件名

<div id="app">
    {{num}}
    <!-- `v-on` 监听子组件实例的任意事件 -->
    <!-- 在模板中,可以通过 `$event` 访问到被抛出的值 -->
    <Foo
        :title="titleValue"
        @change-title="handleChangeTitle"
        @showValue="num = $event"
    ></Foo>
</div>
const Foo = {
    props: ["title"],
    template: `
        <div>Foo
            title: {{title}}
            <button @click="changeTitle">changeTitle</button>
            <button @click="$emit('showValue', 100)">showValue</button>
        </div>
    `,
    methods: {
        changeTitle() {
            // this.$props.title = xx

            // 子组件通过调用内建的 `$emit` 方法并 传入事件名称 来触发一个事件
            // $emit 第一个参数为事件名称,后续参数为要传给父组件的值。多个参数用 , 隔开
            this.$emit("change-title", "new title", "old title");
        },
    },
};

const app = new Vue({
    el: "#app",
    components: {
        Foo,
    },
    data: {
        titleValue: "app title",
        num: 0,
    },
    methods: {
        // 接收,修改子组件标题
        handleChangeTitle(v, b) {
            console.log(v, b);
            this.titleValue = v;
        },
    },
});

在组件上使用 v-model

自定义事件也可以用于创建支持 v-model 的自定义输入组件

<div id="app">
    <button @click="showFoo">show foo</button>
    <Foo v-model="isShow"></Foo>
</div>
const Foo = {
    template: `
        <div v-show="visible">
            Foo
            <button @click="handleClose">X</button>
        </div>
    `,
    methods: {
        handleClose() {
        
        },
    },
};

const app = new Vue({
    el: "#app",
    components: {
        Foo,
    },
    data: {
        isShow: false,
    },
    methods: {
        showFoo() {
            this.isShow = true;
        },
        handleClose() {
            this.isShow = false;
        },
    },
});

一个组件上的 v-model 默认会利用名为 valueprop 和名为 input 的事件。

为了 v-model 在组件上正常工作,必须:

  • 将其 value 属性绑定到一个名叫 valueprop
  • 在其 input 事件被触发时,将新的值通过自定义的 input 事件抛出
const Foo = {
    props: ["value"], // 默认属性名
    template: `
        <div v-show="visible">
            Foo
            <button @click="handleClose">X</button>
        </div>
    `,
    methods: {
        handleClose() {
            // 去修改 props 的值
            // v-model -> input
            this.$emit("input", false); // 默认事件名 input
        },
    },
};

但是像单选框、复选框等类型的输入控件可能会将 value 用于不同的目的。可以通过组件的 model 修改 v-model 对应的属性名与事件名,用来避免这样的冲突:

const Foo = {
    // props: ["value"], // 默认属性名
    props: ["visible"], // 修改后的属性名 visible
    // 通过 model 修改 v-model 的 参数名与事件名
    model: {
        prop: "visible", // 修改属性名
        event: "close", // 修改事件名
    },

    template: `
        <div v-show="visible">
            Foo
            <button @click="handleClose">X</button>
        </div>
    `,
    methods: {
        handleClose() {
            // 去修改 props 的值
            // v-model -> input
            // this.$emit("input", false); // 默认事件名 input
            this.$emit("close", false); // 修改后的事件名 close
        },
    },
};

多个v-model .sync

update:myPropName 的模式触发事件(伪“双向绑定”)。

// text-document 组件
this.$emit('update:title', newTitle)
<!-- 父组件 -->
<text-document
    :title="doc.title"
    @update:title="doc.title = $event"
></text-document>

<!-- 缩写 `.sync` 修饰符 -->
<text-document :title.sync="doc.title"></text-document>

带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是无效的)。你只能提供你想要绑定的 property 名,类似 v-model

<div id="app">
    <button @click="showFoo">show foo</button>
    <!-- <Foo @update:visible="isShow" @update:title="titleValue"></Foo> -->
    <!-- 缩写 -->
    <Foo :visible.sync="isShow" :title.sync="titleValue"></Foo>
</div>
const Foo = {
    props: ["visible", "title"],
    template: `
        <div v-show="visible">
            Foo
            {{title}}
            <button @click="handleClose">X</button>
            <button @click="handleChangeTitle">change title</button>
        </div>
    `,
    methods: {
        handleClose() {
            // 去修改 props 的值
            this.$emit("update:visible", false);
        },
        handleChangeTitle() {
            this.$emit("update:title", "xxx");
        },
    },
};

const app = new Vue({
    el: "#app",
    components: {
        Foo,
    },
    data: {
        isShow: false,
        titleValue: "heihei",
    },
    methods: {
        showFoo() {
            this.isShow = true;
        },
        handleClose() {
            this.isShow = false;
        },
    },
});

vue3 时,会取消 .sync,直接支持多个 v-model

当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:

<text-document v-bind.sync="doc"></text-document>

这样会把 doc 对象中的每一个 property (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器

v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync="{ title: doc.title }",是无法正常工作的

五、插槽

Vue 实现了一套内容分发的 API,将 <slot> 元素作为承载分发内容的出口。插槽内可以包含任何模板代码,包括 HTML,甚至其它的组件。

<!-- navigation-link 组件 -->
<a
    v-bind:href="url"
    class="nav-link"
>
    <slot></slot>
</a>

插槽内容:

<!-- 文字 -->
<navigation-link url="/profile">
    Your Profile
</navigation-link>

<!-- html -->
<navigation-link url="/profile">
    <!-- 添加一个 Font Awesome 图标 -->
    <span class="fa fa-user"></span>
</navigation-link>

<!-- 组件 -->
<navigation-link url="/profile">
  <!-- 添加一个图标的组件 -->
  <font-awesome-icon name="user"></font-awesome-icon>
</navigation-link>

如果 <navigation-link>template 中没有包含一个 <slot> 元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。

后备内容

有时为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染

<div id="app">
    {{msg}}
    
    <!--没设置插槽内容,使用后备内容 -->
    <Foo></Foo>
    <!--设置插槽内容,使用设置的插槽内容 -->
    <Foo>123</Foo>
</div>
<body>
    
    <script>
        const Foo = {
            template: `<div>Foo
                <div>
                    <slot>456</slot>
                </div>
            </div>`,
        };

        const app = new Vue({
            el: "#app",
            components: {
                Foo,
            },
            data: {
                msg: "hello ",
            },
        });
    </script>
</body>

微信图片_202105220137576.png

具名插槽

有时我们需要多个插槽,使用 <slot> 元素的name属性,定义额外的插槽(除默认插槽外的插槽)

向具名插槽提供内容:在一个 <template> 元素上使用 v-slot 指令,以参数的形式提供其名称v-slot 可简写为:#,即<template v-slot:two>two</template><template #two>two</template>是等价的。

<template> 元素中的所有内容都将会被传入 v-slot 指定的插槽,其余未指明的内容对应默认插槽。如果你希望更明确一些,仍然可以在一个 <template> 中包裹默认插槽的内容:

<Foo>
    <template v-slot:[dynamicSlotName]>
        ...
    </template>
</Foo>

还可以使用动态插槽名

<Foo>
    <template v-slot:default>123</template>
    <!-- 也可以这么写,不带参数的 `v-slot` 被假定对应默认插槽 -->
    <!-- <template v-slot>123</template> -->
    <template v-slot:three>three</template>
</Foo>

使用:

<div id="app">
    {{msg}}
    <Foo></Foo>

    <Foo>
        123
        <!-- 具名插槽 -->
        <template v-slot:two>two</template>
        <!-- <template v-slot:three>three</template> -->
        <!-- 简写 -->
        <template #three>three</template>
    </Foo>
</div>
const Foo = {
    template: `
        <div>Foo
            <div>
                <!-- 一个不带 `name` 的 `<slot>` 出口会带有隐含的名字 “`default`” -->
                <slot>456</slot>
            </div>

            <div class="two">
                <slot name="two"></slot>
            </div>
            <div class="three">
                <slot name="three"></slot>
            </div>
        </div>
    `,
};

const app = new Vue({
    el: "#app",
    components: {
        Foo,
    },
    data: {
        msg: "hello ",
    },
});

微信图片_202105220137578.png

作用域插槽(父模板访问子模板里的内容)

编译作用域:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。父级模板不能访问子模板里的内容

为了子模板里的内容父级的插槽内容 中可用,我们可以将 子模板里的内容 作为 <slot> 元素的一个 attribute 绑定上去。绑定在 <slot> 元素上的 attribute 被称为插槽 prop

而在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字

<div id="app">
    {{msg}}
    <!-- data 是一个对象,由插槽传回的多个字段组成 --> 
    <Foo v-slot="data">{{data.count}} --{{data}} </Foo>
</div>
const Foo = {
    data() {
        return {
            count: 1,
            double: 2,
        };
    },
    template: `<div>Foo
        <div>
            <slot :count="count" :double="double">456</slot>
        </div>

        <div class="two">
            <slot name="two"></slot>
        </div>
        <div class="three">
            <slot name="three"></slot>
        </div>
    </div>`,
};

const app = new Vue({
    el: "#app",
    components: {
        Foo,
    },
    data: {
        msg: "hello ",
    },
});

微信图片_2021052201375710.png

独占默认插槽的缩写语法

v-slot 只能添加在 <template> 上。但当被提供的内容只有默认插槽时,组件的标签可以被当作插槽的模板来使用,可以v-slot 直接用在组件上

<current-user v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
</current-user>

<!-- 也可以这么写,不带参数的 `v-slot` 被假定对应默认插槽 -->
<current-user v-slot="slotProps">
    {{ slotProps.user.firstName }}
</current-user>

默认插槽的缩写语法不能和具名插槽混用,会导致作用域不明确

<!-- 无效,会导致警告 -->
<current-user v-slot="slotProps">
    {{ slotProps.user.firstName }}
    <template v-slot:other="otherSlotProps">
        slotProps is NOT available here
    </template>
</current-user>

只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法

<current-user>
    <template v-slot:default="slotProps">
        {{ slotProps.user.firstName }}
    </template>

    <template v-slot:other="otherSlotProps">
        ...
    </template>
</current-user>

解构插槽 prop

作用域插槽的内部工作原理插槽内容包裹在一个拥有单个参数的函数function (slotProps) { // 插槽内容 }

这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。所以可以使用解构来传入具体的插槽 prop

<!-- 解构 user -->
<current-user v-slot="{ user }">
    {{ user.firstName }}
</current-user>

<!-- 解构 user,并重命名为 person -->
<current-user v-slot="{ user: person }">
    {{ person.firstName }}
</current-user>

<!-- 定义后备内容,当插槽 prop 是 undefined 时使用该内容 -->
<current-user v-slot="{ user = { firstName: 'Guest' } }">
    {{ user.firstName }}
</current-user>

todolist 案例

扩展组件

<body>
    <div id="app">
        {{msg}}

        <Todolist :list="list" v-slot="data">
            <div>{{data.item}} + "auto"</div>
        </Todolist>
    </div>
    
    <script>
        const Todolist = {
            props: ["list"],
            template: `<div>todolist
                <ul>
                    <li v-for="item in list">
                        <slot :item="item">{{item}}</slot>
                    </li>
                </ul>
            </div>`,
        };

        const app = new Vue({
            el: "#app",
            components: {
                Todolist,
            },
            data: {
                msg: "hello ",
                list: ["a", "b", "c"],
            },
        });
    </script>
</body>

微信图片_2021052201375711.png

微信图片_2021052201375712.png

六、动态组件 & 异步组件

动态组件

通过 Vue 的 <component> 元素加一个特殊的 is 属性 来实现 组件动态切换 功能。

<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component v-bind:is="currentTabComponent"></component>

is 属性用于常规 HTML 元素 ???

这个 is 属性可以用于常规 HTML 元素,但这些元素将被视为组件,这意味着所有的 attribute 都会作为 DOM attribute 被绑定。对于像 value 这样的 property,若想让其如预期般工作,你需要使用 .prop 修饰器。

<div id="todo-list-example">
    <ul>
        <!-- is="todo-item" <li> 元素将被视为组件 <todo-item>。
        这种做法在使用 DOM 模板时是十分必要的,因为在 <ul> 元素内只有 <li> 元素会被看作有效内容。
        这样做实现的效果与 <todo-item> 相同,但是可以避开一些潜在的浏览器解析错误。 -->
        <li
            is="todo-item"
            v-for="(todo, index) in todos"
            :key="todo.id"
            :title="todo.title"
            @remove="todos.splice(index, 1)"
        ></li>
    </ul>
</div>
Vue.component('todo-item', {
    template: '\
        <li>\
            {{ title }}\
            <button v-on:click="$emit(\'remove\')">Remove</button>\
        </li>\
    ',
    props: ['title']
})

new Vue({
    el: '#todo-list-example',
    data: {
        newTodoText: '',
        todos: [
            { id: 1, title: 'Do the dishes' },
            { id: 2, title: 'Take out the trash' },
            { id: 3, title: 'Mow the lawn' }
        ],
        nextTodoId: 4
    }
})

在动态组件上使用 keep-alive

我们可以通过 Vue 的 <component> 元素加一个特殊的 is 属性 来切换不同的组件。当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题。为了解决这个问题,我们可以用一个 <keep-alive> 元素将动态组件包裹起来

<!-- 失活的组件将会被缓存!-->
<keep-alive>
    <component v-bind:is="currentTabComponent"></component>
</keep-alive>

官网案例

<keep-alive> 要求被切换到的组件都有自己的名字,不论是通过组件的 name 选项还是局部/全局注册。

异步组件 ???

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染

一个推荐的做法是将异步组件和 webpack 的 code-splitting 功能一起配合使用:

Vue.component('async-webpack-example', function (resolve) {
    // 这个特殊的 `require` 语法将会告诉 webpack
    // 自动将你的构建代码切割成多个包,这些包
    // 会通过 Ajax 请求加载
    require(['./my-async-component'], resolve)
})

也可以在工厂函数中返回一个 Promise:

// 全局注册
Vue.component(
  'async-webpack-example',
  // 这个动态导入会返回一个 `Promise` 对象。
  () => import('./my-async-component')
)

// 局部注册
new Vue({
  components: {
    'my-component': () => import('./my-async-component') // 返回一个 `Promise` 对象
  }
  // ...
})

这里的异步组件工厂函数也可以返回一个如下格式的对象:

const AsyncComponent = () => ({
    // 需要加载的组件 (应该是一个 `Promise` 对象)
    component: import('./MyComponent.vue'),
    // 异步组件加载时使用的组件
    loading: LoadingComponent,
    // 加载失败时使用的组件
    error: ErrorComponent,
    // 展示加载时组件的延时时间。默认值是 200 (毫秒)
    delay: 200,
    // 如果提供了超时时间且组件加载也超时了,
    // 则使用加载失败时使用的组件。默认值是:`Infinity`
    timeout: 3000
})

具体详情参考官网

七、处理边界情况

访问元素 & 组件

访问根实例 $root

// Vue 根实例
new Vue({
    data: {
        foo: 1
    },
    computed: {
        bar: function () { /* ... */ }
    },
    methods: {
        baz: function () { /* ... */ }
    }
})

所有的子组件都可以将这个实例作为一个全局 store 来访问或使用。

this.$root.foo // 获取根组件的数据
this.$root.foo = 2 // 写入根组件的数据
this.$root.bar // 访问根组件的计算属性
this.$root.baz() // 调用根组件的方法

对于 demo 或非常小型的有少量组件的应用来说这是很方便的。不过这个模式扩展到中大型应用来说就不然了。因此在绝大多数情况下,我们强烈推荐使用 Vuex 来管理应用的状态。

子组件访问父级组件实例 $parent

从一个子组件访问父组件的实例。可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。

// 子组件中获取父组件
this.$parent

在绝大多数情况下,触达父级组件会使得你的应用更难调试和理解,尤其是当你变更了父级组件的数据的时候。当我们稍后回看那个组件的时候,很难找出那个变更是从哪里发起的。

父组件访问子级组件实例 $children

// 父组件中获取子组件
this.$children[0]

访问子组件实例或子元素 ref/$refs

尽管存在 prop 和事件,有时候你仍可能需要在 JavaScript 里直接访问一个子组件。可以通过 ref 为子组件赋予一个 ID 引用

<!-- 设置组件引用 ID --> 
<Child ref="child"></Child>
// 访问 <Child> 实例
this.$refs.child

refv-for 一起使用的时候,你得到的 ref 将会是一个包含了对应数据源的这些子组件的数组

$refs 只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的“逃生舱”——你应该避免在模板或计算属性中访问 $refs

依赖注入 provide/inject

provide 选项允许我们指定想要提供给后代组件的数据/方法

然后在任意后代组件里,都可以使用 inject 选项来接收 指定的 想要添加在这个实例上的 property

// 必须成对使用
provide: function () {
  return {
    getMap: this.getMap
  }
}

inject: ['getMap']

依赖注入的负面影响:它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的 property 是非响应式的

推荐还是使用 Vuex

程序化的事件侦听器

  • 通过 $emit(eventName, eventHandler) 调用一个事件
  • 通过 $on(eventName, eventHandler) 侦听(添加)一个事件
  • 通过 $once(eventName, eventHandler) 一次性侦听一个事件(只调用一次)
  • 通过 $off(eventName, eventHandler) 停止侦听(删除)一个事件

你通常不会用到这些,但是当你需要在一个组件实例上手动侦听事件时,它们是派得上用场的。

如果你发现自己不得不在单个组件里做很多建立和清理的工作,最好的方式通常还是创建更多的模块化组件。

循环引用

递归组件

组件是可以在它们自己的模板中调用自身的。不过它们只能通过 name 选项来做这件事。稍有不慎,递归组件就可能导致无限循环:

const Stack = {
    name: 'stack-overflow',
    template: '<div><stack-overflow></stack-overflow></div>'
};

确保递归调用是条件性的(例如使用一个最终会得到 falsev-if)。

组件间的循环引用

假设你需要构建一个文件目录树,像访达或资源管理器那样的。你可能有一个 <tree-folder> 组件,模板是这样的:

<p>
    <span>{{ folder.name }}</span>
    <tree-folder-contents :children="folder.children"/>
</p>

还有一个 <tree-folder-contents> 组件,模板是这样的:

<ul>
    <li v-for="child in children">
        <tree-folder v-if="child.children" :folder="child"/>
        <span v-else>{{ child.name }}</span>
    </li>
</ul>

当你仔细观察的时候,你会发现这些组件在渲染树中互为对方的后代和祖先——一个悖论!

当通过 Vue.component 全局注册组件的时候,这个悖论会被自动解开。

然而,如果你使用一个模块系统依赖/导入组件,例如通过 webpack 或 Browserify,你会遇到一个错误:Failed to mount component: template or render function not defined.

为了解决这个问题,我们需要给模块系统一个点,在那里“A 是需要 B 的,但是我们不需要先解析 B。”

在我们的例子中,把 <tree-folder> 组件设为了那个点。我们知道那个产生悖论的子组件是 <tree-folder-contents> 组件,所以我们会等到生命周期钩子 beforeCreate 时去注册它:

beforeCreate: function () {
    this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}

或者,在局部注册组件的时候,你可以使用 webpack 的异步 import

components: {
    TreeFolderContents: () => import('./tree-folder-contents.vue')
}

模板定义的替代品

内联模板 inline-template

inline-template 这个特殊的 attribute 出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容。内联模板需要定义在 Vue 所属的 DOM 元素内。

<my-component inline-template>
  <div>
    <p>These are compiled as the component's own template.</p>
    <p>Not parent's transclusion content.</p>
  </div>
</my-component>

不过,inline-template 会让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 template 选项或 .vue 文件里的一个 <template> 元素来定义模板。

X-Template

另一个定义模板的方式是使用<script type="text/x-template"> 元素中,通过 id 引用模板。需要定义在 Vue 所属的 DOM 元素外。

<script type="text/x-template" id="hello-world-template">
  <p>Hello hello hello</p>
</script>
Vue.component('hello-world', {
  template: '#hello-world-template'
})

这些可以用于模板特别大的 demo极小型的应用,但是其它情况下请避免使用,因为这会将模板和该组件的其它定义分离开。

控制更新

感谢 Vue 的响应式系统,它始终知道何时进行更新 (如果你用对了的话)。不过还是有一些边界情况,你想要强制更新,尽管表面上看响应式的数据没有发生改变。也有一些情况是你想阻止不必要的更新

强制更新 ???

如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事。

你可能还没有留意到数组或对象的变更检测注意事项,或者你可能依赖了一个未被 Vue 的响应式系统追踪的状态。

然而,如果你已经做到了上述的事项仍然发现在极少数的情况下需要手动强制更新,那么你可以通过 $forceUpdate 来做这件事。

通过 v-once 创建低开销的静态组件

渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种情况下,你可以在根元素上添加 v-once 以确保这些内容只计算一次然后缓存起来,就像这样:

Vue.component('terms-of-service', {
  template: `
    <div v-once>
      <h1>Terms of Service</h1>
      ... a lot of static content ...
    </div>
  `
})

不要过度使用这个模式。当你需要渲染大量静态内容时,极少数的情况下它会给你带来便利,除非你非常留意渲染变慢了,不然它完全是没有必要的——再加上它在后期会带来很多困惑。例如,设想另一个开发者并不熟悉 v-once 或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新。