(二)Vue2.0 高级用法

437 阅读10分钟

一、 实例属性

1、参数

  • vm.$data
  • vm.$props
  • vm.$el
  • vm.$options

2、通讯

  • vm.$parent:父实例
  • vm.$root:根实例,无父实例,会是自己
  • vm.$children:当前实例的直接子组件数组,不保证顺序,非响应式
  • vm.$refs:一个对象,持有注册过 ref attribute的所有 DOM 元素和组件实例,用于读取子组件实例信息。
  • vm.$attrs:包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (classstyle 除外),当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (classstyle 除外),并且可以通过 v-bind="$attrs" 传入内部组件
  • vm.$listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器,它可以通过 v-on="$listeners" 传入内部组件

3、插槽

  • vm.$slots:用来访问插槽分发中内容,适用于具名插槽,用于渲染函数。

    • { [name: string]: ?Array<VNode> }
  • vm.$scopedSlots:用来访问作用域插槽,该对象都包含一个返回相应 VNode 的函数,用于渲染函数。

    • { [name: string]: props => Array<VNode> | undefined }
<blog-post>
  <template v-slot:header>
    <h1>About Me</h1>
  </template>

  <p>Here's some page content, which will be included in vm.$slots.default, because it's not inside a named slot.</p>

  <template v-slot:footer>
    <p>Copyright 2016 Evan You</p>
  </template>

  <p>If I have some content down here, it will also be included in vm.$slots.default.</p>.
</blog-post>

Vue.component('blog-post', {
  render: function (createElement) {
    var header = this.$slots.header
    var body   = this.$slots.default
    var footer = this.$slots.footer
    return createElement('div', [
      createElement('header', header),
      createElement('main', body),
      createElement('footer', footer)
    ])
  }
})

4、服务器渲染

  • vm.$isServer:判断是否运行在服务器,用于服务器渲染。

二、 插槽用法

1、插槽分类

  • 具名插槽:通过name指定插槽名字

    • 组件支持多个插槽,组件内未命名的为默认插槽,被隐式命名为default,其他不同的插槽需要显示命名
    • 插槽使用slot组件,name属性命名插槽,父组件使用v-slot指令
    • v-slot指令只能添加在<template>上,例外为作用域插槽只有默认插槽情况
  • 作用域插槽:通过v-slot指令的值指定作用域变量的名称

    有时让插槽内容(父组件)能够访问子组件中才有的数据,为了让子组件的数据/方法能够在父组件的插槽内容中可用,可以将这些数据/方法作为slot元素的属性绑定上去,使用v-slot指令的值指定作用域变量的名称

<!-- 具名插槽:子组件 -->
<template>
	<div>
    <h2>
      <slot name="header">Title</slot>
  	</h2>
    <slot>default</slot>
  </div>
</template>

<!-- 具名插槽:父组件 -->
<template>
	<componA>
    <template v-slot:header>
      My name is Jian
		</template>
		<!-- The code below goes into the default slot -->
    <img src="./img.jpg">
  </componA>
</template>
<!-- 作用域插槽:子组件 -->
<template>
	<span>
    <slot v-bind:user="user">
      {{user.lastName}}
    </slot>
  </span>
</template>
<!-- 作用域插槽:父组件 -->
<template>
	<current-user>
    <template v-slot:default="slotProps">
      {{slotProps.user.firstName}}
		</template>
  </current-user>
</template>
<!-- 只有默认插槽的缩写,直接放组件上,无需使用template -->
<current-user v-slot="slotProps">
  {{ slotProps.user.firstName }}
</current-user>
<!-- 对象解构, #简写 -->
<current-user #default="{user}">
  {{ user.firstName }}
</current-user>

2、插槽用途

1、组件复用

插槽允许通过html片段或其他组件自定义内容,可以方便的支持自定义内容并提供呢统一的属性

<!-- 按钮封装 -->
<template>
	<button>
    <slot>添加</slot>
  </button>
</template>
<!-- 调用:带图标的按钮 -->
<template>
	<my-button>
		<img src="/img.jpg">
  </my-button>
</template>

<!-- 尾部添加按钮关闭弹框 -->
<template>
	...
	<div class="modal-footer">
    <slot name="footer" :closeModal="closeModal"></slot>
  </div>
</template>
<script>
  export default {
    methods: {
      closeModal(){/* */}
    }
  }
</script>
<!-- 父组件调用 -->
<template #footer="{closeModal}">
	<button @click="closeModal">
    关闭对话框
  </button>
</template>

2、无渲染组件

利用插槽,可以创建无渲染组件,用于业务逻辑和视图的解耦:只提供函数而不渲染自己html模版的组件。数据和事件以及dom元素内容全部由父组件的插槽内容提供。

  • 数据和事件:作用域插槽可以将数据和事件从子组件传递给父组件,这就相当于对外暴露了接口。
  • dom元素:将 HTML 中的 DOM 以及 CSS 交给父组件(调用方)去维护,子组件通过 <slot> 标签插入。
<!-- 两组件,业务逻辑相同,视图不同,利用插槽剥离业务逻辑代码进行复用 -->
<!-- 子组件:业务逻辑代码,开关切换 -->
<template>
	<div class="toggle-container">
    <slot :currrentState="currentState" :setOn="openState" 
          :setOff="closeState" :traggle="toggle"></slot>
  </div>
</template>
<script>
  export default {
    props: {
      state: {
        type: Boolean,
        default: false
      }
    },
    data(){
      return {
        currentState: this.state
      }
    },
    methods: {
      openState(){
        this.currentState = true
      },
      closeState(){
        this.currentState = false
      },
      toggle(){
        this.currentState = !this.currentState
      }
    }
  }
</script>

<!-- 父组件调用:通过作用域插槽调用子组件函数,通过插槽传递dom元素 -->
<template>
	...
  <template v-slot:default="{currentState, setOn, setOff, toggle}">
    <button @click="toggle">切换</button>
    <button @click="setOn">打开</button>
    <button @click="setOff">关闭</button>
    <div v-if="currentState">已打开</div>
    <div v-else>已关闭</div>
  </template>
</template>

<!-- 无渲染组件:使用render去除模版,使用this.$scopedSlots(作用域插槽)属性替代slot组件 -->
<script>
  export default {
    props: {
      state: {
        type: Boolean,
        default: false
      }
    },
    data(){
      return {
        currentState: this.state
      }
    },
    //(createElement: () => VNode) => VNode
    /* render: function (createElement) {
  			return createElement('h1', this.blogTitle)
		}*/
    render(){
      return this.$scopedSlots.default({
        currentState: this.currentState,
        setOn: this.openState,
        setOff: this.closeState,
        toggle: this.toggle
      })
    },
    methods: {
      openState(){
        this.currentState = true
      },
      closeState(){
        this.currentState = false
      },
      toggle(){
        this.currentState = !this.currentState
      }
    }
  }
</script>

3、slot 实现原理

slot本质上是返回VNode的函数,一般情况下,Vue中的组件要渲染到页面上需要经过template -> render function -> VNode -> DOM 过程。

比如一个带slot的组件

Vue.component('button-counter', {
  template: '<div> <slot>我是默认内容</slot></div>'
})

new Vue({
    el: '#app',
    template: '<button-counter><span>我是slot传入内容</span></button-counter>',
    components:{buttonCounter}
})

经过vue编译, 组件渲染函数会变成这样

(function anonymous(
) {
with(this){return _c('div',[_t("default",[_v("我是默认内容")])],2)}
})

而这个_t就是slot渲染函数:

function renderSlot (
  name,
  fallback,
  props,
  bindObject
) {
  // 得到渲染插槽内容的函数    
  var scopedSlotFn = this.$scopedSlots[name];
  var nodes;
  // 如果存在插槽渲染函数,则执行插槽渲染函数,生成nodes节点返回
  // 否则使用默认值
  nodes = scopedSlotFn(props) || fallback;
  return nodes;
}

而scopedSlots其实就是递归解析各个节点, 获取slot

function resolveSlots (
    children,
    context
  ) {
    if (!children || !children.length) {
      return {}
    }
    var slots = {};
    for (var i = 0, l = children.length; i < l; i++) {
      var child = children[i];
      var data = child.data;
      // remove slot attribute if the node is resolved as a Vue slot node
      if (data && data.attrs && data.attrs.slot) {
        delete data.attrs.slot;
      }
      // named slots should only be respected if the vnode was rendered in the
      // same context.
      if ((child.context === context || child.fnContext === context) &&
        data && data.slot != null
      ) {
        // 如果slot存在(slot="header") 则拿对应的值作为key
        var name = data.slot;
        var slot = (slots[name] || (slots[name] = []));
        // 如果是tempalte元素 则把template的children添加进数组中,这也就是为什么你写的template标签并不会渲染成另一个标签到页面
        if (child.tag === 'template') {
          slot.push.apply(slot, child.children || []);
        } else {
          slot.push(child);
        }
      } else {
        // 如果没有就默认是default
        (slots.default || (slots.default = [])).push(child);
      }
    }
    // ignore slots that contains only whitespace
    for (var name$1 in slots) {
      if (slots[name$1].every(isWhitespace)) {
        delete slots[name$1];
      }
    }
    return slots
}

三、 mixin 使用

1、mixin 实现原理

  • 优先递归处理 mixins
  • 先遍历合并 parent 中的key,调用 mergeField 方法进行合并,然后保存在变量 options
  • 再遍历 child,合并补上 parent 中没有的key,调用 mergeField 方法进行合并,保存在变量 options
  • 通过 mergeField 函数进行了合并
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {

if (child.mixins) { // 判断有没有mixin 也就是mixin里面挂mixin的情况 有的话递归进行合并
    for (let i = 0, l = child.mixins.length; i < l; i++) {
    parent = mergeOptions(parent, child.mixins[i], vm)
    }
}

  const options = {} 
  let key
  for (key in parent) {
    mergeField(key) // 先遍历parent的key 调对应的strats[XXX]方法进行合并
  }
  for (key in child) {
    if (!hasOwn(parent, key)) { // 如果parent已经处理过某个key 就不处理了
      mergeField(key) // 处理child中的key 也就parent中没有处理过的key
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key) // 根据不同类型的options调用strats中不同的方法进行合并
  }
  return options
}

2、使用策略

主要的逻辑就是合并mixin和当前组件的各种数据, 细分为四种策略:

  • 替换型策略 - 同名的props、methods、inject、computed会被后来者代替
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  if (!parentVal) return childVal // 如果parentVal没有值,直接返回childVal
  const ret = Object.create(null) // 创建一个第三方对象 ret
  extend(ret, parentVal) // extend方法实际是把parentVal的属性复制到ret中
  if (childVal) extend(ret, childVal) // 把childVal的属性复制到ret中
  return ret
}
  • 合并型策略 - data, 通过set方法进行合并和重新赋值
strats.data = function(parentVal, childVal, vm) {    
    return mergeDataOrFn(
        parentVal, childVal, vm
    )
};

function mergeDataOrFn(parentVal, childVal, vm) {    
    return function mergedInstanceDataFn() {        
        var childData = childVal.call(vm, vm) // 执行data挂的函数得到对象
        var parentData = parentVal.call(vm, vm)        
        if (childData) {            
            return mergeData(childData, parentData) // 将2个对象进行合并                                 
        } else {            
            return parentData // 如果没有childData 直接返回parentData
        }
    }
}

function mergeData(to, from) {    
    if (!from) return to    
    var key, toVal, fromVal;    
    var keys = Object.keys(from);   
    for (var i = 0; i < keys.length; i++) {
        key = keys[i];
        toVal = to[key];
        fromVal = from[key];    
        // 如果不存在这个属性,就重新设置
        if (!to.hasOwnProperty(key)) {
            set(to, key, fromVal);
        }      
        // 存在相同属性,合并对象
        else if (typeof toVal =="object" && typeof fromVal =="object") {
            mergeData(toVal, fromVal);
        }
    }    
    return to
}
  • 队列型策略 - 生命周期函数和watch,原理是将函数存入一个数组,然后正序遍历依次执行
function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}

LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})
  • 叠加型策略 - component、directives、filters,通过原型链进行层层的叠加
strats.components=
strats.directives=

strats.filters = function mergeAssets(
    parentVal, childVal, vm, key
) {    
    var res = Object.create(parentVal || null);    
    if (childVal) { 
        for (var key in childVal) {
            res[key] = childVal[key];
        }   
    } 
    return res
}

四、 全局方法

1、Vue.extend(options)

  • 使用Vue构造器,创建一个子类
  • options为包含组件选项的对象,data特殊为函数

2、Vue.use(plugin)

  • 安装 Vue.js 插件,需在new Vue()之前使用,多个调用只安装一次
  • 如果插件是一个对象,必须提供 install 方法
  • 如果插件是一个函数,它会被作为 install 方法。
  • install 方法调用时,会将 Vue 作为参数传入。
// 注册组件,传入一个扩展过的构造器
Vue.component('my-component', Vue.extend({ /* ... */ }))

// 创建构造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')
什么是插件(plugin)?

简单来说,插件就是指对Vue的功能的增强或补充。

下面如何编辑一个插件:

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) {
    // 逻辑...
  }
}


Vue.use(MyPlugin, options);
Vue.use做了什么?
  • 判断当前插件是否已经安装过, 防止重复安装
  • 处理参数, 调用插件的install方法, 第一个参数是Vue实例.
// Vue源码文件路径:src/core/global-api/use.js

import { toArray } from '../util/index'

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}

3、组件注册component

  • 全局注册:所有组件及子组件都可用
<!--
解析:
w3c标准html中使用小写的kebab-case短横线隔开式;
字符串模板以及单文件组件可使用PascalCase驼峰式,最好统一使用kebab-case短横线隔开式。
-->
//组件名使用 kebab-case短横线隔开式
//使用:<my-component-name></my-component-name>
Vue.component('my-component-name', { /* ... */ })

//组件名使用 PascalCase驼峰式
//使用:<my-component-name></my-component-name>或<MyComponentName>
Vue.component('MyComponentName', { /* ... */ })

<!--
模板字符串(支持空格和缩进、变量输出和函数调用):`This is a ${basket.count}`.
字符串模板:使用字符串生成vue模板
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
-->
// 定义一个名为 button-counter 的新组件,在new Vue()之前。
Vue.component('button-counter', {
  //data是个函数,不为对象
  data: function () {
    return {
      count: 0
    }
  },
  //字符串模板
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

//模块化全局注册
import BetterScroll from './components/BetterScroll'
Vue.component('BetterScroll', BetterScroll)
  • 局部注册:当前组件可使用
var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})

//模块化
import ComponentA from './ComponentA.vue'

export default {
  components: {
    ComponentA //等价于ComponentA:ComponentA
  },
}
  • 应用:基础组件的自动全局注册,多个组件的自动加载
//应用入口文件:src/main.js
//组件目录:./components
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'

const requireComponent = require.context(
  // 其组件目录的相对路径
  './components',
  // 是否查询其子目录
  false,
  // 匹配基础组件文件名的正则表达式
  /Base[A-Z]\w+.(vue|js)$/
)

requireComponent.keys().forEach(fileName => {
  // 获取组件配置
  const componentConfig = requireComponent(fileName)

  // 获取组件的 PascalCase 命名
  const componentName = upperFirst(
    camelCase(
      // 获取和目录深度无关的文件名
      fileName
        .split('/')
        .pop()
        .replace(/.\w+$/, '')
    )
  )

  // 全局注册组件
  Vue.component(
    componentName,
    // 如果这个组件选项是通过 `export default` 导出的,
    // 那么就会优先使用 `.default`,
    // 否则回退到使用模块的根。
    componentConfig.default || componentConfig
  )
})

/* 解析:
1、webpack的api:require.context函数获取特定上下文
require.context(directory,useSubdirectories,regExp))
(1)接收三个参数:
directory {String} -读取文件的路径
useSubdirectories {Boolean} -是否遍历文件的子目录
regExp {RegExp} -匹配文件的正则
(2)返回一个函数,且有如下3个属性
function webpackContext(req) {return __webpack_require__(webpackContextResolve(req))};
resolve {Function} -接受一个参数request,request为匹配文件的相对路径,返回这个匹配文件相对于整个工程的相对路径
keys {Function} -返回匹配成功模块的名字组成的数组
id {String} -执行环境的id,返回的是一个字符串,主要用在module.hot.accept */

五、组件间通信

1、props和$emit

父子通讯

  • props:父组件单向数据流向子组件
  • $emit:子组件通过事件流向父组件
<!-- 父组件 -->
<template>
  <div>
    <compB :title="value" @more="onMore"/>
  </div>
</template>
<script>
  import compB from '/compB'
  export default{
    name: 'compA',
    data(){
      return {
        value: ""
      }
    },
    methods:{
      onMore(value){
        console.log('value', value)
      }
    }
  }
</script>

<!-- 子组件 -->
<template>
	 <div>
     <div>{{title}}</div>
     <button @click="handleMore">查看更多</button>
  </div>
</template>
<script>
  export default{
    name:'compB',
    props:{
      title:{
        type: String,
        default: 'Jian'
      }
    },
    methods:{
      handleMore(){
        this.$emit('more', 'message to father')
      }
    }
  }
</script>

2、$parent/$children

父子通讯

  • $parent 可以用来从一个子组件访问父组件的实例,提供了一种随时访问父级组件的机会,可以替代将数据以props的方式传入子组件的方式
  • $children 可以遍历当前组件的全部子组件,$children 并不保证顺序,也不是响应式的。vue3 中移除了实例的$children属性,推荐使用$refs来访问子组件的实例
<!-- 父组件 -->
<template>
  <child-comp></child-comp>
</template>
<script>
  import childComp from './child'
  export default {
    name: 'parentComp',
    data(){
      return {
        parentMsg: 'father component'
      }
    },
    components: {
      childComp
    },
    mounted(){
      //取子组件的属性值
      console.log(this.$children[0].childMsg)
    }
  }
</script>

<!-- 子组件 -->
<template>
	<span>{{magTxt}}</span>
</template>
<script>
  export default {
    name: 'childComp',
    data(){
      return {
        msgTxt: ''
        childMsg: 'child component'
      }
    },
    created(){
      //取父组件的data属性值
      this.msgTxt = this.$parent.parentMsg
    }
  }
</script>

3、$root /$refs

父子通讯

  • $root:每个new Vue实例的子组件中都有$root属性,可以通过$root属性访问根实例,若当前组件没有父组件实例,则$root为自己
  • $refs:可以通过ref为子组件赋予一个id引用,从而实现在js里直接访问一个子组件。$refs返回一个对象,包括注册过ref的所有dom元素和组件实例,用于父组件访问子组件。
new Vue({
	data:{
		foo: 1
	},
  computed: {
		bar: function(){/* */}
  },
  methods: {
		baz: function(){/* */}
  }
})

//$root使用实例
console.log(this.$root.foo)
console.log(this.$root.bar)
console.log(this.$root.baz())
this.$root.foo = 2

//$refs使用: 父组件
<template>
  <div>
  	<my-component ref="childrenCompA"></my-component>
		<my-component ref="childrenCompB"></my-component>
  </div>
</template>
<script>
    export defalut {
      methods: {
        getMsg(){
          return this.$refs.childrenCompA.msg + this.$refs.childrenCompB.msg
        }
      }
    }
</script>

4、provide/inject

隔代通讯:祖先组件通过provide来提供变量,然后在子孙组件中通过inject来注入变量。provide/inject主要解决了跨级组件间的通信问题,不过主要使用场景为子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

  • provide选项是一个对象或者返回一个对象的函数,该对象包含可注入其子孙的属性。
  • inject选项是一个字符串数组或一个对象
  • provide和inject绑定不是可响应式的,但传入的可监听对象的property仍然可响应
<!-- 父组件 -->
<template>
	<com-a></com-a>
</template>
<script>
  import ComA from './a'
  export default {
    name: 'home',
    components: {
      ComA
    },
    /*
    provide:{
    	'a': 'Hello',
      'show': val => !val
    }
    */
    provide(){
      return {
        'a': 'Hello',
        'show': val => !val
      }
    }
  }
</script>

<!-- 子组件 -->
<template>
	<div>
    <button @click="showFn">{{a}}</button>
  </div>
</template>
<script>
  import ComA from './a'
  export default {
    /*
    inject:{
    	a_child: 'a',
      show_child: 'show'
    }
    */
    inject:['a', 'show'],
    methods:{
      showFn(){
        this.show('xxx')
      }
    }
  }
</script>

5、$attrs/$listeners

隔代通讯

  • $attrs:存放的是父组件中绑定的非Props属性

    • 包含了父作用域中不被prop所识别(且获取)的特性绑定(class和style除外)
    • 当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定(class和style除外)
    • 可以通过v-bind=“$attrs”传入子组件,通常配合inheritAttrs选项一起使用
  • $listeners:存放的是父组件中绑定的非原生事件,vue3移除

    • 包含了父作用域中的(不含.native修饰器的)v-on事件监听器
    • 可以通过v-on=“$listeners”传入子组件
<!-- 父组件 -->
<template>
  <div>
  	<child-a :name="name" :age="age" :job="job" 
             title="this is a title" @click="console.log("hello")"></child-a>
  </div>
</template>
<script>
  import ChildA from './ChildA'
  export default {
    components:{
      ChildA
    },
    data(){
      return {
        name: 'tao',
        age: "28",
        job: "worker"
      }
    }
  }
</script>

<!-- 子组件 -->
<template>
	<div>
    <child-b v-bind="$attrs" v-on="$listeners"></child-b>
  </div>
</template>
<script>
  import ChildB from "./ChildB"
  export default{
    name: "child-a",
    components: {
      ChildB
    },
    created(){
      console.log(this.$attrs) //{name: "tao", age: "28", job: "worker", title: "this is title"}
      this.$listeners.click() //Hello
    }
  }
</script>

<!-- 孙组件 -->
<template>
	<div>
    <p>B-listeners: {{this.$listeners.click()}}</p>
  </div>
</template>
<script>
  export default {
    props: ["name"], //name作为props属性绑定
    created(){
      console.log(this.$attrs) //{age: "28", job: "worker", title: "this is title"}
      this.$listeners.click()  //hello
    }
  }
</script>

6、bus/js对象

组件间通讯

  • 事件总线EventBus

    • 通过一个空的vue实例作为中央事件总线,用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级通信。注意销毁自定义事件,避免内存泄露。
    • vue3中移除了$on$off$once三个vue实例方法,无法使用中央事件总线,可以通过使用外部库来替换。
  • 局部js对象

    • 利用esmodule引用特性,声明一个包含属性和方法的对象,在多组件共享,适用于功能复杂且与模块无关的模块开发场景
    • 对象中的数据为非响应式的
//事件总线
let Bus = new Vue();

//componentA
Bus.$emit('add-todo', {text: this.newTodoText})

//componentB
Bus.$on('add-todo', this.addTodo)
Bus.$off('add-todo', this.addTodo) //移除事件

//局部js对象
//shareState.js
const store = {
  state: {
    user: []
  },
  setUsers(users){
    this.state.users = users
  }
}
export default store
//componentA
import shareState from '@/shareState'
export default {
  data(){
    return {
      sharedState: shareState.state
    }
  },
  methods:{
    todo(value){
      shareState.setUsers(value)
    }
  }
}

7、Vuex

组件间通讯

8、缓存

localStorage, sessionStorage

六、 自定义指令

1、指令添加

  • 全局添加:Vue.directive(id, [definition])

    • 注册或获取全局指令
    • {string} id
    • {Function | Object} [definition]:指令对象,有5个可选钩子
  • 局部添加:组件内directives属性

//全局指令
Vue.directive('focus', {
  inserted:function(el){
    el.focus()
  }
})
//局部指令
export default {
  directives: {
    focus: {
      inserted: function(el){
        el.focus()
      }
    }
  }
}
//使用
<input v-focus>

2、钩子函数

//vue2全量钩子函数
{
  bind(){},       
  inserted(){},
  update(){},
  compotentUpdated(){},
  unbind(){}
}

//注意:vue3中钩子api进行了调整,与生命周期靠拢,无法兼容
{
  beforeMount(){}, //bind
  mounted(){},    //inserted
  beforeUpdate(){},
  undated(){}, //componentUpdated
  beforeUnmount(){},
  unmounted(){} //unbind
}

//简写,只想在bind和update时触发相同行为,不关心其他的钩子
Vue.directive('focus', function(el, binding){
  el.style.backgroundColor = binding.value
})
  • bind:只调用一次,指令第一次绑定到元素时调用,在这里可以进行一次性初始化操作。
  • inserted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)
  • update:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新后的值来忽略不必要的模版更新。
  • compotentUpdated:指令所在组件的VNode以及子VNode全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

3、钩子函数入参

  • el:指令绑定的元素,可以直接用来操作dom

  • binding:一个对象,包含如下property:

    • name:指令名,不包含v-前缀
    • value:指令的绑定值,eg:v-xx=”1+2“,value=3
    • oldValue:指令绑定的前一个值,仅在update和componentUpdated钩子中可用,无论值是否改变。
    • expression:字符串形式的指令表达式,eg:v-xx=”1+2“,expression="1+2"
    • arg:传给指令的参数,eg:v-xx:foo,arg=“foo”
    • modifiers:一个包含修饰符的对象,eg:v-xx.foo.bar中,修饰符对象为{foo:true, bar:true}
  • vnode:vue编译生成的虚拟节点

  • oldVnode:上一个虚拟节点,仅在update和componentUpdated钩子中可用

4、指令传参

  • 动态指令参数:通过[]传入动态参数
  • 对象字面量:传入一个 JavaScript 对象字面量
<div v-demo="{color: 'white', text: 'hello'}"></div>
Vue.directive('demo', function(el, binding){
	console.log(binding.value.color) //"white"
	console.log(binding.value.text)  //"hello"
})

<div id="app">
  <p v-pin:[direction]="200">text</p>
</div>
Vue.direction('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: '#app',
  data: function(){
		return {
			direction: 'left'
    }
  }
})

5、典例

//按钮防重自定义指令
Vue.directive('preventClick', {
  inserted(el, binding){
    el.addEventListener('click',()=>{
      if(!el.disabled){
        el.disabled = true
        el.style.cursor = 'not-allowed'
        setTimeout(()=>{
          el.disabled = false
          el.style.cursor = 'pointer'
        }, binding.value || 2000)
      }
    })
  }
})

<el-button v-preventClick="3000">click</el-button>

七、vue修饰符

1、表单修饰符

  • v-model.lazy:改变输入框的值时value不会改变,当光标离开输入框时,v-model绑定的值value才会改变

  • v-model.trim:类似于trim(),把v-model绑定的值的首尾空格过滤

  • v-model.number:将值转换为数字

    • 先输入数字的话,只取前面数字部分
    • 先输入字母的话,number修饰符无效
  • v-bind.sync:对一个prop进行双向绑定的简写,父组件通过绑定属性的方式向子组件传值,而在子组件中可以通过$emit向父组件通信,通过这种间接的方式改变父组件的data,从而实现子组件改变props的值

    • 子组件传递的事件名必须是update:value,其中value必须与子组件中的props中声明的名称完全一致
    • 带有.sync修饰符的v-bind不能和表达式一起使用(可使用计算属性),必须使用要绑定的属性名
<!-- 父组件 -->
<child-com :age="age" @setAge="res => {age = res}"></child-com>
<!-- 子组件 -->
this.$emit('setAge', 18)

<!-- 等价写法 -->
<child-com :age.sync="age"></child-com>
this.$emit('update:age', 18)

2、事件修饰符

  • v-on.stop:阻止事件冒泡,event.stopPropagation()方法

  • v-on.prevent:阻止原生事件,event.preventDefault()方法

  • v-on.self:点击事件绑定的本身才会触发事件

  • v-on.capture:反向冒泡,事件默认是由里往外冒泡,使用.capture修饰符时候,事件触发由外往内捕获

  • v-on.passive:当监听元素的滚动事件的时候,会一直触发onscroll事件,在移动端页面会卡顿,使用.passive会直接执行默认行为,不等onscroll事件完成。

    • 常用于监听scoll、touchmove事件使用
    • 每次事件产生,浏览器都会去查询一下是否有preventDefault阻止该次事件的默认动作,通过passive将内核线程查询跳过,可以大大提升滑动的流畅度
    • passive和prevent冲突,不能同时绑定在一个监听器上
  • .native:在某个组件的根元素上监听一个原生事件。一般将事件绑定用在html原生标签**,**在组件标签上使用的时候,就要加上native修饰符,这样就可以像原生标签一样使用事件绑定

<!-- 执行顺序:1,2,4,3 -->
<div @click.capture="shout(1)">
  obj1
  <div @click.capture="shout(2)">
    obj2
  	<div @click="shout(3)">
      obj3
  		<div @click="shout(4)">obj4</div>
    </div>
  </div>
</div>

<!-- 滚动事件的默认行为(即滚动行为)将会立即触发,不会等待onScroll完成 -->
<!-- 其中包含event.preventDefault()情况 -->
<div v-on:scroll.passive="onScroll">滑动</div>

<!-- myself-button 可绑定click事件,类似button -->
<myself-button @click.native="add('组件标签,包含native的点击')" /></myself-button>      

3、鼠标按键修饰符

  • @click.left:鼠标左键
  • @click.rigth:鼠标右键
  • @click.midele:鼠标中键

4、键值修饰符

  • @keydown/@keyup:键盘事件监听

    • 按键码:.enter/.tab/.delete/.space等
    • 系统修饰键:.ctrl/.alt/.shift/.meta
    • .exact:控制精确的系统修饰符组合触发的事件
<button @keyup.enter="submit">key为Enter时触发</button>
<div @click.ctrl="submit">ctrl + click时触发</div>
<button @click.ctrl.exact="submint">有且只有ctrl被按下时触发</button>
<button @click.exact="submint">没有任何系统修饰符被按下时触发</button>

八、特殊组件

1、动态组件

  • component组件配合is属性进行组件动态切换

    • 属性is:可以为组件名字或组件对象
  • 使用keep-alive组件动态组件:用于保留组件状态或避免重新渲染

    • include:字符传或正则,名称匹配的组件被缓存
    • exclude:字符传或正则,名称匹配的组件不被缓存
    • max:缓存最大组件数目
<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component v-bind:is="currentTabComponent"></component>
import compA from './compA'
import compB from './compB'
export default {
	data(){
		return {
			currentTabComponent: compA  			//组件对象
			//currentTabComponent: 'tab-home' //组件名字
		}
  },
  components: {
		"tab-home": {
        template: "<div>Home component</div>"
     }
  }
}

<!-- 基本 -->
<keep-alive>
  <component :is="view"></component>
</keep-alive>

<!-- 多个条件判断的子组件 -->
<!-- if值变化时,comp-a和comp-b不会重新渲染,执行全部生命周期 -->
<keep-alive>
  <comp-a v-if="a > 1"></comp-a>
  <comp-b v-else></comp-b>
</keep-alive>

2、异步组件

组件拆分,异步加载使用:以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义,Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。

/* 全局定义异步组件 */
Vue.component(
  'async-webpack-example',
  // 这个动态导入会返回一个 `Promise` 对象。
  () => import('./my-async-component')
)

/* 局部定义异步组件  */
new Vue({
  // ...
  components: {
    'my-component': () => import('./my-async-component')
  }
})

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

3、函数式组件

无状态 (没有响应式数据),也没有实例 (没有 this 上下文),接受props数据。

Vue.component('my-component', {
  functional: true,
  // Props 是可选的
  props: {
    // ...
  },
  // `render` 函数返回虚拟节点使它们渲染的代价更小
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文
  render: function (createElement, context) {
    // ...
  }
})