Vue 高级用法

329 阅读5分钟

mixin 复用(Vue2)

Mixin(混入)是一种在 Vue 组件中重用代码的方法。它允许我们将一些可复用的逻辑提取出来,然后在多个组件中进行共享。在 Vue 中,Mixin 通过在组件的 mixins 选项中引入一个对象来实现。

mixin 实现复用的同时带来了很多问题,例如:命名污染、依赖不透明

Vue3 用 Composition API替代了

基础使用

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

// 定义一个混入对象
var myMixin = {
  created: function () {
    this.hello()
  },
  methods: {
    hello: function () {
      console.log('hello from mixin!')
    }
  }
}

// 定义一个使用混入对象的组件
var Component = Vue.extend({
  mixins: [myMixin]
})

var component = new Component() // => "hello from mixin!"

选项合并

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

比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先

var mixin = {
  data: function () {
    return {
      message: 'hello',
      foo: 'abc'
    }
  }
}

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

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

var mixin = {
  created: function () {
    console.log('混入对象的钩子被调用')
  }
}

new Vue({
  mixins: [mixin],
  created: function () { //同名时可以理解为方法名覆盖,但是外部方法的内容与当前内容合并,且先外后内
    console.log('组件钩子被调用')
  }
})

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

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

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

var vm = new Vue({
  mixins: [mixin],
  methods: {
    bar: function () {
      console.log('bar')
    },
    conflicting: function () {
      console.log('from self')
    }
  }
})

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

注意:Vue.extend() 也使用同样的策略进行合并。

全局混入

混入也可以进行全局注册。使用时格外小心!一旦使用全局混入,它将影响每一个之后创建的 Vue 实例。使用恰当时,这可以用来为自定义选项注入处理逻辑。

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

new Vue({
  myOption: 'hello!'
})
// => "hello!"

mixin 存在的问题

命名冲突

mixin模式在运行时合并两个对象,如果他们两个都共享同名属性,会发生覆盖

const mixin = {
  data: () => ({
    myProp: null
  })
}

export default {
  mixins: [mixin],
  data: () => ({ // 同名
    myProp: null
  })
}

Vue组件的默认(可选配置)合并策略指示本地选项将覆盖 mixin 选项(内部覆盖外部)。不过也有例外,例如,如果我们有多个相同类型的生命周期钩子,这些钩子将被添加到一个钩子数组中,并且所有的钩子都将被依次调用。

依赖不透明

换句话说,依赖不是局部声明式的。

mixin 和使用它的组件之间没有层次关系。这意味着组件可以使用mixin中定义的数据属性(例如myData),但是mixin 也可以使用假定在组件中定义的数据属性(例如myData)。以后的某天如果想修改 mixin,包袱有点重。

vue.js 动画特效& 常见组件库

进入/离开 & 列表过渡

Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。包括以下工具:

  • 在 CSS 过渡和动画中自动应用 class
  • 可以配合使用第三方 CSS 动画库,如 Animate.css
  • 在过渡钩子函数中使用 JavaScript 直接操作 DOM
  • 可以配合使用第三方 JavaScript 动画库,如 Velocity.js

Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡

  • 条件渲染 (使用 v-if)
  • 条件展示 (使用 v-show)
  • 动态组件
  • 组件根节点

v-if & v-show

在 vue 中 v-show 与 v-if 的作用效果是相同的(不含v-else),都能控制元素在页面是否显示。在用法上也是相同的

<Model v-show="isShow" />
<Model v-if="isShow" />

当表达式为true的时候,都会占据页面的位置,而表达式都为false时,都不会占据页面位置

v-show与v-if的区别

  • 控制手段不同
  • 编译过程不同
  • 编译条件不同
  • 性能消耗不同,v-if有更高的切换消耗;v-show有更高的初始渲染消耗;

控制手段v-show隐藏是为该元素添加css:display:nonedom元素依旧还在v-if显示隐藏是将dom元素整个添加或删除

编译过程v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换

编译条件v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染

  • v-show 由false变为true的时候不会触发组件的生命周期
  • v-iffalse变为true的时候,触发组件的beforeCreatecreatebeforeMountmounted钩子,由true变为false的时候触发组件的beforeDestorydestoryed方法

v-show与v-if的使用场景

v-if 与 v-show 都能控制dom元素在页面的显示
v-if 相比 v-show 开销更大(直接操作dom节点增加与删除)

如果需要非常频繁地切换,则使用 v-show 较好
如果在运行时条件很少改变,则使用 v-if 较好

基础使用示例

//components/TransitionDemo.vue
<div id="demo">
  <button v-on:click="show = !show">
    Toggle
  </button>
  <transition name="fade">
    <p v-if="show">hello</p>
  </transition>
</div>
.fade-enter-active, .fade-leave-active {
  transition: all .5s;
  transform: translate(0);
}
.fade-enter, .fade-leave-to {
  opacity: 0;
  transform: translate(-100px);
}
export default {
  data() {
    show: true
  }
}

自定义过渡类名

<link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1" rel="stylesheet" type="text/css">
<div id="example-3">
  <button @click="show = !show">
    Toggle render
  </button>
  <transition
    name="custom-classes-transition"
    enter-active-class="animated tada"
    leave-active-class="animated bounceOutRight"
  >
    <p v-if="show">hello</p>
  </transition>
</div>

动画钩子

<transition
  v-on:before-enter="beforeEnter"
  v-on:enter="enter"
  v-on:after-enter="afterEnter"
  v-on:enter-cancelled="enterCancelled"
  v-on:before-leave="beforeLeave" 
  v-on:leave="leave" 
  v-on:after-leave="afterLeave" 
  v-on:leave-cancelled="leaveCancelled"
>
  <!-- ... -->
</transition>

这些方法要记得定义,以下以enter与leave为例:

<script>
export default{
    data(){
        return{
            show:ture,
        };
    }
    methods:{
        //钩子函数其实本身跟副作用概念类似
        enter(el,done){
            consloe.log("enter");
            document.body.style.backgroundColor="red";
            done();
            done();
        },
        leave(el,done){
            console.log("leave");
            document.body.style.backgroundColor="blue";
            done();
        }
    }
}
</script>

多组件过渡与列表过渡

<div id="list-demo" class="demo">
  <button v-on:click="add">Add</button>
  <button v-on:click="remove">Remove</button>
  <transition-group name="list" tag="p">
    <span v-for="item in items" v-bind:key="item" class="list-item">
      {{ item }}
    </span>
  </transition-group>
</div>
export default{
  data: {
    items: [1,2,3,4,5,6,7,8,9],
    nextNum: 10
  },
  methods: {
    randomIndex: function () {
      return Math.floor(Math.random() * this.items.length)
    },
    add: function () {
      this.items.splice(this.randomIndex(), 0, this.nextNum++)
    },
    remove: function () {
      this.items.splice(this.randomIndex(), 1)
    },
  }
}
.list-item {
  display: inline-block;
  margin-right: 10px;
}
.list-enter-active, .list-leave-active {
  transition: all 1s;
}
.list-enter, .list-leave-to
/* .list-leave-active for below version 2.1.8 */ {
  opacity: 0;
  transform: translateY(30px);
}

状态过渡

这个概念其实就是通过状态去驱动视图更新从而实现动画过渡。

在这里首先要了解计算属性与数据监听(computed、watch)

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.4/gsap.min.js"></script>

<div id="animated-number-demo">
  <input v-model.number="number" type="number" step="20">
  <p>{{ animatedNumber }}</p>
</div>
export default{
  data: {
    number: 0,
    tweenedNumber: 0
  },
  //计算属性:通过已有状态派生出新状态
  computed: {
    animatedNumber: function() {
      return this.tweenedNumber.toFixed(0);
    }
  },
  //监听某个状态
  watch: {
    number: function(newValue) {
      gsap.to(this.$data, { duration: 0.5, tweenedNumber: newValue });
    }
  }
}

常用动画相关库

gsap、animated.css、tween.js等

插槽

插槽基本使用

插槽的概念可以与 react 中 renderProps 对比。

假设我们现在有这样的需求

<div class="container">
  <header>
    <!-- 我们希望把页头放这里 -->
  </header>
  <main>
    <!-- 我们希望把主要内容放这里 -->
  </main>
  <footer>
    <!-- 我们希望把页脚放这里 -->
  </footer>
</div>

我们在编写代码时,组件内部并不清楚这块内容的具体实现,我就需要将这个坑位留出,需要开发者传进来。

我们为每个插槽取个名字

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

插槽中使用数据

有时让插槽内容能够访问子组件中才有的数据是很有用的。例如,设想一个带有如下模板的 <current-user> 组件:

<span>
  <slot>{{ user.lastName }}</slot>
</span>

我们可能想换掉备用内容,用 名 而非 姓 来显示。如下:

<current-user>
  {{ user.firstName }}
</current-user>

然而上述代码不会正常工作,因为只有 <current-user> 组件可以访问到 user,而我们提供的内容是在父级渲染的。

为了让 user 在父级的插槽内容中可用,我们可以将 user 作为 <slot> 元素的一个 attribute 绑定上去:

<span>
  <slot v-bind:user="user">
    {{ user.lastName }}
  </slot>
</span>

多个插槽与数据,我们可以这样实现

<template>
  <div class="slot-demo">
    <slot name="demo">this is demo slot.</slot>
    <slot text="this is a slot demo , " :msg="msg"></slot>
  </div>
</template>

<script>
export default {
  name: 'SlotDemo',
  data () {
    return {
      msg: 'this is scoped slot content.'
    }
  }
}
</script>
<template>
  <slot-demo>
    <template v-slot:demo>
        this is custom slot.
    </template>
    <template v-slot="scope">
      <p>{{ scope.text }}{{ scope.msg }}</p>
    </template>
  </slot-demo>
</template>

概念

默认插槽

<template v-slot:default></template>

具名插槽
具名插槽就是给我们的插槽起一个名字,即给<slot></slot>定义一个name属性

// App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" @click="this.$myMethod1" />
    <HelloWorld ref="h" msg="Welcome to Your Vue.js App">
      <template v-slot:demo>
          this is custom slot.haha
      </template>
      <template v-slot:footer>
          this is footer custom slot.heihei
      </template>
      <template v-slot="footer" >
        <p>{{ footer.text }}{{ footer.msg }}</p>
      </template>
    </HelloWorld>
  </div>
</template>

<script>
import HelloWorld from "./components/HelloWorld.vue";

export default {
  name: "App",
  data() {
    return {
      header: "Hello World",
    };
  },
  components: {
    HelloWorld,
  },
  methods: {
    $myMethod1() {
      console.log("Hello World", this.$refs.h.$scopedSlots);
    },
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <p>
      For a guide and recipes on how to configure / customize this project,<br />
      check out the
      <a href="https://cli.vuejs.org" target="_blank" rel="noopener"
        >vue-cli documentation</a
      >.
    </p>
    <h1>1
      <slot name="demo">this is demo slot.</slot>
    </h1>
    <h2>2
      <slot name="footer" text="this is a slot demo , " :msg="msg"></slot>
    </h2>
    <h3>3
      <slot text="this is a slot demo , " :msg="msg"></slot>
    </h3>
    <h3>Installed CLI Plugins</h3>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  props: {
    msg: String,
  },
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

控制台打印:

插槽的本质就是函数

插件

插件可以是对象,或者是一个函数。如果是对象,那么对象中需要提供 install 函数,如果是函数,形态需要跟 install 函数保持一致。

install 是组件安装的一个方法,跟 npm install 完全不一样,npm install 是一个命令

定义插件

const MyPlugin = {
    install(Vue, options) {
      // 1. 添加全局方法或 property
      Vue.myGlobalMethod = function () {
        // 逻辑...
      }
    
      // 2. 添加全局资源
      Vue.directive('my-directive', {
        bind (el, binding, vnode, oldVnode) {
          // 逻辑...
        }
        ...
      })
    
      // 3. 注入组件选项
      Vue.mixin({
        created: function () {
          // 逻辑...
        }
        ...
      })
    
      // 4. 添加实例方法
      Vue.prototype.$myMethod = function (methodOptions) {
        // 逻辑...
      }
    }
};

使用插件

Vue.use(MyPlugin);

{{ $myMethod }}

插件化机制原理

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // 获取已经安装的插件
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    // 看看插件是否已经安装,如果安装了直接返回
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // toArray(arguments, 1)实现的功能就是,获取Vue.use(plugin,xx,xx)中的其他参数。
    // 比如 Vue.use(plugin,{size:'mini', theme:'black'}),就会回去到plugin意外的参数
    const args = toArray(arguments, 1)
    // 在参数中第一位插入Vue,从而保证第一个参数是Vue实例
    args.unshift(this)
    // 插件要么是一个函数,要么是一个对象(对象包含install方法)
    if (typeof plugin.install === 'function') {
      // 调用插件的install方法,并传入Vue实例
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    // 在已经安装的插件数组中,放进去
    installedPlugins.push(plugin)
    return this
  }
}

具体实践

Vue-Router


    install(app: App) {
      const router = this
      app.component('RouterLink', RouterLink)
      app.component('RouterView', RouterView)

      app.config.globalProperties.$router = router
      Object.defineProperty(app.config.globalProperties, '$route', {
        enumerable: true,
        get: () => unref(currentRoute),
      })

      // this initial navigation is only necessary on client, on server it doesn't
      // make sense because it will create an extra unnecessary navigation and could
      // lead to problems
      if (
        isBrowser &&
        // used for the initial navigation client side to avoid pushing
        // multiple times when the router is used in multiple apps
        !started &&
        currentRoute.value === START_LOCATION_NORMALIZED
      ) {
        // see above
        started = true
        push(routerHistory.location).catch(err => {
          if (__DEV__) warn('Unexpected error when starting the router:', err)
        })
      }

      const reactiveRoute = {} as {
        [k in keyof RouteLocationNormalizedLoaded]: ComputedRef<
          RouteLocationNormalizedLoaded[k]
        >
      }
      for (const key in START_LOCATION_NORMALIZED) {
        // @ts-expect-error: the key matches
        reactiveRoute[key] = computed(() => currentRoute.value[key])
      }

      app.provide(routerKey, router)
      app.provide(routeLocationKey, reactive(reactiveRoute))
      app.provide(routerViewLocationKey, currentRoute)

      const unmountApp = app.unmount
      installedApps.add(app)
      app.unmount = function () {
        installedApps.delete(app)
        // the router is not attached to an app anymore
        if (installedApps.size < 1) {
          // invalidate the current navigation
          pendingLocation = START_LOCATION_NORMALIZED
          removeHistoryListener && removeHistoryListener()
          removeHistoryListener = null
          currentRoute.value = START_LOCATION_NORMALIZED
          started = false
          ready = false
        }
        unmountApp()
      }

      // TODO: this probably needs to be updated so it can be used by vue-termui
      if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && isBrowser) {
        addDevtools(app, router, matcher)
      }
    },
  }