Vue3.2 源码分析补充一[狗头]

1,688 阅读5分钟

95600931_p0_master1200.jpg

前言

前面分享了很多的Vue源码分析的内容,主要包括了响应式及响应式API的实现、几个Vue内置组件的实现、propsattrs的初始化和更新,以及Vue的初始化和一点点DOM diff的内容,在这些文章中或许我有一些遗漏的内容,在本篇文章中我会把那些遗漏的内容和一些其他的细节进行补充,(其实我后面有偷偷更新,不知道你们有没有发现[狗头保命])。

image.png (位置在runtime-core/src/components.ts)

补充一

在初始化流程中,执行setup之前会有产生一个setupContextsetup.length > 1指的是setup至少设置了一个形参,就会执行createSetupContext创建setupContext,这个方法传递的是当前组件实例,

image.png

函数开头创建了一个函数expose,这个函数是用来限制$parent$root等公共实例可以访问prototype,这个函数只能在setup中执行且只能执行一次,具体原理后面回会说明。

image.png

最后返回的是一个对象,里面带有attrsslotsemitexpose,这个对象在后面执行setup时会传递,在setup可以拿到也是因为这个原因,在dev中使用getter,以防像test-utils这样的库覆盖实例属性(不应该在prod中进行覆盖)。

  • expose的实现

当用户调用expose传递了一个对象,这个对象会挂到实例上(exposed属性),对象里面的数据就是用户访问$parent等公共实例可以看到的内容,但是这并不是全部,options api有一个expose选项,是一个数组,也是列举了一些公共实例可以访问到的。

image.png

runtime-core/src/componentOptions.ts中,会将这个两个进行合并,首先确认expose选项是一个数组才会进行合并,且长度不是0,取出实例上exposed,将expose选项中的每一项定义到exposed属性中,需要注意的是expose中的每一项都要在publicThis中存在,publicThis是当前组件实例的代理对象。

如果expose长度是0且实例上的exposed不存在,那么实例上的exposed属性默认给一个空对象。

  • expose访问限制

记得之前Vue3.2 DOM diff流程分析之一:props和attrs的初始化和更新这篇文章中提到过,$xxx属性其实是一些方法,放在一个对象中,而expose的限制就是在这些方法中实现的。

image.png

举个例子,$parent$root通过方法getPublicInstance获取父组件,在getPublicInstance中就会调用getExposeProxy或许限制过的组件实例代理对象。

image.png

这个方法会获取实例上的exposeProxy,如果不存在就创建,这里做了两层拦截,外层是getExposeProxy中看到的,拦截数据访问也拦截了$xxx的访问。内层是数据对象访问拦截。这个函数调用地方有很多,都是为了限制公共实例访问。

补充二

image.png

在前面返回的对象中调用了createAttrsProxy创建了一个attrs代理,主要是对attrs访问修改进行限制,只允许访问不允许修改。在dev中访问时会调用markAttrsAccessed,这是为了确认是否有跟踪渲染时是否使用了$attrs,如果使用了可以抑制失败的$attrs警告。调用将accesseAttrs即改为true,(ps:这是attrs在setup执行时做的处理)

image.png

image.png

runtime-core/src/componentRenderUtils.ts中是一系列警告。accesseAttrstrue可以抑制这些警告的出现,

补充三

image.png

setup函数执行完毕之后,会产生一个setupResult,这是一个对象,里面是用户在setup函数中返回的数据,可以在模板或者其他的地方使用,Vue会对这个数据对象进行响应式处理,(位置在:runtime-core/src/components.ts中的handleSetupResult方法)

image.png

没有使用baseHanlder.tscollectionHanlder.ts中的响应式处理,而是使用了单独另外响应式处理,

image.png

获取没有什么区别,主要是修改,兼顾了refreactive。最后产生的响应式的数据对象会挂载实例上,名称为setupState

当然上述都是属于返回的是一个数据对象,也有返回的不是数据对象的情况,还有渲染函数和Proomise这两个种情况。

image.png

先说说返回的是Promise的情况,首先是服务端渲染(SSR)返回的也是Promise,是为了便于服务端渲染器等待它。 yibu 使用内置组件Suspense也会返回Promise,在返回异步结果,无论结果如何都会去关闭suspense组件实例的依赖作用域,然后会在组件实例挂上一个异步的dep,后面的流程就交给了Suspense内部处理。

image.png

再者返回的是一个渲染函数,会在hanlderSetupResult中将函数放在实例上,后面就会直接开始vue2的兼容处理。

补充四

既然补充三说到了v2兼容处理,补充四就将v2兼容处理完整的分析一遍,位置在runtime-core/src/componentOptions.ts

image.png

函数开头,执行了resolveMergedOptions,传递了组件实例,是为了找到实例所有的存在的options api,以及合并全局和局部的mixins以及extends,最后返回的才是组件最终的配置。

publicThis是组件实例的代理对象,shouldCacheAccess是用来对缓存公共代理上的属性访问的,但是在初始化期间不要缓存公共代理上的属性访问,接着必须要访问选项之前先执行beforeCreate,因为beforeCreate钩子函数内部可能改变选项中的内容。

image.png

解构出options中每一个选项,开始处理每一个选项中的配置。处理的顺序是:props > inject > methods > data > computed > watch

image.png image.png

创建一个检测函数,在dev模式下用于检查各个选项中是否有重复的配置,方法就是将存在的key和它存在那个选项中作为键值对缓存起来,等到有重复的出现就可以准确的告诉用户哪里有重复的key

  • 处理inject

image.png

props在函数之外以及处理完毕了,所以变成先处理inject,拿到inject配置传递给resolveInjections函数处理。

image.png

resolveInjections函数拿到inject的配置,如果是数组形式会先进行规范化,其实就是将数组形式变成对象形式,所以直接传递对象也是可以的,这里不多说明。

在对inject的处理有用到injectapi,这里先提前说明一下injectapi的实现。

image.png

拿到当前实例(如果当前实例时null,那就回退到当前渲染实例,方便函数式组件调用,没有找到实例直接结束),通过实例拿到provides,这里做了一个判断,因为是可以在根组件使用app.use进行自我注入。provides一定来自父组件,根组件则是在应用上下文中。

image.png

接下来就可以在provides中取数据,一般情况下只会传递一个实参,就是key,会去provides中找到返回,没有告诉用户该key不存在,第二种情况,用户不知传递了key,还传递了默认值和第三个参数(可能是true也可能是false),返回的东西是以第三个参数而定的,true代表默认值是一个工厂函数,执行才可以返回值,false就是直接的值可以直接返回。

image.png

回到resolveInjections中,根据用户写的inject选项通过injectapi 拿到值,后面就开始放入上下文ctx中。

image.png

如果数据是ref,Vue中有一个配置:unwrapInjectRef,为true会自动展开ref后使用Object.defineProperty定义入ctx中,为false,是直接放入ctx中,并会告诉用户请将unwrapInjectRef改为true,这将是一个新行为,但是是临时的,后面版本可能不再需要。数据不是ref,就正常的放入ctx中。函数的最后检查是否重复即可,inject处理完毕。

  • 处理methods

image.png

接下来就是处理methods选项中的方法,这些都是用户自己写的方法,Vue中处理也是非常的简单,遍历methods选项,每次取出一个,先进行判断是不是函数,不是不做任何处理直接报警告,是把他们定义在ctx上,并将方法的this改为当前实例的代理对象,所以在调用方法时候this就是当前组件实例。最后进行验证是否有重复的。methods处理完成。

  • 处理data

image.png

现在轮到data了,在Vue3.2版本以及以后data选项推荐函数形式不推荐对象形式,执行data函数,会将this改为当前组件实例的代理对象,并传入函数中,data函数必须返回一个对象,不然不做处理且报警告。

返回数据对象之后,现在进行响应式处理,然后进行校验,暴露到ctx需要排除并排除$_开头的属性。data处理完成。

data处理完成,代表着状态初始化完成,开始缓存访问。

  • 处理computed选项

image.png

因为计算属性是一定带有getset是可能有可能没有,所以getset需要分开处理,并且因为用户可以写对象或者直接是函数两种形式,所以需要判断是函数还是对象,函数可以直接绑定this,对象需要从里面取出get或者set再绑定this,产生新的setget交给computed api执行,返回的结果是get执行后的结果,再把值定义在上下文中,最后校验是否有重复的key,结束。

  • 处理 watch选项

image.png

image.png

watch选项没啥好分析的,执行的createWatcher函数,内部通过v3的watch api产生watcher。需要注意的是,需要对用户写的观察目标进行详细判断,比如是数组形式,也就是多个观察目标需要进行遍历,然后每一个观测目标在执行一次createWatcher

  • 处理 provide

image.png

先说一下provide api的实现原理,这个api接收当前组件实例,会拿到当前组件实例中provides和父组件的provides,默认情况下,实例继承其父级的提供对象,但当它需要提供自己的值时,它会使用父级provides对象作为原型创建自己的provides对象。通过这种方式,在“inject”中,我们可以简单地查找来自直接父级的注入,并让原型链完成工作。最后把值放入provides对象中即可。

image.png

用户写的provides可能是一个函数或者对象,需要判断执行才能得到provides对象,遍历这个对象执行provides api,解析provides完成。

接下来的注册生命周期钩子函数和解析expoes,生命周期函数以后再说,expoes前面说过了,

  • 剩下的部分

image.png

如果前面setup返回的不是函数,在这里会拿到render选项,放到实例上,继承属性inheritAttrscomponentsdirectivesfilters不是null同样放入实例中。兼容处理结束。

总结

这次补充了前面所遗漏的地方,进行逐步的分析:

  1. 比如setup函数执行结果的几种情况(分别由数据对象、Promise和渲染函数)和这几种情况是如何处理的。数据对象做完响应式处理就放到实例上,Promise则在.then之后交给服务端渲染器或者是Suspense处理,渲染函数就是直接放在实例上。

  2. exposed限制公共实例访问限制原理,其实是产生一个expose代理对象,在访问$parent等公共实例的时候先去获取expose代理对象,不存在才会去拿原本的组件实例代理对象,在Vue3.2中新增了expose api所以在后的兼容处理会有一个合并操作

  3. 兼容v2处理,其实只是用来v3的api来实现v2的功能,比如computed选项使用computed api实现的,watch是用watch api实现的。至于生命周期函数和一些钩子函数后面还会专门有一篇文章说明的。

好了,到了文章的最后,还是希望各位哥哥姐姐能指导指导。有说错或者遗漏的欢迎在评论区讲解,谢谢。