10个你可能不知道的Vue开发技巧

使用Vue开发已经几年的时间了,今天将 10 个日常工作中实践以及从其他国外文章看到的小技巧分享出来,希望能够让大家可以更愉快的撸码!

1. .sync修饰符实现props双向数据绑定

Vue的数据流向是单向数据流,即:父组件通过属性绑定将数据传给子组件,子组件通过props接收,但子组件中无法对props的数据修改来更新父组件的数据,只能通过$emit派发事件的方式,父组件接收到到事件后执行修改。Vue2.30 以后新增了一个sync属性,可以实现子组件派发事件时执行修改父组件数据,无需再父组件接收事件进行更改。

示例:在父组件中控制子组件的显示隐藏

  • 普通实现方式:
// 父组件
<template>
  <div>
    <button @click="handleClick">click me</button>
    <child
      :visible="visible"
      @on-success="handleSuccess"
      @on-cancel="handleCancel"
    ></child>
  </div>
</template>
<script>
  export default {
      data() {
          return {
              visible: false
          }
      }
      methods: {
          handleClick() {
              this.visible = true
          },
          handleSuccess() {
              this.visible = false
          },
          handleCancel() {
              this.visible = false
          }
      }
  }
</script>

// 子组件
<template>
  <div class="box" v-show="visible">
    <input type="text" />
    <div>
      <button @click="cancel">取消</button>
      <button @click="submit">确定</button>
    </div>
  </div>
</template>
<script>
  export default {
    props: {
      visible: {
        type: Boolean,
        default: false,
      },
    },
    methods: {
      submit() {
        this.$emit("on-success");
      },
      cancel() {
        this.$emit("on-cancel");
      },
    },
  };
</script>
  • .sync`修饰符实现方式
// 父组件
<template>
  <div>
    <button @click="handleClick">click me</button>
    // 添加sync修饰符,相当于<child
      :visible="visible"
      @update:visible="visible=$event"
    ></child>
    <child :visible.sync="visible"></child>
  </div>
</template>
<script>
  export default {
      data() {
          return {
              visible: false
          }
      }
      methods: {
          handleClick() {
              this.visible = true
          }
      }
  }
</script>

// 子组件
<template>
  <div class="box" v-show="visible">
    <input type="text" />
    <div>
      <button @click="cancel">取消</button>
      <button @click="submit">确定</button>
    </div>
  </div>
</template>
<script>
  export default {
    props: {
      visible: {
        type: Boolean,
        default: false,
      },
    },
    methods: {
      submit() {
        this.$emit("update:visible", false);
      },
      cancel() {
        this.$emit("update:visible", false);
      },
    },
  };
</script>

2. 监听生命周期Hook

2.1. 组件外部(父组件)监听(子)其他组件的生命周期函数

在有些业务场景下,在父组件中我们需要监听子组件,或者第三方组件的生命周期函数,然后来进行一些业务逻辑处理,但是组件内部有没有提供change事件时,此时我们可以使用hook来监听所有的生命周期函数。方式: @hook:钩子函数

<template>
  <!--通过@hook:updated监听组件的updated生命钩子函数-->
  <!--组件的所有生命周期钩子都可以通过@hook:钩子函数名 来监听触发-->
  <custom-select @hook:updated="handleSelectUpdated" />
</template>
<script>
  import CustomSelect from "../components/custom-select";
  export default {
    components: {
      CustomSelect,
    },
    methods: {
      handleSelectUpdated() {
        console.log("custom-select组件的updated钩子函数被触发");
      },
    },
  };
</script>

2.2. 监听组件内部的生命周期函数

在组件内部,如果想要监听组件的生命周期钩子,可以使用$on,$once

示例:使用 echart 时,监听窗口改变事件,组件销毁时取消监听,通常是在mounted生命周期中设置监听,beforeDestroy钩子中销毁监听。这样就是要写在不同的地方,可以使用this.$once('hook:beforeDestroy'),()=> {}这种方式监听beforeDestroy钩子,在这个钩子处罚时销毁。
一次性监听使用$once,一直监听使用$on

export default {
  mounted() {
    this.chart = echarts.init(this.$el);
    // 监听窗口发生变化,resize组件
    window.addEventListener("resize", this.handleResizeChart);
    // 通过hook监听组件销毁钩子函数,并取消监听事件
    this.$once("hook:beforeDestroy", () => {
      window.removeEventListener("resize", this.handleResizeChart);
    });
  },
  methods: {
    handleResizeChart() {
      // do something
    },
  },
};

3. 深度作用选择器

我们在写Vue组件的时候为了避免当前组件的样式对子组件产生影响,通常我们会在当前组件的style标签上加上scoped,这样在这个组件中写的样式只会作用于当前组件,不会对子组件产生影响。

<style scoped>
.example {
    color: red;
}
</style>

这样转换后的结果:

<style>
.example[data-v-f3f3eg9] {
    color: red;
}
</style>

但是有时候,我们引入的第三方组件,我们希望在当前组件中修改第三方组件的样式,对子组件也产生作用,同时跟第三方组件无关的样式继续scoped
那么我们可以使用以下两种方式:

  • 混用本地和全局样式
    即:可以在一个组件中同时使用有 scoped 和非 scoped 样式。
<style>
/* 全局样式 */
</style>

<style scoped>
/* 本地样式 */
</style>
  • 使用操作符:>>>/deep/::v-deep
    即:如果你希望scoped 样式中的一个选择器能够作用得“更深”,例如影响子组件,就可以使用操作符。
<style scoped>
.a >>> .b { /* ... */ }
/*或*/
.a /deep/ .b { /* ... */ }
/*或*/
.a ::v-deep .b { /* ... */ }
/* .b选择器的样式不仅可以作用当前组件,也可以作用于子组件 */
</style>

编译后:

.a[data-v-f3f3eg9] .b {
  /* ... */
}

4. 组件初始化时触发Watcher

默认情况下,Watcher在组件初始化的时候是不会运行的,所以如果在watch中监听的数据默认是不会进行初始化的。类似于这样:

watch: {
  title: (newTitle, oldTitle) => {
    // 组件初始化时不会打印
    console.log("Title changed from " + oldTitle + " to " + newTitle);
  };
}

但是,如果我们期望在初始化的时候运行watch,则可以通过添加immediate属性。

watch: {
    title: {
        immediate: true,
        handler(newTitle, oldTitle) {
            // 组件初始化时会被打印
            console.log("Title changed from " + oldTitle + " to " + newTitle)
        }
    }
}

5. 自定义验证Props

我们都知道在子组件接收props时可以对传入的属性进行校验,可以校验为字符串、数字、数组、对象、函数。但我们也可以进行自定义校验。
示例:验证传入的字符串状态必须为successerror

props: {
  status: {
    type: String,
    required: true,
    validator: function (value) {
      return [
        'success',
        'error',
      ].indexOf(value) !== -1
    }
  }
}

6. 动态指令参数

Vue在绑定事件的时候支持将指令参数动态传递给组件,假设有一个按钮组件,并且在某些情况下想监听单击事件,而在其他情况下想监听双击事件。此时就可以使用动态指令参数。

<template>
  ...
  <aButton @[someEvent]="handleSomeEvent()" />
  ...
</template>

<script>
  ...
  data(){
    return{
      ...
      someEvent: someCondition ? "click" : "dblclick"
    }
  },

  methods:{
    handleSomeEvent(){
      // handle some event
    }
  }
  ...
</script>

7. 组件路由复用

在开发当中,有时候我们不同的路由复用同一个组件,默认情况下,我们切换组件,Vue出于性能考虑可能不会重复渲染。

但是我们可以通过给router-view绑定一个key属性来进行切换的时候路由重复渲染。

<template>
  <router-view :key="$route.fullPath"></router-view>
</template>

8. 批量属性继承——使用$props将父组件的所有的props传递到子组件

在开发中当前组件从父组件接收传递下来的数据使用props接收,如果再将这些props数据传递到子组件,通常情况下,我们同样是使用属性绑定的方式一个一个的属性去绑定。但是如果props的数据很多,那么一个个的绑定方式就很不优雅。

此时我们可以使用$props来传递。

  • Bad
<template>
    <!-- 将从父组件接收到的props数据传递到子组件 -->
    <childComponent
        :value1='value1'
        :value2='value2'
        :value3='value3'
        :value4='value4'
        :value5='value5'
    />
</template>
<script>
export default {
    // 从父组件接收到的props数据
    props: ['value1','value2','value3','value4','value5'],
    data() {
        return {....}
    .....
    }
}
</scrript>

// childComponent.vue
<script>
export default {
    props: ['value1','value2','value3','value4','value5'],
    data() {
        return {....}
    .....
    },
    mounted() {
        // 子组件可以接收到数据
        console.log(this.value1)
        console.log(this.value2)
        console.log(this.value3)
        console.log(this.value4)
        console.log(this.value5)
    }
}
</scrript>

  • Good
<template>
    <!-- 将从父组件接收到的props数据传递到子组件
        使用v-bind="$props" 批量传递
    -->
    <childComponent
        v-bind="$props"
    />
</template>
<script>
export default {
    // 从父组件接收到的props数据
    props: ['value1','value2','value3','value4','value5'],
    data() {
        return {....}
    .....
    }
}

// childComponent.vue
<script>
export default {
    props: ['value1','value2','value3','value4','value5'],
    data() {
        return {....}
    .....
    },
    mounted() {
        // 子组件可以接收到数据
        console.log(this.value1)
        console.log(this.value2)
        console.log(this.value3)
        console.log(this.value4)
        console.log(this.value5)
    }
}
</scrript>

属性继承在开发表单组件时,是不得不解决的问题,使用$props就可以很好的解决批量属性传递问题。

下面以开发一个XInput为例:

<template>
  <label>姓名</label>
  <!-- 使用XInput组件 -->
  <XInput
    :value="value"
    :placeholder="placeholder"
    :maxlength="maxlength"
    :minlength="minlength"
    :name="name"
    :form="form"
    :value="value"
    :disabled="disabled"
    :readonly="readonly"
    :autofocus="autofocus"
    @input="handleInputChange"
  />
</template>
  • Bad
// XInput.vue
<template>
  <div>
    <input
      @input="$emit('input', $event.target.value)"
      :value="value"
      :placeholder="placeholder"
      :maxlength="maxlength"
      :minlength="minlength"
      :name="name"
      :form="form"
      :value="value"
      :disabled="disabled"
      :readonly="readonly"
      :autofocus="autofocus"
    />
  </div>
</template>

<script>
  export default {
    props: [
      "label",
      "placeholder",
      "maxlength",
      "minlength",
      "name",
      "form",
      "value",
      "disabled",
      "readonly",
      "autofocus",
    ],
  };
</script>
  • Good
<template>
  <div>
    <input v-bind="$props" />
  </div>
</template>
<script>
  export default {
    props: [
      "label",
      "placeholder",
      "maxlength",
      "minlength",
      "name",
      "form",
      "value",
      "disabled",
      "readonly",
      "autofocus",
    ],
  };
</script>

9. 把所有父级组件的事件监听传递到子组件 - $listeners

如果子组件不在父组件的根目录下,则可以将所有事件侦听器从父组件传递到子组件。即在子组件可以获取到所有子组件的事件。

// Parnet.vue
<template>
  <div>父组件</div>
  <!-- 组件 -->
  <Child @on-test1="handleTest" 1 />
</template>
// Child.vue
<template>
  <div>子组件</div>
  <!-- 组件 -->
  <!-- 使用v-on='$listeners'将所有父组件非原生事件传递到子组件 -->
  <sub-child
    @on-test2="handleTest2"
    @on-test3.native="handleTest3"
    v-on="$listeners"
  />
</template>
// SubChild.vue
<template>
  <div>孙子组件</div>
</template>
<script>
  export default {
      ...
      mounted() {
          console.log(this.$listeners)
          /*
              {
                  on-test1: ƒ invoker()
                  on-test2: ƒ invoker()
              }
          */
          // 调要祖父组件的事件
          this.$listeners.on-test1()
          // 调要父组件的事件
          this.$listeners.on-test2()
      }
  }
</script>

注意:如果使用native修改的事件则获取不到。即无法获取到原生事件。

10. 基础组件自动注册

在项目开发中我们通常对于通用组件都是用到的地方挨个import引入,这种方式虽然没有问题,但是作为一个有追求的程序狗怎么能做这种重复性的劳动呢。

你可以尝试下面这种基础组件自动全局注册的方式,通用组件只需要定义在components/base/文件夹下,就可以实现自动全局注册。需要使用的地方可以直接使用,无需单独引入。

// utils/globals.js
/*
    这个方法负责基础组件的全局注册;
    这些组件可以在项目的任何地方使用而无需引入;
    所有的通用组件文件要定义在/components/base/文件夹下;
    组件命名采用:Base<componentName>.vue 的方式
*/
export const registerBaseComponents = vm => {
    // 引入通用组件
    const requireComponent = require.context(
        // 读取文件的路径
        './components/base',
        // 是否遍历文件的子目录
        false,
        // 匹配文件的正则
        /Base[\w-]+\.vue$\
    )
    requireComponent.keys().forEach(fileName => {
        // 获取每个组件文件配置
        const componentConfig = requireComponent(fileName)
        // 转换组件命名为驼峰命名
        const componentName = upperFirst(
            camelCase(fileName.replace(/^\.\//,'').replace(/\.\w+$/,''))
        )
        // 全局注册组件
        vm.component(componentName,componentConfig.default || componentConfig)
    })
}

然后,在入口文件main.js中引入并初始化。

import Vue from 'vue'
import { registerBaseComponents } from '@/utils/globals'
registerBaseComponents(Vue)
.....

11、自定义 v-model

Vue 的特性之一是单向数据流,父组件传递给子组件的数据,无法做到完全同步,子组件如果想更新父组件的数据需要派发事件给父组件。但vue也提供了一种方式给我们,自定义v-model,让我们可以实现双向数据绑定的效果。

  • 方式一:
// Parent.vue
<Button @click="handleChange"></Button>
.....
<Child v-model="visible" />
...
data() {
    return {
        visible: false
    }
},
methods: {
    handleChange() {
        this.visible = true
    }
}

// Child.vue
<template>
    <Drawer
        v-model="isVisible">
    ......
    </Drawer>
</template>
<script>
    export default {
        model: {
            prop: 'value',
            event: 'change'
        },
        props: {
            // value为接收到父组件的数据
            value: Boolean
        },
        computed: {
            isVisible: {
                get() {
                    return this.value
                },
                set(val) {
                    // 更新父组件数据
                    this.$emit('change', val)
                }
            }
        },
    }
</script>
  • 方式二:
// Parent.vue
<Button @click="handleChange"></Button>
.....
<Child v-model="visible" />
...
data() {
    return {
        visible: false
    }
},
methods: {
    handleChange() {
        this.visible = true
    }
}

// Child.vue
<template>
    <Drawer
        v-model="isVisible">
    ......
    </Drawer>
</template>
<script>
    export default {
        props: {
            // value为接收到父组件的数据
            value: Boolean
        },
        computed: {
            isVisible: {
                get() {
                    return this.value
                },
                set(val) {
                    // 默认派发input事件
                    this.$emit('input', val)
                }
            }
        },
    }
</script>

至此,我们的 11 个小技巧就分享完了,如果你觉得有用,请你动动小手点个赞让我知道[笔芯]

参考文献 1

参考文献 2

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335