vue3源码分析——实现组件通信provide,inject

简介: 问题解决: 问题1和问题2都很好解决,对外导出函数,传递对应的参数,只是数据存储在哪里的问题,经过仔细的思考,会发现,组件的数据是需要进行共享的,父组件存入的数据,里面的所有子组件和孙子组件都可以共享,那么存储在实例上,是不是一个不错的选择呢? inject 是获取父级组件的数据,那么在实列上还需要传入parent

引言


<<往期回顾>>


1.vue3源码分析——rollup打包monorepo

2.vue3源码分析——实现组件的挂载流程

3.vue3源码分析——实现props,emit,事件处理等

4.vue3源码分析——实现slots


本期来实现, vue3组件通信的provide,inject,所有的源码请查看


getCurrentInstance


在实现provide/inject之前,先来实现getCurrentInstance,由于在provide/inject中会使用到这个api,在开发的时候,这个api使用的频率也是挺频繁的。


getCurrentInstance 是获取当前组件的实列,并且只能在setup函数中使用


测试用例


test('test getCurrentInstance', () => {
    const Foo = {
      name: 'Foo',
      setup() {
      // 获取子组件的实列,并且期望是子组件的名称是Foo
        const instance = getCurrentInstance();
        expect(instance.type.name).toBe('Foo');
        return {
          count: 1
        }
      },
      render() {
        return h('div', {}, '122')
      }
    }
    const app = createApp({
      name: 'App',
      setup() {
      // 获取父组件的实例,期待父组件的名称是定义的App
        const instance = getCurrentInstance();
        expect(instance.type.name).toBe('App');
        return {
          count: 2
        }
      },
      render() {
        return h('div', { class: 'container' }, [h(Foo, {}, {})])
      }
    })
    // 挂载组件
    const appDoc = document.querySelector('#app')
    app.mount(appDoc);
  })


分析


在上面的测试拥立中,可以得到以下内容:


1.getCurrentInstance只能在setup函数中使用

2.对外导出的api,获取的是当前组件的实列


问题解决:


对于上面两个问题,只需要导出一个函数,并且在全局定义一个变量,在setup执行的时候,赋值全局变量即可拿到当前组件的实例,然后setup执行之后,清空即可


编码


// setup执行是在setupStatefulComponent函数中执行的,来进行改造
// 定义全局的变量,存储当前实例
let currentInstance = null;
function setupStatefulComponent(instance: any) {
  // ……省略其他
  // 获取组件的setup
  const { setup } = Component;
  if (setup) {
      currentInstance = instance
      const setupResult = setup(shallowReadonly(instance.props), { emit: instance.emit })
      // 情况操作
       currentInstance = null
  }
  // ……省略其他
 }
// 对外导出函数,提供全局的api
export function getCurrentInstance() {
  return currentInstance
}


getCurrentInstance 有没有想到实现方式这么简单哇!😀😀😀


provide/inject


provide和inject需要配套使用才方便用于测试,这里就从功能分析,来逐步完成这两个api.


父子组件传值


父子组件传值可以使用props/emit来实现,还记得是怎么实现的么?🙄🙄😶


测试用例


test('test provide basic use', () => {
    const Foo = {
      name: 'Foo',
      setup() {
      // 子组件接受数据
        const count = inject('count')
        const str = inject('str')
        return {
          count,
          str
        }
      },
      render() {
        return h('div', {}, this.str + this.count)
      }
    }
    const app = createApp({
      name: 'App',
      setup() {
      // 父组件提供数据,
        provide('count', 1);
        provide('str', 'str');
      },
      render() {
        return h('div', { class: 'container' }, [h(Foo, {})])
      }
    })
    const appDoc = document.querySelector('#app')
    app.mount(appDoc);
    const container = document.querySelector('.container') as HTMLElement;
    expect(container.innerHTML).toBe('<div>str1</div>')
  })


分析


从上面的测试用例中进行需求分析,


1.provide api是需要有两个参数,一个key,另一个是value, 有点类似与sessionStorage这种set值的方式


2.inject api则是只需要一个key,来进行get操作


3.provide存的数据,存在哪里呢?


问题解决: 问题1和问题2都很好解决,对外导出函数,传递对应的参数,只是数据存储在哪里的问题,经过仔细的思考,会发现,组件的数据是需要进行共享的,父组件存入的数据,里面的所有子组件和孙子组件都可以共享,那么存储在实例上,是不是一个不错的选择呢? inject 是获取父级组件的数据,那么在实列上还需要传入parent


编码


由于需要在实例上存储provide,首先就在createInstance中的实例,在初始化就赋值
export function createComponentInstance(vnode, parent) {
  const instance = {
    // ……省略其他属性
    // 提供数据
    provides: {},
    parent,
  }
  return instance
}
// 有了实例,分别创建provide,inject函数
export function provide(key, val){
  // 将数据存在实例上,先进行获取
  const instance = getCurrentInstance();
  if(instance){
      instance.provides[key] = val
  }
}
export function inject(key){
 // 从实列上取值
 const instance = getCurrentInstance();
 if(instance){
     // 获取父级provides
     const provides = instance.parent?.provides;
     if(key in provides){
        return provides[key]
     }
     return null
 }
}


一个简单的prvide/inject就实现啦,接下来进行需求升级,爷孙组件数据传递


爷孙组件传值


无可厚非,就是孙子组件需要从爷爷组件中获取值,父组件不提供数据


测试用例


test('test provide exit grandfather', () => {
    const Child = {
      name: 'Foo',
      setup() {
      // 孙子组件也可以取值
        const count = inject('count')
        const str = inject('str')
        return {
          count,
          str
        }
      },
      render() {
        return h('div', {}, this.str + this.count)
      }
    }
    const Father = {
      name: 'Father',
      setup() {
      // 子组件可以取值
        const count = inject('count')
        return {
          count
        }
      },
      render() {
        return h('div', {}, [h('p', {}, this.count), h(Child, {})])
      }
    }
    const app = createApp({
      name: 'App',
      setup() {
       // 爷爷提供数据
        provide('count', 1);
        provide('str', 'str');
        return {}
      },
      render() {
        return h('div', { class: 'container' }, [h(Father, {})])
      }
    })
    const appDoc = document.querySelector('#app');
    app.mount(appDoc);
    const container = document.querySelector('.container') as HTMLElement;
    expect(container.innerHTML).toBe('<div><p>1</p><div>str1</div></div>')
  })


分析


上面的测试用例相对于父子组件的测试用例来说,增加了一个孙子组件。


1.孙子(Child组件) 和 父亲(Foo组件) 都可以获取 爷爷(App组件) 的值

2.其他的没啥变化


问题解决: 想要让孙子组件获取爷爷组件的数据,那是否可以让父组件Foo在初始化就获取他父组件App的provides


编码


// 需要在组件初始化的时候,获取父组件的数据,修改下初始化的内容
export function createComponentInstance(vnode, parent) {
  const instance = {
    // ……省略其他属性
    // 存在则用,不存在还是空对象
    provides: parent ? parent.provides : {},
    parent,
  }
  return instance
}


是不是感觉非常简单哇,那接下来在升级下,inject获取provide的数据,需要就近原则来进行获取


就近原则获取数据


就近原则的意思是说,如果父组件有就拿父组件的,父组件没有就那爷爷组件的,爷爷组件没有继续往上找,直到找到跟组件App上,如果还没有就为null


测试用例


 test('get value by proximity principle(就近原则) ', () => {
 // 孙子组件来获取数据
  const GrandSon = {
      name: 'GrandSon',
      setup() {
        const count = inject('count')
        const str = inject('str')
        return {
          count,
          str
        }
      },
      render() {
        return h('div', {}, this.str + this.count)
      }
    }
    // 子组件提供count
    const Child = {
      name: 'Child',
      setup() {
        provide('count', 100)
      },
      render() {
        return h(GrandSon)
      }
    }
    // 父亲组件,不提供数据
    const Father = {
      name: 'Father',
      render() {
        return h(Child)
      }
    }
   // 跟组件app,提供,count,str
    const app = createApp({
      name: 'App',
      setup() {
        provide('count', 1);
        provide('str', 'str');
        return {}
      },
      render() {
        return h('div', { class: 'container' }, [h(Father, {})])
      }
    })
   // ……省略挂载
    const container = document.querySelector('.container') as HTMLElement;
    expect(container.innerHTML).toBe('<div>str100</div>')
  })


分析


在上面的测试用例中,存在4个组件,只有app组件和Child组件提供数据,其他只是嵌套,不提供数据。存在下面问题:


1.inject怎么去查找provides的数据,一层一层的查找


问题解决: 怎么查找呢,在inject里面递归? NO😱😱😱,换一个角度,inject查找数据的时候,是不是有点像原型链的方式来进行查找呢?YES😆😆😆,那就是需要在provide里面来构建一条原型链。


原型链, 啥叫做原型链呢?请查看


编码


// 只需要改造provide函数即可
export function provide(key, val) {
  // 数据需要存储在当前的实例上面
  const instance = getCurrentInstance();
  if (instance) {
    let { provides } = instance;
    // 正对多层组件,需要把当前组件的__proto__绑定到父级上面,形成原型链,可以访问到最顶层的数据
    const parentProvides = instance.parent && instance.parent.provide;
    // 只有父级的provides和当前的provides是相同的时候为第一次调用provide,后续调用就不需要绑定原型了
    if (parentProvides === provides) {
      provides = instance.providers = Object.create(parentProvides || {});
    }
    provides[key] = val;
  }
}


总结


本期主要完成了getCurrentInstance,provide,inject的实现,在getCurrentInstance中只是用了一个中间变量,而provide是把数据存在当前的instance当中,provide里面还用到了原型链的知识,通过原型的方式来查询key是否存在,不存在则往上查找

相关文章
|
13天前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
116 64
|
13天前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
|
26天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
26天前
|
存储 JavaScript
Vue 组件间如何通信
Vue组件间通信是指在Vue应用中,不同组件之间传递数据和事件的方法。常用的方式有:props、自定义事件、$emit、$attrs、$refs、provide/inject、Vuex等。掌握这些方法可以实现父子组件、兄弟组件及跨级组件间的高效通信。
|
1月前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
52 1
|
1月前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
56 1
|
19天前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
34 1
vue学习第四章
|
19天前
|
JavaScript 前端开发
vue学习第九章(v-model)
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生,自学前端2年半,正向全栈进发。此篇介绍v-model在不同表单元素中的应用及修饰符的使用,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
28 1
vue学习第九章(v-model)
|
19天前
|
JavaScript 前端开发 开发者
vue学习第十章(组件开发)
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文深入讲解Vue组件的基本使用、全局与局部组件、父子组件通信及数据传递等内容,适合前端开发者学习参考。持续更新中,期待您的关注!🎉🎉🎉
33 1
vue学习第十章(组件开发)
|
25天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。