vue.js了解篇4(自定义组件)

目录
    1. 注册组件(全局注册/局部注册)
    2. (向组件模版)传递数据 
    3. (响应组件模版中的)事件
    4. 组件添加v-model
    5. is (解决特殊限制)
    6. 插槽
    7. 动态组件

前言

除了可使用普通html元素外,Vue还可以自定义组件。

使用自定义组件的好处:
  1. 便于维护:可以使代码结构清晰,分散到各文件中。
  2. 可复用:减少代码冗余。

注意:
  1. 组件只能有一个根元素
  2. 组件可以使用指令
  3. 组件有和Vue相同的选项(因为组件就是可复用的Vue实例),例如 data、computed、watch、methods 以及生命周期钩子等。仅有的例外是像 el 这样根实例特有的选项。
  4. 每添加一个组件,就会有一个它的新实例被创建。data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。
  6. 一个组件默认可以拥有任意数量的 prop,任何值都可以传递给任何 prop。
  7. 多行template更易读,但它们在 IE 下并没有被支持,所以如果需要在不 (经过 Babel 或 TypeScript 之类的工具) 编译的情况下支持 IE,请使用折行转义字符。
例

<div id="app">
  <app-nav></app-nav>  <!--导航栏-->
  <app-view>  <!--内容-->
    <app-sidebar></app-sidebar>  <!--左侧栏-->
    <app-content></app-content>  <!--右侧内容-->
  </app-view>
</div>
...
1、全局/局部 注册组件
Vue.component('my-component-name', { /* ... */ })
/* 
第一个参数:组件名。2种命名方式
   第一种命名方式:全小写 加 连字符-(W3C规范,建议使用),使用<hello-world>
   第二种命名方式:HelloWorld,使用<hello-world>或<HelloWorld>
*/

格式一:全局(可在本页中使用)

全局注册的组件可以用在其被注册之后的任何 (通过 new Vue) 新创建的 Vue 根实例。
例1

<div id="app">
    <!--第2步:使用全局组件-->
    <hello-world></hello-world>
</div>

<script>
// 第1步:注册全局组件
Vue.component('hello-world', {
  template: '<h1>自定义组件!</h1>'
})

// 创建根实例
new Vue({
  el: '#app'
})
</script>
例2

<div id="components-demo">
  <button-counter></button-counter>
</div>
...
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
new Vue({ el: '#components-demo' })

格式二:局部(仅在该Vue实例下可用)

注意:局部注册的组件作为其他组件的子组件时不可用,解决如下
  var ComponentA = { /* ... */ }
  var ComponentB = {
    components: {
      'component-a': ComponentA
    },
    // ...
  }
例

<div id="app">
    <!--第2步、使用局部组件-->
    <hello></hello >
</div>

<script>
var Child = {
  template: '<h1>自定义组件!</h1>'
}
// 创建根实例
new Vue({
  el: '#app',
  // 第1步、注册局部组件(仅在id为app中使用)
  components: {
    'hello': Child,
    'hello2': {
      template: '<h1>自定义组件!</h1>'
    },
  }
})
</script>
2. 向组件模版传递数据

父组件的数据通过props传给子组件

<div id="app">
    <child message="hello!"></child>
</div>
 

<script>
// 注册
Vue.component('child', {
  // 声明 props
  props: ['message'],
  // 同样也可以在 vm 实例中像 "this.message" 这样使用
  template: '<span>{{ message }}</span>'
})
// 创建根实例
new Vue({
  el: '#app'
})
</script>

单向动态绑定(当父组件的数据变化时,该变化会传导给子组件)

<div id="app">
    <div>
      <input v-model="parentMsg">
      <br>
      <child v-bind:message="parentMsg"></child>
    </div>
</div>
 
<script>
// 注册
Vue.component('child', {
  // 声明 props
  props: ['message'],
  // 同样也可以在 vm 实例中像 "this.message" 这样使用
  template: '<span>{{ message }}</span>'
})
// 创建根实例
new Vue({
  el: '#app',
  data: {
    parentMsg: '父组件内容'
  }
})
</script>
循环

<div id="app">
    <ol>
      <todo-item v-for="item in sites" v-bind:todo="item"></todo-item>
    </ol>
</div>
 
<script>
Vue.component('todo-item', {
  props: ['todo'],
  template: '<li>{{ todo.text }}</li>'
})
new Vue({
  el: '#app',
  data: {
    sites: [
      { text: 'Runoob' },
      { text: 'Google' },
      { text: 'Taobao' }
    ]
  }
})
</script>

属性详解

单向动态绑定
  props 使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致应用的数据流向难以理解(即不应该在一个子组件内部改变 prop)。
例-prop 用来传递一个初始值:
props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter
  }
}

例-prop 以一种原始的值传入且需要进行转换:
props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}
1、Vue中的驼峰属性名在html中要使用连字符-(因为html大小写不敏感)。但{{}}中不受限制。如下:

  <blog-post post-title="hello!"></blog-post>

  Vue.component('blog-post', {
    props: ['postTitle'],
    template: '<h3>{{ postTitle }}</h3>'
  })
2、属性类型验证。属性类型type可以是:String、Number、Boolean、Function、Object、Array、Date、Symbol、一个自定义的构造函数。
  1、prop 会在一个组件实例创建之前进行验证,所以实例的属性 (如 data、computed 等) 在 default 或 validator验证 函数中是不可用的;
  2、如果有一个属性类型验证失败,(开发环境构建版本的) Vue 将会产生一个控制台的警告。

  Vue.component('example', {
    props: {
      // 基础类型检测 (`null` 和 `undefined` 会通过任何类型验证)
      propA: Number,
      // 多种类型
      propB: [String, Number],
      // 必传且是字符串
      propC: {
        type: String,
        required: true
      },
      // 数字,有默认值
      propD: {
        type: Number,
        default: 100
      },
      // 数组/对象,有默认值。默认值应当由一个工厂函数返回
      propE: {
        type: Object,
        default: function () {
          return { message: 'hello' }
        }
      },
      // 自定义验证函数
      propF: {
        validator: function (value) {
          return value > 10
        }
      }
    }
  })
3、传值(静态、动态)

<!-- 静态赋值 -->
<blog-post title="My journey with Vue"></blog-post>

<!-- 动态赋值 -->
<blog-post v-bind:title="post"></blog-post>
<!-- 动态赋予一个复杂表达式的值 -->
<blog-post
  v-bind:title="post.title + ' by ' + post.author.name"
></blog-post>

<!-- 传入post对象{  id: 1,  title: 'My Journey with Vue'}的所有属性 -->
<blog-post v-bind="post"></blog-post>
等价于
<blog-post
  v-bind:id="post.id"
  v-bind:title="post.title"
></blog-post>
数字

<!-- 即便 `42` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:likes="42"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:likes="post.likes"></blog-post>
bool

<!-- 包含该 prop 没有值的情况在内,都意味着 `true`。-->
<blog-post is-published></blog-post>

<!-- 即便 `false` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:is-published="false"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:is-published="post.isPublished"></blog-post>
数组

<!-- 即便数组是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:comment-ids="post.commentIds"></blog-post>
对象

<!-- 即便对象是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post
  v-bind:author="{
    name: 'Veronica',
    company: 'Veridian Dynamics'
  }"
></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:author="post.author"></blog-post>
4、组件可以接受任意特性(动态添加属性)
  1、因为组件并不总能预见组件会被用于怎样的场景。
  2、这些特性会被添加到这个组件的根元素上。

<blog-post title="My journey with Vue"></blog-post>
title会被加在根元素上
5、对于绝大多数特性来说,从外部提供给组件的值会替换掉(覆盖)组件内部设置好的值。但class 和 style 则会合并。
6、禁止 根元素继承特性
  注意 inheritAttrs: false 选项并不会影响 style 和 class 的绑定。

Vue.component('my-component', {
  inheritAttrs: false,
  props: ['label', 'value'],
  // ...
})
label、value不再从<my-component value=''>中获取


$attrs 属性决定将一个组件的特性名和特性值传递给指定元素
Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on:input="$emit('input', $event.target.value)"
      >
    </label>
  `
})
3. 响应组件模版中的事件

父组件可以使用props传递数据给子组件,但如果子组件要把数据传递回去,就需要使用事件

每个 Vue 实例都实现了事件接口:
  使用 $on(eventName) 监听事件
  使用 $emit(eventName) 触发事件

组件内部使用 $emit方法并传入事件名称来触发一个外部事件。使用 $emit 的第二个参数来传递参数,外部可以通过 $event 访问到。

事件名不存在任何自动化的大小写转换,必须完全一致。
注意:v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 v-on:myEvent 将会变成 v-on:myevent——导致 myEvent 不可能被监听到。所以极力推荐使用全小写加连字符-。

举例1

<div id="blog-posts-events-demo">
  <div :style="{ fontSize: postFontSize + 'em' }">
    <blog-post
      v-for="post in posts"
      v-bind:key="post.id"
      v-bind:post="post"
      v-on:hello-text="postFontSize += 0.1"
    ></blog-post>
  </div>
</div>

Vue.component('blog-post', {
  props: ['post'],
  template: `
    <div class="blog-post">
      <h3>{{ post.title }}</h3>
      <button v-on:click="$emit('hello-text')">
        Enlarge text
      </button>
      <button v-on:click="$emit('enlarge-text', 0.1)"> 
        Enlarge text
      </button>
      <div v-html="post.content"></div>
    </div>
  `
})

==========================
==========================
传值1

<button v-on:click="$emit('enlarge-text', 0.1)">
  Enlarge text
</button>
...
<blog-post
  ...
  v-on:enlarge-text="postFontSize += $event"
></blog-post>


==========================
==========================
传值2 

<blog-post
  ...
  v-on:enlarge-text="onEnlargeText"
></blog-post>
...
methods: {
  onEnlargeText: function (enlargeAmount) {
    this.postFontSize += enlargeAmount
  }
}

举例2

<div id="app">
    <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>
</div>
 
<script>
Vue.component('button-counter', {
  template: '<button v-on:click="incrementHandler">{{ counter }}</button>',
  data: function () {    // data 必须是一个函数。这样的好处就是每个实例可以维护一份被返回对象的独立的拷贝,如果 data 是一个对象则会影响到其他实例
    return {
      counter: 0
    }
  },
  methods: {
    incrementHandler: function () {
      this.counter += 1
      this.$emit('increment')    
      // this.$emit('increment',1) 带参数    ,在function (count)可获取 或 html 元素中 $event可获取
    }
  },
})
new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  }
})
</script>

.sync(2.3.0+ 新增)

在有些情况下可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题。推荐以 update:myPropName 的模式触发事件来达到效果:
this.$emit('update:title', newTitle)

<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>
简写为
<text-document v-bind:title.sync="doc.title"></text-document>

整个对象的属性
<text-document v-bind.sync="person"></text-document>

注意:
  注意带有 .sync 修饰符的 v-bind 使用表达式、对象。只能提供想要绑定的属性名。

在一个组件的根元素上直接监听一个原生事件,而不是监听子组件时

<base-input v-on:focus.native="onFocus"></base-input>

注意:在尝试监听一个类似 <input> 的非常特定的元素时,且根元素并不是input时会失效,listeners 属性(一个对象,里面包含了作用在这个组件上的所有监听器)可以解决这个问题:

例:
Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  computed: {
    inputListeners: function () {
      var vm = this
      // `Object.assign` 将所有的对象合并为一个新对象
      return Object.assign({},
        // 从父级添加所有的监听器
        this.$listeners,
        // 然后添加自定义监听器,
        // 或覆写一些监听器的行为
        {
          // 这里确保组件配合 `v-model` 的工作
          input: function (event) {
            vm.$emit('input', event.target.value)
          }
        }
      )
    }
  },
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on="inputListeners"
      >
    </label>
  `
})
4. 组件添加v-model
<custom-input v-model="searchText"></custom-input>
<!--
等价于
<custom-input
  v-bind:value="searchText"
  v-on:input="searchText = $event"
></custom-input>
-->

Vue.component('custom-input', {
  props: ['value'],
  template: `
    <input
      v-bind:value="value"
      v-on:input="$emit('input', $event.target.value)"
    >
  `
})
组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件。
但是像单选框、复选框(使用的是checked而不是value)等类型的输入控件可能会将 value特性用于不同的目的。可以使用model选项来解决(仍然需要在组件的 props 选项里声明 checked 这个 prop):

<base-checkbox v-model="lovingVue"></base-checkbox>
Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})
5. is (解决特殊限制)
有些 HTML 元素,诸如 <ul>、<ol>、<table> 和 <select>,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li>、<tr> 和 <option>,只能出现在其它某些特定的元素内部。

使用这些有约束条件的元素时会遇到一些问题。例如:
  <table>
    <customRow></customRow>
  </table>
这个自定义组件 <blog-post-row> 会被作为无效的内容提升到外部,并导致最终渲染结果出错。

注意:三种情况不存在这个限制:字符串、单文件组件.vue、<script type="text/x-template">
解决

<table>
  <tr is="customRow"></tr>    <!--自定义tr组件-->
</table>
6. 插槽
组件开始标签和结束标签之间的内容(包括html元素、纯文本、组件)称之为插槽,插槽 prop 可以将插槽转换为可复用的模板。
在 2.6.0 中,为具名插槽和作用域插槽引入了一个新的统一的语法 (即 `v-slot` 指令)。它取代了 `slot` 和 `slot-scope` 这两个目前已被废弃但未被移除且仍在[文档中]的特性。

<alert-box>
  Something bad happened.
</alert-box>
Vue.component('alert-box', {
  template: '
    <div class="demo-alert-box">
      <strong>Error!</strong>
      <slot></slot>
    </div>
  '
})

说明:
  1、<slot></slot> 会替换组件起始标签和结束标签之间的任何内容,这里会直接将文本‘Something bad happened.’替换过来。
  2、如果没有<slot></slot> ,则起始标签和结束标签之间的任何内容会被忽略。
  1. 作用域插槽
父级模板里的所有内容都是在父级作用域中编译的;
子模板里的所有内容都是在子作用域中编译的。
<navigation-link url="/profile">
  {{ url: 访问不到。不能访问父级组件的作用域}}  <!--undefined-->
</navigation-link>
<current-user>
  {{ user.firstName }}
</current-user>
模版
<span>
  <slot>{{ user.lastName }}</slot>
</span>

上述代码不会正常工作,解决:

<current-user>
  <template v-slot:default="hello">  <!--hello:包含所有插槽 prop 的对象-->
    {{ hello.user.firstName }}
  </template>
</current-user>
<span>
  <slot v-bind:user="user">  <!--为了让 user 在父级的插槽内容中可用,将 user 作为 <slot> 元素的一个特性绑定上去。绑定在 <slot> 元素上的特性被称为插槽 prop-->
    {{ user.lastName }}
  </slot>
</span>
只有默认插槽时的缩写(注意默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确):

<current-user v-slot="hello">
    {{ hello.user.firstName }}
</current-user>
多个插槽

<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>

  <template v-slot:other="otherSlotProps">
    ...
  </template>
</current-user>
作用域插槽工作原理:
  将插槽内容包括在一个传入单个参数的函数里function (slotProps) {    // 插槽内容  }。这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式

<current-user v-slot="{ user }">
  {{ user.firstName }}
</current-user>

重命名
<current-user v-slot="{ user: person }">
  {{ person.firstName }}
</current-user>

默认值
<current-user v-slot="{ user = { firstName: 'Guest' } }">
  {{ user.firstName }}
</current-user>

<!-- <submit-button>Hello</submit-button>  会显示Hello(覆盖默认的Submit)-->
<submit-button></submit-button>  会显示默认的Submit
<button type="submit">
  <slot>Submit</slot>
</button>
  1. 具名插槽(多个插槽时分别指定名字)
有时需要多个插槽。例:

<base-layout>
<!--
2.6.0新增缩写格式:<template #header>
只在其有参数的时候才可用,这里的参数header
-->
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

<!--不写默认为<template v-slot:default> ,
-->
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>
<!--</template>-->

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

模版:
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>  <!--一个不带 name 的 <slot> 出口会带有隐含的名字“default”-->
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
动态插槽名(2.6.0)

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>
7. 动态组件
<!--
  缓存组件,当再次切换回到该组件时保持状态,而不是重新创建。
  被切换到的组件必须都有自己的名字,不论是通过组件的 name 选项还是局部/全局注册。
-->
<keep-alive>
  <!--Tab切换: 每次切换新标签的时候,Vue 都创建了一个新的 currentTabComponent 实例-->
  <component v-bind:is="currentTabComponent"></component>
</keep-alive>
8. 异步组件
在大型应用中,可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。
示例1

Vue.component('async-webpack-example', function (resolve) {
  // 这个特殊的 `require` 语法将会告诉 webpack,自动将你的构建代码切割成多个包,这些包会通过 Ajax 请求加载
  require(['./my-async-component'], resolve)
})
示例2

// 全局注册
Vue.component(
  'async-webpack-example',
  // 这个 `import` 函数会返回一个 `Promise` 对象。
  () => import('./my-async-component')
)
// 局部注册
new Vue({
  // ...
  components: {
    'my-component': () => import('./my-async-component') //  webpack 2 和 ES2015 语法加在一起,异步加载“并不会被 Browserify 支持”
  }
})
示例3(仅作演示,模拟)

Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // 向 `resolve` 回调传递组件定义
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})
异步组件工厂函数也可以返回一个如下格式的对象:

const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})
9. render渲染函数
Vue的模板最终会被编译成render渲染函数。
Vue推荐在绝大多数情况下使用模板来创建HTML,但在某些情况下使用渲染函数更高效。
  1. 举例(render的好处)
<anchored-heading :level="1">Hello world!</anchored-heading>

使用模版(冗余)

<script type="text/x-template" id="anchored-heading-template">
  <h1 v-if="level === 1">
    <slot></slot>
  </h1>
  <h2 v-else-if="level === 2">
    <slot></slot>
  </h2>
  <h3 v-else-if="level === 3">
    <slot></slot>
  </h3>
  <h4 v-else-if="level === 4">
    <slot></slot>
  </h4>
  <h5 v-else-if="level === 5">
    <slot></slot>
  </h5>
  <h6 v-else-if="level === 6">
    <slot></slot>
  </h6>
</script>
Vue.component('anchored-heading', {
  template: '#anchored-heading-template',
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

使用render函数

Vue.component('anchored-heading', {
  render: function (createElement) {
    // "虚拟 DOM”是对由 Vue 组件树建立起来的整个 VNode 树的称呼
    return createElement(  // 虚拟节点 (virtual node)VNode
      'h' + this.level,   // 标签名称
      this.$slots.default // 子元素数组,子节点被存储在组件实例中的 $slots.default 中
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})
  1. 组件树中的所有 VNodes 必须是唯一的,以下的做法是错误的
render: function (createElement) {
  var myParagraphVNode = createElement('p', 'hi')
  return createElement('div', [
    // 错误-重复的 VNodes
    myParagraphVNode, myParagraphVNode
  ])
}

解决:使用工厂函数
render: function (createElement) {
  return createElement('div',
    Array.apply(null, { length: 20 }).map(function () {
      return createElement('p', 'hi')
    })
  )
}
  1. createElement 方法说明
// Vue通过建立一个虚拟 DOM 对真实 DOM 发生的变化保持追踪。
// @returns {VNode}
createElement(
  // 1、{String | Object | Function} 必填项。
  // 一个 HTML 标签字符串,或组件选项对象,或解析上述任何一种的一个 async 异步函数。
  'div',

  // 2、{Object}  可选参数。
  // 一个包含模板相关属性的数据对象,可以在模版中使用这些特性。
  {
  },

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

createElement 第二个参数(数据对象)详解

正如在模板语法中,v-bind:class 和 v-bind:style,会被特别对待一样,在 VNode 数据对象中,下列属性名是级别最高的字段。

{
  // 和`v-bind:class`一样的 API,接收一个字符串、对象、数组(字符串和对象组成的)
  'class': {
    foo: true,
    bar: false
  },
  // 和`v-bind:style`一样的 API,接收一个字符串、对象或对象组成的数组
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 普通的 HTML 特性
  attrs: {
    id: 'foo'
  },
  // 组件 props
  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
    }
  }
})
  1. javascript替换指令

v-if 和 v-for 在render函数里则使用 if、map语句 替换

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>
...
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.')
  }
}

render 函数中没有与 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)
      }
    }
  })
}

事件修饰符

on
  .passive        &
  .capture        !
  .once           ~
  .capture.once   or
  .once.capture   ~!


举例
  on: {
    '!click': this.doThisInCapturingMode,
    '~keyup': this.doThisOnce,
    '~!mouseover': this.doThisOnceInCapturingMode
  }
.stop   
  event.stopPropagation()

.prevent    
  event.preventDefault()

.self   
  if (event.target !== event.currentTarget) return

.enter, .13
  if (event.keyCode !== 13) return

.ctrl, .alt, .shift, .meta
  if (!event.ctrlKey) return (change ctrlKey to altKey, shiftKey, or metaKey, respectively)

示例
on: {
  keyup: function (event) {
    // 如果触发事件的元素不是事件绑定的元素
    // 则返回
    if (event.target !== event.currentTarget) return
    // 如果按下去的不是 enter 键或者
    // 没有同时按下 shift 键
    // 则返回
    if (!event.shiftKey || event.keyCode !== 13) return
    // 阻止 事件冒泡
    event.stopPropagation()
    // 阻止该元素默认的 keyup 事件
    event.preventDefault()
    // ...
  }
}
  1. 插槽
this.$slots.default 访问静态插槽的内容,得到的是一个 VNodes 数组.


render: function (createElement) {
  // `<div><slot></slot></div>`
  return createElement('div', this.$slots.default)
}
this.$scopedSlots.default访问作用域插槽,得到的是一个返回 VNodes 的函数


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

如果要用渲染函数向子组件中传递作用域插槽,可以利用 VNode 数据对象中的 scopedSlots 域
render: function (createElement) {
  return createElement('div', [
    createElement('child', {
      // 在数据对象中传递 `scopedSlots`
      // 格式:{ name: props => VNode | Array<VNode> }
      scopedSlots: {
        default: function (props) {
          return createElement('span', props.text)
        }
      }
    })
  ])
}
  1. JSX
使用Babel插件:用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。

import AnchoredHeading from './AnchoredHeading.vue'
...
new Vue({
  el: '#demo',
  render: function (h) {// 将 h 作为 createElement 的别名是 Vue 生态系统中的一个通用惯例,实际上也是 JSX 所要求的。从 Vue 的 Babel 插件的 3.4.0 版本开始,会在以 ES2015 语法声明的含有 JSX 的任何方法和 getter 中 (不是函数或箭头函数中) 自动注入 const h = this.$createElement,这样就可以去掉 (h) 参数了。对于更早版本的插件,如果 h 在当前作用域中不可用,应用会抛错。
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})
  1. 函数式组件(functional: true)
当使用函数式组件时,该引用将会是 HTMLElement,因为他们是无状态的也是无实例的。
注意:在 2.3.0 之前的版本中,如果一个函数式组件想要接收 prop,则 props 选项是必须的。在 2.3.0 或以上的版本中,可以省略 props 选项,所有组件上的特性都会被自动隐式解析为 prop。

Vue.component('my-component', {
  functional: true,
  props: {
    // ...
  },
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文
  render: function (createElement, context) {
    // ...
  }
})
因为函数式组件只是一个函数,所以渲染开销也低很多。
组件需要的一切都是通过上下文传递,包括
  props:提供所有 prop 的对象
  children: VNode 子节点的数组
  slots: 一个函数,返回了包含所有插槽的对象
  scopedSlots: (2.6.0+) 一个暴露传入的作用域插槽以及函数形式的普通插槽的对象。
  data:传递给组件的数据对象   
  parent:对父组件的引用
  listeners: (2.3.0+) 一个包含了所有在父组件上注册的事件侦听器的对象。这只是一个指向 `data.on`的别名。
  injections: (2.3.0+) 如果使用了inject选项,则该对象包含了应当被注入的属性。

this.$slots.default 更新为 context.children,
this.level 更新为 context.props.level

向子元素或子组件传递数据

在普通组件中,没有被定义为 prop 的特性会自动添加到组件的根元素上,将现有的同名特性替换或与其合并。
然而函数式组件要求显式定义该行为:

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


如果使用基于模板的函数式组件,那么还需要手动添加特性和监听器。因为我们可以访问到其独立的上下文内容,所以可以使用 data.attrs 传递任何 HTML 特性,也可以使用 listeners (即 data.on 的别名) 传递任何事件监听器。
<template functional>
  <button
    class="btn btn-primary"
    v-bind="data.attrs"
    v-on="listeners"
  >
    <slot/>
  </button>
</template>

示例(根据传入 prop 的值来代为渲染更具体的组件)

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

slots().default 和 children 对比

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

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

推荐阅读更多精彩内容

  • 一、了解Vue.js 1.1.1 Vue.js是什么? 简单小巧、渐进式、功能强大的技术栈 1.1.2 为什么学习...
    蔡华鹏阅读 3,323评论 0 3
  • 什么是组件? 组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装...
    youins阅读 9,480评论 0 13
  • 前几天想学学Vue中怎么编写可复用的组件,提到要对Vue的render函数有所了解。可仔细一想,对于Vue的ren...
    kangaroo_v阅读 116,062评论 13 171
  • 本文章是我最近在公司的一场内部分享的内容。我有个习惯就是每次分享都会先将要分享的内容写成文章。所以这个文集也是用来...
    Awey阅读 9,451评论 4 67
  • 本章内容:表单 与 v-model、组件、自定义指令 六、表单 与 v-model 6.1、基本用法 Vue.js...
    了凡和纤风阅读 901评论 1 2