系列文章:
- Vue源码解读(设计篇)
- Vue源码解读(Rollup篇)
- Vue源码解读(入口到构造函数整体流程)
介绍
分析过initState()
方法的整体流程,知道它会处理props
、methods
和data
等等相关的内容:
那么深入响应式原理介绍会以initState()
方法开始,逐步分析Vue
中响应式的原理,下面这张图可以很好的展示响应式的原理。
前置核心概念
Object.defineProperty介绍
也许你已经从很多地方了解到,Vue.js
利用了Object.defineProperty(obj, key, descriptor)
方法来实现响应式,其中Object.defineProperty()
方法的参数介绍如下:
obj
:要定义其属性的对象。key
:要定义或修改属性的名称。descriptor
:要定义或修改属性的描述符。
其中descriptor
有很多可选的键值, 然而对Vue
响应式来说最重要的是get
和set
方法,它们分别会在获取属性值触发getter
和设置属性值的时候触发setter
。在介绍原理之前,来使用Object.defineProperty()
来实现一个简单的响应式例子:
为了在别的地方方便的使用Object.defineProperty()
方法,把其封装成一个defineReactive
函数。
proxy代理
在开发过程中,经常会直接使用this.xxx
的形式直接访问props
或者data
中的值,这是因为Vue
为props
和data
默认做了proxy
代理。关于什么是proxy
代理,请先看一个简单的例子:
接下来详细介绍proxy()
方法是如何实现的,在instance/state.js
文件中定义了proxy
方法,它的代码也很简单:
可以从上面的代码中发现,proxy
方法主要是做了属性的get
和set
方法劫持。
$options属性
在之前的介绍中, 知道当 初始化Vue
实例的时候传递的options
会根据不同的情况进行配置合并,关于具体的options
合并策略我们会在之后的章节详细介绍,现阶段我们只需要知道$options
可以拿到合并后的所有属性,例如props
、methods
以及data
等等。
假设定义了如下实例:
那么在之后可以通过下面的方式来取这些属性。
props处理
介绍完以上前置核心概念后,第一个要学习的就是Vue.js
是如何处理与props
相关的逻辑的。把与props
相关的逻辑主要分成三个部分,分别是props
规范化、props
初始化和props
更新。
props规范化
在了解规范化之前,先来列举一下在日常的开发过程中,主要有如下几种撰写组件props
的方式:
- 数组形式:
props
可以写成一个数组,但数组中的key
元素必须为string
类型。
- 键值不为对象:此种方式常见于只需要定义
key
类型的props
。
- 规范格式:此种方式是
Vue.js
接受props
最好的格式,对于一个有很高要求的组件来说,它通过会撰写很严格的props
规则,这在各个开源UI框架中是最常见的。
而props
规范化所做的事情,就是把各种不是规范格式的形式,规范化为规范格式,方便Vue.js
在后续的过程中处理props
。那么接下来,就来分析Vue.js
是如何对props
规范化的。
props
规范化的过程发生在this._init()
方法中的mergeOptions
合并配置中:
其中mergeOptions()
方法是定义在src/core/util/options.js
文件中,它在其中有一段这样的方法调用:
可以发现,规范化props
的代码,主要集中在normalizeProps()
方法中,那么接下来详细分析normalizeProps()
方法:
为了更好的理解normalizeProps()
方法,来撰写几个案例来详细说明:
- 数组形式:当
props
是数组时,会首先倒序遍历这个数组,然后使用typeof
来判断数组元素的类型。如果不是string
类型,则在开发环境下报错,如果是string
类型,则先把key
转化为驼峰形式,然后把这个key
赋值到临时的res
对象中,此时的键值固定为{ type: null }
- 对象形式:当为对象时会使用
for-in
遍历对象,紧接着和数组形式一样使用camelize
来把key
转成驼峰形式,然后使用isPlainObject()
方法来判断是否为普通对象。如果不是,则转成{ type: Type }
对象形式,其中Type
为定义key
时的Type
,如果是,则直接使用这个对象。
- 既不是数组形式也不是对象形式:报错
props初始化
在了解了props
规范化后,紧接着来了解一下props
初始化的过程。props
初始化过程同样是发生在this._init()
方法中,它在initState
的时候被处理:
然后来详细看一下initProps
中的代码:
在仔细阅读initProps()
方法后,可以对initProps()
方法进行总结,它主要做三件事情:props校验和求值、props响应式和props代理。
props响应式
先来看看最简单的props
响应式,这部分的过程主要使用了在之前介绍过的defineReactive
方法:
唯一值得注意的地方就是:在开发环境下,props
的响应式劫持了setter
方法,这样做的是为了保证props
为单项数据流:既不能在子组件中直接修改父组件传递的props
值。
props代理
经过props
响应式后,会在实例上得到this._props
对象,为了方便更好的获取props
的值,需要对props
做一层proxy
代理。关于proxy
的实现,已经在之前的章节中介绍过了。
props校验求值
最后来看稍微复杂一点的props
校验求值,这部分的功能发生在validateProp
,它的代码如下:
代码分析:可以从以上代码中发现,validateProp
虽然说的是带有校验的功能,但它并不会抛出错误进而阻止validateProp()
方法返回value
,而是根据校验的过程中的不同情况尽可能的提示出很清晰的提示。实质上validateProp()
方法最主要的还是返回value
,同时也根据不同的props
写法处理不同的情况。可以将validateProp()
方法进行总结,它主要做如下几件事情:
- 处理
Boolean
类型的props
。 - 处理
default
默认数据。 props
断言。
那么接下来将分别对这几件事情进行详细的描述。
处理Boolean类型
先来看几个props
传递Boolean
的例子:
然后回到源码中处理Boolean
类型getTypeIndex
的地方,这个函数的代码如下:
这个函数的实现逻辑比较清晰:
- 以
Component A
组件为例,它的props
不是一个数组但却是Boolean
类型,因此返回索引0
。 - 以
Component B
组件为例,因为它的props
都是一个数组,所以要遍历这个数组,然后返回Boolean
类型在数组中的索引i
。 - 以
Component C
组件为例,虽然它是一个数组,但数组中没有任何元素,因此返回索引-1
。
在拿到booleanIndex
后,需要走下面这段代码逻辑:
代码分析:
- 在
if
条件判断中absent
代表虽然在子组件中定义了props
,但是父组件并没有传递任何值,然后&
条件又判断了子组件props
有没有提供default
默认值选项,如果没有,那么它的值只能为false
。
- 在
else if
条件判断中,判断了两种特殊的props
传递方式:
对于第一个种情况stringIndex
为-1
,booleanIndex
为0
,因此value
的值为true
。对于第二种情况,则需要根据props
的定义具体区分:
- 对于
ChildComponentA
来说,由于stringIndex
值为1
,booleanIndex
值为0
,booleanIndex < stringIndex
因此可以认为Boolean
具有更高的优先级,此时value
的值为true
。 - 对于
ChildComponentB
来说,由于stringIndex
值为0
,booleanIndex
值为1
,stringIndex < booleanIndex
因此可以认为String
具有更高的优先级,此时value
的值不处理。
处理default默认数据
处理完Boolean
类型后,来处理默认值,既提到过的虽然子组件定义了props
,但父组件没有传递的情况。
对于以上案例会走如下代码的逻辑:
代码分析:
- 首先判断了子组件有没有提供
default
默认值选项,没有则直接返回undefined
。 - 随后判断了
default
如果是引用类型,则提示必须把default
写成一个函数,既:
- 最后再根据
default
的类型来取值,如果是函数类型则调用这个函数,如果不是函数类型则直接使用。 - 其中下面一段代码在这里并不会说明和分析它的具体作用,而是会在
props
更新章节来介绍。
props断言
最后来分析一下props
断言。
在assertProp
中有三种情况需要去断言:
required
:如果子组件props
提供了required
选项,代表这个props
必须在父组件中传递值,如果不传递则抛出错误信息Missing required prop: fixed
。- 对于定义了多个
type
的类型数组,则会遍历这个类型数组,只要当前props
的类型和类型数组中某一个元素匹配则终止遍历。,否则抛出错误提示信息。
- 用户自己提供的
validator
校验器也需要进行断言:
props更新
都知道子组件的props
值来源于父组件,当父组件值更新时,子组件的值也会发生改变,同时触发子组件的重新渲染。先跳过父组件的具体编译逻辑,直接看父组件的值更新,改变子组件props
值的步骤:
代码分析:
- 以上
vm
实例为子组件,propsData
为父组件中传递的props
的值,而_propKeys
是之前props
初始化过程中缓存起来的所有的props
的key。 - 在父组件值更新后,会通过遍历
propsKey
来重新对子组件props
进行校验求值,最后赋值。
以上代码就是子组件props
更新的过程,在props
更新后会进行子组件的重新渲染,这个重新渲染的过程分两种情况:
- 普通
props
值被修改:当props
值被修改后,其中有段代码props[key] = validateProp(key, propOptions, propsData, vm)
根据响应式原理,会触发属性的setter
,进而子组件可以重新渲染。 - 对象
props
内部属性变化:当这种情况发生时,并没有触发子组件prop
的更新,但是在子组件渲染的时候读取到了props
,因此会收集到这个props
的render watcher
,当对象props
内部属性变化的时候,根据响应式原理依然会触发setter
,进而子组件可以重新进行渲染。
toggleObserving作用
toggleObserving
是定义在src/core/observer/index.js
文件中的一个函数,其代码很简单:
它的作用就是修改当前模块的shouldObserve
变量,用来控制在observe
的过程中是否需要把当前值变成一个observer
对象。
接下来来分析,在处理props
的过程中,什么时候toggleObserving(true)
,什么时候toggleObserving(false)
以及为什么需要这样处理?
props
初始化的时候:
可以看到在最开始判断了当为非根实例(子组件)的时候,进行了toggleObserving(false)
的操作,这样做的目的是因为:当非根实例的时候,组件的props
来自于父组件。当props
为对象或者数组时,根据响应式原理,会递归遍历子属性然后进行observe(val)
,而正是因为props
来源于父组件,这个过程其实已经在父组件执行过了,如果不做任何限制,那么会在子组件中又重复一次这样的过程,因此这里需要toggleObserving(false)
,用来避免递归props
子属性的情况,这属于响应式优化的一种手段。在代码最后,又调用了toggleObserving(true)
,把shouldObserve
的值还原。
props
校验的时候:
先来看props
提供了default
默认值,且默认值返回了对象或者数组。
对于以上point
和list
取默认值的情况,这个时候的props
值与父组件没有关系,那么这个时候需要toggleObserving(true)
,在observe
后再把shouldObserve
变量设置为原来的值。
在props
更新的时候:
当父组件更新的时候,会调用updateChildComponent()
方法,用来更新子组件的props
值,这个时候其实和props
初始化的逻辑一样,同样不需要对指向父组件的对象或数组props
进行递归子属性observe
的过程,因此这里需要执行toggleObserving(false)
。
整体流程图
在分析完以上所有与props
相关的逻辑后,可以总结如下流程图。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!