最近一直在读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 的返回值返回出去,也就是计算属性最终计算出来的值,这样页面就渲染出来了。
---来自腾讯云社区的---前端森林
微信扫一扫打赏
支付宝扫一扫打赏