面试题:请阐述vue2 响应式原理
vue官方阐述:cn.vuejs.org/v2/guide/re…
响应式数据的最终目标,是当对象本身或对象属性发生变化时,将会运行一些函数,最常见的就是render函数。
在具体实现上,vue用到了几个核心部件:
- Observer:
- Dep
- Watcher
- Scheduler
Observer
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 来解决。
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()
}
}
}
复制代码
Watcher
这里又出现一个问题,就是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 函数,从而让界面重新渲染同时重新记录当前的依赖。
Scheduler
现在还剩下最后一个问题,就是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
})
}
}
复制代码
总体流程
|