27-mini-vue provide-inject

实现 provide-inject 功能

作用

  • provideinject 用于在组件树中传递数据,避免通过 props 层层传递
  • provide 在祖先组件中提供数据,inject 在后代组件中注入数据
  • 常用于插件开发或跨层级组件通信

实现步骤

  1. 初始化 App.js
js 复制代码
// App.js
import { h, provide, inject } from "../lib/guide-mini-vue.esm.js";

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

const Consumer = {
  name: "Consumer",
  render() {
    return h("div", {}, `Consumer: - ${this.foo} - ${this.bar}`);
  },
  setup() {
    const foo = inject("foo");
    const bar = inject("bar");
    return {
      foo,
      bar,
    };
  },
};

export default {
  name: "App",
  setup() {},
  render() {
    return h("div", {}, [h("p", {}, "apiInject"), h(Provider)]);
  },
};
  1. 先实现一个简单的 provide-inject 功能, 存和取,数据放在组件实例上
js 复制代码
// /runtime-core/apiInject.js ✅ 
import { getCurrentInstance } from "./component";

export function provide(key, value) {
  // 存
  const currentInstance:any = getCurrentInstance() // 这个方法仅在 setup 函数中可以使用,所以 provide、inject 也只能在 setup 函数中使用
  if(currentInstance) {
    const { provides } = currentInstance
    provides[key] = value
  }
}

export function inject(key) {
  // 取
  const currentInstance:any = getCurrentInstance()
  if(currentInstance) {
    const parentProvides = currentInstance.parent.provides
    return parentProvides[key]
  }
}

// runtime-core/component.ts 
export function createComponentInstance(vnode, parent) { 
  // ✅ parent 用来传递父组件实例 往上回溯,renderer.ts 中好多十几个位置需要加上 parent 参数,形参名为 parentComponent, 到源头 patch(subTree, container, instance)
  console.log("createComponentInstance", parent);
  
  const component = {
    vnode,
    type: vnode.type,
    setupState: {},
    props: {},
    emit:()=>{},
    slots:{},
    provides: {}, // ✅ 用来存储 provide 的数据
    parent // ✅ 记录父组件实例
  }
  component.emit = emit.bind(null, component) as any // 这里 null 是this指向, component 作为第一个参数传入
  return component
}

// renderer.ts
`因为上面的函数 createComponentInstance 需要 parent 参数,所以这里也要传入 parentComponent,而且这个页面涉及的地方比较多,全部改成传入 parentComponent 参数`

// runtime-core/index.ts
export { provide, inject } from './apiInject'
  • 结果:我们实现了最简单的 provide-inject 功能,父组件通过 provide 存储数据,子组件通过 inject 获取数据
  • 思想:我们开发项目可以按照先实现一个简单的需求的版本,然后再慢慢完善它,属于小步走开发思想,防止我们过度设计,而且实现起来也没有压力。
  1. 我们在上面已经实现了一个父子组件之间的传递通过 provide-inject 功能,接下来我们实现祖孙组件之间的传递
  • 给 App.js 的 Provider 组件和 Consumer 组件中间再加一层组件 Middle---> ProviderTwo 这个组件
js 复制代码
// App.js
const ProviderTwo = {
  name: 'ProviderTwo',
  render() {
    return h('div', {}, [h('p', {}, "ProviderTwo"), h(Consumer)])
  },
  setup() {
  }
}
  • 对相关的祖孙传递逻辑进行实现
    • 问题?我们加上一层以后就无法在 App.js 的 Consumer 组件中获取到 Provider 组件中 provide 的数据了

      • 分析?因为我们现在的数据是存储在组件实例的 provides 对象上,而 Middle 组件实例的 provides 对象是空的,所以无法获取到 Provider 组件中 provide 的数据
      • 解决方案?
        • 让实例上的 provides 属性指向父组件实例的 provides 属性,这样就可以实现祖孙组件之间的传递了,代码如下:
      js 复制代码
      // runtime-core/component.ts
      export function createComponentInstance(vnode, parent) {
      console.log("createComponentInstance", parent);
      
        const component = {
          vnode,
          type: vnode.type,
          setupState: {},
          props: {},
          emit:()=>{},
          slots:{},
          provides: parent ? parent.provides : {}, // ✅ 让实例上的 provides 属性指向父组件实例的 provides 属性
          parent
        }
        component.emit = emit.bind(null, component) as any // 这里 null 是this指向, component 作为第一个参数传入
        return component
      }
      • 结果:我们实现了祖孙组件之间的传递
    • 我们继续实现更复杂一点的场景,我们在 Middle 组件中也使用 provide 提供一个 provide("foo", "fooTwo"),然后在 Consumer 组件中通过 inject 获取 foo 的值,看看会获取到哪个值

      js 复制代码
      // App.js
      const ProviderTwo = {
        name: 'ProviderTwo',
        render() {
          return h('div', {}, [h('p', {}, "ProviderTwo " + this.foo), h(Consumer)])
        },
        setup() {
          provide("foo", "fooTwo")
          const foo = inject("foo")
          return {
            foo
          }
        }
      }
      • 结果:我们会获取到 Middle 组件中提供的 foo 的值 fooTwo,说明子组件提供的数据会覆盖父组件提供的数据
        • 还有一个问题,当我们在 Middle 组件中通过 inject 获取 foo 的值时,会获取到哪个值呢?
        • 结果:我们会获取到 Middle 组件中提供的 foo 的值 fooTwo, 我们应该获取到 Provider 组件中提供的 foo 的值 fooVal,这就不对了
      • 分析:因为我们在 Middle 组件中调用 provide 的时候,是把数据存储在 Middle 组件实例的 provides 对象上,而 Middle 组件实例的 provides 对象是继承自 Provider 组件实例的 provides 对象的,所以当我们在 Consumer 组件中通过 inject 获取 foo 的值时,会先从 Middle 组件实例的 provides 对象上获取,没有再去祖先组件 Provider 组件实例的 provides 对象上获取
        • 而我们在 Middle 组件中通过 inject 获取 foo 的值时,已经被 Middle 组件实例的 provides 对象上的 foo 覆盖了,所以获取到的是 fooTwo
      • 解决方案?
      js 复制代码
      export function provide(key, value) {
        // 存
        const currentInstance:any = getCurrentInstance() // 这个方法仅在 setup 函数中可以使用,所以 provide、inject 也只能在 setup 函数中使用
        if(currentInstance) {
          let { provides } = currentInstance
          const parentProvides = currentInstance.parent.provides
          if(provides === parentProvides) {
            provides = currentInstance.provides = Object.create(parentProvides)  // ✅ 这里我们需要改造一下
          }
          provides[key] = value
        }
      }
      // 效果:实现了祖先组件 provide, 父亲组件也同时 provide, 子组件 inject,拿到父组件里面的对象里面改过的属性,父组件 inject,拿到是祖先组件的 provides 
      provides: {foo: "fooTwo"}
        [[Prototype]]:Object
              bar:"barVal"
              foo:"fooVal"
      • 结果:我们实现了祖孙组件之间的传递,并且解决了子组件提供的数据覆盖父组件提供的数据的问题
      • 回顾:我们 provide 再次存储值,修改了 provides 对象,让 provides 对象原型指向父组件实例的 provides 对象,这样就实现了数据的继承,同时避免了子组件提供的数据覆盖父组件提供的数据的问题。
  1. 我们已经实现了祖孙传值的功能,我们再复杂一点,我们在中间组件 Middle 中 inject 注入 baz,给一个默认值 defaultVal,在页面访问,会展示什么呢?
js 复制代码
const ProviderTwo = {
  name: 'ProviderTwo',
  render() {
    return h('div', {}, [h('p', {}, "ProviderTwo " + this.foo + this.baz), h(Consumer)]) // ✅ 显示 baz
  },
  setup() {
    provide("foo", "fooTwo")
    const foo = inject("foo")
    const baz = inject("baz", "defaultVal") // ✅ 给一个默认值
    return {
      foo,
      baz
    }
  }
}
  • 结果:我们会获取到 undefined
  • 分析:因为我们在 Middle 组件中通过 inject 获取 baz 的值时,发现没有提供 baz,所以会返回 undefined,而不是默认值 defaultVal
  • 解决方案?
js 复制代码
export function inject(key, defaultVal?) {
  // 取
  const currentInstance:any = getCurrentInstance()
  if(currentInstance) {
    const parentProvides = currentInstance.parent.provides
    if(key in parentProvides) {
      return parentProvides[key]
    } else if(defaultVal) {
      return defaultVal
    }
  }
}
  • 结果:我们实现了在中间组件中注入一个不存在的值时,返回默认值的功能
相关推荐
Mr Xu_5 分钟前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝8 分钟前
RBAC前端架构-01:项目初始化
前端·架构
程序员agions17 分钟前
2026年,微前端终于“死“了
前端·状态模式
万岳科技系统开发17 分钟前
食堂采购系统源码库存扣减算法与并发控制实现详解
java·前端·数据库·算法
程序员猫哥_24 分钟前
HTML 生成网页工具推荐:从手写代码到 AI 自动生成网页的进化路径
前端·人工智能·html
龙飞0525 分钟前
Systemd -systemctl - journalctl 速查表:服务管理 + 日志排障
linux·运维·前端·chrome·systemctl·journalctl
我爱加班、、30 分钟前
Websocket能携带token过去后端吗
前端·后端·websocket
AAA阿giao30 分钟前
从零拆解一个 React + TypeScript 的 TodoList:模块化、数据流与工程实践
前端·react.js·ui·typescript·前端框架
杨超越luckly37 分钟前
HTML应用指南:利用GET请求获取中国500强企业名单,揭秘企业增长、分化与转型的新常态
前端·数据库·html·可视化·中国500强
一 乐1 小时前
校园二手交易|基于springboot + vue校园二手交易系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端