【Vue进阶】 —(你未必知道)如何优雅获取跨层级组件实例(拒绝递归)

3,963 阅读3分钟

ref

ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在自定义组件上,引用就指向该组件实例:

<!-- `vm.$refs.p` will be the DOM node -->
<p ref="p">hello</p>

<!-- `vm.$refs.child` will be the child component instance -->
<child-component ref="child"></child-component>
  • 如何跨层级获取子组件实例 ?

递归查找(平常套路)

A节点要访问E子组件的实例,通过this.children层层递归下一级进行查找。 <br/>
或者F节点要访问A节点的实例,通常的方式是通过this.parent层层递归上一级查找父级实例,但是这样递归的方式代码非常繁琐,包括性能也非常低效。
当你每次去访问实例的时候,都需要走一遍递归的形式。因为实例是不能被缓存的,当你的子组件的实例更新后,父组件并不能及时的知道子组件实例更新了,所以说你每次去使用的时候,都需要这样递归层级去获取。

拒绝递归 (优雅方案)

方案:

  • 主动通知:当子组件实例更新后,主动通知父组件其实例被更新(调用父组件的方法callback)
  • 主动获取:子组件主动通知父组件后,父组件进行缓存子组件的实例,当子组件与子组件需要互相访问实例的时候,直接调用父组件已缓存的对应的子组件实例

一、A节点记录子节点实例:

// A节点

<template>
  <div class="border">
    <h1>A 结点</h1>
    <button @click="getEH3Ref">获取E h3 Ref</button>
    <ChildrenB />
    <ChildrenC />
    <ChildrenD />
  </div>
</template>
<script>
import ChildrenB from "./ChildrenB";
import ChildrenC from "./ChildrenC";
import ChildrenD from "./ChildrenD";
export default {
  components: {
    ChildrenB,
    ChildrenC,
    ChildrenD
  },
  provide() {
    return {
      setChildrenRef: (name, ref) => {
        this[name] = ref;
      },
      getChildrenRef: name => {
        return this[name];
      },
      getRef: () => {
        return this;
      }
    };
  },
  data() {
    return {
      color: "blue"
    };
  },
  methods: {
    getEH3Ref() {
      console.log(this.childrenE);
    }
  }
};
</script>

A节点通过provide通知子节点回调setChildrenRef进行记录子节点各自的实例。
其他子节点可通过getChildrenRef获取其他节点的实例。
子节点通过getRef获取A节点的实例。

二、子节点传递自己的实例

// E节点

<template>
  <div class="border2">
    <h3 v-get-ref="c => setChildrenRef('childrenE', c)">
      E 结点
    </h3>
  </div>
</template>
<script>
export default {
  components: {},
  inject: {
    setChildrenRef: {
      default: () => {}
    }
  }
};
</script>

子节点通过指令v-get-ref第一次绑定到元素时(bind生命周期),执行父级的回调callback: "setChildrenRef",传递自己的实例给父节点A记录下来。

(非常实用) 自定义指令 v-get-ref

// ./utils/getRef

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = {
  install: function install(Vue) {
    var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
    var directiveName = options.name || 'ref';
    Vue.directive(directiveName, {
      bind: function bind(el, binding, vnode) {
        binding.value(vnode.componentInstance || el, vnode.key);
      },
      update: function update(el, binding, vnode, oldVnode) {
        if (oldVnode.data && oldVnode.data.directives) {
          var oldBinding = oldVnode.data.directives.find(function (directive) {
            var name = directive.name;
            return name === directiveName;
          });
          if (oldBinding && oldBinding.value !== binding.value) {
            oldBinding && oldBinding.value(null, oldVnode.key);
            binding.value(vnode.componentInstance || el, vnode.key);
            return;
          }
        }
        // Should not have this situation
        if (vnode.componentInstance !== oldVnode.componentInstance || vnode.elm !== oldVnode.elm) {
          binding.value(vnode.componentInstance || el, vnode.key);
        }
      },
      unbind: function unbind(el, binding, vnode) {
        binding.value(null, vnode.key);
      }
    });
  }
};

自定义指令全局注册

// main.js

import Vue from "vue";
import getRef from './utils/getRef'

Vue.use(getRef, { name: 'get-ref'});

三、获取父节点实例、获取兄弟节点的实例

// F 结点

<template>
  <div class="border2">
    <h3>F 结点</h3>
    <button @click="getARef">获取A Ref</button>
    <button @click="getHRef">获取H Ref</button>
  </div>
</template>
<script>
export default {
  components: {},
  inject: {
    getParentRef: {
      from: "getRef",
      default: () => {}
    },
    getParentChildrenRef: {
      from: "getChildrenRef",
      default: () => {}
    }
  },
  methods: {
    getARef() {
      console.log(this.getParentRef());
    },
    getHRef() {
      console.log(this.getParentChildrenRef("childrenH"));
    }
  }
};
</script>

getARef F节点获取父节点A的实例
getHRef F节点获取H节点的实例