面试官问你Vue2的响应式原理该如何回答?
作者:清风丶
前言
可能很多小伙伴之前都了解过 Vue2实现响应式的核心是利用了ES5的Object.defineProperty
但是面对面试官时如果只知道一些模糊的概念,回答肯定是虎头蛇尾的,只有深入底层了解响应式的原理,才能在关键时刻对答如流,百毒不侵。
响应式对象
Object.defineProperty
方法的官方解释是可以直接在一个对象上定义一个新属性或者修改一个对象的现有属性
let object1 = {}; Object.defineProperty(object1, 'property1', { value: 42, }); console.log(object1); //{property1: 42}
经过Object.defineProperty定义后,object1就有了一个property1属性
并且通过这种方式能为属性添加get
与set
方法,
当一个对象的属性都拥有get和set方法时,就可以称这个对象为响应式对象
let object1 ={} Object.defineProperty(object1, "name", { get() { return 1; }, set(x) { console.log("数据变化了",1+x) } }); console.log(object1) 当我们为object1添加name属性以及get和set方法时 //{ // name:1 // get name:function()... // set name:function()... //} console.log(object1.name) //1 object1.name = 1 //数据变化了 2
当我们读取object1的name值会触发get
方法 这里会打印出1,
当我们修改object1的name值会触发set
方法 这里会打印出 ”数据变化了 2“
响应式开始的地方
vue源码中在初始化data的方法initData
有一句 observe(data)
这就是梦开始的地方,让我们看一下observe具体实现
function observe(value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else{ ob = new Observer(value) } return ob }
首先对传入的值做了一些类型校验,如果不是引用类型或者是vNode实例就直接return返回
接下来判断对象下是否有_ob_
属性,如果有直接返回否则执行new Observer(value)
那么这个_ob_
以及 Observer
是什么东西呢?我们接着往下看
Observer
class Observer { value: any; dep: Dep; vmCount: number; constructor(value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { this.observeArray(value) } else { this.walk(value) } } walk(obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } observeArray(items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }
Observer
的逻辑也非常简单,首先为传入的这个引用类型new了一个dep
,这个dep主要是为了后续的$set方法使用暂且不看并将当前的observer实例储存在目标的_ob_
里,上面说的observe
方法会根据这个_ob_
进行判断,这样是为了防止data里的属性相互引用导致多次生成新实例接下来判断如果是对象类型则对每个属性执行defineReactive
方法
如果是数组类型则遍历数组对每个子项执行observe
方法,observe
方法上面我们说过,它会根据值的类型进行判断如果是数组或者对象就执行new Observer
这一层的套娃实际上是对数组的层层解析,目的就是为了让数组里的对象都执行defineReactive方法
实现响应式的defineReactive
vue2的源码中是通过递归调用defineReactive
方法将所有对象变为响应式对象接下来我们简单看一下defineReactive
的主要逻辑
function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() ... let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } }) }
首先为每一个对象属性都添加的get
与set
方法
并且为每个属性都new
一个dep,这个dep接下来会介绍,值得一提的是这句let childOb = !shallow && observe(val)
observe我们上面说了它会对所有对象以及数组里嵌套的对象执行defineReactive
这段逻辑就是在递归调用defineReactive
方法,这样不管我们对象套了多少层,它都能实现响应vue的响应式实际上的经典的观察者模式,dep
在get方法里实现对观察者watcher
进行收集,在set方法里通知每个观察者watcher执行 update
方法,想要了解过程,接下来我们重点看一下dep
与watcher
的定义
dep
dep.js export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
dep
充当一个中间桥梁的作用,收集以及维护观察者,在目标属性发生变化时调用自己的notify
方法,对每个观察者都执行update
方法通知观察者需要更新
watcher
watcher.js export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; ... constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (typeof expOrFn === 'function') { this.getter = expOrFn } if (this.computed) { this.value = undefined this.dep = new Dep() } else { this.value = this.get() } } } get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value } addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } } update () { if (this.computed) { if (this.dep.subs.length === 0) { this.dirty = true } else { this.getAndInvoke(() => { this.dep.notify() }) } } else if (this.sync) { this.run() } else { queueWatcher(this) } } }
watcher
中的逻辑不适合单独拆开解析,接下来我们结合流程分析,watcher
首次创建实例的场景,这是在第一次渲染页面组件的时候,我们传入的expOrFn
参数对应的是updateComponent
,updateComponent
是vue定义的用于重新渲染页面组件的函数,在代码中updateComponent
又被赋值给了this.getter
new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true )
接着往下看,由于当前不是计算属性所以this.computed
是false,执行this.value = this.get()
在执行get()的时候会执行到pushTarget
方法
Dep.target = null function pushTarget (_target: ?Watcher) { ... Dep.target = _target }
这个方法实际上是将当前的watcher
赋值给了Dep.target
,Dep.target
是一个全局变量,为啥要这么干呢
因为会有组件嵌套的情况,所以可能会有多个渲染watcher
,但是通过这种方式就这样保证了 Dep.target
指向的是最新创建的watcher
,接下来执行了value = this.getter.call(vm, vm)
,上面说了这个this.getter就是传入的updateComponent
,这个updateComponent
就是页面组件重新渲染的方法,
流程分别是:生成vnode->根据vnode树生成真实的dome树->挂载到页面上,在使用rander
生成vnode的时候就会读取到模版语法中的值,当访问到值时就触发了我们通过defineReactive
方法添加的get
方法,就触发了依赖收集过程。
依赖收集
function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() ... let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() } } return value }, ... }) }
刚刚我们说到Dep.target
就是当前的watcher,在上面👆🏻看dep的定义中,dep.depend就是调用当前watcher
的addDep
方法,将当前的watcher观察者收集到dep的subs
数组,这句childOb.dep.depend()
是为$set
量身定做的,事先将当前的watcher收集到Observer
实例里的dep的subs数组中后续当我们使用$set方法时,只需要调用引用对象保存在_ob_的Observer实例中的dep的notify
方法就能实现手动派发更新
watcher.js addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } }
this
在这里是指向当前创建的watcher
实例,这样这个watcher就被收集到该属性的dep对象的subs数组中了,到这里一依赖收集的主干流程就完成了
派发更新
function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() ... let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, ... set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } }) }
触发set
方法时就会进行派发更新,在set方法里我们首先对修改后的值和原来的值进行对比,如果相同就return
,如果不相同就继续执行下面的逻辑,将新值赋值给旧值,childOb = !shallow && observe(newVal)
这一段的逻辑是如果新值也是对象的话就对新值执行defineReactive
将新值变为响应式对象,接下来我们看一下dep.notify()
的定义
notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }
notify
方法会遍历当前收集的watcher
,调用每个watcher的update
方法
update () { /* istanbul ignore else */ if (this.computed) { ... } else if (this.sync) { ... } else { queueWatcher(this) } }
update方法分别判断了是否是计算属性以及是否是同步的watcher
,在当前执行的环境下都为false
,所以执行了queueWatcher(this)
const queue: Array<Watcher> = [] const activatedChildren: Array<Component> = [] let has: { [key: number]: ?true } = {} let circular: { [key: number]: number } = {} let waiting = false let flushing = false function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } if (!waiting) { waiting = true nextTick(flushSchedulerQueue) } } }
因为多个dep
有可能收集的是同一个watcher
,当多个dep收集的同一个watcher时这个watcher
只需要更新一次,所以通过这种方式去除重复的watcher
,由于flushing
与waiting
初始值都为false
,所以会将经过过滤的watcher
push进queue数组,然后会在下一个tick内执行flushSchedulerQueue
function flushSchedulerQueue () { flushing = true let watcher, id queue.sort((a, b) => a.id - b.id) for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { watcher.before() } id = watcher.id has[id] = null watcher.run() ... }
首先会根据id进行一次排序因为如果有嵌套组件的情况出现的话,是先更新父组件再更新子组件,所以进行了一次由小到大的排序,后续对每个watcher
都调用了run
方法
run () { if (this.active) { this.getAndInvoke(this.cb) } }
active的默认值是true所以run方法里调用了getAndInvoke
getAndInvoke (cb: Function) { const value = this.get() .... }
绕了这么大一圈最后还是执行的watcher中的get方法,get方法在上面讲解watcher的时候就已经说过,这个get会执行this.getter
而这个this.getter是创建watcher
时传入的updateComponent
,updateComponent的执行会导致重新渲染页面组件,也就是在属性被修改时触发了set
方法,而这个set方法会将依赖当前属性的页面组件重新渲染,从而达到数据驱动的效果。
总结
vue用Object.defineProperty重写属性的get和set方法
- 在get中new了一个dep收集当前的渲染watcher
- 在set方法中遍历收集的渲染watcher执行update方法
- vue在new渲染watcher的时候会将组件挂载更新的方法(updateComponent)传入,存储在渲染watcher中,触发渲染watcher的update方法时实际上是触发这个组件挂载更新方法 也就是在属性被修改时触发了set方法,而这个set方法会将依赖当前属性的页面重新渲染,从而达到数据驱动的效果。
以上就是面试官问你Vue2的响应式原理该如何回答?的详细内容,更多关于Vue2响应式原理的资料请关注脚本之家其它相关文章!