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
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 */)}
export classObserver{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=0// 根实例数def(value,'__ob__',this)if(Array.isArray(value)){// .. 数组方法的拦截处理this.observeArray(value)}else{this.walk(value)}}// 遍历对象属性,处理getter/setterwalk(obj: Object){constkeys=Object.keys(obj)for(leti=0;i<keys.length;i++){defineReactive(obj,keys[i])}}// 循环数组元素observeobserveArray(items: Array<any>){for(leti=0,l=items.length;i<l;i++){observe(items[i])}}}
let circular: {[key: number]: number}={}exportconstMAX_UPDATE_COUNT=100functionflushSchedulerQueue(){currentFlushTimestamp=getNow()flushing=trueletwatcher,id// watcher按id排序,父watcher先调用queue.sort((a,b)=>a.id-b.id)// 这是循环的queue每次都重新获取长度,是为了处理循环watcher的问题for(index=0;index<queue.length;index++){watcher=queue[index]if(watcher.before){watcher.before()}id=watcher.idhas[id]=nullwatcher.run()// 循环watcherif(process.env.NODE_ENV!=='production'&&has[id]!=null){circular[id]=(circular[id]||0)+1if(circular[id]>MAX_UPDATE_COUNT){warn('You may have an infinite update loop '+(watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`),watcher.vm)break}}}// ...}
run(){if(this.active){// 获取新值constvalue=this.get()if(value!==this.value||// Deep watchers and watchers on Object/Arrays should fire even// when the value is the same, because the value may// have mutated.isObject(value)||this.deep){// set new valueconstoldValue=this.valuethis.value=valueif(this.user){try{this.cb.call(this.vm,value,oldValue)}catch(e){handleError(e,this.vm,`callback for watcher "${this.expression}"`)}}else{this.cb.call(this.vm,value,oldValue)}}}}
consthasProto='__proto__'in{}
export classObserver{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)){// 如果不存在_proto_,直接把方法挂载到数组上if(hasProto){protoAugment(value,arrayMethods)}else{copyAugment(value,arrayMethods,arrayKeys)}this.observeArray(value)}else{this.walk(value)}}/** * Observe a list of Array items. */observeArray(items: Array<any>){for(leti=0,l=items.length;i<l;i++){observe(items[i])}}}
exportfunctionset(target: Array<any>|Object,key: any,val: any): any{if(process.env.NODE_ENV!=='production'&&(isUndef(target)||isPrimitive(target))){warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)}if(Array.isArray(target)&&isValidArrayIndex(key)){target.length=Math.max(target.length,key)target.splice(key,1,val)returnval}// 原来的key直接赋值if(keyintarget&&!(keyinObject.prototype)){target[key]=valreturnval}constob=(target: any).__ob__if(target._isVue||(ob&&ob.vmCount)){process.env.NODE_ENV!=='production'&&warn('Avoid adding reactive properties to a Vue instance or its root $data '+'at runtime - declare it upfront in the data option.')returnval}// 不是响应式数据也直接赋值if(!ob){target[key]=valreturnval}defineReactive(ob.value,key,val)// 通知订阅者ob.dep.notify()returnval}
exportfunctiondel(target: Array<any>|Object,key: any){if(process.env.NODE_ENV!=='production'&&(isUndef(target)||isPrimitive(target))){warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)}if(Array.isArray(target)&&isValidArrayIndex(key)){target.splice(key,1)return}constob=(target: any).__ob__if(target._isVue||(ob&&ob.vmCount)){process.env.NODE_ENV!=='production'&&warn('Avoid deleting properties on a Vue instance or its root $data '+'- just set it to null.')return}if(!hasOwn(target,key)){return}deletetarget[key]if(!ob){return}ob.dep.notify()}
当影响页面的数据发生改变,以往我们需要手动操作DOM来显示最新视图。通过Vue编程,我们只需重点关注数据状态的逻辑处理,Vue会帮我们自动完成视图的渲染工作,这就是Vue的数据响应式机制。现在,我们通过源码来看看Vue的响应式原理。
整体流程
在看源码之前,应该要对Vue的响应式原理有个大概了解。当我们把一个普通的对象传入作为实例的
data
选项,Vue会遍历这个对象的所有属性,利用Object.defineProperty
把这些属性全部转为getter/setter
。在获取对象属性值的时候,会触发该属性的
getter
,这时候的工作是收集该属性的依赖,这里的依赖可以理解为用到这个属性的地方,可以是视图模版,监听属性或者计算属性,在Vue中抽象成一个Watcher
类来维护,所以我们说的依赖就是Watcher
实例。在对象属性赋值的时候,会触发该属性的
setter
,这时候的工作是通知该属性的依赖,执行watcher
的相关处理,比如组件重新渲染或者watch
的回调函数。在Vue的响应式原理中也做了很多优化的处理,比如对数组的响应处理,执行
watcher
时机等等,我们稍后在源码中会做分析。所以,响应原理的流程可以总结成下图:Observe
我们知道对
data
数据的处理是在vm._init
入口的initState()
方法,该方法是对实例状态的初始化工作,包括初始化props
,method
,data
和监听watch
等。我们重点关注初始化data
的方法initData()
,它定义在src/core/instance/state.js
中:这个方法首先判断
data
对象是否和props
和methods
存在同名的key,然后把data
代理到vm
实例上,所有我们就可以通过vm.key
访问data
的数据。最后,调用observe()
方法把data
转为一个响应式数据。来看下observe
方法的定义:这个方法定义在
src/core/observer/index.js
。它主要通过新建一个Observe
实例把value值转为一个响应的数据,并把实例存在对象的__ob__
属性,我们后续的其他处理会用到。现在来看下Observe
类:这个方法也很简单,如果对象是一个数组,则循环数组每个元素进行
observe
转为响应数据。如果是对象,则遍历属性利用defineReactive
方法处理getter/setter
。这里我们先省略对数组的拦截方法的处理,先来看下defineReactive
方法:这个方法就是对
Object.defineProperty
方法设置对象属性的getter/setter
的一个封装。需要考虑的点就是不要忽略对象属性原本的getter/setter
。另外,方法一开始还新建了一个Dep类实例,它是对依赖进行管理的一个类。比如我们要记录每个属性的所有依赖,肯定要用一个数组来维护。所以Dep
类的subs属性就是依赖的队列,我们来看下这个类几个重要的属性和方法:这个方法定义在
src/core/observer/dep.js
。depend()
方法是把依赖加入到subs数组,notify()
方法是循环数组,执行依赖的update
方法。我们上面说了依赖就是一个watcher
实例,所以就是调用watcher
的update
方法,其中Dep.target
这个静态属性很重要,它表示此时正在执行get
方法的watcher
,为什么要这样记录。稍后我们在看依赖收集过程就会恍然大悟。依赖收集
我们现在来看下
defineReactive
方法处理getter
中的依赖收集过程:在设置
getter
前,如果属性值是一个对象,应该递归把对象也转为响应数据,并获取值的__ob__
属性。然后判断Dep.target
有值,就调用depend()
方法进行依赖收集,注意的是属性值的Observe
实例也要进行依赖收集,这是为我们后面数组的响应处理和全局set
方法时能获取到依赖做铺垫。那什么时候
Dep.target
有值呢,那就是当新建一个watcher
并执行它的get
方法的时候。我们先来简单看下Watcher类的定义:在
Watcher
构造函数里面先对属性一些初始值,最后在一般情况都会直接调用this.get()
。在get
方法里面,先把当前watcher
实例存到全局静态变量,然后执行getter
方法。看到这里,在回到我们之前的组件渲染watcher
的定义:很明显,
updateComponent
会作为watcher
的getter
被执行,并且在执行过程中,对于访问到的data
属性,会触发相应的getter
进行依赖收集,这时候的Dep.target
就是当前的渲染watcher
。所以在vue组件模版中用到的data
属性的依赖列表中都会包含该渲染watcher
。在
get
方法的最后会把全局watcher
进行恢复。什么意思?来看下pushTarget
和popTarget
的定义:在处理
watcher
的时候,如果发现有新的watcher
生成,会先把老的watcher
押入栈,待新的watcher
被收集完后,再进行恢复。最后执行cleanupDeps
方法,维护该watcher
最新的依赖情况,也就是watcher
被收集进哪些Dep实例中:watcher
用depIds
和deps
记录上一次的收集情况,用newDepIds
和newDeps
记录本次被收集的情况。这样做的目前为了处理v-if
动态显示模版的情况:当组件初始化
render
的时候,渲染watcher
会被flag和var1作为依赖收集,点击按钮后执行handleClick,flag状态发生改变,再次组件的render
时候,var2会收集该渲染watcher
。你会发现,如果不进行cleanupDeps
的话,这个时候渲染watcher
的会被3个状态收集,但是我们这时候无论怎么修改var1状态,都不会影响视图,所以没必要执行var1里的依赖。所以,我们知道依赖收集的时机,那就是
watcher
实例执行自身的getter
,会把自身缓存在一个全局变量Dep.target
,然后触发属性的getter
进行依赖收集。派发更新
我们来看一下,当组件的状态发生时,会触发状态的
setter
,这时会进行组件的派发更新。我们看下setter
的处理过程:在
setter
先处理新值的判断,如果不变就直接返回,否则回调用依赖实例dep的notify
方法,我们该方法就是循环调用依赖实例(watcher
)的update
方法。我们来下看watcher
的update
方法:其他的条件逻辑先不管,对于渲染的
watcher
会执行queueWatcher
方法:这个是Vue的一个重要的更新优化手段。它会先把触发的
watcher
放到一个去重的队列中,然后调用nextTick(flushSchedulerQueue)
在下个tick去执行flushSchedulerQueue
方法,我们Vue中nextTick是优先采用Promise
微任务的形式模拟异步,这样做的目前是为了在DOM更新前触发这个异步任务,因为DOM的更新是在微任务执行完后执行。如果放在宏任务去执行flushSchedulerQueue
的话,就会浪费最近的一轮DOM更新。现在,来看下
flushSchedulerQueue
的定义:这个方法先把队列的
watcher
按id小到大排序,然后执行watcher
的run
方法,执行外后把has的标记置空。但是如果出现循环watcher
的情况,又会重新执行queueWatcher
方法,并在else条件逻辑中把该watcher
插入相同watcher
的前面,所以这个has[id]
就不是null
了。比如下面情况的循环watch
:所以我们要规避这个情况的发生。在下一个tick中会调用
watcher
的run
方法。看下这个方法:run
方法其实就是执行watcher
的回调函数,并且要调用get
方法获取最新的值。因此,对于组件的渲染的watcher
,会在get
中重新调用updateComponent
,从而用最新的状态渲染视图。到此,我们就走完了状态更新触发视图重新渲染的整个流程。
数组的处理
利用
Object.defineProperty
方法只能监测对象属性,并不能监测数组元素的修改或者添加,对于依赖数组渲染的视图就需要特殊处理了。Vue的做法就拦截响应数据中数组的原型对象,代理的对象自定义push
,shift
等修改数组元素的方法,并在方法通知依赖派发更新。先来看下代理对象的定义,在
src/core/observer/array.js
中:首先,我们肯定要保存本来原型的方法定义,然后在拦截对象中先调用原来对应方法,然后获取数组的依赖实例ob,这里就是之前在
defineReactive
方法中进行递归observe
的保存结果:对于新增元素的方法比如
push
,splice
,还要获取对应的新增元素,循环进行observe
。最后通知该数组的依赖进行更新。回到前面的Observe
类对数组的处理:某些浏览器是不支持
__proto__
属性来设置原型的,这时候Vue的处理是直接把方法挂载到数组对象上。来看下相应的处理函数:于是,我们就可以利用数组
push
等方法在修改数据的同时更新视图。set和delete
上面是对于数组的响应处理,其实对于对象也有对应的问题。比如,我们无法增加一个对象属性使它的值变成可侦测,另外删除属性也是不会被拦截。于是Vue增加了全局和实例上的
Vue.set
和Vue.delete
方法来增加和删除属性,并会触发视图更新。我们先来看下set的定义:对于数组,调用
splice
方法插入,因为这个方法是响应方法,所以会触发更新。对于对象,如果不是响应的对象或者key本身就在对象上面直接赋值即可。如果不是上面两种情况,证明为新增的属性并且要使它可侦测,调用defineReactive
方法进行设置即可,最后通知对象的依赖。对于del也类似,我们来看看:
总结
Vue的响应原理其实理解起来并不复杂,它本后的设计思想就是典型的发布-订阅模式。我们数据状态可以看成是发布者,
watcher
可以看出是订阅者。当数据状态发生,发布者就会通知订阅者,执行订阅者相应的处理。在触发更新的过程,Vue不会马上直接执行
watcher
的回调。而是把watcher
压入到一个队列,在下个tick中再执行,这是一个很重要的优化。The text was updated successfully, but these errors were encountered: