# 处理边界情况

# 访问元素和组件

在绝大多数情况下,我们最好不要访问另一个组件实例内部或手动操作 DOM 元素。但某些情况做这些事情是合适的。

# $root访问根实列

new Vue实例的子组件中,子组件内部可以通过$root属性访问根实例的方法、属性、计算属性等。

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

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

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

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

实例

<div id="app">
  <p>foo: {{ foo }} </p>
  <p>baz: {{ baz }} </p>
  <component-a></component-a>
</div>
<script>
  Vue.component('component-a', {
    methods: {
      doSomething: function(event) {
        alert(this.$root.foo);
        this.$root.foo = 100;
        this.$root.bar();
        setTimeout(()=> {
          alert(this.$root.baz) 
        }, 2000)
      }
    },
    template: `
      <div>
        <button @click="doSomething">执行</button>  
      </div>
    `
  })
  var app = new Vue({
    el: '#app',
    data: {
      foo: 1
    },
    computed: {
      baz: function() {
        return this.foo + 2
      }
    },
    methods: {
      bar: function() {
        alert('bar');
      }
    }
  })
</script>

# $parent访问父组件实列

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

# 访问子组件实列或子元素 ref

下面的例子中app vue实例:

  • 为其子元素p指定了一个ref特性p,在app的实例中可以通过this.$refs.p 访问对应的p元素
  • 为其子组件component-a指定了一个ref特性zxinput,在app的实例中,可以通过this.$refs.zxinput访问该子组件的实例,包括实例子组件实例的属性、方法
  • $refs 只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的“逃生舱”——你应该避免在模板或计算属性中访问 $refs。
<div id="app">
  <p ref="p">这是一个p元素</p>
  <component-a ref="zxinput"></component-a>
  <button @click="bar">点击</button>
</div>
<script>
  Vue.component('component-a', {
    data: function() {
      return {
        foo: 1
      }
    },
    methods: {
      doSomething: function(event) {
        alert('doSomethind');
      }
    },
    template: `
      <button @click="doSomething">执行</button>  
    `
  })
  var app = new Vue({
    el: '#app',
    data: {
    },
    methods: {
      bar: function() {
        alert(this.$refs.p.innerHTML);

        this.$refs.zxinput.doSomething();
        alert(this.$refs.zxinput.foo)
      }
    }
  })
</script>

# provide与inject依赖注入

provide 选项允许我们指定想要提供给后代组件的数据,方法:

provide: function() {
  return {
    getMap: this.getMap
  }
}

然后在后代组件里,都可以使用inject(注入)选项来接收provide提供的属性

inject: ['getMap']

实列:

<div id="app">
  <component-a></component-a>
</div>
<script>
  Vue.component('component-a', {
    inject: ['getMap'],
    methods: {
      doSomething: function(event) {
        alert(this.getMap);
      }
    },
    template: `
      <button @click="doSomething">执行</button>  
    `
  })
  var app = new Vue({
    el: '#app',
    data: {
      getMap: '这是父组件提供的值'
    },
    provide: function() {
      return {
        getMap: this.getMap
      }
    }
  })
</script>

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

# 程序化的事件监听器

除了之前使用的$emit(触发父组件的一个事件), Vue 实例同时在其事件接口中提供了其它的方法

  • $on(eventName, eventHandler) 侦听一个事件
  • $once(eventName, eventHandler) 一次性侦听一个事件(只触发一次,在第一次触发之后移除监听器。)
  • $off(eventName, eventHandler) 停止侦听一个事件
    • 如果没有提供参数,则移除所有的事件监听器
    • 如果只提供了事件,则移除该事件所有的监听器
    • 如果同时提供了事件与回调,则只移除这个回调的监听器。
// 通过实例名监听事件或触发事件
vm.$on('test', function (msg) {
  console.log(msg)
})

vm.$emit('test', 'hi')
// => "hi"


// 子组件触发父组件的方法:
Vue.component('welcome-button', {
  template: `
    <button v-on:click="$emit('welcome')">
      Click me to be welcomed
    </button>
  `
})

<div id="emit-example-simple">
  <welcome-button v-on:welcome="sayHi"></welcome-button>
</div>

# 循环引用

# 递归组件

组件是可以在它们自己的模板中调用自身的。不过它们只能通过 name 选项来做这件事:

<div id="app">
  <component-a></component-a>
</div>
<script>
  Vue.component('component-a', {
    template: `
      <div>
        test
        <component-a></component-a>  
      </div>
    `
    /**
     * 上述的组件将会导致“ Uncaught RangeError: 
     * Maximum call stack size exceeded” 错误
     * 要确保归调用是条件性的 (例如使用一个最终会得到 false 的 v-if)。
     */
  })
  var app = new Vue({
    el: '#app',
    data: {
    }
  })
</script>

实例,递归菜单

<div id="app">
  <menu-a v-bind:menu="menu"></menu-a>
</div>
<script>
  Vue.component('menu-a', {
    props: ['menu'],
    methods: {
      isTrue: function() {
        return this.count-- === 0;
      }
    },
    template: `
      <ul>
        <li v-for="item in menu"">
          {{ item.menuName }}
          <menu-a 
            v-bind:menu="item.menuSubs" 
            v-if="item.menuSubs.length !== 0"
          ></menu-a>
        </li>
      </ul>
    `
  })
  var app = new Vue({
    el: '#app',
    data: {
      menu: [
        { 
          "menuName": '用户管理', 
          "menuSubs": [
            { 
              "menuName": '用户列表', 
              "menuSubs": [
                { "menuName": '用户信息', "menuSubs":[] },
                { "menuName": '用户统计', "menuSubs":[] },
              ] 
            },
            { "menuName": '审核列表', "menuSubs":[] },
            { "menuName": '用户周报', "menuSubs":[] }
          ]
        },
        { 
          "menuName": '客户管理', 
          "menuSubs": [
            { "menuName": '客户列表', "menuSubs":[] },
          ] 
        }
      ]
    }
  })
</script>

6_0_递归组件.png

# 组件之间的循环引用

A组件引用了B,B组件引用了A, 组件在渲染树中互为对方的后代和祖先——一个悖论!当通过 Vue.component 全局注册组件的时候,这个悖论会被自动解开。

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

如果非全局注册,解决方法

  • 1.等到生命周期钩子 beforeCreate 时去注册tree-folder-contents
beforeCreate: function () {
  this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}
  • 2.如果是本地注册组件,可以使用webpack 的异步import来解决
components: {
  TreeFolderContents: () => import('./tree-folder-contents.vue')
}

# 内联模板

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

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

<div id="app">
  <hello-world inline-template>
    <div>
      使用内容的内容作为默认的template  foo: {{ foo }}
    </div>
  </hello-world>
</div>
<script>
  Vue.component('hello-world', {
    data: function() {
      return {
        foo: 1
      }
    }
    // 这里不用定义template属性,
    // 而是直接使用hello-world内联的内容作为template
  })
  var app = new Vue({
    el: '#app',
    data: {
    }
  })
</script>

# x-template

在一个script元素中,并为其带上 text/x-template 的类型,然后通过一个 id 将模板引用过去。

x-template 需要定义在 Vue 所属的 DOM 元素外, 这些可以用于模板特别大的 demo 或极小型的应用,但是其它情况下请避免使用,因为这会将模板和该组件的其它定义分离开。

<div id="app">
  <hello-world></hello-world>
</div>

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

<script>
  Vue.component('hello-world', {
    template: '#hello-world-template'
  })
  var app = new Vue({
    el: '#app',
    data: {
    }
  })
</script>

# 控制更新

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

# 强制更新

如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事。

如果你已经做到了上述的事项仍然发现在极少数的情况下需要手动强制更新,那么你可以通过 $forceUpdate 来做这件事

# 通过v-once创建低开销的静态组件

如果渲染一些不需要动态更新的静态页面,可以通过v-once来说明只渲染一次。

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

不要过度使用这个模式。当你需要渲染大量静态内容时,极少数的情况下它会给你带来便利,除非你非常留意渲染变慢了,不然它完全是没有必要的——再加上它在后期会带来很多困惑。例如,设想另一个开发者并不熟悉 v-once 或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新。

上次更新: 2020/10/29 22:59:19