Vue知识点补缺补漏

918 阅读14分钟

image-20210119141418551

最近重读了一遍vue2.0文档,感触颇深。在经过大量业务实践之后,回过头再读它们,会带着全新的视角去认识和理解。

另外在读文档的过程中还发现了一些盲区,有的是以前读文档时没注意一略而过,有的是实际业务开发中用的很少而渐渐忘记了,重读之后,感觉豁然开朗,有一种如汤化雪、水到渠成的舒畅感。

于是趁着这个机会,把一些或常用、或生僻的知识点提取出来,以记之。

1、v-bind、v-on绑定动态属性

在实际开发中,常用的是v-bind、v-on绑定参数值:

<a :href="url">...</a>
<a @click="doSomething">...</a>

但重读文档才发现属性也可以动态绑定,以前倒是一直没注意:

<!-- 动态属性的绑定 (2.6.0+) -->
<a :[key]="url"> ... </a>

<!-- 动态事件的绑定 (2.6.0+) -->
<a @[event]="doSomething"> ... </a>

思考: 仔细想想,感觉应用场景并不多,因为一般html标签的属性并不需要动态更换, 需要什么属性在开发业务的过程都能提前预知并准备好,只要更改属性绑定的值就好了。不过还是mark一下,以便在以后遇到了合适的开发场景,能提供一个很好的解决方案。

2、prop中传入对象的所有属性

在实际开发中,最常用的是传单个属性:

<blog-post :title="title"></blog-post>

有多少属性都一个个的传,但一次性传所有属性也很好用:

// 组件中
<blog-post v-bind="post"></blog-post>

// data中的值
post: {
  id: 1,
  title: 'My Journey with Vue'
}

上面这种也就等价于:

<blog-post
  :id="post.id"
  :title="post.title"
></blog-post>

思考: 在组件上面需要传递很多属性的时候,如果一个个的传,那么组件上会显得特别臃肿,而且每一个属性绑定的动态值都要放在data里,形成getter和setter,增加额外开销,通过传入对象的所有属性就能很好的解决这个问题。此外,配合$attr$listeners等属性使用,成为封装组件的利器

3、prop中类型可以自定义构造函数

vue中组件传递数据的prop,一般使用如下:

props: {
   propA: {
      type: Number,
      default: 100
   } 	
}

其中type指的是传递数据的类型,可以定义原生构造函数中的8种类型之一:

StringNumberBooleanArrayObjectDateFunctionSymbol

但实际上type 还可以是一个自定义的构造函数,如定义一个构造函数:

function Person (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}

props: {
    author: {
      type: Person,
      default: () => new Person('Jeyson', 'Liu')
    }
}

这样在author的值不是构造函数Person的实例的时候,就会验证不通过而报错。

思考: 在自己封装一些组件的时候,需要自定义一些专属的属性,可以通过自定义构造函数,来对传递参数进行检查验证,从而增强组件的健壮性。

4、计算属性缓存 vs 方法的区别

在实际开发中,计算属性和方法常常能达到一样的最终结果,如下:

// 使用计算属性
<p>Reversed message: "{{ reversedMessage }}"</p>
// 使用方法
<p>Reversed message: "{{ reversedMessage() }}"</p>

// 在组件中
data: {
  return {
	message: ''
  }
}
computed: {
  reversedMessage: function () {
    return this.message.split('').reverse().join('')
  }
},
methods: {
  reversedMessage: function () {
    return this.message.split('').reverse().join('')
  }
}

但是它们是有区别的。

  • 计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。
  • 方法是每当触发重新渲染时,调用方法将总会再次执行函数

另外计算属性侦听属性的区别是:

  • 计算属性适合应用在有一些数据需要随着其它数据变动而变动时,需要计算。
  • watch需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。比如当data里某个数据变化了,需要重新像后台发送请求获取数据,或者需要重新执行某个方法,就需要用watch。

思考: 在实际开发的过程中,要灵活的使用着三种方式,虽然大部分情况下多种方式都能实现,但是各自都有各自优劣的地方,理解它们,扬长避短。

5、自定义v-model

组件上v-model默认是value和input事件,如果是单选框和复选框等自定义类型的输入,就需要用model属性改变。

Vue.component('base-checkbox', {
  model: {
    // 用model属性将默认的props:value和event:input改成想要的checked和change
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})

现在在这个组件上使用 v-model 的时候,就可以实现单选框、复选框等自定义双向绑定:

<base-checkbox v-model="lovingVue"></base-checkbox>

6、禁止继承 + $attr来接传递的属性

// 子组件
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>
  `
})

// $attrs即父组件传递的额外参数
// 因为子组件设置了inheritAttrs: false, 所以父组件传递的额外参数不会落在根元素上
// 而是进入了$attrs里面,可以你让你决定这些 attribute 会被赋予哪个元素上。
// 注:style 和 class 属性依然还是在根元素上
// 父组件
<base-input
  v-model="username"
  required
  placeholder="Enter your username"
></base-input>

思考: 封装组件很好用,可以接父组件传递的额外参数,尤其是针对原生元素进行封装。

7、.native + $listeners来接监听的事件

组件的根元素上直接监听一个原生事件,用 .native 修饰符:

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

但是.native只作用在子组件的根元素上,如果根元素没有这个事件,则是无效的。

<label>
  {{ label }}
  <input
    v-bind="$attrs"
    v-bind:value="value"
    v-on:input="$emit('input', $event.target.value)"
  >
</label>

如果你想在父组件调用.native是作用在子组件里的某个元素上,可以使用Vue 提供了 $listeners 属性,它是一个对象,里面包含了作用在这个组件上的所有监听器。例如:

{
  focus: function (event) { /* ... */ }
  input: function (value) { /* ... */ },
}

如同给子组件内指定的元素继承属性$attrs一样,通过$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>
  `
})

在子组件想要监听事件的元素上中通过v-on="YourListenersObj",绑定一个对象,里面是从父组件添加的监听事件,这个对象可以通过computed计算属性里面处理一些逻辑后返回(比如覆写一些监听器行为,防止被父组件添加的覆盖),拿父组件监听事件通过$listeners属性。如果不需要处理逻辑,则可以直接v-on="$listeners"。

8、.sync对prop 进行“双向绑定”

在有些情况下,我们可能需要对一个 prop 进行“双向绑定”,可以通过.sync修饰符来实现。

// 子组件text-document中
export default {
  props: {
    title: {
      type: String,
      default: ''
    }
  },
  methods: {
    // 在需要修改props中title的值的时候,调用update:myPropName方法
    handleChangeEvent () {
      this.$emit('update:title', 'testTitle')
    }
  }
}

然后在父组件中:

<text-document :title.sync="doc.title"></text-document>

.sync修饰符写法是简写,这样写实际上等价于:

<text-document
  :title="doc.title"
  :update:title="doc.title = $event"
></text-document>

这种双向绑定的思路实际上和v-model很类似。实际上都是子组件通过$emit向上通知父组件去改动数据。

当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:

<text-document v-bind.sync="doc"></text-document>

这样会把 doc 对象中的每一个 property (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器。

9、插槽基础

#具名插槽

有时我们需要多个插槽,通过name属性具名。

<div class="container">
  <header>
    <!-- 我们希望把页头放这里 -->
    <slot name="header"></slot>
  </header>
  <main>
    <!-- 我们希望把主要内容放这里 -->
    <slot></slot>
  </main>
  <footer>
    <!-- 我们希望把页脚放这里 -->
    <slot name="footer"></slot>
  </footer>
</div>

一个不带 nameslot 出口会带有隐含的名字“default”。

在父组件使用插槽的时候通过在<template>元素上使用v-slot:slotName的方式(可缩写成 #slotName):

<base-layout>
  <!-- 这里面的内容在头部 -->
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <!-- 没有指定slot名字的,默认放在default部分的slot里 -->
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <!-- 这里面的内容在底部 -->
  <!-- 可缩写成#footer,注:如果使用缩写,后面必须有参数,如#default才行 -->
  <template #footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

注意 v-slot 只能添加在 <template>,除非只有一个默认插槽。

#作用域插槽

插槽内容和子组件里的数据是各自在独立的作用域内,如果想插槽内容可以访问子组件内的数据,可以在子组件的<slot>元素上绑定该数据,如:

<span>
  <!-- 子组件中插槽里绑定了user属性,值为user,默认插槽内是具体数据 -->
  <!-- 绑定在 <slot> 元素上的 attribute 被称为 插槽prop -->
  <slot :user="user">
    {{ user.lastName }}
  </slot>
</span>

在父组件中引用时,可以定义插槽prop来取子组件提供的属性值:

<current-user>
  <!-- 将包含所有 插槽prop 的对象命名为 slotProps,这个可以自定义名字 -->
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>

因为只有一个默认插槽,可以简洁写法:

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

作用域插槽的内部工作原理是将插槽内容包裹在一个拥有单个参数的函数里:

function (slotProps) {
  // 插槽内容
  // slotProps即包含所有 插槽prop 的对象,也就是形参,可以自由命名
}

所以形参slotProps也可以通过es6来解构:

<current-user v-slot="{ user, detail, info: newInfo }">
  {{ user.firstName + newInfo }}
</current-user>

另外可以实现动态插槽

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>

注:以上插槽用法是2.6.0版本后更新的,有一些ui组件或者插件还是之前废弃的写法,大致如下:

具名插槽使用:

<template slot="header">
  <h1>Here might be a page title</h1>
</template>
<!-- 或者直接作用在元素上 -->
<h1 slot="header">Here might be a page title</h1>

作用域插槽:

<template slot="header" slot-scope="slotProps">
  {{ slotProps.msg }}
</template>

<!-- 或者直接作用在元素上,如果是default插槽可以简写 -->
<span slot-scope="slotProps">
  {{ slotProps.msg }}
</span>

10、插槽妙用:复用模版

例如,我们要实现一个 todo-list 组件,它是一个列表且包含布局和过滤逻辑:

<ul>
  <li v-for="todo in filteredTodos" :key="todo.id">
    {{ todo.text }}
  </li>
</ul>

我们可以在每个li元素里面写一个同名插槽,并传递插槽prop出去,在父组件对其进行统一控制:

<ul>
  <li v-for="todo in filteredTodos" :key="todo.id">
    <!-- 每个todo里面都有一个同名插槽,并绑定了数据向外传递。-->
    <slot name="todo" :todo="todo">
      <!-- 后备内容 -->
      {{ todo.text }}
    </slot>
  </li>
</ul>

然后在父组件使用时为所有同名插槽定义一个不一样的 <template> 作为替代方案,并且可以从子组件获取传递过来的数据:

<todo-list :todos="todos">
  <template #todo="{ todo }">
    <span v-if="todo.isComplete"></span>
    {{ todo.text }}
  </template>
</todo-list>

这个同名插槽的 <template> 模版将插入子组件所有同名的插槽里,并且渲染不同的数据。

因此,可以设置不同名字的插槽,设置不同的模版:

<!-- 子组件 -->
<ul>
  <li v-for="(item, index) in list" :key="item.id + index">
     <slot name="slotTitle" :todo="item">
        {{ item.title }}
     </slot>
     <slot name="slotData" :todo="item">
        {{ item.data }}
     </slot>         
  </li>
</ul>

<!-- 父组件 -->
<todo-list>
  <template #slotTitle="{ todo }">
    <span v-if="todo.isComplete"></span>
    {{ todo.title }}
  </template>
  <template #slotData="{ todo }">
    {{ todo.data }}
  </template>
</todo-list>

11、依赖注入provide、inject

场景:父组件下有多个子组件,并且子组件内可能嵌套了子组件。此时多个子组件内需要访问父组件的一些数据/方法,可以尝试使用依赖注入:

// 在父组件内,通过provide提供一些数据/方法
provide: function () {
  // 注:这里需要使用function ()的形式,如果使用() => 箭头函数,则this执行的不是本组件实例
  return {
    getData: this.getData
  }
},
// 另外如果是直接一个对象,里面的this是调用者的this,也就是子组件调用,则this是子组件
// 因此最好是通过function 返回一个对象
provide: {
  getData () {
    console.log(this)
  },
  foo: 123
},
  
methods: {
  getData () {
    console.log(123)
  }
}

// 子组件内,通过inject注入父组件提供的数据或者方法,然后就可以在组件内访问了
// 所以在子组件内可以尝试更新父组件的数据,从而影响到子组件,但这样会造成读起来不容易理解
inject: ['getData'],
methods: {
  test () {
    this.getData()
  }
}

在任何后代组件里,我们都可以使用 inject 选项来接收从父组件提供的数据/方法,实际上,可以把依赖注入看作针对父组件下任何后代组件的“大范围有效的 prop”。

注:使用依赖注入容易使各组件结构耦合,负面影响类似于通过rootroot、parents来访问组件,所以更好的推荐是使用vuex。provideinject 主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。

12、程序化的侦听器去清理自己

$emit可以派发一个事件,然后可以侦听:

  • 通过 $on(eventName, eventHandler) 侦听一个事件
  • 通过 $once(eventName, eventHandler) 一次性侦听一个事件
  • 通过 $off(eventName, eventHandler) 停止侦听一个事件

你通常不会用到这些,但是当你需要在一个组件实例上手动侦听事件时,它们是派得上用场的:

mounted: function () {
  // Pikaday 是一个第三方日期选择器的库
  this.picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })
},
// 在组件被销毁之前,也销毁这个日期选择器。
beforeDestroy: function () {
  this.picker.destroy()
}

这样有两个问题:

1、需要在组件实例data中保存这个picker,如果别的地方不需要调用,则最好不要这样保存,只在生命函数钩子里可以访问到它就好了。

2、创建代码和清理代码相互独立,不利于程序化的去管理。

所以可以通过一个程序化的侦听器解决这两个问题:

mounted: function () {
  var picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })

  // 侦听钩子函数事件beforeDestroy,触发清理
  this.$once('hook:beforeDestroy', function () {
    picker.destroy()
  })
}

这样就可以程序化的清理自己:

mounted: function () {
  this.attachDatepicker('startDateInput')
  this.attachDatepicker('endDateInput')
},
methods: {
  attachDatepicker: function (refName) {
    var picker = new Pikaday({
      field: this.$refs[refName],
      format: 'YYYY-MM-DD'
    })

    this.$once('hook:beforeDestroy', function () {
      picker.destroy()
    })
  }
}

13、递归组件和循环引用

#递归组件

组件是可以在它们自己的模板中调用自身的。不过它们只能通过 name 选项来做这件事,稍有不慎,递归组件就可能导致无限循环:

name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'

类似上述的组件将会导致“max stack size exceeded”错误,所以请确保递归调用是条件性的 (例如使用一个最终会得到 falsev-if)。

#组件循环引用

假设需要构建一个文件目录树,模板是这样的:

<!-- <tree-folder>组件模版 -->
<p>
  <span>{{ folder.name }}</span>
  <tree-folder-contents :children="folder.children"/>
</p>

<!-- <tree-folder-contents>组件模版 -->
<ul>
  <li v-for="child in children">
    <tree-folder v-if="child.children" :folder="child"/>
    <span v-else>{{ child.name }}</span>
  </li>
</ul>

可以看到<tree-folder>组件中引用了<tree-folder-contents>组件,而<tree-folder-contents>组件中又引用了<tree-folder>组件,这样就会出现问题。

解决的方法是:

1、通过 Vue.component 全局注册组件。

2、本地注册组件的时候,你可以使用 webpack 的异步 import

components: {
  TreeFolderContents: () => import('./tree-folder-contents.vue')
}

14、X-Template另一种模板方式

另一个定义模板的方式是在一个 `` 元素中,并为其带上 text/x-template 的类型,然后通过一个 id 将模板引用过去。例如:

<script type="text/x-template" id="hello-world-template">
  <p>Hello hello hello</p>
</script>

Vue.component('hello-world', {
  template: '#hello-world-template'
})

x-template 需要定义在 Vue 所属的 DOM 元素外

注:这些可以用于模板特别大的 demo 或极小型的应用,但是其它情况下请避免使用,因为这会将模板和该组件的其它定义分离开

15、自定义指令directive

除了vue内置的v-modal、v-show之外,可以自定义指令:

// 定义一个自定义指令v-focus
directives: {
  focus: {
    inserted: function (el) {
      el.focus()
    }
  }
}
// 在需要的地方使用
// <input v-focus>

一个指令定义对象可以提供如下几个钩子函数 (均为可选):

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 。

  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

  • unbind:只调用一次,指令与元素解绑时调用。

钩子函数的参数,以下面这个v-demo为例子:

// 这是模版部分
// <div v-demo:foo.a.b="message"></div>

// 这是script部分
directives: {
  demo: {
    bind: function (el, binding, vnode) {
      // 处理一些事情
      console.log(el) // 本例即div元素,指定绑定的元素
      console.log(binding.name) // demo,不包括 v- 前缀的指令名
      console.log(binding.value) // hello,指定的绑定值
      console.log(binding.expression) // message,字符串形式的指令表达式
      console.log(binding.arg) // foo,传给指令的参数
      console.log(binding.modifiers) // {a: true, b: true} 一个包含修饰符的对象
      console.log(vnode) // vue编译生成的虚拟节点
    }
  }
},
data () {
	return {
		message: 'hello'
	}
}
  • el:指令所绑定的元素。
  • binding:一个对象,包含以下 property:
    • name:指令名,不包括 v- 前缀。上例为:
    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
    • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
  • vnode:Vue 编译生成的虚拟节点。
  • oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用。

注:除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的dataset来进行。

16、自定义指令动态参数妙用

v-mydirective:[argument]="value" 中,argument 参数可以根据组件实例数据进行更新。

如下面这个例子把元素动态的固定在距离页面顶部(或者左侧) 200 px的位置:

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

17、渲染函数render

大部分情况下用模版来创建html就好了,但是在某些情况下,需要js完全编程的能力,可以用渲染函数:

// 新建一个js文件 render-test.js
export default {
  render: function (h) {
    return h('h1', this.title)
  },
  props: {
    title: {
      type: String,
      default: '标题'
    }
  }
}

在vue文件中引用:

<template>
  <div>
    <render-test title="测试标题"></render-test>
    <!-- 上面这个组件渲染为 <h1>测试标题</h1> -->
  </div>
</template>

<script>
import RenderTest from './render-test.js'
export default {
  components: { RenderTest }
}
</script>

实际场景如:需要动态创建html标签h1-h6,里面内容通过插槽来传入。

// 新建一个js文件 render-title.js
export default {
  render: function (h) {
    // this.$slots.default是一个数组,里面包含这个元素之间的虚拟节点
    return h('h' + this.level, this.$slots.default)
  },
  props: {
    level: {
      type: Number,
      default: 1,
      required: true,
      validator: (val) => [1, 2, 3, 4, 5, 6].includes(val)
    }
  }
}

在vue文件中引用:

<template>
  <div>
    <render-title :level="3">
      <!-- 这里面的内容即为render-title组件的$slot.default中内容 -->
      <!-- $slot.default是虚拟节点组成的数组 -->
      <i class="title-icon"></i>我是标题
  	</render-title>
    <!-- 上面这个组件渲染为 <h3>【图标】我是标题</h3> -->
  </div>
</template>

<script>
import RenderTitle from './render-title.js'
export default {
  components: { RenderTitle }
}
</script>

渲染函数的实现过程:

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

通过createElement()方法返回的是虚拟dom,createElement方法中的参数如下:

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

  // {Object}
  // 对上述标签/组件属性的描述。可选。
  {
    'class': { foo: true, bar: false }
    // (详情见vue文档中渲染函数部分)
    // https://cn.vuejs.org/v2/guide/render-function.html#%E6%B7%B1%E5%85%A5%E6%95%B0%E6%8D%AE%E5%AF%B9%E8%B1%A1
  },

  // {String | Array}
  // 子级虚拟节点,可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)

上述内容可渲染为:

<div class="foo">
  先写一些文字
  <h1>一则头条</h1>
  <my-component someProp="foobar"></my-component>
  <!-- 这里只是演示,my-component组件将渲染为html -->
</div>

18、用渲染函数重写模版功能

  • 在模板中使用的 v-ifv-for

    <ul v-if="items.length">
      <li v-for="item in items">{{ item.name }}</li>
    </ul>
    <p v-else>No items found.</p>
    

    使用渲染函数重写:

    export default {
      render: function (h) {
        if (this.items.length) {
          return h('ul', this.items.map(item => {
            return h('li', item.name)
          }))
        } else {
          return h('p', 'No items found.')
        }
      },
      props: {
        items: {
          type: Array,
          default: [
            { name: 'one' },
            { name: 'two' }
          ]
        }
      }
    }
    
  • 使用渲染函数重写v-model:

    export default {
      props: ['value'],
      render: function (h) {
        const self = this
        return h('input', {
          domProps: {
            value: self.value
          },
          on: {
            input: function (event) {
              self.$emit('input', event.target.value)
            }
          }
        })
      }
    }
    
    // 使用
    // <render-input :value="message" @input="message = $event"></render-input>
    
  • 渲染函数中的事件&修饰符

    对于 .passive.capture.once 这些事件修饰符,Vue 提供了相应的前缀可以用于 on

    修饰符前缀
    .passive&
    .capture!
    .once~
    .capture.once.once.capture~!

    例如:

    on: {
      '!click': this.doThisInCapturingMode,
      '~keyup': this.doThisOnce,
      '~!mouseover': this.doThisOnceInCapturingMode
    }
    

    对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:

    修饰符处理函数中的等价操作
    .stopevent.stopPropagation()
    .preventevent.preventDefault()
    .selfif (event.target !== event.currentTarget) return
    按键: .enter, .13if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码)
    修饰键: .ctrl, .alt, .shift, .metaif (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKeyshiftKey 或者 metaKey)

    这里是一个使用所有修饰符的例子:

    on: {
      keyup: function (event) {
        // 如果触发事件的元素不是事件绑定的元素
        // 则返回
        if (event.target !== event.currentTarget) return
        // 如果按下去的不是 enter 键或者
        // 没有同时按下 shift 键
        // 则返回
        if (!event.shiftKey || event.keyCode !== 13) return
        // 阻止 事件冒泡
        event.stopPropagation()
        // 阻止该元素默认的 keyup 事件
        event.preventDefault()
        // ...
      }
    }
    
  • 渲染函数中的插槽和插槽作用域

    使用this.$slots访问静态插槽的内容,每个插槽都是一个 VNode 数组。

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

    使用this.$scopedSlots访问作用域插槽,每个作用域插槽都是一个返回若干 VNode 的函数。

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

    如果要用渲染函数向子组件中传递作用域插槽,可以利用描述节点对象中的 scopedSlots 字段:

    render: function (createElement) {
      // <div><child v-slot="props"><span>{{ props.text }}</span></child></div>
      return h('div', [
        h('child', {
          scopedSclots: {
            default: function (props) {
              return h('span', props.text)
            }
          }
        })
      ])
    }
    

19、JSX语法

写render函数通常没有写模版直观简单,所以有个babel插件用来在vue中写jsx语法,让我们更接近模版的写法:

import AnchoredHeading from './AnchoredHeading.vue'

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

通过在render函数中返回一个类似模版的写法,这种就是jsx语法。

20、函数式组件

上述渲染函数可以像正常的组件一样拥有各种属性和生命周期:

export default {
  // functional: true,
  props: ['title'],
  data () {
    return {
      test: '11111'
    }
  },
  created () {
    console.log('created')
  },
  mounted () {
    console.log('mounted')
    console.log(this.test)
  },
  render: function (h, context) {
    // 注:只有上面设置了functional: true,这里context才有值,否则是undefined
    console.log(context)
    return h('div', '233333')
  }
}

但是如果它只是一个接受一些 prop,没有响应数据,没有实例,这样的场景下可以使用函数式组件,将组件标记为 functional

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

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

  • props:提供所有 prop 的对象
  • children:VNode 子节点的数组
  • slots:一个函数,返回了包含所有插槽的对象
  • scopedSlots:(2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
  • data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
  • parent:对父组件的引用
  • listeners:(2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。
  • injections:(2.3.0+) 如果使用了 inject选项,则该对象包含了应当被注入的 property。

这种函数式组件的好处是:因为函数式组件只是函数,所以渲染开销也低很多

21、开发插件

Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象。通过全局方法 Vue.use() 使用插件。

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

如下面这个例子:

// 引入组件模版
import FullLoadingComponent from './FullLoading'

const fullLoading = {
  install: Vue => {
    // 创建组件构造器
    const FullLoadingConstructor = Vue.extend(FullLoadingComponent)
    // 实例化组件
    const instance = new FullLoadingConstructor()
    // 将组件挂载到div上
    instance.$mount(document.createElement('div'))
    // 将div元素放到body里
    document.body.appendChild(instance.$el)

    // 给vue绑定全局对象$fullLoading 提供两个方法show、hide
    Vue.prototype.$fullLoading = {
      show: () => {
        instance.show()
      },
      hide: () => {
        instance.hide()
      }
    }
  }
}
export default fullLoading

调用该插件:

import Vue from 'vue'
import FullLoadingComponent from '@/plugins/FullLoading'

Vue.use(FullLoadingComponent) // 这样就在Vue上绑定了 $fullLoading 提供的两个方法show、hide

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