Vue知识总结(1)

image.png

Vue是一款高度封装的开箱即用的一栈式的前端框架,既可以结合webpack进行编译式前端开发,也适用基于gulp、grunt等自动化工具直接挂载至全局window使用。本文成文于Vue2.4.x版本发布之初,笔者生产环境当前使用的最新版本为2.5.2。在经历多个前端重度交互项目的开发实践之后,笔者结合官方文档对Vue技术栈进行了全面的梳理、归纳和注解,因此本文可以作为Vue2官方tutorial的补充性读物。建议暂不具备Vue2开发经验的同学,完成官方tutorial的学习之后再行阅读本文。

Vue2.2.x之后,Vue框架及其技术栈功能日趋完善,Vue更加贴近W3C技术规范(例如实现仍处于W3C草案阶段的<template><slot>is等新特性,提供了良好易用的模板书写环境),并且技术栈和开源生态更加完整和易于配置,将React中大量需要手动编码处理的位置,整合成最佳实践并抽象为简单的语法糖(比如Vuex中提供的store的模块化特性),让开发人员始终将精力聚焦于业务逻辑本身。

Vue2的API结构相比Angular2更加简洁,可以自由的结合TypeScript或是ECMAScript6使用,并不特定于具体的预处理语言去获得最佳使用体验,框架本身的特性也并不强制依赖于各类炫酷的语法糖。Vue2总体是一款非常轻量的技术栈,设计实现上紧随W3C技术规范,着力于处理HTML模板组件化事件和数据的作用域分离多层级组件通信三个单页面前端开发当中的重点问题。本文在行文过程中,穿插描述了Angular、React等前端框架的异同与比较,供徘徊于各类前端技术选型的开发人员参考。

Vue与Angular的比较

组件化

Angular的设计思想照搬了Java Web开发当中MVC分层的概念,通过Controller切割并控制页面作用域,然后通过Service来实现复用,是一种对页面进行纵向分层的解耦思想。而Vue允许开发人员将页面抽象为若干独立的组件,即将页面DOM结构进行横向切割,通过组件的拼装来完成功能的复用、作用域控制。每个组件只提供props作为单一接口,并采用Vuex进行state tree的管理,从而便捷的实现组件间状态的通信与同步。

Angular在1.6.x版本开始提供component()方法和Component Router来提供组件化开发的体验,但是依然需要依赖于controllerservice的划分,实质上依然没有摆脱MVC纵向分层思想的桎梏。

双向绑定与响应式绑定

Vue遍历data对象上的所有属性,并通过原生Object.defineProperty()方法将这些属性转换为getter/setter只支持IE9及以上浏览器)。Vue内部通过这些getter/setter追踪依赖,在属性被修改时触发相应变化,从而完成模型到视图的双向绑定。每个Vue组件实例化时,都会自动调用$watch()遍历自身的data属性,并将其记录为依赖项,当这些依赖项的setter被触发时会通知watcher重新计算新值,然后触发Vue组件的render()函数重新渲染组件。

image.png

与Aangular双向数据绑定不同,Vue组件不能检测到实例化后data属性的添加、删除,因为Vue组件在实例化时才会对属性执行getter/setter处理,所以data对象上的属性必须在实例化之前存在,Vue才能够正确的进行转换。因而,Vue提供的并非真正意义上的双向绑定,更准确的描述应该是单向绑定,响应式更新,而Angular即可以通过$scope影响view上的数据绑定,也可以通过视图层操作$scope上的对象属性,属于真正意义上的视图与模型的双向绑定

var vm = new Vue({
  data:{
    a:1
  }
})
vm.a = 1  // 响应的
vm.b = 2 // 非响应的

因此,Vue不允许在已经实例化的组件上添加新的动态根级响应属性(即直接挂载在data下的属性),但是可以使用Vue.set(object, key, value)方法添加响应式属性。

Vue.set(vm.someObject, "b", 2)

// vm.$set()实例方法是Vue.set()全局方法的别名
this.$set(this.someObject, "b",2)

// 使用Object.assign()或_.extend()也可以添加响应式属性,但是需要创建
// 同时包含原属性、新属性的对象,从而有效触发watch()方法
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

Vue对DOM的更新是异步的,观察到数据变化后Vue将开启一个队列,缓冲在同一事件循环(Vue的event loop被称为tick* [tɪk] n.标记,记号*)中发生的所有数据变化。如果同一个watcher被多次触发,只会向这个队列中推入一次。

Vue内部会通过原生JavaScript的Promise.thenMutationObserversetTimeout(fn, 0)来执行异步队列当中的watcher。

在需要人为操作DOM的场景下,为了在Vue响应数据变化之后再更新DOM,可以手动调用Vue.nextTick(callback),并将DOM操作逻辑放置在callback回调函数中,从而确保响应式更新完成之后再进行DOM操作。

<div id="example">{{message}}</div>

<script>
// 使用Vue实例上的.$nextTick()
var vue = new Vue({
  el: "#example",
  data: {
    message: "123"
  }
})
vue.message = "new message" // 更改数据
vue.$el.textContent === "new message" // false
vue.nextTick(function () {
  vm.$el.textContent === "new message" // true
})
</script>

<script>
// 组件内使用vm.$nextTick(),不需要通过全局Vue,且回调函数中this自动指向当前Vue实例
Vue.component("example", {
  template: "<span>{{ message }}</span>",
  data: function () {
    return {
      message: "没有更新"
    }
  },
  methods: {
    updateMessage: function () {
      this.message = "更新完成"
      console.log(this.$el.textContent) // 没有更新
      this.$nextTick(function () {
        console.log(this.$el.textContent) // 更新完成
      })
    }
  }
})
</script>

虚拟DOM

Vritual DOM这个概念最先由React引入,是一种DOM对象差异化比较方案,即将DOM对象抽象成为Vritual DOM对象(即render()函数渲染的结果),然后通过差异算法对Vritual DOM进行对比并返回差异,最后通过一个补丁算法将返回的差异对象应用在真实DOM结点。

Vue当中的Virtual DOM对象被称为VNodetemplate当中的内容会被编译为render()函数,而render()函数接收一个createElement()函数,并最终返回一个VNode对象),补丁算法来自于另外一个开源项目snabbdom,即将真实的DOM操作映射成对虚拟DOM的操作,通过减少对真实DOM的操作次数来提升性能。

➜  vdom git:(dev) tree
├── create-component.js
├── create-element.js
├── create-functional-component.js
├── helpers
│   ├── extract-props.js
│   ├── get-first-component-child.js
│   ├── index.js
│   ├── is-async-placeholder.js
│   ├── merge-hook.js
│   ├── normalize-children.js
│   ├── resolve-async-component.js
│   └── update-listeners.js
├── modules
│   ├── directives.js
│   ├── index.js
│   └── ref.js
├── patch.js
└── vnode.js

VNode的设计出发点与Angular的$digest循环类似,都是通过减少对真实DOM的操作次数来提升性能,但是Vue的实现更加轻量化,摒弃了Angular为了实现双向绑定而提供的$apply()$eval()封装函数,有选择性的实现Angular中$compile()$watch()类似的功能。

Vue对象的选项

通过向构造函数new Vue()传入一个option对象去创建一个Vue实例。

var vm = new Vue({
  // 数据
  data: "声明需要响应式绑定的数据对象",
  props: "接收来自父组件的数据",
  propsData: "创建实例时手动传递props,方便测试props",
  computed: "计算属性",
  methods: "定义可以通过vm对象访问的方法",
  watch: "Vue实例化时会调用$watch()方法遍历watch对象的每个属性",
  // DOM
  el: "将页面上已存在的DOM元素作为Vue实例的挂载目标",
  template: "可以替换挂载元素的字符串模板",
  render: "渲染函数,字符串模板的替代方案",
  renderError: "仅用于开发环境,在render()出现错误时,提供另外的渲染输出",
  // 生命周期钩子
  beforeCreate: "在Vue实例初始化之后,data observer和event/watcher事件被配置之前",
  created: "发生在Vue实例初始化以及data observer和event/watcher事件被配置之后",
  beforeMount: "挂载开始之前被调用,此时render()首次被调用",
  mounted: "el被新建的vm.$el替换,并挂载到实例上之后调用",
  beforeUpdate: "数据更新前调用,在虚拟DOM重新渲染和打补丁之前触发",
  updated: "数据更改导致虚拟DOM重新渲染和打补丁之后被调用",
  activated: "keep-alive组件激活时调用",
  deactivated: "keep-alive组件停用时调用",
  beforeDestroy: "实例销毁之前调用,Vue实例依然可用",
  destroyed: "Vue实例销毁后调用,事件监听和子实例全部被移除,释放系统资源",
  // 资源
  directives: "包含Vue实例可用指令的哈希表",
  filters: "包含Vue实例可用过滤器的哈希表",
  components: "包含Vue实例可用组件的哈希表",
  // 组合
  parent: "指定当前实例的父实例,子实例用this.$parent访问父实例,父实例通过$children数组访问子实例",
  mixins: "将属性混入Vue实例对象,并在Vue自身实例对象的属性被调用之前得到执行",
  extends: "用于声明继承另一个组件,从而无需使用Vue.extend,便于扩展单文件组件",
  provide&inject: "2个属性需要一起使用,用来向所有子组件注入依赖,类似于React的Context",
  
  // 其它
  name: "允许组件递归调用自身,便于调试时显示更加友好的警告信息",
  delimiters: "改变模板字符串的风格,默认为{{}}",
  functional: "让组件无状态(没有data)和无实例(没有this上下文)",
  model: "允许自定义组件使用v-model时定制prop和event",
  inheritAttrs: "默认情况下,父作用域的非props属性绑定会应用在子组件的根元素上。
        当编写嵌套有其它组件或元素的组件时,可以将该属性设置为false关闭这些默认行为",
  comments: "设为true时会保留并且渲染模板中的HTML注释"
});

Vue实例通常使用 vm 变量(View Model)来命名。

属性计算computed

在HTML模板表达式中放置太多业务逻辑,会让模板过重且难以维护。因此,可以考虑将模板中比较复杂的表达式拆分到computed属性当中进行计算。

<!-- 不使用计算属性 -->
<div id="example">
  {{ message.split("").reverse().join("") }}
</div>

<!-- 将表达式抽象到计算属性 -->
<div id="example">
  <p>Original message: "{{ message }}"</p>
  <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>

<script>
  var vm = new Vue({
    el: "#example",
    data: {
      message: "Hello"
    },
    computed: {
      reversedMessage: function () {
        return this.message.split("").reverse().join("")
      }
    }
  })
</script>

计算属性只在相关依赖发生改变时才会重新求值,这意味只要上面例子中的message没有发生改变,多次访问reversedMessage计算属性总会返回之前的计算结果,而不必再次执行函数,这是computed和method的一个重要区别。

计算属性默认只拥有getter方法,但是可以自定义一个setter方法。

<script>
... ... ...
computed: {
  fullName: {
    // getter
    get: function () {
      return this.firstName + " " + this.lastName
    },
    // setter
    set: function (newValue) {
      var names = newValue.split(" ")
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}
... ... ...
// 下面语句触发setter方法,firstName和lastName也会被相应更新
vm.fullName = "John Doe"
</script>

观察者属性watch

通过watch属性可以手动观察Vue实例上的数据变动,当然也可以调用实例上的vm.$watch达到相同的目的。

<div id="watch-example">
  <p>Ask a yes/no question: <input v-model="question"></p>
  <p>{{ answer }}</p>
</div>

<script>
  var watchExampleVM = new Vue({
    el: "#watch-example",
    data: {
      question: "",
      answer: "I cannot give you an answer until you ask a question!"
    },
    watch: {
      // 如果question发生改变,该函数就会运行
      question: function (newQuestion) {
        this.answer = "Waiting for you to stop typing..."
        this.getAnswer()
      }
    },
    methods: {
      // _.debounce是lodash当中限制操作频率的函数
      getAnswer: _.debounce(
        function () {
          if (this.question.indexOf("?") === -1) {
            this.answer = "Questions usually contain a question mark. ;-)"
            return
          }
          this.answer = "Thinking..."
          var vm = this
          axios.get("https://yesno.wtf/api")
            .then(function (response) {
              vm.answer = _.capitalize(response.data.answer)
            })
            .catch(function (error) {
              vm.answer = "Error! Could not reach the API. " + error
            })
        },
        // 这是用户停止输入等待的毫秒数
        500
      )
    }
  })
</script>

使用watch属性的灵活性在于,当监测到数据变化的时候,可以做一些设置中间状态之类的过渡处理。

生命周期

每个Vue实例在创建时,都需要经过一系列初始化过程(设置数据监听、编译模板、挂载实例到DOM、在数据变化时更新DOM),并在同时运行一些钩子函数,让开发人员能够在特定生命周期内执行自己的代码。

image.png

不要在Vue实例的属性和回调上使用箭头函数,比如created: () => console.log(this.a)vm.$watch("a", newValue => this.myMethod())。因为箭头函数的this与父级上下文绑定,并不指向Vue实例本身,所以前面代码中的this.athis.myMethod将会是undefined

通过jQuery对DOM进行的操作可以放置在Mounted属性上进行,即当Vue组件已经完成在DOM上挂载的时候。

数据绑定

Vue视图层通过Mustache["mʌstæʃ]语法与Vue实例中的data属性进行响应式绑定,但是也可以通过内置指令v-once完成一个单向的绑定,再或者通过v-html指令将绑定的字符串输出为HTML,虽然这样很容易招受XSS攻击。

<span>Message: {{ result }}</span>
<span v-once>一次性绑定: {{ msg }}</span>
<div v-html="rawHtml"></div>

Mustache不能用于HTML属性,此时需要借助于v-bind指令。

<div v-bind:id="dynamicId"></div>
<button v-bind:disabled="isButtonDisabled">Button</button>

绑定HTML的class和style

直接操作classstyle属性是前端开发当中的常见需求,Vue通过v-bind:classv-bind:style指令有针对性的对这两种操作进行了增强。

v-bind:class

绑定HTML的class属性。

<!-- Vue对象中的data -->
<script>
  ... ...
  data: {
    isActive: true,
    hasError: false,
    classObject: {
      active: true,
      "text-danger": false
    }
  }
  ... ...
</script>

<!-- 直接绑定class到一个对象 -->
<div v-bind:class="classObject"></div>

<!-- 直接绑定class到对象的属性 -->
<div class="static" v-bind:class="{ active: isActive, 
                                text-danger: hasError }"></div>

<!-- 渲染结果 -->
<div class="static active"></div>

可以传递一个数组给v-bind:class从而同时设置多个class属性。

<!-- Vue对象中的data -->
<script>
  ... ...
  data: {
    activeClass: "active",
    errorClass: "text-danger"
  }
  ... ...
</script>

<!-- 绑定class到计算属性 -->
<div v-bind:class="[activeClass, errorClass]"></div>

<!-- 渲染结果 -->
<div class="active text-danger"></div>

<!-- 使用三目运算符,始终添加errorClass,只在isActive为true时添加activeClass -->
<div v-bind:class="[isActive ? activeClass : "", errorClass]"></div>

<!-- 在数组中使用对象可以避免三目运算符的繁琐 -->
<div v-bind:class="[{ active: isActive }, errorClass]"></div>

当在自定义组件上使用class属性时,这些属性将会被添加到该组件的根元素上面,这一特性同样适用于v-bind:class

<!-- 声明一个组件 -->
<script>
  Vue.component("my-component", {
    template: "<p class="foo bar">Hi</p>",
    data: {
      isActive: true
    },
  })
</script>

<!-- 添加2个class属性 -->
<my-component class="baz boo"></my-component>

<!-- 渲染结果 -->
<p class="foo bar baz boo">Hi</p>

<!-- 使用v-bind:class -->
<my-component v-bind:class="{ active: isActive }"></my-component>

<!-- 渲染结果 -->
<p class="foo bar active">Hi</p>

v-bind:style

绑定HTML的style属性。

<script>
  ... ...
  data: {
    styleObject: {
      color: "red",
      fontSize: "13px"
    },
    styleHeight: {
      height: 10rem;
    }
    styleWidth: {
      width: 20rem;
    }
  }
  ... ...
</script>

<div v-bind:style="styleObject"></div>

<!-- 使用数组可以将多个样式合并到一个HTML元素上面 -->
<div v-bind:style="[styleHeight, styleWidth]"></div>

使用v-bind:style时Vue会自动添加prefix前缀,常见的prefix前缀如下:

  • -webkit- Chrome、Safari、新版Opera、所有iOS浏览器(包括iOS版Firefox),几乎所有WebKit内核浏览器。
  • -moz- 针对Firefox浏览器。
  • -o- 未使用WebKit内核的老版本Opera。
  • -ms- 微软的IE以及Edge浏览器。

使用JavaScript表达式

Vue对于所有数据绑定都提供了JavaScript表达式支持,但是每个绑定只能使用1个表达式。

<span>{{ number + 1 }}</span>
<button>{{ ok ? "YES" : "NO" }}</button>
<p>{{ message.split("").reverse().join("") }}</p>
<div v-bind:id=""list-" + id"></div>

<!-- 这是语句,不是表达式 -->
{{ var a = 1 }}

<!-- if流程控制属于多个表达式,因此不会生效,但可以使用三元表达式 -->
{{ if (ok) { return message } }}

v-model双向数据绑定

v-model指令实质上是v-onv-bind的糖衣语法,该指令会接收一个value属性,存在新值时则触发一个input事件

<!-- 使用v-model的版本 -->
<input v-model="something">
<!-- 使用v-on和v-bind的版本 -->
<input v-bind:value="something"
       v-on:input="something = $event.target.value">
<!-- 也可以自定义输入域的响应式绑定 -->
<custom-input
  v-bind:value="something"
  v-on:input="something = arguments[0]">
</custom-input>

单选框、复选框一类的输入域将value属性作为了其它用途,因此可以通过组件的model选项来避免冲突:

内置指令

带有v-前缀,当表达式值发生变化时,会响应式的将影响作用于DOM。指令可以接收后面以:表示的参数被指令内部的arg属性接收),或者以.开头的修饰符指定该指令以特殊方式绑定)。

<p v-if="seen">Hello world!</p>

<!-- 绑定事件 -->
<a v-bind:href="url"></a>

<!-- 绑定属性 -->
<a v-on:click="doSomething">

<!-- .prevent修饰符会告诉v-on指令对于触发的事件调用event.preventDefault() -->
<form v-on:submit.prevent="onSubmit"></form>

Vue为v-bindv-on这两个常用的指令提供了简写形式:@

<!-- v-bind -->
<a v-bind:href="url"></a>
<a :href="url"></a>

<!-- v-on -->
<a v-on:click="doSomething"></a>
<a @click="doSomething"></a>

目前,Vue在2.4.2版本当中提供了如下的内置指令:

<html
  v-text = "更新元素的textContent"
  v-html = "更新元素的innerHTML"
  v-show = "根据表达式的true/false,切换HTML元素的display属性"
  v-for = "遍历内部的HTML元素"
  v-pre = "跳过表达式渲染过程,可以显示原始的Mustache标签"
  v-cloak = "保持在HTML元素上直到关联实例结束编译,可以隐藏未编译的Mustache"
  v-once = "只渲染元素和组件一次"
></html>

<!-- 根据表达式的true和false来决定是否渲染元素 -->
<div v-if="type === "A"">A</div>
<div v-else-if="type === "B"">B</div>
<div v-else-if="type === "C"">C</div>
<div v-else>Not A/B/C</div>

<!-- 动态地绑定属性或prop到表达式 -->
<p v-bind:attrOrProp
  .prop = "被用于绑定DOM属性"
  .camel = "将kebab-case特性名转换为camelCase"
  .sync = "语法糖,会扩展成一个更新父组件绑定值的v-on监听器"
></p>

<!-- 绑定事件监听器 -->
<button
  v-on:eventName
  .stop = "调用event.stopPropagation()"
  .prevent = "调用event.preventDefault()"
  .capture = "添加事件监听器时使用capture模式"
  .self = "当事件是从监听器绑定的元素本身触发时才触发回调" 
  .native = "监听组件根元素的原生事件"-
  .once = "只触发一次回调"
  .left = "点击鼠标左键触发"
  .right = "点击鼠标右键触发"
  .middle = "点击鼠标中键触发"
  .passive = "以{passive: true}模式添加监听器"
  .{keyCode | keyAlias} = "触发特定键触事件"
>
</button>

<!-- 表单控件的响应式绑定 -->
<input 
  v-model
  .lazy = "取代input监听change事件"
  .number = "输入字符串转为数字"
  .trim = "过滤输入的首尾空格" />

组件

组件可以扩展HTML元素功能,并且封装可重用代码。可以通过Vue.component( id, [definition] )注册或者获取全局组件。

// 注册组件,传入一个扩展过的构造器
Vue.component("my-component", Vue.extend({ ... }))

// 注册组件,传入一个option对象(会自动调用Vue.extend)
Vue.component("my-component", { ... })

// 获取注册的组件(始终返回构造器)
var MyComponent = Vue.component("my-component")

下面代码创建了一个Vue实例,并将自定义组件my-component挂载至HTML当中。

<script>
  // 注册自定义组件
  Vue.component("my-component", {
    template: "<div>A custom component!</div>"
  })

  // 创建Vue根实例
  new Vue({
    el: "#example"
  })
</script>

<!-- 原始模板 -->
<div id="example">
  <my-component></my-component>
</div>

<!-- 渲染结果 -->
<div id="example">
  <div>A custom component!</div>
</div>

  • is属性

浏览器解析完HTML之后才会渲染Vue表达式,但是诸如<ul> <ol> <table> <select>限制了可以被包裹的HTML元素,而<option>只能出现在某些HTML元素内部,造成Vue表达式可能不会被正确的渲染。因此,Vue提供is作为属性别名来解决该问题。

<!-- 不正确的方式 -->
<table>
  <my-row>...</my-row>
</table>

<!-- 使用is的正确方式 -->
<table>
  <tr is="my-row"></tr>
</table>

  • data必须是函数

Vue.component()传入的data属性不能是对象,而必须是函数。这样做的目的是避免组件在相同模板的多个位置被复用时,仅仅返回对象会造成组件间的数据被相互污染,而通过函数每次都返回全新的data对象能完美的规避这个问题。

Vue.component("simple-counter", {
  template: "<button v-on:click="counter += 1">{{ counter }}</button>",
  data: function () {
    return {
      a: "",
      b: ""
    }
  }
});

  • 父子组件之间的通信

父组件通过props向下传递数据给子组件,子组件通过events给父组件发送消息,即props 向下传, events 向上传

props-events.png

props

虽然每个组件的作用域都是独立的,但是可以通过props属性向子组件传递数据,这是一种单向数据流的体现形式。

Vue.component("child", {
  // 声明props
  props: ["message"],
  // 和data属性一样,prop也可以在vm通过this.message进行引用
  template: "<span>{{ message }}</span>"
})

不要在子组件内部修改props,这样会导致后台报错。

命名方式转换

因为HTML并不区分大小写,所以kebab-case(驼峰)风格命名的props,在组件中会以camelCased(短横线隔开)风格被接收。

<!-- camelCase in JavaScript -->
<script>
Vue.component("child", {
  props: ["myMessage"],
  template: "<span>{{ myMessage }}</span>"
})
<script>

<!-- kebab-case in HTML -->
<child my-message="hello!"></child>

动态props

可以通过v-bind指令,响应式的绑定父组件数据到子组件的props。当父组件数据变化时,该变化也会传导至子组件。

<div>
  <input v-model="parentMsg">
  <br>
  <child v-bind:my-message="parentMsg"></child>
</div>

使用v-bind可以让其参数值能够以JavaScript表达式的方式被解析,否则所有传入的props都会被子组件认为是字符串类型。

<!-- 传递的是字符串"1" -->
<comp some-prop="1"></comp>
<!-- 传递实际的 number -->
<comp v-bind:some-prop="1"></comp>

验证props

可以为组件的props指定验证规则,如果传入数据不符合要求,Vue会发出相应警告,这样可以有效提高组件的健壮性。

Vue.component("example", {
  props: {
    // 基础类型检测
    propA: Number,
    // 多种类型
    propB: [String, Number],
    // 必传且是字符串
    propC: {
      type: String,
      required: true
    },
    // 数字,有默认值
    propD: {
      type: Number,
      default: 100
    },
    // 数组或对象的默认值由1个工厂函数返回
    propE: {
      type: Object,
      default: function () {
        return { message: "hello" }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        return value > 10
      }
    }
  }
});

props会在组件实例创建之前进行校验。

组件的非props属性

组件可以接收任意传入的属性,这些属性都会被添加到组件HTML模板的根元素上(无论有没有在props中定义)。

<!-- 带有属性的自定义组件 -->
<bs-date-input
  data-3d-date-picker="true"
  class="date-picker-theme-dark">
</bs-date-input>

<!-- 渲染出来的组件,class属性被合并 -->
<input type="date" data-3d-date-picker="true" 
      class="form-control date-picker-theme-dark">

父组件传递给子组件的属性可能会覆盖子组件本身的属性,因而会对子组件造成破坏和污染。

事件

子组件可以通过Vue的自定义事件与父组件进行通信。

每个Vue实例都实现了如下API,但是并不能直接通过$on监听子组件冒泡的事件,而必须使用v-on指令。

  1. $on(eventName) 监听事件
  2. $emit(eventName) 触发事件

$on$emit并不是addEventListenerdispatchEvent的别名。

<div id="counter-event-example">
  <p>{{ total }}</p>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>

<script>
  Vue.component("button-counter", {
    template: "<button v-on:click="incrementCounter">{{counter}}</button>",
    data: function () {
      return {
        counter: 0
      }
    },
    methods: {
      // 子组件事件
      incrementCounter: function () {
        this.counter += 1
        this.$emit("increment") //向父组件冒泡事件
      }
    },
  })

  new Vue({
    el: "#counter-event-example",
    data: {
      total: 0
    },
    methods: {
      // 父组件事件
      incrementTotal: function () {
        this.total += 1
      }
    }
  })
</script>

  • .native修饰符

开发人员也可以在组件的根元素上监听原生事件,这个时候需要借助到.native修饰符。

<my-component v-on:click.native="doTheThing"></my-component>

  • .sync修饰符

Vue中的props本质是不能进行响应式绑定的,以防止破坏单向数据流,造成多个子组件对父组件状态形成污染。但是生产环境下,props响应式绑定的需求是切实存在的。因此,Vue将.sync修饰符封装为糖衣语法,父组件在子组件的props使用该修饰符后,父组件会为props自动绑定v-on事件,子组件则在监听到props变化时向父组件$emit更新事件,从而让父组件的props能够与子组件进行同步。

<!-- 使用.sync修饰符 -->
<comp :foo.sync="bar"></comp>

<!-- 被自动扩展为如下形式,该组件的子组件会通过this.$emit("update:foo", newValue)
    显式触发更新事件 -->
<comp :foo="bar" @update:foo="val => bar = val"></comp>

  • 平行组件通信

非父子关系的组件进行通信时,可以使用一个的Vue实例作为中央事件总线

var bus = new Vue()
// 触发组件A中的事件
bus.$emit("id-selected", 1)
// 在组件B监听事件
bus.$on("id-selected", function (id) {
  ... ... ...
})

更好的方式是借助VueX或者Redux之类的flux状态管理库。

slot

可以将父组件的内容混入到子组件的模板当中,此时可以在子组件中使用<slot>作为父组件内容的插槽。

父组件模板的内容在父组件作用域内编译,子组件模板的内容在子组件作用域内编译。

匿名插槽

当子组件只有一个没有属性的<slot>时,父组件全部内容片段将插入到插槽所在的DOM位置,并替换插槽标签本身。

<!-- 子组件my-component的模板 -->
<div>
  <h2>Child</h2>
  <slot>
    父组件没有需要插入的内容时显示
  </slot>
</div>

<!-- 父组件模板中使用my-component -->
<div>
  <h1>Parent</h1>
  <child>
    <p>Content 1</p>
    <p>Content 2</p>
  </child>
</div>

<!-- 渲染结果 -->
<div>
  <h1>Parent</h1>
  <div>
    <h2>Child</h2>
    <p>Content 1</p>
    <p>Content 2</p>
  </div>
</div>

<slot>标签中的内容会在子组件作用域内编译,并在父组件没有需要插入的内容时才会显示。

具名插槽

可以通过<slot>元素的name属性来配置如何分发内容。

<!-- 子组件 -->
<div id="app">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

<!-- 父组件 -->
<app>
  <div slot="header">Header</div>
  <p>Content 1</p>
  <p>Content 2</p>
  <div slot="footer">Footer</div>
</app>

<!-- 渲染结果 -->
<div id="app">
  <header>
    <div>Header</div>
  </header>
  <main>
    <p>Content 1</p>
    <p>Content 2</p>
  </main>
  <footer>
    <p>Footer</p>
  </footer>
</div>

匿名slot会作为没有匹配内容的父组件片段的插槽。

作用域插槽

子组件通过props传递数据给<slot>插槽,父组件使用带有scope属性的<template>来表示表示当前作用域插槽的模板,scope值对应的变量会接收子组件传递来的props对象。

<!-- 子组件通过props传递数据给插槽 -->
<div class="child">
  <slot text="hello from child"></slot>
</div>

<!-- 父组件使用带有scope属性的<template> -->
<div class="parent">
  <child>
    <template scope="props">
      <span>hello from parent</span>
      <span>{{ props.text }}</span>
    </template>
  </child>
</div>

<!-- 渲染结果 -->
<div class="parent">
  <div class="child">
    <span>hello from parent</span>
    <span>hello from child</span>
  </div>
</div>

函数化组件

即无状态(没有data)无实例(没有this上下文)的组件,渲染开销较小,且不会出现在Vue devtools当中。

Vue.component("my-component", {
  functional: true,
  // 通过提供context参数为没有实例的函数组件提供上下文信息
  render: function (createElement, context) {},
  // Props可选
  props: {}
})

动态组件

使用<component>元素并动态绑定其is属性,可以让多个组件使用相同的Vue对象挂载点,并实现动态切换。

<script>
var vm = new Vue({
  el: "#example",
  data: {
    currentView: "home"
  },
  components: {
    home: { /* ... */ },
    posts: { /* ... */ },
    archive: { /* ... */ }
  }
})
</script>

<component v-bind:is="currentView">
  <!-- 组件在vm.currentview变化时改变! -->
</component>

如果需要将切换的组件保持在内存,保留其状态并且避免重新渲染,可以使用Vue内置的keep-alive指令。

<keep-alive>
  <component :is="currentView">
    <!-- 非活动组件将被缓存! -->
  </component>
</keep-alive>

组件异步加载

Vue允许将组件定义为工厂函数,从而异步的解析组件定义。Vue只会在组件渲染时才触发工厂函数,并将结果缓存起来用于后续渲染。定义组件的工厂函数将会接收resolve(接收到从服务器下载的Vue组件options时被调用)和reject(当远程Vue组件options加载失败时调用)回调函数作为参数。

Vue.component("async-example", function (resolve, reject) {
  setTimeout(function () {
    // 将组件定义传递到resolve回调函数当中
    resolve({
      template: "<div>I am async!</div>"
    })
  }, 1000)
})

可以结合Webpack提供的代码切割功能,将Vue组件的options对象提取到单独JavaScript文件,从而实现异步的按需加载。

// 使用webpack的require()来进行异步代码块切割
Vue.component("async-webpack-example", function (resolve) {
  require(["./my-async-component"], resolve)
})

// 使用webpack的import()来进行异步代码块切割
Vue.component(
  "async-webpack-example", () => import("./my-async-component")
)

从Vue 2.3.0版本开始,可以通过下面的方式来定义一个异步组件。

const AsyncWebpackExample = () => ({
  component: import("./MyComp.vue"),   // 需要加载的组件
  loading: LoadingComp,                // loading时渲染的组件
  error: ErrorComp,                    // 出错时渲染的组件
  delay: 200,                    // 渲染loading组件前的等待时间(默认:200ms)
  timeout: 3000             // 最长等待时间,超出则渲染error组件(默认:Infinity)
})

在路由组件上使用这种写法,需要使用vue-router的2.4.0以上版本。

组件的循环引用

循环引用,即两个组件互相引用对方,例如下面代码中tree-foldertree-folder-contents两个组件同时成为了对方的父或子节点,如果使用Webpack模块化管理工具requiring/importing组件的时候,会报出Failed to mount component: template or render function not defined.错误。

<template>
  <p>
    <span>{{ folder.name }}</span>
    <tree-folder-contents :children="folder.children"/>
  </p>
</template>

<template>
  <ul>
    <li v-for="child in children">
      <tree-folder v-if="child.children" :folder="child"/>
      <span v-else>{{ child.name }}</span>
    </li>
  </ul>
</template>

因为tree-foldertree-folder-contents相互引用对方之后,无法确定组件加载的先后顺序陷入死循环,所以需要事先指明webpack组件加载的优先级。解决上面例子中Vue组件循环引用的问题,可以在tree-folder组件的beforeCreate()生命周期函数内注册引发问题的tree-folder-contents组件。

beforeCreate: function () {
  this.$options.components.TreeFolderContents 
      =  require("./tree-folder-contents.vue").default
}

组件命名约定

JavaScript中命名组件组件时可以使用kebab-casecamelCasePascalCase,但HTML模板中只能使用kebab-case格式。

<kebab-cased-component></kebab-cased-component>
<camel-cased-component></camel-cased-component>
<pascal-cased-component></pascal-cased-component>
<!-- 也可以通过自关闭方式使用组件 -->
<kebab-cased-component />

<script>
components: {
  "kebab-cased-component": {},
  "camelCasedComponent": {},
  "PascalCasedComponent": {}
}
</script>

推荐JavaScript中通过PascalCase方式声明组件, HTML中则通过kebab-case方式使用组件。

组件递归

当局部注册的Vue组件递归调用自身时,需要在创建组件时添加name选项,全局注册的组件则可以省略该属性,因为Vue会自动进行添加。

// 局部注册
new Vue({
  el: "#my-component",
  name: "my-component",
  template: "<div><my-component></my-component></div>"
})

// 全局注册
Vue.component("my-component", {
  // name: "my-component", 可以省略name属性
  template: "<div><my-component></my-component></div>"
})

组件递归出现死循环时,会提示max stack size exceeded错误,所以需要确保递归操作都拥有一个终止条件(比如使用v-if并返回false)。

组件模板

  • 可以在Vue组件上使用inline-template属性,组件会将内嵌的HTML内容作为组件本身的模板进行渲染,而非将其作为slot分发的内容。
<my-component inline-template>
  <div>
    <p>These are compiled as the component"s own template.</p>
    <p>Not parent"s transclusion content.</p>
  </div>
</my-component>

  • 也可以通过在<script>标签内使用type="text/x-template"id属性来定义一个内嵌模板。
<script type="text/x-template" id="hello-world-template">
  <p>Hello hello hello</p>
</script>

<script>
Vue.component("hello-world", {
  template: "#hello-world-template"
})
</script>

混合属性mixins

用来将指定的mixin对象复用到Vue组件当中。

// mixin对象
var mixin = {
  created: function () {
    console.log("混合对象的钩子被调用")
  },
  methods: {
    foo: function () {
      console.log("foo")
    },
    conflicting: function () {
      console.log("from mixin")
    }
  }
}

// vue属性
var vm = new Vue({
  mixins: [mixin],
  created: function () {
    console.log("组件钩子被调用")
  },
  methods: {
    bar: function () {
      console.log("bar")
    },
    conflicting: function () {
      console.log("from self")
    }
  }
})

// => "混合对象的钩子被调用"
// => "组件钩子被调用"
vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"

同名组件option对象的属性会被合并为数组依次进行调用,其中mixin对象里的属性会被首先调用。如果组件option对象的属性值是一个对象,则mixin中的属性会被忽略掉。

渲染函数render()

用来创建VNode,该函数接收createElement()方法作为第1个参数,该方法调用后会返回一个虚拟DOM(即VNode)。

直接使用表达式,或者在render()函数内通过createElement()进行手动渲染,Vue都会自动保持blogTitle属性的响应式更新。

<h1>{{ blogTitle }}</h1>

<script>
  render: function (createElement) {
      return createElement("h1", this.blogTitle)
  }
</script>

如果组件是一个函数组件,render()还会接收一个context参数,以便为没有实例的函数组件提供上下文信息。

通过render()函数实现虚拟DOM比较麻烦,因此可以使用Babel插件babel-plugin-transform-vue-jsx在render()函数中应用JSX语法。

import AnchoredHeading from "./AnchoredHeading.vue"

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

Vue对象全局API

Vue.extend(options)                 // 通过继承一个option对象来创建一个Vue实例。
Vue.nextTick([callback, context])   // 在下次DOM更新循环结束之后执行延迟回调。
Vue.set(target, key, value)  // 设置对象的属性,如果是响应式对象,将会触发视图更新。
Vue.delete(target, key)     // 删除对象的属性,如果是响应式对象,将会触发视图更新。
Vue.directive(id, [definition])     // 注册或获取全局指令。
Vue.filter(id, [definition])        // 注册或获取全局过滤器。
Vue.component(id, [definition])     // 注册或获取全局组件。
Vue.use(plugin)                     // 安装Vue插件。
Vue.mixin(mixin)                    // 全局注册一个mixin对象。
Vue.compile(template)               // 在render函数中编译模板字符串。
Vue.version                         // 提供当前使用Vue的版本号。

Vue.mixin(mixin)

使用全局mixins将会影响到所有之后创建的Vue实例。

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

new Vue({
  myOption: "hello!"
})

// => "hello!"

Vue.directive(id, [definition])

Vue允许注册自定义指令,用于对底层DOM进行操作。

Vue.directive("focus", {
  bind: function() {
    // 指令第一次绑定到元素时调用,只会调用一次,可以用来执行一些初始化操作。
  },
  inserted: function (el) {
    // 被绑定元素插入父节点时调用。
  },
  update: function() {
    // 所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。
  },
  componentUpdated: function() {
    // 所在组件VNode及其子VNode全部更新时调用。
  },
  unbind: function() {
    // 指令与元素解绑时调用,只会被调用一次。
  }
})

钩子之间共享数据可以通过HTMLElementdataset属性来进行(即HTML标签上通过data-格式定义的属性)。

上面的钩子函数拥有如下参数:

  • el: 指令绑定的HTML元素,可以用来直接操作DOM。
  • vnode: Vue编译生成的虚拟节点。
  • oldVnode: 之前的虚拟节点,仅在updatecomponentUpdated钩子中可用。
  • binding: 一个对象,包含以下属性:
    • name: 指令名称,不包括v-前缀。
    • value: 指令的绑定值,例如v-my-directive="1 + 1"value的值是2
    • oldValue: 指令绑定的之前一个值,仅在updatecomponentUpdated钩子中可用。
    • expression: 绑定值的字符串形式,例如v-my-directive="1 + 1"当中expression的值为"1 + 1"
    • arg: 传给指令的参数,例如v-my-directive:fooarg的值是"foo"
    • modifiers: 包含修饰符的对象,例如v-my-directive.foo.barmodifiers的值是{foo: true, bar: true}

上面参数除el之外,其它参数都应该是只读的,尽量不要对其进行修改操作。

Vue.filter(id, [definition])

Vue可以通过定义过滤器,进行一些常见的文本格式化,可以用于mustache插值和v-bind表达式当中,使用时通过管道符|添加在表达式尾部。

<!-- in mustaches -->
{{ message | capitalize }}

<!-- in v-bind -->
<div v-bind:id="rawId | formatId"></div>

<!-- capitalize filter -->
<script>
  new Vue({
    filters: {
      capitalize: function (value) {
        if (!value) return ""
        value = value.toString()
        return value.charAt(0).toUpperCase() + value.slice(1)
      }
    }
  })
</script>

过滤器可以串联使用,也可以传入参数。

<span>{{ message | filterA | filterB }}</span>
<span>{{ message | filterA("arg1", arg2) }}</span>

Vue.use(plugin)

Vue通过插件来添加一些全局功能,Vue插件都会覆写其install()方法,该方法第1个参数是Vue构造器, 第2个参数是可选的option对象:

MyPlugin.install = function (Vue, options) {
  // 1\. 添加全局方法或属性
  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()使用指定插件,使用的时候也可以传入一个option对象。

Vue.use(MyPlugin, {someOption: true})

vue-router等插件检测到Vue是全局对象时会自动调用Vue.use(),如果在CommonJS模块环境中,则需要显式调用Vue.use()

实例属性和方法

Vue实例暴露了一系列带有前缀$的实例属性与方法。

let vm = new Vue();
vm = {
  // Vue实例属性的代理
  $data: "被watch的data对象",
  $props: "当前组件收到的props",
  $el: "Vue实例使用的根DOM元素",
  $options: "当前Vue实例的初始化选项",
  $parent: "父组件Vue对象的实例",
  $root: "根组件Vue对象的实例",
  $children: "当前实例的直接子组件",
  $slots: "访问被slot分发的内容",
  $scopedSlots: "访问scoped slots",
  $refs: "包含所有拥有ref注册的子组件",
  $isServer: "判断Vue实例是否运行于服务器",
  $attrs: "包含父作用域中非props的属性绑定",
  $listeners: "包含了父作用域中的v-on事件监听器",
  // 数据
  $watch: "观察Vue实例变化的表达式、计算属性函数",
  $set: "全局Vue.set的别名",
  $delete: "全局Vue.delete的别名",
  // 事件
  $on: "监听当前实例上的自定义事件,事件可以由vm.$emit触发",
  $once: "监听一个自定义事件,触发一次之后就移除监听器",
  $off: "移除自定义事件监听器",
  $emit: "触发当前实例上的事件",
  // 生命周期
  $mount: "手动地挂载一个没有挂载的Vue实例",
  $forceUpdate: "强制Vue实例重新渲染,仅影响实例本身和插入插槽内容的子组件",
  $nextTick: "将回调延迟到下次DOM更新循环之后执行",
  $destroy: "完全销毁一个实例",
}

$refs属性

组件指定ref属性之后,可以通过组件的$refs实例属性对其进行访问 。

<div id="parent">
  <user-profile ref="profile"></user-profile>
</div>

<script>
var parent = new Vue({ el: "#parent" })
var child = parent.$refs.profile // 访问子组件
</script>

$refs会在组件渲染完毕后填充,是非响应式的,仅作为需要直接访问子组件的应急方案,因此要避免在模板或计算属性中使用$refs

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • Vue 实例 属性和方法 每个 Vue 实例都会代理其 data 对象里所有的属性:var data = { a:...
    云之外阅读 2,124评论 0 6
  • vue概述 在官方文档中,有一句话对Vue的定位说的很明确:Vue.js 的核心是一个允许采用简洁的模板语法来声明...
    li4065阅读 7,108评论 0 25
  • 每个 Vue 应用都是通过用 Vue 函数创建一个新的 Vue 实例开始的: 实例生命周期钩子 每个 Vue 实例...
    Timmy小石匠阅读 1,340评论 0 11
  • 主要还是自己看的,所有内容来自官方文档。 介绍 Vue.js 是什么 Vue (读音 /vjuː/,类似于 vie...
    Leonzai阅读 3,269评论 0 25
  • 早晨醒来,听到了汀姐在很委屈滴哭,原来是做噩梦了。我一顿心疼,抱着她亲了亲她的脸颊,告诉依然在睡梦中的她:别怕,妈...
    小妖丁儿阅读 105评论 0 0