🤯vue3核心源码剖析(八)

341 阅读3分钟

🚀Vue3 getCurrentInstance以及provide&inject的实现

getCurrentInstance 的实现

getCurrentInstance 可以获取到内部函数的实例

注意:只能在 setup 或生命周期中使用

基于官网描述的特性,我们来试一下实现它。

既然只能在 setup 内部使用,我们自然联想到之前章节setupStatefulComponent(),它内部调用了instance.type.setup()函数

function setupStatefulComponent(instance: any) {
  const Component = instance.type
  instance.proxy = new Proxy({ _: instance } as Data, publicInstanceProxyHandlers)
  const { setup } = Component
  if (setup) {
    const setupResult = setup(shallowReadonly(instance.props), {
      emit: instance.emit,
    })

    handleSetupResult(instance, setupResult)
  }
  finishComponentSetup(instance)
}

为了提升该对象的公用性,我们在全局定义一个currentInstance变量

export let currentInstance = null
// 获取当前实例
export function getCurrentInstance() {
  return currentInstance
}
// 设置当前实例
function setCurrentInstance(instance: any) {
  currentInstance = instance
}

setCurrentInstance的调用时机决定了currentInstance当前指向哪一个组件实例。

那什么时候设置setCurrentInstance为当前组件实例最好呢?

其实是确保instance.type.setup有值并且在instance.type.setup调用之前这个时机就是最佳时机。

function setupStatefulComponent(instance: any) {
  const Component = instance.type
  instance.proxy = new Proxy({ _: instance } as Data, publicInstanceProxyHandlers)
  const { setup } = Component
  if (setup) {
    // currentInstance设置为instance
    setCurrentInstance(instance)
    const setupResult = setup(shallowReadonly(instance.props), {
      emit: instance.emit,
    })
    handleSetupResult(instance, setupResult)
    // 让currentInstance变为null
    setCurrentInstance(null)
  }
  finishComponentSetup(instance)
}

最后我们把currentInstance变为 null,其实是为了使 currentInstance 在 setup 内部有值,这遵循了官方给出的特性。

这就是 getCurrentInstance 的实现。

provide & inject 实现

父组件和子组件之间共享数据我们可以通过很多种方式:

  1. vuex
  2. propsemit
  3. 全局对象globalProperties
  4. parent & 模板 ref

注意:eventbus 在 vue3 中已不再支持,需要自己手写实现。废除了$on$children、$listeners

那如果子组件和祖先组件数据如何共享?props 一层一层的传递?emit 一层一层的暴露方法给父组件?这未免也太不优雅了吧!

provideinject是这种应用场景的解决方案。

如何实现 provide 和 inject?

下面我们把使用 provide 的组件称为提供者,把使用 inject 的组件称为接收者

把提供者实例上的 provide 属性作为一个容器,这个容器就是提供给接收者的共享数据。

接收者如何拿到提供者的共享数据呢?

可以在接收者实例上添加一个 parent 字段,用来指定该组件实例的父组件实例是谁,从而拿到父组件实例的身上的 provide 这个容器。

在创建组件实例的时候为 instance 新增两个属性(provides、parent)

export function createComponentInstance(vnode: any, parentComponent: any) {
  const type = vnode.type
  const instance = {
    vnode,
    type,
    render: null,
    setupState: {},
    props: {},
    emit: () => {},
    slots: {},
    provides: {} as Record<string, any>, // 新增
    parent: parentComponent, // 新增  父组件的组件实例
  }
  instance.emit = emit.bind(null, instance) as any
  return instance
}

根据官方的描述和例子,provide 接收两个参数namevalue,name 用于标识那些提供给子组件的数据,value 就是我们要对外提供的数据。

这里我把instance.provides初始化为一个对象,之所以选择用对象作为容器是因为 provide 具有键值关系。

provide把要提供的数据存储起来,所以大体上provide的实现如下:

export function provide<T>(key: string | number, value: T) {
  // 提供者

  const currentInstance: any = getCurrentInstance()
  if (currentInstance) {
    let { provides } = currentInstance
    provides[key] = value
  }
}

provide已经实现了,那么inject他就是一个从容器中拿取数据的一个过程,不过这个容器要在 parent(父组件)上获取父组件的 provides。

export function inject<T>(key: string, defaultValue?: unknown) {
  // 接收者
  // 在哪里拿value呢?在instance的parent上面获取到父组件的instance然后点出provide
  const currentInstance: any = getCurrentInstance()
  if (currentInstance) {
    const parentProvides = currentInstance.parent.provides

    return parentProvides[key]
  }
}

此时我在这里准备的 demo 就已经可以使用 provide 和 inject 完成父子组件的数据传参了。

// 提供者
const Provider = {
  name: 'Provider',
  setup() {
    provide('foo', 'fooVal')
    provide('bar', 'barVal')
  },
  render() {
    return h('div', {}, [h('p', {}, 'Provider'), h(Consumer)])
  },
}
// 接收者
const Consumer = {
  name: 'Consumer',
  setup() {
    const fooVal = inject('foo')
    const barVal = inject('bar')
    return {
      fooVal,
      barVal,
    }
  },
  render() {
    return h('div', {}, `Consumer-${this.fooVal}-${this.barVal}`)
  },
}

XdJRrF.md.png

当然,不一定是父子组件这种关系这么简单,它可以是爷爷和孙子组件的关系、太爷爷和太孙子组件的关系......,这种情况给你如何处理?比如下面的情况:

我在 Provider 和 Consumer 中间加了一层组件叫ProviderTwo,用来模拟跨组件数据共享这样的情景。我们依然沿用之前的逻辑,发现Consumer的 foo 和 bar 为 undefined。

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() {},
  render() {
    return h('div', {}, [h('p', {}, `ProviderTwo`), h(Consumer)])
  },
}
const Consumer = {
  name: 'Consumer',
  setup() {
    const fooVal = inject('foo')
    const barVal = inject('bar')
    return {
      fooVal,
      barVal,
    }
  },
  render() {
    return h('div', {}, `Consumer-${this.fooVal}-${this.barVal}`)
  },
}

XdNaDO.md.png

这是为什么呢?其实 Consumer 的父组件 ProviderTwo 并没有给 provide 属性提供数据,是个空对象。Consumer 在使用 inject 的时候拿了 ProviderTwo 的空对象,结果当然为 undefined。

export function createComponentInstance(vnode: any, parentComponent: any) {
  const type = vnode.type
  const instance = {
    vnode,
    type,
    render: null,
    setupState: {},
    props: {},
    emit: () => {},
    slots: {},
    provides: {} as Record<string, any>,
    parent: parentComponent, // 父组件的组件实例
  }
  instance.emit = emit.bind(null, instance) as any
  return instance
}

我能想到的就,provides 不再指向空对象,而是指向上一级父组件的 provides,一层一层的指向父组件的 provides,直到没有父组件为止。

我们来改写一下createComponentInstance()

export function createComponentInstance(vnode: any, parentComponent: any) {
  const type = vnode.type
  const instance = {
    vnode,
    type,
    render: null,
    setupState: {},
    props: {},
    emit: () => {},
    slots: {},
    provides: parentComponent ? parentComponent.provides : ({} as Record<string, any>), // 确保中间层的组件没有提供provide时,子组件拿最近的有provide的父组件的数据
    parent: parentComponent, // 父组件的组件实例
  }
  instance.emit = emit.bind(null, instance) as any
  return instance
}

这样就解决了。 XdU1L8.md.png

需求再次升级,这次我们想在 ProviderTwo 组件内使用 provide 和 inject,期望是:ProviderTwo 组件能接收 Provider 的 provide 数据,Consumer 能接收 ProviderTwo 的 provide 数据。

export 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')
    provide('bar', 'barTwo')
    // 期望得到provider的foo---fooVal,实际上得到的是fooTwo
    const foo = inject('foo')
    const bar = inject('bar')
    return {
      foo,
      bar,
    }
  },
  render() {
    return h('div', {}, [h('p', {}, `ProviderTwo-${this.foo}-${this.bar}`), h(Consumer)])
  },
}
const Consumer = {
  name: 'Consumer',
  setup() {
    const fooVal = inject('foo')
    const barVal = inject('bar')
    return {
      fooVal,
      barVal,
    }
  },
  render() {
    return h('div', {}, `Consumer-${this.fooVal}-${this.barVal}`)
  },
}

XdYhy8.md.png

我们期望 ProviderTwo 的 foo 和 bar 应该是 Provider 所提供的 fooVal 和 barVal,现实却是 fooTwo 和 barTwo。这是为什么呢?

原因是:在createComponentInstance()的时候 instance 的 provides 是直接指向父组件的 provides,而 ProviderTwo 组件中 provides 被重新赋值为 fooTwo 和 barTwo,又因为 provides 是引用类型,所以它事实上间接改变了父组件的 provides 的值。

举个栗子 🌰:

let father = {
  foo: 'fooVal',
}
let obj3 = {
  provides: father,
}

obj3.provides['foo'] = 'changed'

console.log(father.foo) // output: changed

那么如何解决这个问题呢?

我们可以用原型链的思想,为当前的组件的 provides 创建一个原型链,原型对象指向父组件的 provides。这样就不必担心对象的引用问题,当前组件的 provides 没有该数据的时候,他会沿着原型链向上寻找该数据,知道找不到为止。如下:我用Object.create创建一个原型对象是 father 的对象。

let father = {
  foo: 'fooVal',
}
let obj3 = {
  provides: father,
}

obj3.provides = Object.create(father)

obj3.provides['foo'] = 'changed'

console.log(father.foo) // output: fooVal
console.log(obj3.provides['foo']) // output: changed

用这种思想解决 provide 的问题,代码将会是如下:

export function provide<T>(key: string | number, value: T) {
  // 提供者
  // key和value存在哪呢?挂在instance的provides属性上吧!

  const currentInstance: any = getCurrentInstance()
  if (currentInstance) {
    let { provides } = currentInstance
    const parentProvides = currentInstance.parent?.provides
    if (provides === parentProvides) {
      // 把provide原型指向父组件的provide
      provides = currentInstance.provides = Object.create(parentProvides)
    }
    provides[key] = value
  }
}

❗ 里面的判断条件provides === parentProvides是为了避免重复使用 provide 造成组件实例的 provides 被初始化。

这样 provide 和 inject 就实现了! Xd4NtA.md.png

如果你还想实现 inject 的默认值功能,代码将会是如下:

export function inject<T>(key: string, defaultValue?: T) {
  // 接收者
  // 在哪里拿value呢?在instance的parent上面获取到父组件的instance然后点出provide
  const currentInstance: any = getCurrentInstance()
  if (currentInstance) {
    const parentProvides = currentInstance.parent.provides

    if (key in parentProvides) {
      return parentProvides[key]
    } else {
      // 找不到注入的
      // 如果默认值是函数,执行函数
      if (isFunction(defaultValue)) {
        return defaultValue()
      }
      return defaultValue
    }
  }
}

对了,这个默认值功能支持传入一个返回默认值的函数。

用法:

let injectValue = inject('foo', () => 'this is default value')

最后肝血阅读,栓 Q!