写在前面(不看错过一个亿)
最近一直在读Vue
源码,也写了一系列的源码探秘
文章。
但,收到很多朋友的反馈都是:源码晦涩难懂,时常看着看着就不知道我在看什么了,感觉缺乏一点动力,如果你可以出点面试中会问到的源码
相关的面试题,通过面试题去看源码,那就很棒棒。
看到大家的反馈,我丝毫没有犹豫:安排!!
我通过三篇文章整理了大厂面试中会经常问到的一些Vue
面试题,通过源码角度去回答,抛弃纯概念型回答,相信一定会让面试官对你刮目相看。
❝文中源码基于 Vue
❞2.6.11
版本
请说一下响应式数据的原理?
Vue
实现响应式数据的核心API
是Object.defineProperty
。
其实默认Vue
在初始化数据时,会给data
中的属性使用Object.defineProperty
重新定义所有属性,当页面取到对应属性时。会进行依赖收集(收集当前组件的watcher
) 如果属性发生变化会通知相关依赖进行更新操作。
这里,我用一张图来说明Vue
实现响应式数据的流程:
- 首先,第一步是初始化用户传入的
data
数据。这一步对应源码src/core/instance/state.js
的 112 行
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
// ...
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
// ...
}
// observe data
observe(data, true /* asRootData */)
}
- 第二步是将数据进行观测,也就是在第一步的
initData
的最后调用的observe
函数。对应在源码的src/core/observer/index.js
的 110 行
/**
* 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 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 if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
这里会通过new Observer(value)
创建一个Observer
实例,实现对数据的观测。
- 第三步是实现对对象的处理。对应源码
src/core/observer/index.js
的 55 行。
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
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
this.dep = new Dep()
this.vmCount = 0
def(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) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// ...
}
- 第四步就是循环对象属性定义响应式变化了。对应源码
src/core/observer/index.js
的 135 行。
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
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() // 收集依赖
// ...
}
return value
},
set: function reactiveSetter (newVal) {
// ...
dep.notify() // 通知相关依赖进行更新
}
})
}
- 第五步其实就是使用
defineReactive
方法中的Object.defineProperty
重新定义数据。在get
中通过dep.depend()
收集依赖。当数据改变时,拦截属性的更新操作,通过set
中的dep.notify()
通知相关依赖进行更新。
Vue 中是如何检测数组变化?
Vue
中检测数组变化核心有两点:
- 首先,使用函数劫持的方式,重写了数组的方法
Vue
将data
中的数组,进行了原型链重写。指向了自己定义的数组原型方法,这样当调用数组api
时,就可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次进行观测。
这里用一张流程图来说明:
❝这里第一步和第二步和上题
❞请说一下响应式数据的原理?
是相同的,就不展开说明了。
- 第一步同样是初始化用户传入的 data 数据。对应源码
src/core/instance/state.js
的 112 行的initData
函数。 - 第二步是对数据进行观测。对应源码
src/core/observer/index.js
的 124 行。 - 第三步是将数组的原型方法指向重写的原型。对应源码
src/core/observer/index.js
的 49 行。
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
// ...
}
也就是protoAugment
方法:
/**
* Augment a target Object or Array by intercepting
* the prototype chain using __proto__
*/
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
- 第四步进行了两步操作。首先是对数组的原型方法进行重写,对应源码
src/core/observer/array.js
。
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [ // 这里列举的数组的方法是调用后能改变原数组的
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) { // 重写原型方法
// cache original method
const original = arrayProto[method] // 调用原数组方法
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted) // 进行深度监控
// notify change
ob.dep.notify() // 调用数组方法后,手动通知视图更新
return result
})
})
第二步呢,是对数组调用observeArray
方法:
// src/core/observer/index.js line:74
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
其实就是遍历数组,对里面的每一项都调用observe
方法,进行深度观测。
为什么Vue
采用异步渲染?
我们先来想一个问题:如果Vue
不采用异步更新,那么每次数据更新时是不是都会对当前组件进行重写渲染呢?
答案是肯定的,为了性能考虑,会在本轮数据更新后,再去异步更新视图。
通过一张图来说明Vue
异步更新的流程:
- 第一步调用
dep.notify()
通知watcher
进行更新操作。对应源码src/core/observer/dep.js
中的 37 行。
notify () { // 通知依赖更新
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // 依赖中的update方法
}
}
- 第二步其实就是在第一步的
notify
方法中,遍历subs
,执行subs[i].update()
方法,也就是依次调用watcher
的update
方法。对应源码src/core/observer/watcher.js
的 164 行
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) { // 计算属性
this.dirty = true
} else if (this.sync) { // 同步watcher
this.run()
} else {
queueWatcher(this) // 当数据发生变化时会将watcher放到一个队列中批量更新
}
}
- 第三步是执行
update
函数中的queueWatcher
方法。对应源码src/core/observer/scheduler.js
的 164 行。
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id // 过滤watcher,多个属性可能会依赖同一个watcher
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher) // 将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
}
nextTick(flushSchedulerQueue) // 调用nextTick方法,在下一个tick中刷新watcher队列
}
}
}
- 第四步就是执行
nextTick(flushSchedulerQueue)
方法,在下一个tick
中刷新watcher
队列
谈一下nextTick
的实现原理?
Vue.js
在默认情况下,每次触发某个数据的 setter
方法后,对应的 Watcher
对象其实会被 push
进一个队列 queue
中,在下一个 tick
的时候将这个队列 queue
全部拿出来 run
( Watcher
对象的一个方法,用来触发 patch
操作) 一遍。
因为目前浏览器平台并没有实现 nextTick
方法,所以 Vue.js
源码中分别用 Promise
、setTimeout
、setImmediate
等方式在 microtask
(或是task
)中创建一个事件,目的是在当前调用栈执行完毕以后(不一定立即)才会去执行这个事件。
nextTick
方法主要是使用了宏任务和微任务,定义了一个异步方法.多次调用nextTick
会将方法存入队列中,通过这个异步方法清空当前队列。
❝所以这个
❞nextTick
方法是异步方法。
通过一张图来看下nextTick
的实现:
- 首先会调用
nextTick
并传入cb
。对应源码src/core/util/next-tick.js
的 87 行。
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
- 接下来会定义一个
callbacks
数组用来存储nextTick
,在下一个tick
处理这些回调函数之前,所有的cb
都会被存在这个callbacks
数组中。 - 下一步会调用
timerFunc
函数。对应源码src/core/util/next-tick.js
的 33 行。
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
timerFunc = () => {
// ...
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
timerFunc = () => {
// ...
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
来看下timerFunc
的取值逻辑:
1、 我们知道异步任务有两种,其中 microtask
要优于 macrotask
,所以优先选择 Promise
。因此这里先判断浏览器是否支持 Promise
。
2、 如果不支持再考虑 macrotask
。对于 macrotask
会先后判断浏览器是否支持 MutationObserver
和 setImmediate
。
3、 如果都不支持就只能使用 setTimeout
。这也从侧面展示出了 macrotask
中 setTimeout
的性能是最差的。
❝❞
nextTick
中if (!pending)
语句中pending
作用显然是让if
语句的逻辑只执行一次,而它其实就代表callbacks
中是否有事件在等待执行。
这里的flushCallbacks
函数的主要逻辑就是将 pending
置为 false
以及清空 callbacks
数组,然后遍历 callbacks
数组,执行里面的每一个函数。
nextTick
的最后一步对应:
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
这里 if
对应的情况是我们调用 nextTick
函数时没有传入回调函数并且浏览器支持 Promise
,那么就会返回一个 Promise
实例,并且将 resolve
赋值给 _resolve
。回到nextTick
开头的一段代码:
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
当我们执行 callbacks
的函数时,发现没有 cb
而有 _resolve
时就会执行之前返回的 Promise
对象的 resolve
函数。
你知道Vue
中computed
是怎么实现的吗?
这里先给一个结论:计算属性computed
的本质是 computed Watcher
,其具有缓存。
一张图了解下computed
的实现:
- 首先是在组件实例化时会执行
initComputed
方法。对应源码src/core/instance/state.js
的 169 行。
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
initComputed
函数拿到 computed
对象然后遍历每一个计算属性。判断如果不是服务端渲染就会给计算属性创建一个 computed Watcher
实例赋值给watchers[key]
(对应就是vm._computedWatchers[key]
)。然后遍历每一个计算属性调用 defineComputed
方法,将组件原型,计算属性和对应的值传入。
defineComputed
定义在源码src/core/instance/state.js
210 行。
// src/core/instance/state.js
export function defineComputed(
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering();
if (typeof userDef === "function") {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef);
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop;
sharedPropertyDefinition.set = userDef.set || noop;
}
if (
process.env.NODE_ENV !== "production" &&
sharedPropertyDefinition.set === noop
) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
);
};
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
首先定义了 shouldCache
表示是否需要缓存值。接着对 userDef
是函数或者对象分别处理。这里有一个 sharedPropertyDefinition
,我们来看它的定义:
// src/core/instance/state.js
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop,
};
sharedPropertyDefinition
其实就是一个属性描述符。
回到 defineComputed
函数。如果 userDef
是函数的话,就会定义 getter
为调用 createComputedGetter(key)
的返回值。
❝因为
❞shouldCache
是true
而 userDef
是对象的话,非服务端渲染并且没有指定 cache
为 false
的话,getter
也是调用 createComputedGetter(key)
的返回值,setter
则为 userDef.set
或者为空。
所以 defineComputed
函数的作用就是定义 getter
和 setter
,并且在最后调用 Object.defineProperty
给计算属性添加 getter/setter
,当我们访问计算属性时就会触发这个 getter
。
❝对于计算属性的
❞setter
来说,实际上是很少用到的,除非我们在使用computed
的时候指定了set
函数。
- 无论是
userDef
是函数还是对象,最终都会调用createComputedGetter
函数,我们来看createComputedGetter
的定义:
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value;
}
};
}
❝我们知道访问计算属性时才会触发这个
❞getter
,对应就是computedGetter
函数被执行。
computedGetter
函数首先通过 this._computedWatchers[key]
拿到前面实例化组件时创建的 computed Watcher
并赋值给 watcher
。
❝在
❞new Watcher
时传入的第四个参数computedWatcherOptions
的lazy
为true
,对应就是watcher
的构造函数中的dirty
为true
。在computedGetter
中,如果dirty
为false
(即依赖的值没有发生变化),就不会重新求值。相当于computed
被缓存了。
接着有两个 if
判断,首先调用 evaluate
函数:
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
首先调用 this.get()
将它的返回值赋值给 this.value
,来看 get
函数:
// src/core/observer/watcher.js
/**
* Evaluate the getter, and re-collect dependencies.
*/
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
}
get
函数第一步是调用 pushTarget
将 computed Watcher
传入:
// src/core/observer/dep.js
export function pushTarget(target: ?Watcher) {
targetStack.push(target);
Dep.target = target;
}
可以看到 computed Watcher
被 push 到 targetStack
同时将 Dep.target
置为 computed Watcher
。而 Dep.target
原来的值是渲染 Watcher
,因为正处于渲染阶段。回到 get
函数,接着就调用了 this.getter
。
回到 evaluate
函数:
evaluate () {
this.value = this.get()
this.dirty = false
}
执行完get
函数,将dirty
置为false
。
回到computedGetter
函数,接着往下进入另一个if
判断,执行了depend
函数:
// src/core/observer/watcher.js
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
这里的逻辑就是让 Dep.target
也就是渲染 Watcher
订阅了 this.dep
也就是前面实例化 computed Watcher
时候创建的 dep
实例,渲染 Watcher
就被保存到 this.dep
的 subs
中。
在执行完 evaluate
和 depend
函数后,computedGetter
函数最后将 evaluate
的返回值返回出去,也就是计算属性最终计算出来的值,这样页面就渲染出来了。