通过组件父子关系和原型链实现跨层级依赖注入

2025-11-20 17:41:31

在组件开发中,父组件有时需要向后代组件提供一些数据或方法。

如果只通过 props 传递,就需要一层一层地向下传递:

祖先组件 -> 父组件 -> 子组件 -> 后代组件

中间组件即使不使用这些数据,也需要继续声明和转发 props。

provide 和 inject 就是为了解决这个问题:

  • 祖先组件通过 provide 提供依赖
  • 后代组件通过 inject 获取依赖
  • 中间组件不需要关注依赖的传递过程
import { createApp, h, inject, provide } from '../dist/vue.esm.js'

const GrandChild = {
  setup() {
    const theme = inject('theme')

    return () => {
      return h('div', `theme: ${theme}`)
    }
  }
}

const Parent = {
  setup() {
    return () => {
      return h(GrandChild)
    }
  }
}

const App = {
  setup() {
    provide('theme', 'dark')

    return () => {
      return h(Parent)
    }
  }
}

createApp(App).mount('#app')

这里 App 和 GrandChild 之间隔着一层 Parent,但 GrandChild 仍然可以直接拿到 App 提供的 theme。

要实现这个功能,首先需要让组件实例知道自己的父组件。

保存组件父子关系

1. 传递 parentComponent

组件实例是在 mountComponent 中创建的。

之前创建组件实例时,只传入了 vnode;现在需要额外传入当前组件的父组件实例:

function processComponent(
  n1,
  n2,
  container,
  anchor,
  parentComponent
) {
  if (n1 == null) {
    mountComponent(n2, container, anchor, parentComponent)
  }
  else {
    updateComponent(n1, n2)
  }
}

function mountComponent(vnode, container, anchor, parentComponent) {
  const instance = createComponentInstance(vnode, parentComponent)

  vnode.component = instance

  setupComponent(instance)
  setupRenderEffect(instance, container, anchor)
}

创建实例时保存 parent:

export function createComponentInstance(vnode, parent) {
  const { type } = vnode

  const instance = {
    type,
    vnode,
    parent
    // ...
  }

  instance.ctx = {
    _: instance
  }

  return instance
}

这样,每个组件实例都可以通过 instance.parent 找到自己的父组件。

2. 在 patch 链路中传递父组件

组件的 render 会生成 subTree,subTree 中可能继续包含元素节点和子组件。

所以, 在执行 patch 时,需要把当前组件实例作为 parentComponent 继续向下传递:

function setupRenderEffect(instance, container, anchor) {
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      const subTree = renderComponentRoot(instance)

      patch(
        null,
        subTree,
        container,
        anchor,
        instance
      )

      instance.subTree = subTree
      instance.isMounted = true
    }
    else {
      const prevTree = instance.subTree
      const subTree = renderComponentRoot(instance)

      patch(
        prevTree,
        subTree,
        container,
        anchor,
        instance
      )

      instance.subTree = subTree
    }
  }

  // ...
}

普通元素和子节点继续接收 parentComponent:

function processElement(
  n1,
  n2,
  container,
  anchor,
  parentComponent
) {
  if (n1 == null) {
    mountElement(n2, container, anchor, parentComponent)
  }
  else {
    patchElement(n1, n2, parentComponent)
  }
}

function mountElement(vnode, container, anchor, parentComponent) {
  const el = vnode.el = hostCreateElement(vnode.type)
  const { children, shapeFlag } = vnode

  // ...

  if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    mountChildren(el, children, parentComponent)
  }

  hostInsert(el, container, anchor)
}

function mountChildren(container, children, parentComponent) {
  for (let i = 0; i < children.length; i++) {
    const child = children[i] = normalizeVNode(children[i])

    patch(
      null,
      child,
      container,
      null,
      parentComponent
    )
  }
}

当父组件的 render 生成子组件 vnode 时,最终会执行:

patch(null, childVNode, container, anchor, parentComponent)

子组件就可以通过这个参数创建自己的实例:

const childInstance = createComponentInstance(
  vnode,
  parentComponent
)

这样,组件树中的父子关系就建立好了。

实现

1. 保存 appContext

根组件没有父组件,因此 instance.parent 在根组件上是 null。

但是,根组件仍然需要一个应用级上下文,用来保存整个应用共享的配置和依赖。

在 createApp 时创建 appContext:

// packages/runtime-core/src/apiCreateApp.ts
import { h } from './h'

export function createAppAPI(render) {
  return function createApp(rootComponent, rootProps) {
    const context = {
      provides: {}
    }

    const app = {
      _container: null,

      mount(container) {
        const vnode = h(rootComponent, rootProps)

        vnode.appContext = context

        render(vnode, container)
        app._container = container
      },

      unmount() {
        render(null, app._container)
      }
    }

    return app
  }
}
  • 有父组件:继承父组件的 appContext
  • 没有父组件:使用根 vnode 上的 appContext
export function createComponentInstance(vnode, parent) {
  const { type } = vnode

  const appContext = parent
    ? parent.appContext
    : vnode.appContext

  const instance = {
    type,
    vnode,
    parent,
    appContext
    // ...
  }

  instance.ctx = {
    _: instance
  }

  return instance
}

这样,根组件和后代组件都可以访问同一个应用上下文。

2. 使用原型链保存 provides

每个组件都需要有自己的 provides 对象。

同时,子组件应该能够读取父组件提供的依赖,所以可以让子组件的 provides 继承自父组件的 provides:

export function createComponentInstance(vnode, parent) {
  const { type } = vnode

  const appContext = parent
    ? parent.appContext
    : vnode.appContext

  const instance = {
    type,
    vnode,
    parent,
    appContext,
    provides: Object.create(
      parent
        ? parent.provides
        : appContext.provides
    )
  }

  instance.ctx = {
    _: instance
  }

  return instance
}
App
└── Parent
    └── GrandChild

创建实例后,provides 的原型关系是:

grandChild.provides
        ↓ [[Prototype]]
parent.provides
        ↓ [[Prototype]]
appContext.provides

因此,后代组件读取某个 key 时,JavaScript 会沿着原型链向上查找:

  1. 先查找当前组件的 provides
  2. 当前组件没有时,查找父组件的 provides
  3. 继续没有时,查找应用级 provides

3. 实现 provide

// packages/runtime-core/src/apiInject.ts
import { getCurrentInstance } from './component'

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

  instance.provides[key] = value
}

由于当前组件的 provides 已经通过原型链继承了父组件的 provides,直接写入当前对象即可:

instance.provides[key] = value

这样做有两个效果:

  • 当前组件的后代可以读取这个值
  • 不会修改父组件的 provides

4. 实现 inject

inject 同样需要获取当前组件实例,然后从当前组件的父级依赖中查找:

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

  if (!instance) {
    return defaultValue
  }

  const provides = instance.parent
    ? instance.parent.provides
    : instance.appContext.provides

  if (key in provides) {
    return provides[key]
  }

  return defaultValue
}

完整的查找流程是:

inject(key)
  -> 获取当前组件实例
  -> 找到父组件或 appContext
  -> 从 provides 原型链查找 key
  -> 找到则返回对应值
  -> 没找到则返回默认值

小结

  1. processComponent 创建组件实例时保存 parent
  2. parentComponent 需要沿着 patch、元素挂载和子节点挂载流程向下传递
  3. 根组件通过 vnode 获取 appContext
  4. 后代组件通过 parent.appContext 共享应用上下文
  5. 每个组件的 provides 通过原型链继承父级依赖
  6. provide 将值写入当前组件的 provides
  7. inject 从父组件的 provides 中查找依赖