Vue 高级用法 - 04

176 阅读5分钟

mixin

全局混入

会影响每一个之后创建的 Vue 实例

  Vue.mixin({
    created: function () {
  // }
  })

局部混入

  mixins: [myMixin]

缺点:数据来源不明确

  const mixin = {
    data () {
      return {
        b: 2
      }
    }
  }

  export default {
    name: 'App',
    components: {
      // HelloWorld
    },
    mixins: [mixin],
    data () {
      a: 1
    }
  }

image.png

当 mixins 与 当前元素下的数据有冲突时,会进行递归合并;如果 key 有冲突,则会以组件的为主,mixins 的值会被覆盖

  const mixin = {
    data () {
      return {
        b: 2,
        info: {
          name: 'mixin'
        }
      }
    }
  }

  export default {
    name: 'App',
    components: {
      // HelloWorld
    },
    mixins: [mixin],
    data () {
      return {
        a: 1,
        info: {
          name: 'appComponent',
          age: 12
        }
      }
    }
  }

image.png

Mixins 数据合并:

  1. 数据类型的合并,如 data props computed 遵循 webpack merge 递归的合并,只在key 冲突的情况下替换

  2. 钩子函数中的合并,如生命周期钩子,相当于将他们放在一个数组中 -> [() => {}, ()=> {}] 但是mixins会定义的先执行

  const mixin = {
    created () {
      console.log('mixin created!')
    }
  }

  export default {
    name: 'App',
    created () {
      console.log('app created!')
    }
  }

image.png

  1. 选项类型的合并,如methods,组件可以直接调用,并以组件的数据为主
  const mixin = {
    methods: {
      test () {
        console.log('mixin test')
      }
    }
  }

  export default {
    name: 'App',
    created () {
      this.test ();
      console.log('app created!')
    },
    methods: {
      test () {
        console.log('app test')
      }
    }
  }

elementUi 中的mixin应用

场景:input 给 form 传递数据

一般思路为父子组件间传递,但是也许中间也会隔一些组件,因此抽离一个mixin

  // 利用递归一层层往上找,找到 组件 name
  broadcast(componentName, eventName, params) {
    broadcast.call(this, componentName, eventName, params);
  }
  export default {
    name: 'ElDialog',
    mixins: [Popup, emitter, Migrating],
    props: {
      title: {
        type: String,
        default: ''
      },
    ...
  updatePopper() {
    this.broadcast('ElSelectDropdown', 'updatePopper');
    this.broadcast('ElDropdownMenu', 'updatePopper');
  },

插槽 - react 中是this.props.children

  1. 默认插槽:

子组件中定义插槽

  <template>
    <div class="">
      <slot></slot>
    </div>
  </template>

父组件定义具体内容

  <template>
    <div id="app">
      <el-dialog>
        <span>这是一段标签</span>
      </el-dialog>
    </div>
  </template>
  1. 具名插槽:

子组件定义名称

<template>
  <div>
    <slot name="footer"></slot>
  </div>
</template>

父组件定义,需要使用 v-slot绑定名字

  <template>
    <div id="app">
      <el-dialog>
        <span>这是一段标签</span>
        <template v-slot:footer>
          这是尾部标签
        </template>
      </el-dialog>
    </div>
  </template>
  1. 插槽作用域

普通插槽:数据可以是当前组件中的data

<el-dialog>
  <span>这是一段标签 {{ a }}</span>
</el-dialog>
  data () {
    return {
      a: 1
    }
  }

作用域插槽:组件把数据传递给插槽

3.1 具名插槽作用域

// el-dialog

  data () {
    return {
      userInfo: {
        name: '',
        age: ''
      }
    }
  },
  mounted () {
    setTimeout(() => {
      this.userInfo = {
        name: '张三',
        age: 12
      }
    }, 2000);
  }

当前组件中传递数据给插槽: v-bind

// el-dialog

  <slot name="footer" :userInfo="userInfo"></slot>

父组件如何拿到数据:v-slot: footer="slotProps" " "内可以随便命名

  <template v-slot:footer="slotProps">
    这是尾部标签 {{ slotProps }}
  </template>

Kapture 2022-02-03 at 16.07.17.gif

3.2 默认插槽作用域

传入数据时,父级中要在<template>标签中设置v-slot:default="slotDefaultProps"

  <template v-slot:default="slotDefaultProps">
    <span>这是一段标签 {{ slotDefaultProps.spanInfo }}</span>
  </template>
  1. 插槽的编译

插槽的编译都是在各自的模版中完成的,父级模版的内容都是在父级模版编译的,子级模版的内容都是在子级模版编译的;template -> render() 渲染:render() -> vnode

普通插槽就在当前作用域中渲染,作用域插槽首先变成function(proprs) {},将其插入到子集组件内部渲染

插件

用插件为 vue 添加全局功能,如vuex vue-router

  1. 注册插件
  Vue.use(Vuex);
  Vue.use(VueRouter);
  1. 编写插件 2.1 暴露install 方法 2.2 Vue.use(MyPlugin, options)会执行插件的install方法,并传入Vue和options
  MyPlugin.install = function (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) {
  // ... }
  }
  1. 实现elementUI中的message 插件

3.1 简单的 Message 组件

  // Message
  <template>
  <div>
    <div v-for="m in messages" :key="m.id"></div>
  </div>
  </template>

  <script>
  export default {
    name: 'ElMessage',
    data () {
      return {
        messages: [] // { id, message, duration }
      }
    }
  }     
  </script>

3.2 调用方式 3.2.1 挂在 Vue.prototype 上,添加全局方法$message,实例上可以使用 Message 方法

暴露install 方法,使其成为插件

  // message.js
  export default {
    install(Vue, options) {
      console.log(options)

      Vue.prototype.$message = {
        info: Message.info,
      }
    }
  }

主文件中注册 插件

  // main.js
  import Message from './components/Plugin/message'

  Vue.use(Message, {
    size: 'small'
  })

3.2.2 单独引用

注册

  // App.vue
  import { Message } from './components/Plugins/message.js';

  // 使用
  // Message.info({ message: '消息', duration: 2000})

写插件

// message.js
  export const Message = {
    info(options) {
      console.log(options)
    }
  }

全局点击调用

  //App.vue
  <button @click="showMessage">全局调用message</button>
  showMessage() {
    Message.info({ message: `消息提示: ${++id}`, duration: 2000})
  }

拿到options 数据之后 通过MessageComponent.add(options) 将数据放入Message 组件中

  mounted() {
    this.id = 0
  },
  methods: {
    add(options) {
      const layer = {
        ...options,
        id: this.id++
      }
      this.messages.push(layer); // 点击加入数据

      // 一段时间后自动消失
      setTimeout(() => {
        this.remove(layer);
      }, layer.duration);
    },
    remove(layer) {
      return this.messages = this.messages.filter(m => m.id !== layer.id)
    }
  }

message.js 中拿到MessageComponent 组件并调用add方法

  // message.js
  import Vue from 'vue'
  import MessageComponent from './Message.vue'

  // 获取实例
  // 单例模式,避免生成多个div
  let vm = null
  const getInstance = () => {
    const getInstance = function() {
      if (!vm) {
    vm = new Vue({
      render: h => h(MessageComponent)
    }).$mount() // 转换成真实DOM
  
    // 将模版显示到页面上
    document.body.appendChild(vm.$el)
  }

  return vm.$children[0] // MessageComponent 组件
  }   

  export const Message = {
    info(options) {
      console.log(options)
      getInstance().add(options)
    }
  }

过滤器

  1. 定义:将原数据进行格式化显示,而不改变原数据
  2. 应用:货币符号、时间格式化(一般用在与业务关联不大的情况下,否则用computed)
  3. 全局过滤器 例子:时间
  // main
  Vue.filter('timeFormat', function(val, function(val, formatter = 'YYYY:MM:DD')) {
    return moment(val).format(formatter)
  })

  1. 局部过滤器
// App.vue
  name: 'App',
  components: {
    // HelloWorld
    // ElDialog,
    // BlogPost
  },
  filters: {
    timeFormat(val, formatter) {
        return moment(val).format(formatter)
    }
  },
  mixins: [mixin],
  1. 无法访问 this

Vue 响应式原理

  1. 核心 API Object.defineProperty
  <div>
    <div>{{ a }}</div>
    <div>{{ info.name }}</div>
  </div>
  data () {
    return {
      a: 1, // get set dep
      info: { // get set dep
        name: 'zhangsan', // get set dep
        age: 12
      }
    }
  },
  watch: {
    a() {

    }
  }

Object.defineProperty() 会递归的遍历这些数据,给数据添加getter 和 setter get 收集依赖,set 通知依赖

  const dep1 = new Dep()
  Object.defineProperty(this.$data, 'a', {
    get() {
      dep1.depend() // 收集依赖
      return value
    },
    set(newValue) {
      if (newValue === value) return;
      value = newValue
      dep1.notify() // 通知依赖
    }
  })

  const dep2 = new Dep()
  Object.defineProperty(this.$data, 'info', {
    ...
  })

  const dep3 = new Dep()
  Object.defineProperty(this.$data.info, 'name', {
    ...
  })
  • 每个组件实例对应一个渲染watcher,对touch 的数据进行求值 -> 触发每个值对应的get 什么是touch -- 模版里面用到了
  • a -> get, info -> get, name -> get ,调用dep.depend()收集依赖,渲染watcher
  • a 的 dep 渲染watcher,watch里面的watcher
  • 当 给a更改值,this.a = 2 -> 触发a - set, 通过dep.notify 通知依赖
  • info.age 没有用的话,改变this.info.age 不会触发

收集依赖、触发更新

  • 收集依赖
    • 每个组件实例对应一个watcher实例
    • 在组件渲染过程中,把“touch”过的数据记录为依赖(触发getter -> 将当前watcher实例收集到属性对应的dep中)
  • 触发更新
    • 数据更新后 -> 会触发属性对应的setter -> 通过dep去通知watcher -> 关联的组件重新渲染

image.png

注意事项:

  1. 对象
  • vue无法监测对象的添加
  • 解决方案:this.$set(this.someObject, 'b' ,2)
  • 注意:Vue 不允许动态添加根级别的响应式 property
  1. 数组
  • Object.defineProperty无法监听数组索引值的变化,比如 this.a[0] = 44
  • 解决方案:
    • this.$set(this.a, 0, 44)
    • this.a.splice(0, 1, 44)
  • 数组长度的变化也无法监听
    • 解决方案:this.a.splice(newLength) // 省略第二个参数,表明删除从newLength及后面的数据
  • 重写了数组的方法(push() pop() shift() unshift() splice() sort()reverse())
  • this.a[1].name = 'lisi' // 这是更改对象的属性值
  1. 其他
  • 递归的循环data中的属性(可能会导致性能问题)
  • 对于一些数据获取后不更改,仅仅用来展示的数据(比如说省、市下拉框,获取一次之后,下拉框的 options 数据就不变了(城市不变),改变的是value (选的city 值))可以使用Object.freeze来优化性能 this.city = Object.freeze(data.city) 不用再去添加setter getter

动画

  1. 单元素/组件动画(transition 组件)
<template>
  <div>
      <h2>单元素 css 动画</h2>
      <button @click="toggleVisible">切换</button>

      <transition 
        name="fade"
      >
        <div class="box" v-show="visible"></div>
      </transition>

      <transition 
        enter-active-class="animate__animated animate__bounce"
        leave-active-class="animate__animated animate__tada
"
      >
        <div class="box" v-show="visible"></div>
      </transition>
  </div>
</template>
import 'animate.css'
export default {
  data() {
    return {
      visible: true,
    }
  },
  methods: {
    toggleVisible() {
      this.visible = !this.visible
    }
  }
}
.box {
  margin-top: 30px;
  width: 100px;
  height: 100px;
  background: red;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
.fade-enter-active, .fade-leave-active {
  transition: opacity 1s ease-out;;
}

1.1 用途:单元素/组件动画(比如:进入/离开) 1.2 css3 transition

  • v-enter

    • 用来定义动画的初始状态
    • 元素插入之前被添加,元素插入后被移除
  • v-enter-active

    • 定义动画语句
    • 元素插入之前被添加,动画结束后被移除
  • v-enter-to

    • 用来定义动画的结束状态(一般不设置,因为元素有默认的显示状态,比如opacity默认为1)
    • 元素插入后被添加,动画结束后被移除
  • v-leave

  • v-leave-active

  • v-leave-to

image.png

  1. 多元素动画 区别在于会真实生成一个 dom 元素

  2. 动画库

animate.css

  • css动画库

velocity

  • Js动画库(类似JQuery的$.animate)处理一些CSS属性,比如opacity, position等
  • 注意 npm install velocity-animate

gasp

  • Js动画库,除了CSS属性外,可提供状态过渡