Vue文档阅读——可复用性&组合

239 阅读6分钟

混入

基础

混入mixin提供了分发Vue组件中的可复用功能。

一个混入对象可以包含任意组件选项。当组件使用混入对象,所有选项会被混合仅该组件本身的选项。

// 定义一个混入对象
var myMixin = {
  created: function () {
    this.hello()
  },
  methods: {
    hello: function () {
      console.log('hello from mixin!')
    }
  }
}

// 定义一个使用混入对象的组件
var Component = Vue.extend({
  mixins: [myMixin]
})

var component = new Component() // => "hello from mixin!"

选项合并

当组件和混入对象有同名选项时,会合并。

数据对象会递归合并,发生冲突以组件优先。

var mixin = {
  data: function () {
    return {
      message: 'hello',
      foo: 'abc'
    }
  }
}

new Vue({
  mixins: [mixin],
  data: function () {
    return {
      message: 'goodbye',
      bar: 'def'
    }
  },
  created: function () {
    console.log(this.$data)
    // => { message: "goodbye", foo: "abc", bar: "def" }
  }
})

同名钩子函数会合并为一个数组,都被调用,混入钩子在组件自身钩子之前调用:

var mixin = {
  created: function () {
    console.log('混入对象的钩子被调用')
  }
}

new Vue({
  mixins: [mixin],
  created: function () {
    console.log('组件钩子被调用')
  }
})

// => "混入对象的钩子被调用"
// => "组件钩子被调用"

值为对象的选项,methods,components、directives会被合并为同一个对象。键名冲突时,取组件的。

var mixin = {
  methods: {
    foo: function () {
      console.log('foo')
    },
    conflicting: function () {
      console.log('from mixin')
    }
  }
}

var vm = new Vue({
  mixins: [mixin],
  methods: {
    bar: function () {
      console.log('bar')
    },
    conflicting: function () {
      console.log('from self')
    }
  }
})

vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"

Vue.extent()也使用同样的策略合并。

全局混入

一旦全局混入会影响每一个之后创建的Vue实例。

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

new Vue({
  myOption: 'hello!'
})
// => "hello!"

谨慎使用全局混入,推荐将其作为插件发布。

自定义选项合并策略

向Vue.config.optionMergeStrategies添加函数:

Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
  // 返回合并后的值
}

对于多数值为对象的选项,可以使用methods相同的合并策略:

var strategies = Vue.config.optionMergeStrategies
strategies.myOption = strategies.methods

自定义指令

简介

需要对普通DOM进行底层操作,就会用到自定义指令。

实现页面加载input框就处于聚焦:

// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})

如果想注册局部指令,组件也接受一个directives的选项:

directives: {
  focus: {
    // 指令的定义
    inserted: function (el) {
      el.focus()
    }
  }
}

然后:

<input v-focus>

钩子函数

一个指令定义对象可以提供一下几个钩子函数:

  • bind 只调用一次,指令第一次绑定到元素时调用
  • inserted 被绑定元素插入父节点时调用
  • update 所在组件的VNode更新时调用
  • componentUpdated 指令所在组件的VNode和子VNode全部更新后调用
  • unbind 指令与元素解绑时调用

钩子函数参数

  • el 绑定的元素,用来直接操作DOM
  • binding 一个对象包含:
    • name 指令明
    • value 指令绑定值
    • oldValue 指令绑定的前一个值,只在update类似的钩子中使用
    • expression 字符串形式的指令表达式
    • arg 传给指令的参数
    • modifiers 包含修饰符的对象
  • vnode Vue编译生成的虚拟节点
  • oldVnode 上一个虚拟节点 只在update系列钩子中用

除了el之外,其他都是只读的,如果要在钩子之间共享数据,建议使用元素的dataset

一个例子:

<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(', ')
  }
})

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

动态指令参数

指令的参数可以是动态的v-mydirective:[argument]="value"

创建一个指令,通过固定布局将元素固定在页面上:

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

new Vue({
  el: '#baseexample'
})

如果不是顶部而是左边,就可以使用动态参数:

<div id="dynamicexample">
  <h3>Scroll down inside this section ↓</h3>
  <p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
</div>
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'
    }
  }
})

函数简写

如果想在bind和update时触发相同行为:

Vue.directive('color-swatch', function (el, binding) {
  el.style.backgroundColor = binding.value
})

对象字面量

<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!"
})

渲染函数&JSX

基础

推荐使用模板来创建HTML。使用渲染函数,生成一些带锚点的标题:

<h1>
  <a name="hello-world" href="#hello-world">
    Hello world!
  </a>
</h1>

定义一个组件接口:

<anchored-heading :level="1">Hello world!</anchored-heading>

使用render函数:

Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // 标签名称
      this.$slots.default // 子节点数组
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

向组件中传递不带v-slot指令的子节点时,这些子节点被存储在组件实例中的$slots.default中。

节点、树以及虚拟DOM

了解一下浏览器的工作原理:

<div>
  <h1>My title</h1>
  Some text content
  <!-- TODO: Add tagline -->
</div>

DOM树:

使用render函数更新这些节点:

render: function (createElement) {
  return createElement('h1', this.blogTitle)
}

虚拟DOM

Vue通过一个虚拟DOM追踪自己要如何改变真实DOM:

return createElement('h1', this.blogTitle)

createElement并不是返回一个实际的DOM元素,它告诉Vue页面上需要渲染什么样的节点,子节点。叫做虚拟节点VNode。虚拟DOM就是VNode树的称呼。

createElement参数

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

  // {Object}
  // 一个与模板中属性对应的数据对象。可选。
  {
    // (详情见下一节)
  },

  // {String | Array}
  // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)

深入数据对象

createElement函数的第二个参数

{
  // 与 `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 属性
  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',
  // 其它特殊顶层属性
  key: 'myKey',
  ref: 'myRef',
  // 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
  // 那么 `$refs.myRef` 会变成一个数组。
  refInFor: true
}

完整示例

var getChildrenTextContent = function (children) {
  return children.map(function (node) {
    return node.children
      ? getChildrenTextContent(node.children)
      : node.text
  }).join('')
}

Vue.component('anchored-heading', {
  render: function (createElement) {
    // 创建 kebab-case 风格的 ID
    var headingId = getChildrenTextContent(this.$slots.default)
      .toLowerCase()
      .replace(/\W+/g, '-')
      .replace(/(^-|-$)/g, '')

    return createElement(
      'h' + this.level,
      [
        createElement('a', {
          attrs: {
            name: headingId,
            href: '#' + headingId
          }
        }, this.$slots.default)
      ]
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

约束

VNode必须唯一

使用JavaScript代替模板功能

v-if和v-for

props: ['items'],
render: function (createElement) {
  if (this.items.length) {
    return createElement('ul', this.items.map(function (item) {
      return createElement('li', item.name)
    }))
  } else {
    return createElement('p', 'No items found.')
  }
}

v-model

props: ['value'],
render: function (createElement) {
  var self = this
  return createElement('input', {
    domProps: {
      value: self.value
    },
    on: {
      input: function (event) {
        self.$emit('input', event.target.value)
      }
    }
  })
}

事件&按键修饰符

Vue提供了响应的前缀用于on:

修饰符 前缀
.passive &
.capture !
.once ~
.captrue.once或者.once.capture ~!
on: {
  '!click': this.doThisInCapturingMode,
  '~keyup': this.doThisOnce,
  '~!mouseover': this.doThisOnceInCapturingMode
}

插槽

可以通过this.$slots访问静态插槽的内容,每个插槽都是一个VNode数组:

render: function (createElement) {
  // `<div><slot></slot></div>`
  return createElement('div', this.$slots.default)
}

也可以通过this.$scopedSlots访问作用域插槽:

props: ['message'],
render: function (createElement) {
  // `<div><slot :text="message"></slot></div>`
  return createElement('div', [
    this.$scopedSlots.default({
      text: this.message
    })
  ])
}

如果要使用render函数想子组件传递作用域插槽,可以利用scopedSlots:

render: function (createElement) {
  return createElement('div', [
    createElement('child', {
      // 在数据对象中传递 `scopedSlots`
      // 格式为 { name: props => VNode | Array<VNode> }
      scopedSlots: {
        default: function (props) {
          return createElement('span', props.text)
        }
      }
    })
  ])
}

JSX

如果你写了很多render函数很麻烦,有一个Babel插件,在Vue中使用JSX语法

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

将h作为createElement的别名是一个习惯,也是JSX要求的。在任何方法中都有const h = this.$createElement

函数式组件

如果一些没有任何状态依赖的组件,我们将他标记为functional,一个函数式组件:

Vue.component('my-component', {
  functional: true,
  // Props 是可选的
  props: {
    // ...
  },
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文
  render: function (createElement, context) {
    // ...
  }
})

如果使用了单文件组件,那么基于模板的函数式组件是这样的:

<template functional>
</template>

组件需要的一切都是通过context参数传递,他是一个包含以下字段的对象:

  • props
  • children VNode子节点的数组
  • slots
  • scopedSlots
  • data
  • parent
  • listeners
  • injections

因为函数式组件只是函数,所以渲染开销低。

这样可以在将children,props,data传递给子组件之前操作他们。

根据传入的props的值来渲染不同的组件:

var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }

Vue.component('smart-list', {
  functional: true,
  props: {
    items: {
      type: Array,
      required: true
    },
    isOrdered: Boolean
  },
  render: function (createElement, context) {
    function appropriateListComponent () {
      var items = context.props.items

      if (items.length === 0)           return EmptyList
      if (typeof items[0] === 'object') return TableList
      if (context.props.isOrdered)      return OrderedList

      return UnorderedList
    }

    return createElement(
      appropriateListComponent(),
      context.data,
      context.children
    )
  }
})

向子元素或子组件传递attribute和事件

普通组件中没有被定义prop的attribute会自动添加到组件根元素,与已有的同名attribute替换或者合并。

然而函数式组件要求显示定义该行为:

Vue.component('my-functional-button', {
  functional: true,
  render: function (createElement, context) {
    // 完全透传任何 attribute、事件监听器、子节点等。
    return createElement('button', context.data, context.children)
  }
})

还要手动添加attribute和监听器可以在子组件中使用data.attrs和listeners传递。

slots()和children对比

<my-functional-component>
  <p v-slot:foo>
    first
  </p>
  <p>second</p>
</my-functional-component>

children会给你两个段落标签,而slots().default只会传递第二个匿名段落标签,slots().foo会传递第一个具名段落标签。

模板编译

使用Vue.compile实施编译模板字符串

插件

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

  1. 添加全局方法或数学
  2. 添加全局资源:指令、过滤器、过渡
  3. 通过全局混入添加些组件
  4. 添加Vue实例方法,把他们加到Vue.prototype上去
  5. 库,提供自己API

使用插件

通过全局方法Vue.use()使用插件,需要在new Vue()之前完成

// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)

new Vue({
  // ...组件选项
})

也可以传入一个可选的选项对象:

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

Vue.use会自动阻止多次注册相同插件,即使多次调用也只会注册一次。

开发插件

插件应该暴露一个install方法,第一个参数是vue构造器,第二个参数是一个可选的选项:

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

过滤器

使用方法,可在花括号中,也可在v-bind中

<!-- 在双花括号中 -->
{{ message | capitalize }}

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

可以在组件的选项中定义本地的过滤器

filters: {
  capitalize: function (value) {
    if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}

或者在创建Vue实例之前全局定义过滤器:

Vue.filter('capitalize', function (value) {
  if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})

new Vue({
  // ...
})

当全局和局部过滤器重名,会采用局部的。

过滤器可以串联:

{{ message | filterA | filterB }}

过滤器是js函数也可以接受参数

{{ message | filterA('arg1', arg2) }}