前言

Vue3 的 Provide / Inject 的实现原理其实就是巧妙利用了原型和原型链来实现的,所以在了解Vue3 的 Provide / Inject 的实现原理之前,我们先复习一下原型和原型链的知识。

原型和原型链的知识回顾

  • prototype 与 __proto__

prototype 一般称为显式原型,__proto__一般称为隐式原型。 每一个函数在创建之后,在默认情况下,会拥有一个名为 prototype 的属性,这个属性表示函数的原型对象。

  • 原型链

当我们访问一个JS对象属性的时候,JS先会在这个对象定义的属性里找,找不到就会沿着这个对象的__proto__这个隐式原型关联起来的链条向上一个对象查找,这个链条就叫原型链。

function Fn() {}
Fn.prototype.name = 'coboy'
let fn1 = new Fn()
fn1.age = 18
console.log(fn1.name) // coboy
console.log(fn1.age) // 18

fn1是Fn函数new出来的实例对象,fn1.age是这个实例对象上属性,fn1.name则从Fn.prototype原型对象而来,因为fn1的__proto__隐式原型就是指向Fn这个函数的原型对象Fn.prototype。原型链某种意义上是让一个引用类型继承另一个引用类型的属性和方法。

function Fn() {}
Fn.prototype.name = 'coboy'
let fn1 = new Fn()
fn1.name = 'cobyte'
console.log(fn1.name) // cobyte

当访问fn1这个实例对象的属性name的时候,JS先会在fn1这个实例对象的属性里查找,刚好fn1定义了一个name属性,所以就直接返回自身属性的值cobyte,否则就会继续沿着原型链向Fn.prototype上去找,那么就会返回coboy。

复习完原型和原型链的知识之后,我们就开始进入Provide/Inject的实现原理探索。

使用 Provide

在 setup() 中使用 provide 时,我们首先从 vue 显式导入 provide 方法。这使我们能够调用 provide 来定义每个 property。

provide 函数允许你通过两个参数定义 property

  • name (<String> 类型)
  • value
import { provide } from 'vue'
export default {
  setup() {
    provide('name', 'coboy')
  }
}

provide API实现原理

那么这个provide API实现原理是什么呢?

provide 函数可以简化为

export function provide(key, value) {
    // 获取当前组件实例
    const currentInstance: any = getCurrentInstance()
    if(currentInstance) {
        // 获取当前组件实例上provides属性
        let { provides } = currentInstance
        // 获取当前父级组件的provides属性
        const parentProvides = currentInstance.parent.provides
        // 如果当前的provides和父级的provides相同则说明还没赋值
        if(provides === parentProvides) {
            // Object.create() es6创建对象的另一种方式,可以理解为继承一个对象, 添加的属性是在原型下。
            provides = currentInstance.provides = Object.create(parentProvides)
        }
        provides[key] = value
    }
}

综上所述provide API就是通过获取当前组件的实例对象,将传进来的数据存储在当前的组件实例对象上的provides上,并且通过ES6的新API Object.create把父组件的provides属性设置到当前的组件实例对象的provides属性的原型对象上。

组件实例对象初始化时provides属性的处理

源码位置:runtime-core/src/component.ts

我们通过查看instance对象的源码,可以看到,在instance组件实例对象上,存在parent和provides两个属性。在初始化的时候如果存在父组件则把父组件的provides赋值给当前的组件实例对象的provides,如果没有就创建一个新的对象,并且把应用上下文的provides属性设置为新对象的原型对象上的属性。

使用 Inject

在 setup() 中使用 inject 时,也需要从 vue 显式导入。导入以后,我们就可以调用它来定义暴露给我们的组件方式。

inject 函数有两个参数:

  • 要 inject 的 property 的 name
  • 默认值 (可选)
import { inject } from 'vue'
export default {
  setup() {
    const name = inject('name', 'cobyte')
    return {
      name
    }
  }
}

inject API实现原理

那么这个inject API实现原理是什么呢?

inject 函数可以简化为

export function inject(
  key,
  defaultValue,
  treatDefaultAsFactory = false
) {
  // 获取当前组件实例对象
  const instance = currentInstance || currentRenderingInstance
  if (instance) {
    // 如果intance位于根目录下,则返回到appContext的provides,否则就返回父组件的provides
    const provides =
      instance.parent == null
        ? instance.vnode.appContext && instance.vnode.appContext.provides
        : instance.parent.provides

    if (provides && key in provides) {
      return provides[key]
    } else if (arguments.length > 1) {
      // 如果存在1个参数以上
      return treatDefaultAsFactory && isFunction(defaultValue)
        // 如果默认内容是个函数的,就执行并且通过call方法把组件实例的代理对象绑定到该函数的this上
        ? defaultValue.call(instance.proxy) 
        : defaultValue
    }
  }
}

通过inject源码分析我们可以知道,inject里面先获取当前组件的实例对象,然后判断是否根组件,如果是根组件则返回到appContext的provides,否则就返回父组件的provides。

如果当前获取的key在provides上有值,那么就返回该值,如果没有则判断是否存在默认内容,默认内容如果是个函数,就执行并且通过call方法把组件实例的代理对象绑定到该函数的this上,否则就直接返回默认内容。

provide/inject实现原理总结

通过上面的分析,可以得知provide/inject实现原理还是比较简单的,就是巧妙地利用了原型和原型链进行数据的继承和获取。provide API调用设置的时候,设置父级的provides为当前provides对象原型对象上的属性,在inject获取provides对象中的属性值时,优先获取provides对象自身的属性,如果自身查找不到,则沿着原型链向上一个对象中去查找。

拓展:Object.create原理

方法说明

  • Object.create()方法创建一个新的对象,并以方法的第一个参数作为新对象的__proto__属性的值(以第一个参数作为新对象的构造函数的原型对象)
  • Object.create()方法还有第二个可选参数,是一个对象,对象的每个属性都会作为新对象的自身属性,对象的属性值以descriptor(Object.getOwnPropertyDescriptor(obj, 'key'))的形式出现,且enumerable默认为false

源码模拟

Object.myCreate = function (proto, propertyObject = undefined) {
    if (propertyObject === null) {
        // 这里没有判断propertyObject是否是原始包装对象
        throw 'TypeError'
    } else {
        function Fn () {}
        // 设置原型对象属性
        Fn.prototype = proto
        const obj = new Fn()
        if (propertyObject !== undefined) {
            Object.defineProperties(obj, propertyObject)
        }
        if (proto === null) {
            // 创建一个没有原型对象的对象,Object.create(null)
            obj.__proto__ = null
        }
        return obj
    }
}

定义一个空的构造函数,然后指定构造函数的原型对象,通过new运算符创建一个空对象,如果发现传递了第二个参数,通过Object.defineProperties为创建的对象设置key、value,最后返回创建的对象即可。

示例

// 第二个参数为null时,抛出TypeError
// const throwErr = Object.myCreate({name: 'coboy'}, null)  // Uncaught TypeError
// 构建一个以
const obj1 = Object.myCreate({name: 'coboy'})
console.log(obj1)  // {}, obj1的构造函数的原型对象是{name: 'coboy'}
const obj2 = Object.myCreate({name: 'coboy'}, {
    age: {
        value: 18,
        enumerable: true
    }
})
console.log(obj2)  // {age: 18}, obj2的构造函数的原型对象是{name: 'coboy'}

拓展:两个连续赋值的表达式

provides = currentInstance.provides = Object.create(parentProvides) 发生了什么?

Object.create(parentProvides) 创建了一个新的对象引用,如果只是把 currentInstance.provides 更新为新的对象引用,那么provides的引用还是旧的引用,所以需要同时把provides的引用也更新为新的对象引用。

来自《JavaScript权威指南》的解析

  • JavaScript总是严格按照从左至右的顺序来计算表达式
  • 一切都是表达式,一切都是运算
provides = currentInstance.provides = Object.create(parentProvides)

上述的provides是一个表达式,它被严格地称为“赋值表达式的左手端(Ihs)操作数”。 而右侧 currentInstance.provides = Object.create(parentProvides) 这一个整体也当做一个表达式,这一个整体赋值表达式的计算结果是赋值给了最左侧的provides currentInstance.provides = Object.create(parentProvides) 这个表达式同时也是一个赋值表达式,Object.create(parentProvides)创建了一个新的引用赋值给了currentInstance这个引用上的provides属性

currentInstance.provides这个表达式的语义是:

  • 计算单值表达式currentInstance,得到currentInstance的引用
  • 将右侧的名字provides理解为一个标识符,并作为“.”运算的右操作数
  • 计算currentInstance.provides表达式的结果(Result)

currentInstance.provides当它作为赋值表达式的左操作数时,它是一个被赋值的引用,而当它作为右操作数时,则计算它的值。

注意:赋值表达式左侧的操作数可以是另一个表达式,而在声明语句中的等号左边,绝不可能是一个表达式。 例如上面的如果写成了let provides = xxx,那么这个时候,provides只是一个表达名字的、静态语法分析期作为标识符来理解的字面文本,而不是一个表达式

总结

到此这篇关于Vue3中Provide / Inject实现原理的文章就介绍到这了,更多相关Vue3 Provide / Inject实现原理内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

Vue3中Provide / Inject的实现原理分享的更多相关文章

  1. vue3获取当前路由地址

    本文详细讲解了vue3获取当前路由地址的方法,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  2. 十分钟带你快速上手Vue3过渡动画

    在开发中我们想要给一个组件的显示和消失添加某种过渡动画,可以很好的增加用户体验,下面这篇文章主要给大家介绍了关于如何快速上手Vue3过渡动画的相关资料,需要的朋友可以参考下

  3. 用vue3封装一个符合思维且简单实用的弹出层

    最近新项目中需要一个弹窗组件,所以我就做了一个,下面这篇文章主要给大家介绍了关于如何利用vue3封装一个符合思维且简单实用的弹出层,文中通过实例代码介绍的非常详细,需要的朋友可以参考下

  4. 使用Vite+Vue3+Vant全家桶快速构建项目步骤详解

    这篇文章主要为大家介绍了使用Vite+Vue3+Vant全家桶快速构建项目步骤详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  5. vue3中$attrs的变化与inheritAttrs的使用详解

    $attrs现在包括class和style属性。 也就是说在vue3中$listeners不存在了,vue2中$listeners是单独存在的,在vue3 $attrs包括class和style属性, vue2中 $attrs 不包含class和style属性,这篇文章主要介绍了vue3中$attrs的变化与inheritAttrs的使用 ,需要的朋友可以参考下

  6. vue3中proxy的基本用法说明

    这篇文章主要介绍了vue3中proxy的基本用法说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  7. 如何利用vue3实现一个俄罗斯方块

    俄罗斯方块这个游戏相信大家都玩过,下面这篇文章主要给大家介绍了关于如何利用vue3实现一个俄罗斯方块的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下

  8. Vue3 携手 TypeScript 搭建完整项目结构

    TypeScript 是JS的一个超级,主要提供了类型系统和对ES6的支持,使用 TypeScript 可以增加代码的可读性和可维护性,在 react 和 vue 社区中也越来越多人开始使用TypeScript,这篇文章主要介绍了Vue3 携手 TypeScript 搭建完整项目结构,需要的朋友可以参考下

  9. vue3中defineComponent 的作用详解

    这篇文章主要介绍了vue3中defineComponent 的作用,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  10. 对Vue3中reactive的深入理解

    这篇文章主要介绍了对Vue3中reactive的深入理解,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部