前言
前面分享了很多的Vue源码分析的内容,主要包括了响应式及响应式API的实现、几个Vue内置组件的实现、props
和attrs
的初始化和更新,以及Vue的初始化和一点点DOM diff
的内容,在这些文章中或许我有一些遗漏的内容,在本篇文章中我会把那些遗漏的内容和一些其他的细节进行补充,(其实我后面有偷偷更新,不知道你们有没有发现[狗头保命])。
(位置在runtime-core/src/components.ts
)
补充一
在初始化流程中,执行setup
之前会有产生一个setupContext
,setup.length > 1
指的是setup
至少设置了一个形参,就会执行createSetupContext
创建setupContext
,这个方法传递的是当前组件实例,
函数开头创建了一个函数expose
,这个函数是用来限制$parent
、$root
等公共实例可以访问prototype
,这个函数只能在setup
中执行且只能执行一次,具体原理后面回会说明。
最后返回的是一个对象,里面带有attrs
、slots
、emit
,expose
,这个对象在后面执行setup
时会传递,在setup
可以拿到也是因为这个原因,在dev中使用getter,以防像test-utils
这样的库覆盖实例属性(不应该在prod
中进行覆盖)。
expose
的实现
当用户调用expose
传递了一个对象,这个对象会挂到实例上(exposed
属性),对象里面的数据就是用户访问$parent
等公共实例可以看到的内容,但是这并不是全部,options api
有一个expose
选项,是一个数组,也是列举了一些公共实例可以访问到的。
在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
的限制就是在这些方法中实现的。
举个例子,$parent
和$root
通过方法getPublicInstance
获取父组件,在getPublicInstance
中就会调用getExposeProxy
或许限制过的组件实例代理对象。
这个方法会获取实例上的exposeProxy
,如果不存在就创建,这里做了两层拦截,外层是getExposeProxy
中看到的,拦截数据访问也拦截了$xxx
的访问。内层是数据对象访问拦截。这个函数调用地方有很多,都是为了限制公共实例访问。
补充二
在前面返回的对象中调用了createAttrsProxy
创建了一个attrs
代理,主要是对attrs
访问修改进行限制,只允许访问不允许修改。在dev中访问时会调用markAttrsAccessed
,这是为了确认是否有跟踪渲染时是否使用了$attrs
,如果使用了可以抑制失败的$attrs
警告。调用将accesseAttrs
即改为true
,(ps:这是attrs在setup执行时做的处理)
在runtime-core/src/componentRenderUtils.ts
中是一系列警告。accesseAttrs
为true
可以抑制这些警告的出现,
补充三
在setup
函数执行完毕之后,会产生一个setupResult
,这是一个对象,里面是用户在setup
函数中返回的数据,可以在模板或者其他的地方使用,Vue会对这个数据对象进行响应式处理,(位置在:runtime-core/src/components.ts
中的handleSetupResult
方法)
没有使用baseHanlder.ts
和collectionHanlder.ts
中的响应式处理,而是使用了单独另外响应式处理,
获取没有什么区别,主要是修改,兼顾了ref
和reactive
。最后产生的响应式的数据对象会挂载实例上,名称为setupState
当然上述都是属于返回的是一个数据对象,也有返回的不是数据对象的情况,还有渲染函数和Proomise
这两个种情况。
先说说返回的是Promise
的情况,首先是服务端渲染(SSR)返回的也是Promise
,是为了便于服务端渲染器等待它。
yibu
使用内置组件Suspense
也会返回Promise
,在返回异步结果,无论结果如何都会去关闭suspense
组件实例的依赖作用域,然后会在组件实例挂上一个异步的dep,后面的流程就交给了Suspense
内部处理。
再者返回的是一个渲染函数,会在hanlderSetupResult
中将函数放在实例上,后面就会直接开始vue2的兼容处理。
补充四
既然补充三说到了v2兼容处理,补充四就将v2兼容处理完整的分析一遍,位置在runtime-core/src/componentOptions.ts
函数开头,执行了resolveMergedOptions
,传递了组件实例,是为了找到实例所有的存在的options api
,以及合并全局和局部的mixins
以及extends
,最后返回的才是组件最终的配置。
publicThis
是组件实例的代理对象,shouldCacheAccess
是用来对缓存公共代理上的属性访问的,但是在初始化期间不要缓存公共代理上的属性访问,接着必须要访问选项之前先执行beforeCreate
,因为beforeCreate
钩子函数内部可能改变选项中的内容。
解构出options
中每一个选项,开始处理每一个选项中的配置。处理的顺序是:props
> inject
> methods
> data
> computed
> watch
。
创建一个检测函数,在dev模式下用于检查各个选项中是否有重复的配置,方法就是将存在的key
和它存在那个选项中作为键值对缓存起来,等到有重复的出现就可以准确的告诉用户哪里有重复的key
。
- 处理
inject
props
在函数之外以及处理完毕了,所以变成先处理inject
,拿到inject
配置传递给resolveInjections
函数处理。
resolveInjections
函数拿到inject
的配置,如果是数组形式会先进行规范化,其实就是将数组形式变成对象形式,所以直接传递对象也是可以的,这里不多说明。
在对inject
的处理有用到inject
api,这里先提前说明一下inject
api的实现。
拿到当前实例(如果当前实例时null
,那就回退到当前渲染实例,方便函数式组件调用,没有找到实例直接结束),通过实例拿到provides
,这里做了一个判断,因为是可以在根组件使用app.use
进行自我注入。provides
一定来自父组件,根组件则是在应用上下文中。
接下来就可以在provides
中取数据,一般情况下只会传递一个实参,就是key
,会去provides
中找到返回,没有告诉用户该key
不存在,第二种情况,用户不知传递了key
,还传递了默认值和第三个参数(可能是true
也可能是false
),返回的东西是以第三个参数而定的,true
代表默认值是一个工厂函数,执行才可以返回值,false
就是直接的值可以直接返回。
回到resolveInjections
中,根据用户写的inject
选项通过inject
api 拿到值,后面就开始放入上下文ctx
中。
如果数据是ref
,Vue中有一个配置:unwrapInjectRef
,为true
会自动展开ref后使用Object.defineProperty
定义入ctx
中,为false
,是直接放入ctx
中,并会告诉用户请将unwrapInjectRef
改为true
,这将是一个新行为,但是是临时的,后面版本可能不再需要。数据不是ref
,就正常的放入ctx
中。函数的最后检查是否重复即可,inject
处理完毕。
- 处理
methods
接下来就是处理methods
选项中的方法,这些都是用户自己写的方法,Vue中处理也是非常的简单,遍历methods
选项,每次取出一个,先进行判断是不是函数,不是不做任何处理直接报警告,是把他们定义在ctx
上,并将方法的this
改为当前实例的代理对象,所以在调用方法时候this
就是当前组件实例。最后进行验证是否有重复的。methods
处理完成。
- 处理
data
现在轮到data
了,在Vue3.2版本以及以后data
选项推荐函数形式不推荐对象形式,执行data
函数,会将this
改为当前组件实例的代理对象,并传入函数中,data函数必须返回一个对象,不然不做处理且报警告。
返回数据对象之后,现在进行响应式处理,然后进行校验,暴露到ctx
需要排除并排除$
和_
开头的属性。data
处理完成。
data
处理完成,代表着状态初始化完成,开始缓存访问。
- 处理
computed
选项
因为计算属性是一定带有get
,set
是可能有可能没有,所以get
和set
需要分开处理,并且因为用户可以写对象或者直接是函数两种形式,所以需要判断是函数还是对象,函数可以直接绑定this
,对象需要从里面取出get
或者set
再绑定this
,产生新的set
和get
交给computed api
执行,返回的结果是get
执行后的结果,再把值定义在上下文中,最后校验是否有重复的key
,结束。
- 处理
watch
选项
watch
选项没啥好分析的,执行的createWatcher
函数,内部通过v3的watch api
产生watcher
。需要注意的是,需要对用户写的观察目标进行详细判断,比如是数组形式,也就是多个观察目标需要进行遍历,然后每一个观测目标在执行一次createWatcher
。
- 处理
provide
先说一下provide api
的实现原理,这个api接收当前组件实例,会拿到当前组件实例中provides
和父组件的provides
,默认情况下,实例继承其父级的提供对象,但当它需要提供自己的值时,它会使用父级provides
对象作为原型创建自己的provides
对象。通过这种方式,在“inject”中,我们可以简单地查找来自直接父级的注入,并让原型链完成工作。最后把值放入provides
对象中即可。
用户写的provides
可能是一个函数或者对象,需要判断执行才能得到provides
对象,遍历这个对象执行provides api
,解析provides
完成。
接下来的注册生命周期钩子函数和解析expoes
,生命周期函数以后再说,expoes
前面说过了,
- 剩下的部分
如果前面setup
返回的不是函数,在这里会拿到render
选项,放到实例上,继承属性inheritAttrs
、components
、directives
、filters
不是null
同样放入实例中。兼容处理结束。
总结
这次补充了前面所遗漏的地方,进行逐步的分析:
-
比如
setup
函数执行结果的几种情况(分别由数据对象、Promise
和渲染函数)和这几种情况是如何处理的。数据对象做完响应式处理就放到实例上,Promise
则在.then
之后交给服务端渲染器或者是Suspense
处理,渲染函数就是直接放在实例上。 -
exposed
限制公共实例访问限制原理,其实是产生一个expose
代理对象,在访问$parent
等公共实例的时候先去获取expose
代理对象,不存在才会去拿原本的组件实例代理对象,在Vue3.2中新增了expose api
所以在后的兼容处理会有一个合并操作 -
兼容v2处理,其实只是用来v3的api来实现v2的功能,比如
computed
选项使用computed api
实现的,watch
是用watch api
实现的。至于生命周期函数和一些钩子函数后面还会专门有一篇文章说明的。
好了,到了文章的最后,还是希望各位哥哥姐姐能指导指导。有说错或者遗漏的欢迎在评论区讲解,谢谢。