作者:前端森林
转发连接:https://mp.weixin.qq.com/s/4g8XCx0olYaqY78q3ravIA
写在前面(不看错过一个亿)
最近一直在读Vue源码,也写了一系列的源码探秘文章。
但,收到很多朋友的反馈都是:源码晦涩难懂,时常看着看着就不知道我在看什么了,感觉缺乏一点动力,如果你可以出点面试中会问到的源码相关的面试题,通过面试题去看源码,那就很棒棒。
看到大家的反馈,我丝毫没有犹豫:安排!!
我通过三篇文章整理了大厂面试中会经常问到的一些Vue面试题,通过源码角度去回答,抛弃纯概念型回答,相信一定会让面试官对你刮目相看。
请说一下响应式数据的原理?
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
})
})
为什么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.js210 行。
// 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为true(即依赖的值没有发生变化),就不会重新求值。相当于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 的返回值返回出去,也就是计算属性最终计算出来的值,这样页面就渲染出来了。
推荐Vue学习资料文章:
《聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总》
《【新消息】Vue 3.0 Beta 版本发布,你还学的动么?》
《Vue + Koa从零打造一个H5页面可视化编辑器——Quark-h5》
《深入浅出Vue3 跟着尤雨溪学 TypeScript 之 Ref 【实践】》
《手把手教你深入浅出vue-cli3升级vue-cli4的方法》
《Vue 3.0 Beta 和React 开发者分别杠上了》
《手把手教你用vue drag chart 实现一个可以拖动 / 缩放的图表组件》
《Vue3 尝鲜》
《手把手让你成为更好的Vue.js开发人员的12个技巧和窍门【实践】》
《2020 年,Vue 受欢迎程度是否会超过 React?》
《手把手教你Vue解析pdf(base64)转图片【实践】》
《手把手教你Vue之父子组件间通信实践讲解【props、$ref 、$emit】》
《深入浅出Vue3 的响应式和以前的区别到底在哪里?【实践】》
《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》
《基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截原理与实现》
《手把手教你D3.js 实现数据可视化极速上手到Vue应用》
《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【上】》
《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【中】》
《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【下】》
作者:前端森林
转发连接:https://mp.weixin.qq.com/s/4g8XCx0olYaqY78q3ravIA
本文暂时没有评论,来添加一个吧(●'◡'●)