『手写Vue3』getCurrentInstance、provide/inject

337 阅读4分钟

getCurrentInstance

这也是Vue3暴露给用户的一个api,它只能在setup中使用,返回当前组件的instance。我们分别在App和Foo组件的setup中调用。

export const App = {
  name: 'App',
  render() {
    return h('div', {}, [h('p', {}, 'currentInstance demo'), h(Foo)]);
  },

  setup() {
    const instance = getCurrentInstance();
    console.log('App:', instance);
  }
};

export const Foo = {
  name: 'Foo',
  setup() {
    const instance = getCurrentInstance();
    console.log('Foo:', instance);
    return {};
  },
  render() {
    return h('div', {}, 'foo');
  }
};

如何在组件中拿到自己的instance呢,getCurrentInstance不仅不用传参,还限定在setup中调用。考虑使用全局变量currentInstance,在调用setup之前,设置该变量的值。getCurrentInstance只需将其返回即可。

export function getCurrentInstance() {
  return currentInstance;
}

function setCurrentInstance(instance) {
  currentInstance = instance;
}

function setupStatefulComponent(instance: any) {
  const Component = instance.type;

  instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers);

  const { setup } = Component;

  if (setup) {
    // 设置currentInstance,然后再调用setup
    setCurrentInstance(instance);
    const setupResult = setup(shallowReadonly(instance.props), {
      emit: instance.emit
    });
    setCurrentInstance(null);

    handleSetupResult(instance, setupResult);
  }
}

实现完毕,现在可以在控制台看到APP和Foo的instance:

image.png

provide / inject

父组件使用provide传值,子组件使用inject收到值,中间可以跨越很多层,并且provide / inject只能在setup中调用。 有以下难点需要克服:

  • 调用provide后,这些键值对应该存放在哪里,假设它们称为provides。
  • 子组件如何访问父组件的provides,同时子组件provide相同key时,不改变父组件的provides。
  • inject可以传入第二个参数表示默认值,默认值还可以是函数,将拿出它的返回值。

经过之前的学习,不难想象应该给instance添加provides属性。

provide的最简实现:

export function provide(key, value) {
  // 因为用到了getCurrentInstance,所以provide/inject必须在setup中使用
  const currentInstance = getCurrentInstance();

  if (currentInstance) {
    let { provides } = currentInstance;
    provides[key] = value;
  }
}

那么inject的逻辑就应该是,获取父结点的instance,从中取出provides,再用key访问对应的属性。问题来了,怎么获取父结点的instance?

给instance添加属性parent,表示父结点的instance,parent作为第二个参数传给createComponentInstance。随着函数签名的改变,很多函数都需要添加参数parentComponent。

最后到了函数setupRenderEffect中,将instance传入patch,作为parentComponent:

function setupRenderEffect(instance: any, initialVNode, container) {
  const { proxy } = instance;
  const subTree = instance.render.call(proxy);
  // 此处添加第三个参数
  patch(subTree, container, instance);
  
  initialVNode.el = subTree.el;
}

为什么这里传入的instance就是父结点的instance?可以稍微分析一下:

假设Foo是App的子组件,并且App没有其他children:

render -> patch(instance === undefined) -> 处理App的component vnode(生成App的instance) -> patch -> 处理App的element vnode -> mountChildren -> patch(传入App的instance) -> 处理Foo的component vnode(生成Foo的instance,并且拿到父结点的instance) -> patch -> 处理Foo的element vnode -> Foo挂载完毕 -> App挂载完毕

现在的provide/inject已经可以处理相邻父子的传值了,但是爷孙及以上会失败。因为孙只能访问父的provides,访问不到爷上的。

修改provides的初始化方式,父的provides指向爷的provides,子的provides指向父的provides。

export function createComponentInstance(vnode, parent) {
  const component = {
    vnode,
    type: vnode.type,
    setupState: {},
    props: {},
    slots: {},
    // 这里
    provides: parent ? parent.provides : {},
    parent,
    emit: () => {}
  };

但是这样也有问题,因为引用类型,如果父结点使用provide提供了和爷结点相同key的属性,就会修改自己instance的provides,进而修改了爷结点的provides。所以使用原型链,currentInstance.provides = Object.create(parent.provides)。是不是有寄生组合继承内味了?先在当前instance.provides找,找不到就沿着原型链向上找,并且规避了引用类型的影响,修改当前provides也不影响parent。

export function provide(key, value) {
  const currentInstance = getCurrentInstance();

  if (currentInstance) {
    let { provides, parent } = currentInstance;
    const parentProvides = parent.provides;
    // 可能调用多次provide,但只用初始化一次
    // 根据上一个代码块,可知一开始 provides === parentProvides
    if (provides === parentProvides) {
      // 初始化
      provides = currentInstance.provides = Object.create(parentProvides);
    }
    // 后来因为这里修改了,全等关系就被破坏了
    provides[key] = value;
  }
}

最后完成inject可提供默认值的需求,比较简单:

export function inject(key, defaultValue) {
  const currentInstance = getCurrentInstance();

  if (currentInstance) {
    const parentProvides = currentInstance.parent.provides;

    if (key in parentProvides) {
      return parentProvides[key];
    } else if (defaultValue) {
      // 默认值类型是函数,就拿返回值
      if (typeof defaultValue === 'function') {
        return defaultValue();
      }
      return defaultValue;
    }
  }
}

组件:

// 组件 provide 和 inject 功能
import { h, provide, inject } from '../../lib/my-mini-vue.esm.js';

const Provider = {
  name: 'Provider',
  setup() {
    provide('foo', 'fooVal');
    provide('bar', 'barVal');
  },
  render() {
    return h('div', {}, [h('p', {}, 'Provider'), h(ProviderTwo)]);
  }
};

const ProviderTwo = {
  name: 'ProviderTwo',
  setup() {
    provide('foo', 'fooTwo');
    const foo = inject('foo');

    return {
      foo
    };
  },
  render() {
    return h('div', {}, [
      h('p', {}, `ProviderTwo foo:${this.foo}`),
      h(Consumer)
    ]);
  }
};

const Consumer = {
  name: 'Consumer',
  setup() {
    const foo = inject('foo');
    const bar = inject('bar');
    // const baz = inject('baz', 'bazDefault');
    const baz = inject('baz', () => 'bazDefault');

    return {
      foo,
      bar,
      baz
    };
  },

  render() {
    return h('div', {}, `Consumer: - ${this.foo} - ${this.bar}-${this.baz}`);
  }
};

export default {
  name: 'App',
  setup() {},
  render() {
    return h('div', {}, [h('p', {}, 'apiInject'), h(Provider)]);
  }
};