You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
我曾经在 我的博客 介绍过 Object.defineProperty,这里过一遍核心的概念。Object.defineProperty 这个方法可以在一个对象上定义属性或修改现有属性,第三个参数 属性描述符 是一个对象,顾名思义是对属性的描述,有 数据描述符 和 存取描述符 两种。存取描述符 可以设置 get 和 set 两个方法,这样当属性被读取时就会自动调用 get 方法,属性被修改就会自动调用 set 方法。
举个例子说明:
letobj={a: 1};Object.defineProperty(obj,'a',{get(){console.log('a 被访问了');},set(){console.log('a 被修改了');}});letb=obj.a;// a 被访问了obj.a=2;// a 被修改了
可以看到这里针对不同的属性调用了不同的函数来处理,这一节我们只分析针对 props 和 data 的处理,先来看 initProps :
functioninitProps(vm: Component,propsOptions: Object){constpropsData=vm.$options.propsData||{}constprops=vm._props={}// cache prop keys so that future props updates can iterate using Array// instead of dynamic object key enumeration.constkeys=vm.$options._propKeys=[]constisRoot=!vm.$parent// root instance props should be convertedif(!isRoot){toggleObserving(false)}for(constkeyinpropsOptions){// ...}toggleObserving(true)}
initProps 函数一开始拿的 vm.$options.propsData 指的是组件外部传入组件的值,要注意和参数 propsOptions 的区别。可以看到这里在 for 循环前调用了 toggleObserving(false) ,在循环后又调用了 toggleObserving(true) ,这么做的原因稍后会分析。然后是 for 循环中的内容:
for(constkeyinpropsOptions){keys.push(key)constvalue=validateProp(key,propsOptions,propsData,vm)/* istanbul ignore else */if(process.env.NODE_ENV!=='production'){consthyphenatedKey=hyphenate(key)if(isReservedAttribute(hyphenatedKey)||config.isReservedAttr(hyphenatedKey)){warn(`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,vm)}defineReactive(props,key,value,()=>{if(!isRoot&&!isUpdatingChildComponent){warn(`Avoid mutating a prop directly since the value will be `+`overwritten whenever the parent component re-renders. `+`Instead, use a data or computed property based on the prop's `+`value. Prop being mutated: "${key}"`,vm)}})}else{defineReactive(props,key,value)}// static props are already proxied on the component's prototype// during Vue.extend(). We only need to proxy props defined at// instantiation here.if(!(keyinvm)){proxy(vm,`_props`,key)}}
functioninitData(vm: Component){letdata=vm.$options.datadata=vm._data=typeofdata==='function'
? getData(data,vm)
: data||{}if(!isPlainObject(data)){data={}process.env.NODE_ENV!=='production'&&warn('data functions should return an object:\n'+'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',vm)}// proxy data on instanceconstkeys=Object.keys(data)constprops=vm.$options.propsconstmethods=vm.$options.methodsleti=keys.lengthwhile(i--){constkey=keys[i]if(process.env.NODE_ENV!=='production'){if(methods&&hasOwn(methods,key)){warn(`Method "${key}" has already been defined as a data property.`,vm)}}if(props&&hasOwn(props,key)){process.env.NODE_ENV!=='production'&&warn(`The data property "${key}" is already declared as a prop. `+`Use prop default value instead.`,vm)}elseif(!isReserved(key)){proxy(vm,`_data`,key)}}// observe dataobserve(data,true/* asRootData */)}
initData 通过 vm.$options.data 拿到 data 后,如果 data 是一个函数则将它返回的对象赋值给 data 和 vm._data 。然后会遍历 data 的属性,先检查 data 属性和 methods、props 是否重名,然后调用 proxy 将 vm._data.xxx 代理到 vm.xxx 上。最后调用 observe 函数来观察 data 。
initData 最后调用了 observe 函数来观察 data ,observe 函数定义在 src/core/observer/index.js 文件中:
// /src/core/observer/index.js/** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */
export functionobserve(value: any,asRootData: ?boolean): Observer|void{if(!isObject(value)||valueinstanceofVNode){
return
}letob: Observer|voidif(hasOwn(value,'__ob__')&&value.__ob__instanceofObserver){ob=value.__ob__}elseif(shouldObserve&&!isServerRendering()&&(Array.isArray(value)||isPlainObject(value))&&Object.isExtensible(value)&&!value._isVue){ob=newObserver(value)}if(asRootData&&ob){ob.vmCount++}returnob}
从注释我们可以看出来,observe 函数的作用是给传入的 value 创建一个观察者实例并返回(如果已经有了则直接返回)。函数首先对 value 的类型做了限制,必须是一个非 VNode 类型的对象,然后通过 __ob__ 检查观察者实例是否已存在,存在则直接返回,不存在的话会判断是否要创建一个实例。创建观察者实例需要满足的第一个条件是 shouldObserve ,在 src/core/observer/index.js 文件中有这么几行代码:
// /src/core/observer/index.jsexportclassObserver{value: any;dep: Dep;vmCount: number;// number of vms that have this object as root $dataconstructor(value: any){this.value=valuethis.dep=newDep()this.vmCount=0def(value,'__ob__',this)if(Array.isArray(value)){if(hasProto){protoAugment(value,arrayMethods)}else{copyAugment(value,arrayMethods,arrayKeys)}this.observeArray(value)}else{this.walk(value)}}/** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */walk(obj: Object){constkeys=Object.keys(obj)for(leti=0;i<keys.length;i++){defineReactive(obj,keys[i])}}/** * Observe a list of Array items. */observeArray(items: Array<any>){for(leti=0,l=items.length;i<l;i++){observe(items[i])}}}
前言
在第二章,我们学习了数据驱动的概念,分析了 Vue 的初始化过程以及数据是如何渲染到
DOM
上的整个过程,但是还没有分析数据变化后是如何驱动视图更新的。这一章我们就来分析由于用户交互或者其他方面导致数据发生变化引起页面重新渲染的原理,下面先看一个例子:
在这个例子中,点击
div
会修改msg
的值从而引发视图更新,那在 Vue 中是怎么实现的呢,接下来就来研究 Vue 响应式原理的源码实现。响应式对象
根据 Vue 官方文档 的介绍,当我们把一个普通的
js
对象传入 Vue 实例作为data
选项时,Vue 会遍历此对象的所有属性,并使用Object.defineProperty
给这些属性添加getter/setter
,使之成为一个响应式对象。Object.defineProperty
我曾经在 我的博客 介绍过
Object.defineProperty
,这里过一遍核心的概念。Object.defineProperty
这个方法可以在一个对象上定义属性或修改现有属性,第三个参数 属性描述符 是一个对象,顾名思义是对属性的描述,有 数据描述符 和 存取描述符 两种。存取描述符 可以设置get
和set
两个方法,这样当属性被读取时就会自动调用get
方法,属性被修改就会自动调用set
方法。举个例子说明:
Vue 正是使用了上述
Object.defineProperty
的用法来实现响应式对象的,接下来我们从源码的角度来分析。initState
我们知道在
_init
函数中调用了一系列init
方法做初始化工作,其中有一个方法initState
,它的作用是初始化props
、data
、methods
等属性,定义在src/core/instance/state.js
文件中:可以看到这里针对不同的属性调用了不同的函数来处理,这一节我们只分析针对
props
和data
的处理,先来看initProps
:initProps
函数一开始拿的vm.$options.propsData
指的是组件外部传入组件的值,要注意和参数propsOptions
的区别。可以看到这里在for
循环前调用了toggleObserving(false)
,在循环后又调用了toggleObserving(true)
,这么做的原因稍后会分析。然后是for
循环中的内容:for
循环首先将propsOptions
中的key
push 到vm.$options._propKeys
中,然后调用了validateProp
函数,现在我们只需要知道validateProp
函数的作用是校验每个key
给定的prop
数据是否符合预期的类型,并返回相应prop
的值(或默认值)。之后在if else
逻辑都调用了defineReactive
函数,这个函数作用是把prop
中的每个key
都变成响应式的,最后执行的proxy
函数作用是把对vm._props.xxx
的访问代理到vm.xxx
上,这两个函数都会在稍后具体分析。这样我们大概了解了
initProps
函数后,再来看initData
函数:initData
通过vm.$options.data
拿到data
后,如果data
是一个函数则将它返回的对象赋值给data
和vm._data
。然后会遍历data
的属性,先检查data
属性和methods
、props
是否重名,然后调用proxy
将vm._data.xxx
代理到vm.xxx
上。最后调用observe
函数来观察data
。在大概浏览了
initProps
和initData
的执行流程后,我们了解到了这两个函数最主要的作用是将props
、data
转换为响应式对象,接下来就逐个来分析initProps
和initData
中调用的函数。proxy
本来
data
的属性是保存在vm._data
上的,props
的属性是保存在vm._props
上的,但我们在实际使用 Vue 时却可以在vm
上访问这些属性,这是因为内部调用proxy
函数做了一层访问代理,我们来看proxy
函数是怎么实现的:可以看到,这里先定义了一个全局的属性描述符
sharedPropertyDefinition
,在调用proxy
函数的时候(以proxy(vm, '_data', key)
为例),会在vm
定义一个key
属性,通过getter
和setter
,使得访问vm.key
时会返回vm._data.key
,修改vm.key
时实际会修改vm._data.key
,相当于做了一层访问代理。observe
initData
最后调用了observe
函数来观察data
,observe
函数定义在src/core/observer/index.js
文件中:从注释我们可以看出来,
observe
函数的作用是给传入的value
创建一个观察者实例并返回(如果已经有了则直接返回)。函数首先对value
的类型做了限制,必须是一个非VNode
类型的对象,然后通过__ob__
检查观察者实例是否已存在,存在则直接返回,不存在的话会判断是否要创建一个实例。创建观察者实例需要满足的第一个条件是shouldObserve
,在src/core/observer/index.js
文件中有这么几行代码:可以看到 ,
shouldObserve
相当于一个开关,决定是否要观察,而toggleObserving
函数控制shouldObserve
的开和关,回顾initProps
函数:可以看到,是否应该被观察取决于当前实例是不是根实例。回到
observe
,创建观察者实例还需要满足以下条件:value
是一个数组或者是纯对象value
是可扩展的(可添加属性)value
不是 Vue 实例满足这些条件后就会调用
Observer
创建一个观察者实例并返回出去。Observer
定义如下:在
Observer
的constructor
中,初始化了几个属性的值,其中dep
会在下一节详细介绍,接着执行def(value, '__ob__', this)
,来看def
函数的定义:可以看到,
def
函数就是调用Object.defineProperty
函数给value
(对应到initData
函数 就是data
对象)添加一个__ob__
属性,属性值为这个观察者实例。这里为什么不直接value.__proto__ = this
而要调用def
函数呢,带着这个疑问我们继续往下分析。接下来针对不同类型的
value
做不同处理:value
是一个数组,则调用observeArray
函数。observeArray
函数的逻辑很简单,就是遍历value
的每一项递归调用observe
函数。value
是一个纯对象,则调用walk
函数,walk
函数会遍历value
对象的属性调用defineReactive
函数。可以看到无论是
observeArray
还是walk
函数都会遍历value
,这就是为什么要调用def
函数来设置__ob__
属性,通过设置__ob__
属性描述符的enumerable = false
(不可枚举),这样可以避免__ob__
属性被遍历。在
Observer
类中调用了defineReactive
函数,在initProps
中也有调用,这个函数其实就是给对象属性添加getter
和setter
。下面就来分析defineReactive
函数。defineReactive
defineReactive
函数定义在src/core/observer/index.js
中:defineReactive
函数首先会通过Object.getOwnPropertyDescriptor
拿到对象指定属性的属性描述符,如果属性描述符的configurable
属性(表示该属性是否可配置)为false
则return
结束函数。之后会尝试获取属性描述符的getter
和setter
,如果没有传入value
参数并且没有getter
或者有setter
(因为getter
可能会影响取值),则主动通过obj[key]
拿到value
。接着递归调用
observe(val)
,因为val
本身可能也是一个对象,要递归调用实现深度观察。这样data
下的所有非VNode
对象属性都会有__ob__
属性,所以可以通过__ob__
属性来判断一个对象是不是响应式对象,这个在后面章节也会有所涉及。最后就是调用Object.defineProperty
重新定义属性key
,配置存取描述符:包括可枚举,可配置,定义getter
和setter
。getter
要做的就是依赖收集,setter
要做的是派发更新,这两个函数的具体逻辑会放在下一节详细介绍。总结
这一节我们学习了 Vue 是如何深度观察
data
、props
对象的,它的核心就是调用Object.defineProperty
给对象的属性添加getter
和setter
使之成为一个响应式对象,并且如果属性也是对象则递归调用observe
函数实现深度观察。这一节我们还没有分析
getter
和setter
的具体逻辑,下一节我们先来分析getter
,也就是 依赖收集 的原理。The text was updated successfully, but these errors were encountered: