Vue 经典面试题解析(一)
1. MVVM
MVC、MVVM 这类模式的目的都是为了职责划分和分层,如果代码都堆在一起,就很臃肿,也不利于维 护。
MVVM 借鉴了后端的 MVC 模式,比如 java 中比较原始的 servlet + jsp 技术。
但是对于前端来说,数据变化无法同步到视图中,还是需要将逻辑聚拢在 controller 层。
所以就产生了 MVVM 模式,MVVM 模式是对映射关系的简化,隐藏了 Controller 层。
Vue 就借鉴了 MVVM 的思想,但是它并没有完全遵循 MVVM 模型。
在 vue 中是可以使用 JS 操作视图的,MVVM 模式不推荐直接操作视图。
2. vue2 响应式原理
首先响应式原理和双向绑定并不是一个概念。
响应式原理是指当数据变化时,会驱动视图变化。
vue2 的响应式原理是通过 Object.defineProperty 实现的。
处理对象
使用 Object.defineProperty 将属性进行劫持(只会劫持已经存在的属性)。多层对象是通过递归实现劫持。
src/core/observer/index.js
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])
}
}
}
javascript
src/core/observer/index.js
// 定义响应式数据
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
// 如果不可以配置直接 return
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()
if (childOb) {
// 对象本身进行依赖收集
childOb.dep.depend()
if (Array.isArray(value)) {
// 如果是数组,让 arr 属性和外层数组进行收集
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()
}
})
}
javascript
处理数组
数组则是通过重写数组方法来实现的。
数组考虑性能原因没有用 defineProperty 对数组的每一项进行拦截,而是选择重写数组方法。
push、shift、pop、unshift、reverse、sort、splice
也正因如此,数组的索引和长度变化是无法监控到的。
src/core/observer/index.js
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)
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
javascript
src/core/observer/array.js
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
})
})
javascript
对于数组中新增(splice、push、unshift )的数据也会进行观测。
3. Vue3 响应式原理
export function reactive (target) {
// 创建响应式对象
return createReactiveObject(target, mutableHandler);
}
function createReactiveObject (target, baseHandler) {
if (!isObject(target)) {
return target;
}
const observed = new Proxy(target, baseHandler);
return observed;
}
javascript
/**
* @file 对象和数组相关的处理函数
*/
const get = createGetter(),
set = cretaeSetter();
function createGetter () {
return function get (target, key, receiver) {
const res = Reflect.get(target, key, receiver);
// 依赖收集
track(target, TrackOpTypes.GET, key);
// 如果是数组或对象
if (isObject(res)) {
return reactive(res);
}
return res;
}
}
function cretaeSetter () {
return function setter (target, key, value, receiver) {
// 检查一个属性是否已经存在
const hasKey = hasOwn(target, key);
const oldValue = target[key];
const res = Reflect.set(target, key, value, receiver); // target[key] = value
if (!hasKey) {
// 新增属性
trigger(target, TriggerOpTypes.ADD, key, value);
} else if (hasChanged(value, oldValue)) {
// 设置属性值
trigger(target, TriggerOpTypes.SET, key, value, oldValue);
}
return res;
}
}
export const mutableHandler = {
get,
set
}
javascript
vue3 相对于 vue2 是懒代理,vue2 中会递归对属性进行定义。其次使用 Proxy 是可以代理数组的。
4. 依赖收集
每个属性都拥有自己的 dep 属性,存放所依赖的 watcher,当属性变化后会通知自己对应的 watcher 去更新。
默认在初始化时会调用 render 函数,此时会触发属性依赖收集 dep.depend。
当属性发生修改时会触发 watcher 更新 dep.notify()。
src/core/observer/index.js
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
// ....
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()
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()
}
})
}
javascript
vue 初始化的时候,如果是 Runtime + Compiler 版本的,就会执行模板编译相关逻辑,生成 render 函数。
然后会调用 mountComponent 方法实例化渲染 Watcher,同时执行自身 get 方法。
这时会触发 Dep.pushTarget 方法,然后会执行传入的 getter,即 updateComponent 方法。
updateComponent 会触发 vm._update(vm._render())
方法,这个过程就会触发 Object.definePropery 定义的 get 进行依赖收集。
执行完 updataComponnet 方法后会执行 Dep.popTarget 方法。接着处理后续逻辑。
5. 模板编译原理
编译入口
当使用 Runtime + Compiler 版本的 Vue.js 时,就会执行模板编译过程,最终生成 render 函数。
src/platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
// el 元素不能是 body 和 documentElement 元素。
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
// 如果存在模板
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// 模板编译入口
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
javascript
compileToFunctions 方法就是把模板 template 编译生成 render 以及 staticRenderFns。
src/platforms/web/compiler/index.js
/* @flow */
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
javascript
compileToFunctions 是 src/compiler/index.js 的返回值。
核心逻辑
template 模板编译核心逻辑就定义在 createCompilerCreator 传入的函数 baseCompile 中。
如果将 template 转换成 render 函数?
- 解析语法树。将 template 模板转换成 AST 语法树 (parseHTML);
- 优化语法树。比如对静态节点做静态标记,静态节点会跳过 diff 操作(从子到父);
- 生成代码。将优化后的 AST 树转换成可执行代码(codegen)。
src/compiler/index.js
/* @flow */
import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'
// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 1. 解析 ast 语法树
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 2. 对 ast 树进行标记(根据配置项决定是否开启优化)
optimize(ast, options)
}
// 3. 生成代码,返回 render 函数
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
javascript
ast 用来描述语法结构的,virtual dom 用来描述 DOM 节点,层面不同。vue 3 的模板编译流程与 vue2 基本类似。
vue 在执行上述核心编译逻辑之前,还有很多其他处理,过程比较复杂,先看下图。
你可能会问编译过程为啥会这么复杂?
因为 Vue.js 在不同的平台都会有编译的过程,因此编译过程中依赖的配置 baseOptions 会有所不同。
Vue.js 利用了柯里化的技巧把核心的编译函数抽出来,通过 createCompilerCreator(base) 的方式把真正编译的过程和其他逻辑(编译配置处理、缓存处理等)剥离开,这样的设计思路很值得我们学习。
6. 生命周期钩子函数
Vue 的生命周期就是回调函数,当创建组件实例的过程中会调用对应的钩子方法。
实现过程
Vue 内部会对钩子函数进行处理,将钩子函数维护成数组的形式(配置合并)。
**src/core/instance/init.js **
Vue.options 上包含所有全局属性,将全局属性和局部属性进行合并。
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// ...
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
// 属性合并
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// ....
}
}
javascript
src/core/util/options.js
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
javascript
mergeField 对不同的 key 有不同的合并策略,以达到处理不同的逻辑的功能。
/**
* Hooks and props are merged as arrays.
*/
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
const res = childVal // 子节点
? parentVal
? parentVal.concat(childVal) // 父节点
: Array.isArray(childVal) // 子节点是数组
? childVal
: [childVal] // 不是数组包装成数组
: parentVal
return res
? dedupeHooks(res)
: res
}
javascript
组件也会执行 mergeOptions,是在执行 Vue.entend 方法执行的。
src/core/global-api/extend.js
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
// ...
Sub.options = mergeOptions(
Super.options,
extendOptions
)
// ...
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// ...
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// ...
return Sub
}
javascript
上面的 extendOptions 对应的就是前面定义的组件对象,它会和 Vue.options 合并到 Sub.options 中。
调用 Vue.mixin 方法时,也会进行合并配置的操作。
src/core/global-api/mixin.js
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
javascript
执行阶段
源码中执行生命周期的函数都是调用 callhook 方法。
src/core/instance/lifecycle.js
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
javascript
callhook 函数会根据传入的字符串,获取对应的回调函数数组,然后遍历执行。
beforeCreate、created
beforeCreate 和 created 都是定义在实例化 Vue 的阶段,在 init 方法中执行。
src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// ...
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// ...
}
}
javascript
beforeCreate 和 created 钩子调用在 initState 的前后。
所以 beforeCreate 的钩子函数中并不能获取到 props、data 中定义的值,也不能调用 methods 中定义的函数。
执行 created 钩子函数时,虽然已经初始化数据,但是并没有渲染 DOM,也不能访问 DOM,即 $el 对象。
之前我们说过的响应式原理就是发生在执行 initState 方法的阶段。
beforeMount、mounted
beforeMount 即在 DOM 挂载之前,在 mountComponent 中执行。
src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// ...
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
javascript
beforeMount 钩子函数在渲染 VNode 之前(vm._render()
)调用。
首次渲染时,mounted 钩子函数会在将 VNode patch 到真实 DOM ( vm._update()
)之后调用。
上面调用前 callHook(vm, ‘mounted’) 有个判断逻辑,如果 vm.$node 为空才会执行,说明这是首次 Vue 的初始化过程。
组件的 mouted 时机发生在组件的 VNode patch 到真实 DOM 之后,这时会调用 invokeInsertHook 函数,把 insertedVnodeQueue 里保存的钩子函数执行一遍,继而执行组件的 mouted 过程。
src/core/vdom/patch.js
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
javascript
这时会调用 insert 或者钩子函数,它定义在 componentVNodeHooks 中。
src/core/vdom/create-component.js
// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) { },
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},
destroy (vnode: MountedComponentVNode) { }
}
javascript
每个子组件都是在这个钩子函数中执行 mouted 钩子函数。mouted 钩子函数的执行顺序是先子后父。
beforeUpdate、updated
beforeUpdate 和 updated 的钩子函数都是在数据更新的时候才会执行。
breforeUpdate 的执行时机是在渲染 Watcher 的 before 函数中,只有当组件已经 mouted 之后,才回去调用这个钩子函数。
src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
}
javascript
updated 在 flushScheduerQueue 函数调用的时候执行。
src/core/observer/scheduler.js
function flushSchedulerQueue () {
// ...
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before() // beforeUpdate
}
id = watcher.id
has[id] = null
watcher.run()
// ...
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// ...
}
javascript
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}
javascript
updatedQueue 是执行更新操作的 watcher 数组,在 callUpdatedHooks 函数中,会对数组进行遍历,只有满足当前 watcher 是 vm._watcher 并且组件已经 mouted 且没有销毁时,才会执行。
beforeDestroy、destroyed
beforeDestroy 和 destroyed 钩子函数执行发生在组件销毁的阶段。组件销毁最终会调用 $destroy方法。
src/core/instance/lifecycle.js
export function lifecycleMixin (Vue: Class<Component>) {
// ...
Vue.prototype.$destroy = function () {
// ...
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
}
javascript
beforeDestory 在 $destroy 函数执行最开始的地方。这一步,vm 实例仍然是可用的。
$destroy 的执行过程中,会执行 vm.__patch__
触发组件销毁,destoryed 钩子函数执行顺序也是先子后父,与 mounted 过程一致。
activated、deactiveted
activated 和 deactiveted 钩子函数专门用来处理 keep-alive 组件中。后面会分析 keep-alive 组件。
最后附一张 Vue 官网的生命周期图。
7. Vue.mixin 原理及使用场景
Vue.mixin 主要作用就是抽离公共的业务逻辑,原理类似 ”对象的继承“,当组件初始化时会调用 mergeOptions 方法进行合并,
采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据有冲突,会采用 ”就近原则“ 以组件的数据为准。
如果使用 mixin 不当 ,可能会产生很多问题,比如 ”命名冲突问题“、”依赖问题“、”数据来源问题“。
src/core/global-api/mixin.js
import { mergeOptions } from '../util/index'
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
javascript
8. Vue 组件 data 为什么必须是个函数
每次使用组件时都会对组件进行实例化操作,只有调用 data 函数返回一个对象作为组件的数据,这样才能保证多个组件间的数据互不影响。Vue 组件实例化会使用 Vue.extend 方法构造一个 Vue 的子类,转换成一个继承于 Vue 的构造器 Sub 并返回。
src/core/global-api/extend.js
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
// ....
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// ...
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// ...
}
javascript
组件会调用 mergeOptions 方法将全局属性和自身属性合并,合并到 Sub.options 上。
因为组件使用过程中会创建多次,如果不使用函数的方式,组件的属性还是引用类型,就会产生影响。
9. nextTick 原理及使用场景
nextTick 用来获取更新后的 DOM。
Vue 中数据更新是异步的(同样使用 nextTick 方法),使用 nextTick 方法可以保证用户自定义的逻辑在更新之后执行。
Vue 提供了两种方法调用 nextTick。一种是全局 API 方法 Vue.nextTick,一种是实例上的 vm.$nextTick。
src/core/util/next-tick.js
/* @flow */
/* globals MutationObserver */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
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
})
}
}
javascript
nextTick 是用来维护一个 callbacks 数组,以异步的方式去同步执行任务数组中的任务。
如果调用 nextTick 先于修改数据,同样也获取不到最终的数据。比如下面这个例子。
vm.$nextTick(() => {
console.log(vm.a);
});
vm.a = 'yueluo';
javascript
10. computed 和 watch 区别
computed 和 watch 都是基于 Watcher 实现的。
computed 通常叫做计算属性 watcher(lazy:true),watch 叫做用户自定义 watcher(user:true)。
computed 属性是具备缓存的,依赖的值不发生变化,对其取值时的计算属性不会重新执行。
computed 只有取值时才执行。
watch 则是监控值的变化,当值发生变化时调用对应的回调函数。
src/core/instance/state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
javascript
src/core/instance/state.js
计算属性的实现。
// 设置此属性后,实例化 watcher 时,首次就不会加载。
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)) {
// 定义 computed 属性
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)
}
}
}
}
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)
}
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 如果数据存在变化,才会重新求值
// watcher 调用 update 方法时,会把 dirty 属性设置为 true。
if (watcher.dirty) {
watcher.evaluate()
}
// 让计算属性所依赖的属性,收集计算属性 watcher
if (Dep.target) {
watcher.depend()
}
// 返回数据
return watcher.value
}
}
}
javascript
src/core/instance/state.js
watch 的实现。
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
export function stateMixin (Vue: Class<Component>) {
// ...
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
// 用户 watcher
options.user = true
// 实例化 watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
}
javascript
11. Vue.set 方法是如何实现的
Vue 初始化时给对象和数组本身都增加了 dep 属性,用来收集 watcher。
当给对象新增不存在的属性则主动触发对象依赖的 watcher 去更新。
当修改数组索引时调用数组本身的 splice 方法去更新数组,这时依赖的 watcher 会自动更新。
Vue.set、Vue.$set 都可以使用,这两个方法是同一个方法。只是初始化时机不同,一个是全局方法,一个是原型方法。
src/core/observer/index.js
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
export function set (target: Array<any> | Object, key: any, val: any): any {
// 1. 如果是开发环境,并且 target 没有定义或者 target 是基础类型报错
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 2. 如果是数组,调用重写的 splice 方法触发视图更新
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 3. 如果是对象本身的属性,直接添加即可
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// 4. 如果是 Vue 实例或者根数据 data 时,直接报错
const ob = (target: any).__ob__
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
}
// 5. 如果不是响应式数据也不需要将其定义成响应式数据,直接添加数据即可
if (!ob) {
target[key] = val
return val
}
// 6. 如果是响应式数据,将属性定义成响应式数据
defineReactive(ob.value, key, val)
// 7. 通知视图更新
ob.dep.notify()
return val
}
javascript