Vue学习笔记9-深刻了解组件3-动态组件、异步组件、边界处理

前言

这块基本参照官方文档与API文档进行知识点整理。vue

动态组件

咱们以前曾经在一个多标签的界面中使用 is attribute 来切换不一样的组件(在学习笔记4中)webpack

<component v-bind:is="currentTabComponent"></component>
复制代码

当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以免反复重渲染致使的性能问题。web

每次切换新标签的时候,Vue 都建立了一个新的 currentTabComponent 实例。致使若是切换选项卡,不会保存以前选项卡中的选择结果。api

从新建立动态组件的行为一般是很是有用的,可是在这个案例中,咱们更但愿那些标签的组件实例可以被在它们第一次被建立的时候缓存下来。为了解决这个问题,咱们能够用一个 <keep-alive> 元素将其动态组件包裹起来。数组

<!-- 失活的组件将会被缓存!-->
<keep-alive>
  <component v-bind:is="currentTabComponent"></component>
</keep-alive>
复制代码

如今这个 Posts 标签保持了它的状态 (被选中的文章) 甚至当它未被渲染时也是如此。缓存

注意这个<keep-alive>要求被切换到的组件都有本身的名字,不管是经过组件的 name 选项仍是局部/全局注册。它自身不会渲染一个 DOM 元素,也不会出如今组件的父组件链中。bash

主要用于保留组件状态或避免从新渲染。服务器

<!-- 基本 -->
<keep-alive>
  <component :is="view"></component>
</keep-alive>

<!-- 多个条件判断的子组件 -->
<keep-alive>
  <comp-a v-if="a > 1"></comp-a>
  <comp-b v-else></comp-b>
</keep-alive>

<!-- 和 `<transition>` 一块儿使用 -->
<transition>
  <keep-alive>
    <component :is="view"></component>
  </keep-alive>
</transition>
复制代码

注意,<keep-alive> 是用在其一个直属的子组件被开关的情形。若是你在其中有 v-for 则不会工做。若是有上述的多个条件性的子元素,<keep-alive> 要求同时只有一个子元素被渲染。app

异步组件

在大型应用中,咱们可能须要将应用分割成小一些的代码块,而且只在须要的时候才从服务器加载一个模块。(公司实习中这样的问题更为常见了)异步

Vue 容许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件须要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供将来重渲染。

Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // 向 `resolve` 回调传递组件定义
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})
复制代码

如你所见,这个工厂函数会收到一个 resolve 回调,这个回调函数会在你从服务器获得组件定义的时候被调用。你也能够调用 reject(reason) 来表示加载失败。这里的 setTimeout 是为了演示用的,如何获取组件取决于你本身。一个推荐的作法是将异步组件和 webpack 的 code-splitting 功能一块儿配合使用。

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

你也能够在工厂函数中返回一个 Promise,因此把 webpack 2 和 ES2015 语法加在一块儿,咱们能够这样使用动态导入

Vue.component(
  'async-webpack-example',
  // 这个动态导入会返回一个 `Promise` 对象。
  () => import('./my-async-component')
)

当使用局部注册的时候,你也能够直接提供一个返回 Promise 的函数

new Vue({
  // ...
  components: {
    'my-component': () => import('./my-async-component')
  }
})
复制代码

处理加载状态

异步组件工厂函数也能够返回一个以下格式的对象

const AsyncComponent = () => ({
  // 须要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展现加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 若是提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})
复制代码

处理边界状况

访问元素与组件

在绝大多数状况下,咱们最好不要触达另外一个组件实例内部或手动操做 DOM 元素

访问根实例

在每一个 new Vue 实例的子组件中,其根实例能够经过 $root property 进行访问。

// Vue 根实例
new Vue({
  data: {
    foo: 1
  },
  computed: {
    bar: function () { /* ... */ }
  },
  methods: {
    baz: function () { /* ... */ }
  }
})

全部的子组件均可以将这个实例做为一个全局 store 来访问或使用。

// 获取根组件的数据
this.$root.foo

// 写入根组件的数据
this.$root.foo = 2

// 访问根组件的计算属性
this.$root.bar

// 调用根组件的方法
this.$root.baz()
复制代码

这个模式扩展到中大型应用来讲就否则了。所以在绝大多数状况下,咱们强烈推荐使用 Vuex 来管理应用的状态。

访问父级组件实例

$root 相似,$parent property 能够用来从一个子组件访问父组件的实例。它提供了一种机会,能够在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。

在绝大多数状况下,触达父级组件会使得你的应用更难调试和理解,尤为是当你变动了父级组件的数据的时候。当咱们稍后回看那个组件的时候,很难找出那个变动是从哪里发起的。慎用!慎用!慎用!

在一些可能适当的时候,你须要特别地共享一些组件库。举个例子,在和 JavaScript API 进行交互而不渲染 HTML 的抽象组件内,诸如这些假设性的 Google 地图组件同样

<google-map>
  <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map>
复制代码

这个 <google-map> 组件能够定义一个 map property,全部的子组件都须要访问它。在这种状况下 <google-map-markers>可能想要经过相似 this.$parent.getMap 的方式访问那个地图,以便为其添加一组标记。

经过这种模式构建出来的那个组件的内部仍然是容易出现问题的。

设想一下咱们添加一个新的 <google-map-region> 组件,当 <google-map-markers> 在其内部出现的时候,只会渲染那个区域内的标记。

<google-map>
  <google-map-region v-bind:shape="cityBoundaries">
    <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
  </google-map-region>
</google-map>
复制代码

那么在 <google-map-markers> 内部你可能发现本身须要一些相似这样的 hack:

var map = this.$parent.map || this.$parent.$parent.map
复制代码

很快它就会失控。咱们针对须要向任意更深层级的组件提供上下文信息时推荐依赖注入

依赖注入

使用 $parent property 没法很好的扩展到更深层级的嵌套组件上。这也是依赖注入的用武之地,它用到了两个新的实例选项:provide 和 inject。使用它们子组件就可使用父组件的数据和方法辣!

  • provide 选项容许咱们指定咱们想要提供给后代组件的数据/方法

在这个例子中,就是 内部的 getMap 方法

provide: function () {
  return {
    getMap: this.getMap
  }
}
复制代码
  • 而后在任何后代组件里,咱们均可以使用 inject 选项来接收指定的咱们想要添加在这个实例上的 property
inject: ['getMap']
复制代码

完整demo代码:

<div id="app">
      <google-map>
        <google-map-marker v-bind:places="vueConfCities"></google-map-marker>
        <!-->须要访问父级的map property<-->
      </google-map>
    </div>

    <script>
      Vue.component("google-map", {
        provide: function() {
          return {
            getMap: this.getMap//咱们指定咱们想要提供给后代组件的方法
          };
        },
        data: function() {
          return {
            map: null
          };
        },
        mounted: function() {
          this.map = new google.maps.Map(this.$el, {
            center: { lat: 0, lng: 0 },
            zoom: 1
          });
        },
        methods: {
          getMap: function(found) {
            var vm = this;
            function checkForMap() {
              if (vm.map) {
                found(vm.map);//获得map数据
              } else {
                setTimeout(checkForMap, 50);
              }
            }
            checkForMap();
          }
        },
        template: '<div class="map"><slot></slot></div>'
      });

      Vue.component("google-map-marker", {
        inject: ["getMap"],//使用 inject 选项来接收指定的咱们想要添加在这个实例上的 property
        props: ["places"],
        created: function() {
          var vm = this;
          vm.getMap(function(map) {
            vm.places.forEach(function(place) {
              new google.maps.Marker({
                position: place.position,
                map: map
              });
            });
          });
        },
        render(h) {
          return null;
        }
      });

      new Vue({
        el: "#app",
        data: {
          vueConfCities: [
            {
              name: "Wrocław",
              position: {
                lat: 51.107885,
                lng: 17.038538
              }
            },
            {
              name: "New Orleans",
              position: {
                lat: 29.951066,
                lng: -90.071532
              }
            }
          ]
        }
      });
    </script>
复制代码

访问子组件实例或子元素

尽管存在 prop 和事件,有的时候你仍可能须要在 JavaScript 里直接访问一个子组件。为了达到这个目的,你能够经过 ref 这个 attribute 为子组件赋予一个 ID 引用。

<base-input ref="usernameInput"></base-input>

如今在你已经定义了这个 ref 的组件里,你可使用:

this.$refs.usernameInput  来访问这个 <base-input> 实例
复制代码

假如咱们的目的是从一个父级组件聚焦一个input输入框,该 <base-input> 组件也可使用一个相似的 ref 提供对内部这个指定元素的访问。

例如base-input这个组件内部长这样:
<input ref="input">

也能够经过其父级组件定义方法:
methods: {
  // 定义在子级中
  // 用来从父级组件聚焦输入框
  focus: function () {
    this.$refs.input.focus()
  }
}
复制代码

这样就容许父级组件经过下面的代码聚焦 <base-input> 里的输入框:

this.$refs.usernameInput.focus()
复制代码

当 ref 和 v-for 一块儿使用的时候,你获得的 ref 将会是一个包含了对应数据源的这些子组件的数组。

注意!$refs 只会在组件渲染完成以后生效,而且它们不是响应式的。这仅做为一个用于直接操做子组件的“逃生舱”——你应该避免在模板或计算属性中访问 $refs

相比 $parent 来讲,这个用法可让咱们在任意后代组件中访问 getMap,而不须要暴露整个 <google-map> 实例。这容许咱们更好的持续研发该组件,而不须要担忧咱们可能会改变/移除一些子组件依赖的东西。同时这些组件之间的接口是始终明肯定义的,就和 props 同样。

然而,依赖注入仍是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的 property 是非响应式的。这是出于设计的考虑,由于使用它们来建立一个中心化规模化的数据跟使用 $root作这件事都是不够好的。若是你想要共享的这个 property 是你的应用特有的,而不是通用化的,或者若是你想在祖先组件中更新所提供的数据,那么这意味着你可能须要换用一个像 Vuex 这样真正的状态管理方案了。

有关Vuex的状态管理学习笔记待填坑。

详细官方API文档可看:provide-inject API官方文档说明

程序化的事件侦听器

$emit 能够被 v-on 侦听,可是 Vue 实例同时在其事件接口中提供了其它的方法。咱们能够:

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

当你须要在一个组件实例上手动侦听事件时,它们是派得上用场的。它们也能够用于代码组织工具。

一种集成一个第三方库的模式
// 一次性将这个日期选择器附加到一个输入框上
// 它会被挂载到 DOM 上。
mounted: function () {
  // Pikaday 是一个第三方日期选择器的库
  this.picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })
},
// 在组件被销毁以前,
// 也销毁这个日期选择器。
beforeDestroy: function () {
  this.picker.destroy()
}
复制代码

这里有两个潜在的问题:

  1. 它须要在这个组件实例中保存这个 picker,若是能够的话最好只有生命周期钩子能够访问到它。这并不算严重的问题,可是它能够被视为杂物。
  2. 咱们的创建代码独立于咱们的清理代码,这使得咱们比较难于程序化地清理咱们创建的全部东西。

应该经过一个程序化的侦听器解决这两个问题:

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

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

使用了这个策略,我甚至可让多个输入框元素同时使用不一样的 Pikaday,每一个新的实例都程序化地在后期清理它本身。

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()//一次性侦听一个事件
    })
  }
}
复制代码

在这个例子中,咱们推荐建立一个可复用的<input-datepicker> 组件

循环引用

递归组件

组件是能够在它们本身的模板中调用自身的。不过它们只能经过 name 选项来作这件事

name: 'unique-name-of-my-component'
复制代码

当你使用 Vue.component 全局注册一个组件时,这个全局的 ID 会自动设置为该组件的 name 选项。

稍有不慎,递归组件就可能致使无限循环

name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'
复制代码

相似上述的组件将会致使“max stack size exceeded”错误,因此请确保递归调用是条件性的 (例如使用一个最终会获得 false 的 v-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>
复制代码

当你仔细观察的时候,你会发现这些组件在渲染树中互为对方的后代和祖先

  • 当经过 Vue.component 全局注册组件的时候,这个悖论会被自动解开。
  • 若是你使用一个模块系统依赖/导入组件,例如经过 webpack 或 Browserify,你会遇到Failed to mount component: template or render function not defined.循环依赖报错

为了解决这个问题,咱们须要给模块系统一个点,在那里“A 反正是须要 B 的,可是咱们不须要先解析 B。”

把 组件设为了那个点。咱们知道那个产生悖论的子组件是 组件,因此咱们会等到生命周期钩子 beforeCreate 时去注册

beforeCreate: function () {
  this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}
复制代码

或者,在本地注册组件的时候,你可使用 webpack 的异步 import

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

这样问题就解决了!

模板定义的替代品

内联模板

当 inline-template 这个特殊的 attribute 出如今一个子组件上时,这个组件将会使用其里面的内容做为模板,而不是将其做为被分发的内容。这使得模板的撰写工做更加灵活。

<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>
俺本身就是模板
复制代码

内联模板须要定义在 Vue 所属的 DOM 元素内。

不过,inline-template 会让模板的做用域变得更加难以理解。因此做为最佳实践,请在组件内优先选择 template 选项或 .vue 文件里的一个 <template> 元素来定义模板。

X-Template

另外一个定义模板的方式是在一个 <script>元素中,并为其带上 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 或极小型的应用,可是其它状况下请避免使用,由于这会将模板和该组件的其它定义分离开。

控制更新

因为拥有Vue 的响应式系统,它始终知道什么时候进行更新 (若是你用对了的话)。

不过仍是有一些边界状况,你想要强制更新,尽管表面上看响应式的数据没有发生改变。也有一些状况是你想阻止没必要要的更新。

强制更新

若是你发现你本身须要在 Vue 中作一次强制更新,99.9% 的状况,是你在某个地方作错了事。(艹太真实了)

你可能尚未留意到数组或对象的变动检测注意事项,或者你可能依赖了一个未被 Vue 的响应式系统追踪的状态。

然而,若是你已经作到了上述的事项仍然发如今极少数的状况下须要手动强制更新,那么你能够经过 $forceUpdate 来作这件事。

经过 v-once 建立低开销的静态组件

渲染普通的 HTML 元素在 Vue 中是很是快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种状况下,你能够在根元素上添加 v-once attribute 以确保这些内容只计算一次而后缓存起来。

Vue.component('terms-of-service', {
  template: `
    <div v-once>
      <h1>Terms of Service</h1>
      ... a lot of static content ...
    </div>
  `
})
复制代码

试着不要过分使用这个模式。当你须要渲染大量静态内容时,极少数的状况下它会给你带来便利,除非你很是留意渲染变慢了,否则它彻底是没有必要的——再加上它在后期会带来不少困惑。例如,设想另外一个开发者并不熟悉 v-once 或漏看了它在模板中,他们可能会花不少个小时去找出模板为何没法正确更新。变得难以维护。

相关文章
相关标签/搜索