vue

Vue

Vue.js——渐进式、响应式、组件化

Posted by page on April 18, 2022

Vue

name

声明name选项的必要性:

  • 可被 $options.name 返回,允许组件模板递归地调用自身
  • 便于调试。更友好的警告信息与 vue-devtools 语义的组件结构
  • router-view 的include/exclude值为有效的组件name

v-model

v-model 能够在表单元素上快速创建数据双向绑定(语法糖),不同表单元素代理对应input、change事件;

如果用于组件(component)上,则默认添加名为 value 的 prop 值传递,以及监听内部 input事件;

自定义model

允许在子元素内对model自定义prop与事件通信;

export default {
    model: {prop: 'userProfile', event: 'submit' },
    props: ['userProfile'],
    methods: {
        onSubmit(form) {
            this.$emit('submit', form);
        }
    }
}

指令修饰符

  • v-model.trim
  • v-model.number
  • v-model.lazy

computed

计算属性(由多个属性值固定计算出的属性值)

get/set控制

set(item) {
  this.data.push(item);
},
get() { return this.data.map(item => item.value) }

v-for/if

v-for同时引用v-if,如:

<div v-if="expression" v-for="item in list">...</div>

这会产生分歧:判断条件后决定是否跳过整个列表循环 or 循环列表时判断条件控制显示的列表项

最终实际结果取决于v-ifv-for的执行优先级,事实上vue确定v-for优先级高于v-if;但依旧会提示不可同时使用的警告信息;

解决

嵌套

<ul v-if="expression"><li v-for="item in list"></li></ul>
<template v-if="expression"><div v-for="item in list"></div></template>
<template v-for="item in list"><li v-if="expression"></li></template>

计算属性

<div v-for="item in filterList"></div>

<script>
export default {
    computed: {
        filterList(){
            if(expression) return [];
            return list.filter(expression);
        }
    }
}
</script>

循环前判断

<div v-for="item in list || []"></div>

attrs/listeners

attrs

父组件传递的但未被子组件prop接受的attr集合,可在内部组件上v-bind="$attrs"实现prop穿透传递;

<template>
    <div><input v-model="value" /></div>
</template>
<script>
export default {
    inheritAttrs: false,
    // inherinheritAttrs默认值true,即父组件传递的但未被子组件prop接受的attr会作为标签属性添加至子组件根元素上;
    // false即这一默认行为失效;配合inheritAttrs: false实现更干净的穿透;
    computed: {
        value(){
            return this.$attrs.value;
        }
    }
}
</script>

listeners

父组件v-on监听的事件集合,可在内部组件上v-on="$listeners"实现穿透监听;

$listeners实现“监听子组件下某原生元素事件特性”

<template>
    <div><input v-bind="$attrs" v-on="$listeners" /></div>
</template>

watch

手动添加监听

// 监听子组件的状态
vm.$watch('$refs.elColorPicker.showPicker', function (newVal, oldVal) { ... })

provide/inject

父组件 provide

// 值为一个对象
provide: {
    title: '标题'
    model: { ... }
}

// 值为返回一个对象的函数
provide() {
    return {
        title: this.title,
        claculate: this.claculate,
        getList: this.list
    }
}

子组件inject

// 注入
inject: ['title', 'model'];
inject: { 
    mainTitle: { from: 'title', default: '' },
    model: { default: () => ({}) },
};

// 注入方法
inject: ['claculate', 'getList'],
computed: {
    list(){ return this.getList() };
}

provide/inject的绑定值不会做响应式处理;可传入对象类型的值,借助property响应;

或传入Function类型的值,借助computed计算属性实现响应式;

事件总线

  1. 创建事件总线

    Vue.property.$EventBus = new Vue()
    
  2. 监听事件总线

    mounted(){
        this.$EventBus.$on('add-todo', this.addTodo);
    },
    beforeDestroy() {
      this.$EventBus.$off('add-todo', this.addTodo);
    }
    
  3. 发射事件总线的事件

    this.$EventBus.$emit('delete-todo', options);
    

动态组件

component(元组件)作为Vue内置组件,通过设置is决定哪个具体组件被渲染

<component
  :is="currentPanelComponent"
  ref="currentPanel"
  v-model="editContent"
  :panelInfo="currentPanel"
  :viewType="0"
></component>

<script>
const panleList = [{id: 'xxx', name: 'xxx', component: panel1}]
export default {
    computed: {
        currentPanel(){
            return panleList.find((item) => item.id === this.id) || {};
        },
        currentPanelComponent(){
            return activatedPanel.component;
        }
    }
}
</script>

keep-alive

配合keep-alive缓存组件状态,避免重复渲染的不必要性能损失

<keep-alive>
  <component :is="currentTabComponent"></component>
</keep-alive>

插槽

实现组件的动态内容(VNode合成组件)

this.$slot插槽内容

<comp>
  <div>defalut</div>
  <div #footer>footer</div>
  // 等同于 v-slot
</comp>

// comp.vue
<div>
  <h2><slot></slot></h2>
  <footer><slot name="footer"></slot></footer>
</div>    

解构传值

<comp>
  <div>defalut</div>
  <div #footer>footer</div>
  // 等同于 v-slot
</comp>

// comp.vue
<div>
  <h2><slot></slot></h2>
  <footer><slot name="footer"></slot></footer>
</div>    

异步组件

组件在被需要时异步加载所需资源并构造,声明一个函数作为异步组件引用

() => import('./my-async-component')
// 返回一个带有更多配置的异步组件
const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})

Component

Vue组件是可复用的Vue实例,Vue.component创建Vue组件(component实例)方法

全局注册

// 定义一个名为 button-counter 的新全局组件
Vue.component('button-counter', {
  // 没有el外,其它选项与Vue实例相同
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me 8 times.</button>'
});

在Vue根实例作为组件被使用

<div id="components-demo">
  <button-counter></button-counter>
</div>
<script>
  new Vue({ el: '#components-demo' })
</script>

局部注册

new Vue({
  el: '#app',
  components: {
    'button-counter': ButtonCounter,
    'component-b': ComponentB
  }
})

自定义指令

自定义指令提供了一系列钩子支持对普通DOM元素进行底层操作,包括bind、inserted、update、componentUpdated、unbind;

// 指令
const burySpot = {};
burySpot.install = (Vue) => {
  // bind 和 update简写
  Vue.directive('bury-spot', (el, binding) => {
    el.dataset.buryspot = binding.value;
    el.addEventListener(binding.arg || 'click', burySpotHandle);
  });
};
function burySpotHandle(e) {
  // 提交埋点数据
  request.post({ url: e.currentTarget.dataset.buryspot });
}
export default burySpot;

// 注册
import Vue from 'vue';
import burySpot from './bury-spot';
Vue.use(burySpot);

// 使用
<button type="button" v-bury-spot="https://wwwi.baidu.com"></button>

参数: el为DOM元素

binding: 
    value: 指令值, 支持任何js表达式, 
    oldValue: update/componentUpdated时可用, 
    arg: 指令参数值, 
    modifiers: 修饰符对象, 属性值为布尔值 

常见场景:埋点、图片默认图/懒加载、权限控制

过渡动画

tupai

css过渡

.v-enter-active,v-leace-active{
  transition: opacity 0.3s;
}
.v-enter, .v-leave-to{
  opacity: 0;
}

css动画

.bounce-enter-active {
  animation: bounce-in .5s;
}
.bounce-leave-active {
  animation: bounce-in .5s reverse;
}
@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.5);
  }
  100% {
    transform: scale(1);
  }
}

多内容过渡

多元素过渡需添加key,多组件过渡使用动态组件;

render函数

template选项的替换项(template编译后在render中被使用,本质上基于render),内部创建虚拟节点;

render: function (createElement) {
  // render内创建一个VNode
  return createElement('h1', this.blogTitle)
}

createElement

相比模板语法,以更接近编译器的形式去建立DOM树

createElement(
  // {String | Object | Function}
  // 一个 HTML 标签名、组件选项对象,或者
  // resolve 了上述任何一种的一个 async 函数。必填项。
  'div',

  // {Object}
  // 一个与模板中 attribute 对应的数据对象。可选。
  {
    // (详情见下一面代码)
  },

  // {String | Array}
  // 子级虚拟节点组成的数组 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '文本节点内容',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)
{
  // 与 `v-bind:class` 的 API 相同,
  // 接受一个字符串、对象或字符串和对象组成的数组
  'class': {
    foo: true,
    bar: false
  },
  // 与 `v-bind:style` 的 API 相同,
  // 接受一个字符串、对象,或对象组成的数组
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 普通的 HTML attribute
  attrs: {
    id: 'foo'
  },
  // 组件 prop
  props: {
    myProp: 'bar'
  },
  // DOM property
  domProps: {
    innerHTML: 'baz'
  },
  // 事件监听器在 `on` 内,
  // 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
  // 需要在处理函数中手动检查 keyCode。
  on: {
    click: this.clickHandler
  },
  // 仅用于组件,用于监听原生事件,而不是组件内部使用
  // `vm.$emit` 触发的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
  // 赋值,因为 Vue 已经自动为你进行了同步。
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 作用域插槽的格式为
  // { name: props => VNode | Array<VNode> }
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
  // 如果组件是其它组件的子组件,需为插槽指定名称
  slot: 'name-of-slot',
  // 其它特殊顶层 property
  key: 'myKey',
  ref: 'myRef',
  // 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
  // 那么 `$refs.myRef` 会变成一个数组。
  refInFor: true
}

拓展:函数式组件

JSX

render函数支持返回jsx(类似模板语法)

import AnchoredHeading from './AnchoredHeading.vue'

new Vue({
  el: '#demo',
  render: function (h) {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})