vue

✍️ Tangxt ⏳ 2020-07-17 🏷️ directives、mixin、extends、provide/inject

06-进阶构造属性

★Directive(指令)

1)Demo1

<div id="app">
  <input v-focus>
  <button v-t-click>btn</button>
</div>
// Register a global custom directive called `v-focus`
Vue.directive('focus', {
  // When the bound element is inserted into the DOM...
  inserted: function(el) {
    // Focus the element
    console.dir(el)
    el.focus()
  }
})
Vue.directive('t-click', {
  inserted: function(el) {
    console.dir(el)
    el.onclick = () => {
      alert('t-click')
    }
  }
})
let vm = new Vue({
  el: '#app'
})

效果:

Demo1

我不关注它的写法,只关注它的效果,以及它给我的感受!

2)Demo2

<div id="hook-arguments-example" v-demo:foo.a.b="message"></div>
Vue.directive('demo', {
  bind: function(el, binding, vnode) {
    var s = JSON.stringify
    el.innerHTML =
      'name: ' + s(binding.name) + '<br>' +
      'value: ' + s(binding.value) + '<br>' +
      'expression: ' + s(binding.expression) + '<br>' +
      'argument: ' + s(binding.arg) + '<br>' +
      'modifiers: ' + s(binding.modifiers) + '<br>' +
      'vnode keys: ' + Object.keys(vnode).join(', ')
  }
})

let vm = new Vue({
  el: '#hook-arguments-example',
  data: {
    message: 'hello!'
  }
})

效果:

Demo2

我们能拿到的数据:

指令上的数据


Q:关于 `JSON.stringify` ?

JSON.stringify

Q: `bind` 是什么?Demo1 里边的 `inserted` 又是什么?还有我看到 `model` 有 `componentUpdated` , `show` 有 `updata` 和 `ubind` ?

它们都是 Hook Functions(钩子函数)

这些函数会在特定的时机被调用!

如:

  1. bind :只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
  2. inserted :被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
  3. ……

Q:传给那些钩子函数有哪些参数呢?

  1. el
  2. binding -> 一个指令如 v-my-directive.foo.bar="1 + 1" ,这里边的数据都能通过 binding.xxx 拿到!
  3. vnode
  4. oldVnode

3)Demo3

为什么需要动态参数

<p v-pin="200">Stick me 200px from the top of the page</p>
Vue.directive('pin', {
  bind: function(el, binding, vnode) {
    el.style.position = 'fixed'
    el.style.top = binding.value + 'px'
  }
})

把这个绑定了 v-pin 指令的 p 元素固定在距离页面顶部 200 像素的位置

话说,如果我们想要距离页面页面右边的呢?

难道我又得搞个 v-right 的指令出来? -> 显然,不需要,可以使用动态参数呀!

做法:

<p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
Vue.directive('pin', {
  bind: function(el, binding, vnode) {
    el.style.position = 'fixed'
    var s = (binding.arg == 'left' ? 'left' : 'top')
    el.style[s] = binding.value + 'px'
  }
})
new Vue({
  el: '#dynamicexample',
  data: function() {
    return {
      direction: 'left'
    }
  }
})

效果:

Demo3

4)Demo4

Demo4

这是函数简写,为啥叫「函数简写」,不叫「函数参数」呢?

因为如果你在 bindupdate 时都触发相同行为,而不关心其它的钩子,那么你就可以用所谓的「函数简写」啦!而不是定义一个 {} ,然后整上两个钩子!

5)Demo5

Demo6

Tips:

  1. 指令需要多个值 -> 传入一个 JavaScript 对象字面量
  2. 指令函数能够接受所有合法的 JavaScript 表达式。

6)疑问

1、何时用哪些钩子函数,我并咩有看明白……

  1. bind: 只调用一次,指令第一次绑定到元素时调用。
  2. inserted: 被绑定元素插入父节点时调用。 -> 如为元素绑定事件监听器
  3. update: 被绑定元素所在的模板更新时调用。
  4. componentUpdated: 被绑定元素所在模板完成一次更新周期时调用。
  5. unbind: 只调用一次,指令与元素解绑时调用。 -> 如移除元素的事件监听器

示例:

// 自定义v-on2指令
new Vue({
  el: "#app",
  directives: {
    on2: {
      // binding 是一个对象,包含指令的很多信息
      inserted(el, binding) {
        console.log('hi')
        el.addEventListener(binding.arg, binding.value)
      },
      unbind(el, binding) {
        el.removeEventListener(binding.arg, binding.value)
      }
    }
  },
  template: `
            <button v-on2:click="hi">点我</button>
        `,
  methods: {
    hi() {
      console.log("m");
    }
  }
})

注意:如果你这样写<button v-on2:click="hi()">点我</button>,不用点击就会,先 log m,再log 'hi' -> 原生的@click指令,显然做了hi()这种值的处理!即不会执行hi方法,只有点击了才会执行!

2、钩子函数的三个参数?

  1. el 指令所绑定的元素,可以用来直接操作DOM
  2. binding :一个对象,包含指令的很多信息
  3. vnode :Vue编译生成的虚拟节点

7)小结

  1. 自定义指令的两种写法
    1. 声明一个全局指令:Vue.directive('x',directiveOptions) -> 可以在任何组件里用 v-x
    2. 声明一个局部指令 -> new Vue({...,directives: {"x": directiveOptions}}) -> v-x 只能用在该实例中
  2. directiveOptions -> 有5个函数属性
    1. bind(el,info,vnode,oldVnode)-类似 created
    2. inserted(参数同上)-类似mounted
    3. update(参数同上)-类似updated
    4. componentUpdated(参数同上)-用得不多
    5. unbind(参数同上)-类似destroyed
  3. 指令的作用
    1. 主要用于 DOM 操作
      1. Vue的实例/组件用于数据绑定、事件监听、DOM更新
      2. Vue指令主要目的就是原生DOM操作
    2. 减少重复
      1. 如果某个DOM操作你经常使用,就可以封装为指令
      2. 如果某个DOM操作比较复杂,也可以封装为指令

★Mixin(混入属性,混入就是复制)

1)为什么需要 Mixin?

在开发一个充满组件的应用程序时,通常会遇到两个都有着非常相似功能的组件。

那么,问题来了:

  1. 我们是否可以把这两个组件合二为一?
  2. 或者,依旧分开它们,毕竟它们之间都有足够多的差异,不然,把它们结合起来的话,会导致更多的问题(相较于俩组件没合并前的问题)

幸运的是,Vue 给了我们第三个选择:Mixins

Mixins 是封装一小部分功能的一个有效方法,而这些功能可以在多个组件之间重用

我们要学的:

  1. 如何搞个 Mixins 出来?
  2. 如何有效利用这个方便的 Mixins 工具?

2)创建一个 Mixin(Option Merging)

示例:

var mixin = {
  data: function() {
    return {
      message: 'hello',
      mix: 'abc'
    }
  },
  created: function() {
    console.log('混入对象的钩子被调用')
  },
  methods: {
    foo: function() {
      console.log('foo')
    },
    conflicting: function() {
      console.log('from mixin')
    }
  }
}

var vm = new Vue({
  el: '#app',
  mixins: [mixin],
  data: function() {
    return {
      message: 'goodbye',
      com: 'def'
    }
  },
  created: function() {
    console.log('组件钩子被调用')
  },
  methods: {
    bar: function() {
      console.log('bar')
    },
    conflicting: function() {
      console.log('from self')
    }
  }
})

效果:

冲突了,怎么说

再举一个示例:

<!DOCTYPE html>
<html lang="zh-Hans">

<head>
  <meta charset="utf-8">
  <title>index7</title>
  <style>
    .red {
      width: 100px;
      height: 100px;
      border: 3px solid lightcoral;
    }

    .green {
      width: 100px;
      height: 100px;
      border: 3px solid lightgreen;
    }
  </style>
</head>

<body>
  <div id="app">
    <div>
      <button @click="Child1isShow=false">child1消失</button>
      <child1 v-if="Child1isShow"></child1>
    </div>
    <br />
    <div>
      <button @click="Child2isShow=false">child2消失</button>
      <child2 v-if="Child2isShow"></child2>
    </div>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script>
    const log = {
      data() {
        return {
          name: undefined,
          time: undefined
        };
      },
      created() {
        if (!this.name) {
          throw new Error("need Name")
        }
        this.time = new Date();
        console.log(`${this.name}出生了`)
      },
      beforeDestroy() {
        const now = new Date() - this.time;
        console.log(`${this.name}死亡了,共存活了 ${now} ms`);
      }
    }

    Vue.component('child1', {
      data() {
        return {
          name: 'child1'
        }
      },
      template: `
      <div class="red">Child1消失</div>
      `,
      mixins: [log]
    })
    Vue.component('child2', {
      data() {
        return {
          name: 'child2'
        }
      },
      template: `
     <div class="green">Child2消失</div>
      `,
      mixins: [log]
    })

    new Vue({
      el: '#app',
      data() {
        return {
          Child1isShow: true,
          Child2isShow: true,
        };
      },
    })
  </script>
</body>

</html>

我把mixin写在同一个文件了,一般来说是,创建一个mixins目录,然后在该目录下创建一个log.js -> mixins/log.js

效果:

mixin

3)全局混入(Global Mixin)

示例:

全局混入示例

虽然,全局 mixins 可用于自定义选项,然后最一些万金油的事儿,但最好还是使用插件来搞:插件 — Vue.js

插件通常用来为 Vue 添加全局功能

4)小结

  1. Mixins -> 在组件之间重用代码以共享行为的一种便捷方法! -> 让我们的代码库减少重复、更加模块化
  2. Mixins 的合并策略是怎样的?
    1. 同名钩子,Mixins先行 -> 原理是,数组的第一个元素是Mixins的钩子
    2. 同名 data 旗下的属性 -> 递归之,组件取而代之
    3. 同名选项是对象值 -> 组件取而代之
    4. 不同名,一起过!
  3. 分发:(动)一个个地发给。分发慰问品,如分发奖品,如分发选项给组件实例,全局mixins即主动分发(主动给组件实例,不管你要不要),局部mixins即被动分发(组件实例想要就要,不要就不要
  4. 类比:
    1. directives的作用是减少DOM操作的重复
    2. mixins的作用是减少datamethods、钩子的重复

★Extends 继承、扩展(不推荐使用)

1)概述

这个东西的存在,有着与mixins同样的需求

话说:

你可以使用 Vue.extendoptions.extends

做法,同mixins类似,通过暴露一个extends对象到组件中使用

示例:

// MyVue是一个组件实例哈!
const MyVue = Vue.extend({
  // or mixins: [log]
  data() {
    return {
      name: '',
      time: undefined
    }
  },
  created() {
    if (!this.name) { console.error('no name!') }
    this.time = new Date();
    console.log(`${this.name}出生了`)
  },
  beforeDestroy() {
    const duration = (new Date()) - this.time
    console.log(`${this.name}存活时间${duration}ms`)
  }
})
Vue.component('child1', {
  data() {
    return {
      name: 'child1'
    }
  },
  template: `
  <div class="red">Child1消失</div>
  `,
  extends: MyVue
})
Vue.component('child2', {
  data() {
    return {
      name: 'child2'
    }
  },
  template: `
  <div class="green">Child2消失</div>
  `,
  extends: MyVue
})

new Vue({
  el: '#app',
  data() {
    return {
      Child1isShow: true,
      Child2isShow: true,
    };
  },
})

效果是一样的……

讲真,我完全get不到extends的点!

2)小结

  1. 用法:const Component = Vue.extend({……}) -> new Component(options) or Vue.component('child1', {……,extends:Component}
  2. extends是比mixins更抽象一点的封装
  3. 如果你嫌写五次 mixins 麻烦,可以考虑extends一次! -> 不过实际工作中用得很少

★Provide、Inject(提供和注入)

文档:provide / inject

1)概述

众所周知,在组件式开发中,最大的痛点就在于组件之间的通信。在 Vue 中,Vue 提供了各种各样的组件通信方式,从基础的 props/$emit 到用于兄弟组件通信的 EventBus,再到用于全局数据管理的 Vuex

在这么多的组件通信方式中,provide/inject 显得十分阿卡林(毫无存在感)。但是,其实 provide/inject 也有它们的用武之地

provideinject 主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。

2)是什么?

provide 可以在祖先组件中指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用 inject 来接收 provide 提供的数据或方法。

3)示例1

示例1

话说,如何去掉那个警告呢? -> 用引用类型值,该对象的属性就好了!

4)示例2

示例2

根组件成为了全局状态的容器 -> 很像 React 中的 context

5)示例3

换肤

<!DOCTYPE html>
<html lang="zh-Hans">

<head>
  <meta charset="utf-8">
  <title>index9</title>
  <style>
    .app.theme-lightgreen button {
      background: lightgreen;
      color: white;
    }

    .app.theme-lightgreen {
      color: darkcyan;
    }

    .app.theme-lightcoral button {
      background: lightcoral;
      color: white;
    }

    .app.theme-lightcoral {
      color: darksalmon;
    }

    .app.fontSize-normal {
      font-size: 16px;
    }

    .app button {
      font-size: inherit;
    }

    .app.fontSize-small {
      font-size: 12px;
    }

    .app.fontSize-big {
      font-size: 20px;
    }
  </style>
</head>

<body>
  <div id="xxx" :class="`app theme-${themeName} fontSize-${fontSizeName}`">
    <change-button>换肤</change-button>
    <br>
    <button>🟡</button>
    <button>🟡</button>
    <button>🟡</button>
    <button>🟡</button>
    <button>🟡</button>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script>
    Vue.component('change-button', {
      data() {
        return {}
      },
      template: `
        <div>
          <button @click="changeTheme">换肤</button>
          <button @click="changeFontSize('big')">大字</button>
          <button @click="changeFontSize('small')">小字</button>
          <button @click="changeFontSize('normal')">正常字</button>
        </div>
      `,
      inject: ["themeName", "changeTheme", "changeFontSize"]

    })
    new Vue({
      el: '#xxx',
      provide() {
        return {
          themeName: this.themeName,
          fontSizeName: this.fontSizeName,
          changeTheme: this.changeTheme,
          changeFontSize: this.changeFontSize
        }
      },
      data() {
        return {
          themeName: 'lightcoral', // 'lightgreen'
          fontSizeName: 'normal' // 'big' or 'small'
        }
      },
      methods: {
        changeTheme() {
          if (this.themeName === 'lightcoral') {
            this.themeName = 'lightgreen'
          } else {
            this.themeName = 'lightcoral'
          }
        },
        changeFontSize(size) {
          if (["normal", "big", "small"].indexOf(size) === -1) {
            throw new Error(`no size: ${size}`)
          }
          this.fontSizeName = size
        }
      }
    })


  </script>
</body>

</html>

效果:

效果

6)小结

  1. 慎用 provide/inject -> Vue 官方推荐使用 Vuex,而不是原生的 API
  2. Vuex 和 provide/inject 最大的区别在于:
    1. Vuex 中的全局状态的每次修改是可以追踪回溯
    2. provide/inject 中变量的修改是无法控制的,换句话说,你不知道是哪个组件修改了这个全局状态
  3. Vue 的设计理念借鉴了 React 中的单向数据流原则(虽然有 sync 这种破坏单向数据流的家伙),而 provide/inject 明显破坏了单向数据流原则。试想,如果有多个后代组件同时依赖于一个祖先组件提供的状态,那么只要有一个组件修改了该状态,那么所有组件都会受到影响。这一方面增加了耦合度,另一方面,使得数据变化不可控。如果在多人协作开发中,这将成为一个噩梦。
  4. 两条使用 provide/inject 做全局状态管理的原则:
    1. 多人协作时,做好作用域隔离
    2. 尽量使用一次性数据作为全局状态,即不会二次更改的数据!

★了解更多

➹:Custom Directives — Vue.js

➹:Vue: 自定义指令 - 简书

➹:混入 — Vue.js

➹:Mixins - Next-Level Vue - Vue Mastery

➹:聊聊 Vue 中 provide/inject 的应用 - 掘金

➹:43.Vue全解进阶属性(Directive、Mixin、Extends、Provide、Inject) - 掘金

➹:Vue全解 - 知乎

➹:vue2组件之间双向数据绑定问题 - 前端笔记 - SegmentFault 思否

★总结