面试题:请阐述
vue2
响应式原理
vue官方阐述:cn.vuejs.org/v2/guide/re…
响应式数据的最终目标,是当对象本身或对象属性发生变化时,将会运行一些函数,最常见的就是render函数。
在具体实现上,vue用到了几个核心部件:
Observer要实现的目标非常简单,就是把一个普通的对象转换为响应式的对象
为了实现这一点,Observer把对象的每个属性通过Object.defineProperty
转换为带有getter
和setter
的属性,这样一来,当访问或设置属性时,vue
就有机会做一些别的事情。
代码实现响应式
/** * Define a reactive property on an Object. * 定义一个响应式数据 */ export function defineReactive ( obj: Object, // 传入的对象 key: string, // 对象属性名 val: any, // 对象属性的值 customSetter?: ?Function, // 自定义的setter shallow?: boolean // 不进行深度响应式 ) { // 创建一个依赖实例对象 const dep = new Dep() // 获取当前属性描述符 const property = Object.getOwnPropertyDescriptor(obj, key) // 如果对象不可以进行配置,直接返回 if (property && property.configurable === false) { return } // cater for pre-defined getter/setters 满足预定义的getter/setter const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } // 深度响应式的话,调用observe方法 let childOb = !shallow && observe(val); // 使用Object.defineProperty来进行setter和getter,这样就能进行 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() if (Array.isArray(value)) { dependArray(value) } } } 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() } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) // 数据发生改变进行通知 dep.notify() } }) } 复制代码
上面列举是是单个对象的响应式,实际上如果是对象里面嵌套对象,需要进行递归遍历对象的所有属性,以完成深度的属性转换
Observer是vue内部的构造器,在vue2.6
以后,我们可以通过Vue提供的静态方法Vue.observable( object )
间接的使用该功能。 api的具体实现
// 2.6 explicit observable API Vue.observable = <T>(obj: T): T => { // 将数据进行响应式后直接返回数据,由于对象是引用传递,所以会有以下代码 observe(obj) return obj } 复制代码
observe具体实现
/** * 尝试为值创建观察者实例,如果成功观察,则返回新的观察者,如果该值已有一个观察者,则返回现有的观察者。 */ export function observe (value: any, asRootData: ?boolean): Observer | void { // 如果传入的数据不是对象或者是vue虚拟节点,直接返回 if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void // 判断传入的数据是否有 __ob__的原型并且,value的原型是Observer if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { // 为value创建一个value实例对象,Observer(观察者)将目标对象的属性键转换为getter/setter,用于收集依赖项并发送更新 ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob } 复制代码
在组件生命周期中,这件事发生在beforeCreate
之后,created
之前。
由于遍历时只能遍历到对象的当前属性,因此无法监测到将来动态增加
或删除
的属性,因此vue
提供了$set
和$delete
两个实例方法,让开发者通过这两个实例方法对已有响应式对象添加或删除属性。 $set的实现方法
/** * 设置对象的属性。添加新属性并在该属性不存在时触发更改通知。 */ export function set (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) return val } // 如果属性存在目标对象中,但是不存在于超类Object的原型对象上 if (key in target && !(key in Object.prototype)) { target[key] = val return val } const ob = (target: any).__ob__; // 对象不能是 Vue 实例,或者 Vue 实例的根数据对象 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.' ) return val } // ob 不存在,直接往对象中添加属性 if (!ob) { target[key] = val return val } // 把属性变为响应式的属性 defineReactive(ob.value, key, val) // 依赖通知用到该对象的进行render更新 ob.dep.notify() return val } 复制代码
$del的实现
/** * 删除属性并在必要时触发更新。 */ export function del (target: Array<any> | Object, key: any) { // 和set一样,判断目标是否是引用值 if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`) } // 判断目标是否是数组,并且判断key是否是一个数字 if (Array.isArray(target) && isValidArrayIndex(key)) { target.splice(key, 1) return } const ob = (target: any).__ob__; // 对象不能是 Vue 实例,或者 Vue 实例的根数据对象 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 } // 如果target不存在key,直接返回 if (!hasOwn(target, key)) { return } // 存在进行删除 delete target[key] if (!ob) { return } // 进行通知,render进行渲染 ob.dep.notify() } 复制代码
✨✨✨注意:通过
$set
,$del
, 我们会发现,这两个方法虽然是官方提供的方法,但是尽量少用,毕竟需要进行好多的判断,然后来进行通知render
。
对于数组,vue
会更改它的隐式原型,之所以这样做,是因为vue需要监听那些可能改变数组内容的方法
将数组变为响应式的关键代码
// 判断是否有对象原型 if (hasProto) { // 通过使用__proto__拦截原型链来扩充目标数组 protoAugment(value, arrayMethods) 上面这句话等于 value.__proto__ = arrayMethods } else { // 通过定义隐藏属性来扩充目标对象或数组。 copyAugment(value, arrayMethods, arrayKeys) } // 将数组变成响应式 this.observeArray(value) 复制代码
总之,Observer的目标,就是要让一个对象,它属性的读取、赋值,内部数组的变化都要能够被vue感知到。使得Vue能够在数据改变,来做一些事情。。
这里有两个问题没解决,就是读取属性时要做什么事,而属性变化时要做什么事,这个问题需要依靠Dep
来解决。
Dep的含义是Dependency
,表示依赖的意思。
Vue
会为响应式对象中的每个属性、对象本身、数组本身创建一个Dep
实例,每个Dep
实例都有能力做以下两件事:
响应式对象创建Dep核心代码
export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that have this object as root $data constructor (value: any) { this.value = value // 创建一个dep实例,每一个响应对象都会有一个Dep实例哦 this.dep = new Dep() this.vmCount = 0 // …… 把数据变成响应式的数据 } 复制代码
Dep本是一个发布订阅模式
/** * dep是一个可观察对象,可以有多个指令订阅它 */ 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 () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { // 这里交给update去更新依赖 subs[i].update() } } } 复制代码
这里又出现一个问题,就是Dep如何知道是谁在用我?
要解决这个问题,需要依靠另一个东西,就是Watcher
。
当某个函数执行的过程中,用到了响应式数据,响应式数据是无法知道是哪个函数在用自己的,vue把函数交给一个叫做watcher
的东西去执行,watcher是一个对象
,每个这样的函数执行时都应该创建一个watcher
,通过watcher
去执行. watch 简化版
观察者解析表达式,收集依赖项,并在表达式值更改时触发回调。这用于
$watch()api
和指令
export default class Watcher { constructor( vm: Component, expOrFn: string | Function, // 要 watch 的属性名称 cb: Function, // 回调函数 options?: ?Object, // 配置参数 isRenderWatcher?: boolean // 是否是渲染函数观察者,Vue 初始化时,这个参数被设为 true ) { // 省略部分代码... 这里代码的作用是初始化一些变量 // expOrFn 可以是 字符串 或者 函数 // 什么时候会是字符串,例如我们正常使用的时候,watch: { x: fn }, Vue内部会将 `x` 这个key 转化为字符串 // 什么时候会是函数,其实 Vue 初始化时,就是传入的渲染函数 new Watcher(vm, updateComponent, ...); if (typeof expOrFn === 'function') { this.getter = expOrFn } else { // 当 expOrFn 不为函数时,可能是这种描述方式:watch: {'a.x'(){ //do } },具体到了某个对象的属性 // 这个时候,就需要通过 parsePath 方法,parsePath 方法返回一个函数 // 函数内部会去获取 'a.x' 这个属性的值了 this.getter = parsePath(expOrFn) // 省略部分代码... } // 这里调用了 this.get,也就意味着 new Watcher 时会调用 this.get // this.lazy 是修饰符,除非用户自己传入,不然都是 false。可以先不管它 this.value = this.lazy? undefined: this.get() } get () { // 将 当前 watcher 实例,赋值给 Dep.target 静态属性 // 也就是说 执行了这行代码,Dep.target 的值就是 当前 watcher 实例 // 并将 Dep.target 入栈 ,存入 targetStack 数组中 pushTarget(this) // 省略部分代码... try { // 这里执行了 this.getter,获取到属性的初始值 // 如果是初始化时 传入的 updateComponent 函数,这个时候会返回 udnefined value = this.getter.call(vm, vm) } catch (e) { // 省略部分代码... } finally { // 省略部分代码... // 出栈 popTarget() // 省略部分代码... } // 返回属性的值 return value } // 这里再回顾一下 // dep.depend 方法,会执行 Dep.target.addDep(dep) 其实也就是 watcher.addDep(dep) // watcher.addDep(dep) 会执行 dep.addSub(watcher) // 将当前 watcher 实例 添加到 dep 的 subs 数组 中,也就是收集依赖 // dep.depend 和 这个 addDep 方法,有好几个 this, 可能有点绕。 addDep (dep: Dep) { const id = dep.id // 下面两个 if 条件都是去重的作用,我们可以暂时不考虑它们 // 只需要知道,这个方法 执行 了 dep.addSub(this) if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { // 将当前 watcher 实例添加到 dep 的 subs 数组中 dep.addSub(this) } } } // 派发更新 update () { // 如果用户定义了 lazy ,this.lazy 是描述符,我们这里可以先不管它 if (this.lazy) { this.dirty = true // this.sync 表示是否改变了值之后立即触发回调。如果用户定义为true,则立即执行 this.run } else if (this.sync) { this.run() // queueWatcher 内部也是执行的 watcher实例的 run 方法,只不过内部调用了 nextTick 做性能优化。 // 它会将当前 watcher 实例放入一个队列,在下一次事件循环时,遍历队列并执行每个 watcher实例的run() 方法 } else { queueWatcher(this) } } run () { if (this.active) { // 获取新的属性值 const value = this.get() if ( // 如果新值不等于旧值 value !== this.value || // 如果新值是一个 引用 类型,那么一定要触发回调 // 举个例子,如果旧值本来就是一个对象, // 在新值内,我们只改变对象内的某个属性值,那新值和旧值本身还是相等的 // 也就是说,如果 this.get 返回的是一个引用类型,那么一定要触发回调 isObject(value) || // 是否深度 watch this.deep ) { // set new value const oldValue = this.value this.value = value // this.user 是一个标志符,如果开发者添加的 watch 选项,这个值默认为 true // 如果是用户自己添加的 watch ,就加一个 try catch。方便用户调试。否则直接执行回调。 if (this.user) { try { // 触发回调,并将 新值和旧值 作为参数 // 这也就是为什么,我们写 watch 时,可以这样写: function (newVal, oldVal) { // do } 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) } } } } // 省略部分代码... } 复制代码
watcher会设置一个全局变量,让全局变量记录当前负责执行的watcher等于自己,然后再去执行函数,在函数的执行过程中,如果发生了依赖记录dep.depend()
,那么Dep
就会把这个全局变量记录下来,当Dep进行派发更新时,它会通知之前记录的所有watcher进行更新,执行run函数
每一个vue
组件实例,都至少对应一个watcher
,该watcher
中记录了该组件的render
函数。
watcher
首先会把render
函数运行一次以收集依赖,于是那些在render中用到的响应式数据就会记录这个watcher。
当数据变化时,dep
就会通知该watcher
,而watcher
将重新运行render
函数,从而让界面重新渲染同时重新记录当前的依赖。
现在还剩下最后一个问题,就是Dep通知watcher之后,如果watcher执行重运行对应的函数,就有可能导致函数频繁运行,从而导致效率低下
试想,如果一个交给watcher的函数,它里面用到了属性a、b、c、d,那么a、b、c、d属性都会记录依赖,于是下面的代码将触发4次更新:
state.a = "new data"; state.b = "new data"; state.c = "new data"; state.d = "new data"; 复制代码
这样显然是不合适的,因此,watcher收到派发更新的通知后,实际上不是立即执行对应函数,而是把自己交给一个叫调度器的东西
调度器核心代码
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true // 判断一个极限的情况,是否正在入队 if (!flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } // 进行出队 queue.splice(i + 1, 0, watcher) } // queue the flush 队列正在刷新 if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } // 放入nextick来进行微队列进行执行 nextTick(flushSchedulerQueue) } } } 复制代码
调度器维护一个执行队列,该队列同一个watcher仅会存在一次,队列中的watcher不是立即执行,它会通过一个叫做
nextTick
的工具方法,把这些需要执行的watcher放入到事件循环的微队列中,nextTick的具体做法是通过Promise
完成的
nextTick 通过 this.$nextTick
暴露给开发者
nextTick 的具体处理方式见:cn.vuejs.org/v2/guide/re…
也就是说,当响应式数据变化时,render
函数的执行是异步的,并且在微队列中
nextick 核心方法
export function nextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { // 在callbacks这个栈种维护函数 if (cb) { try { // 改变cb的上下文 cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) // 当不在等着中 if (!pending) { pending = true // 执行timerFuc函数,这个函数的实现会根据当前的环境来决定。 // Vue 在内部对异步队列尝试使用原生的 `Promise.then`、`MutationObserver` 和 `setImmediate`, // 如果执行环境都不支持,则会采用 `setTimeout(fn, 0)` 代替。 timerFunc() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } } 复制代码