# 1.谈谈你对MVVM的理解?
为什么要有这些模式:目的:职责划分、分层 ( 将Model层、View层进行分类 ) 借鉴后端思想。对于前端而言就是如何将数据同步到页面上。
MVC`模式 : `Backbone + underscore + jquery

对于前端而言,数据变化无法同步到视图中。需要将逻辑聚拢在controller层
MVVM模式 : 映射关系的简化 (隐藏controller)

虽然没有完全遵循 MVVM 模型 (opens new window),但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm (ViewModel 的缩写) 这个变量名表示 Vue 实例。
# 2.请说一下Vue2及Vue3响应式数据的理解
数组和对象类型当值变化时如何劫持到。对象内部通过defineReactive方法,使用Object.defineProperty将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。 多层对象是通过递归来实现劫持。Vue3则采用proxy
src/core/observer/index.js:135
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() // {a:1} => {} 外层对象
if (Array.isArray(value)) { // 如果是数组 {arr:[[],[]]} vm.arr取值只会让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()
}
})
}
# 3.Vue中如何检测数组变化?
数组考虑性能原因没有用
defineProperty对数组的每一项进行拦截,而是选择重写数组(push,shift,pop,splice,unshift,sort,reverse)方法。数组中如果是对象数据类型也会进行递归劫持
数组的索引和长度变化是无法监控到的
src/core/observer/index.js:47
src/core/observer/array.js:11
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
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
})
})
# 4.Vue中如何进行依赖收集?
- 每个属性都拥有自己的
dep属性,存放他所依赖的watcher,当属性变化后会通知自己对应的watcher去更新 - 默认在初始化时会调用render函数,此时会触发属性依赖收集
dep.depend - 当属性发生修改时会触发
watcher更新dep.notify()

# 5.如何理解Vue中模板编译原理
问题核心:如何将template转换成render函数 ?
- 1.将template模板转换成
ast语法树 -parserHTML - 2.对静态语法做静态标记 -
markUpdiff来做优化的 静态节点跳过diff操作 - 3.重新生成代码 -
codeGen
src/compiler/index.js:11
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options) // 1.解析ast语法树
if (options.optimize !== false) {
optimize(ast, options) // 2.对ast树进行标记,标记静态节点
}
const code = generate(ast, options) // 3.生成代码
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
# 6.Vue生命周期钩子是如何实现的
- Vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法。
- 内部会对钩子函数进行处理,将钩子函数维护成数组的形式
src/core/instance/init.js:38初始化合并
src/core/util/options.js:388合并选项
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
}
# 7.Vue的生命周期方法有哪些?一般在哪一步发送请求及原因
beforeCreate在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。- created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el
beforeMount在挂载开始之前被调用:相关的 render 函数首次被调用。mountedel 被新创建的vm.$el替换,并挂载到实例上去之后调用该钩子。beforeUpdate数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。updated由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。beforeDestroy实例销毁之前调用。在这一步,实例仍然完全可用。destroyedVue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。keep-alive(activated 和 deactivated)
在哪发送请求都可以,主要看具体你要做什么事
# 8.Vue.mixin的使用场景和原理
Vue.mixin的作用就是抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用mergeOptions方法进行合并,采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据冲突,会采用“就近原则”以组件的数据为准。- mixin中有很多缺陷 "命名冲突问题"、"依赖问题"、"数据来源问题"
src/core/global-api/mixin.js
# 9.Vue组件data为什么必须是个函数?
- 每次使用组件时都会对组件进行实例化操作,并且调用data函数返回一个对象作为组件的数据源。这样可以保证多个组件间数据互不影响
function Vue() {}
function Sub() { // 会将data存起来
this.data = this.constructor.options.data;
}
Vue.extend = function(options) {
Sub.options = options;
return Sub;
}
let Child = Vue.extend({
data: { name: 'zf' }
});
// 两个组件就是两个实例, 希望数据互不干扰
let child1 = new Child();
let child2 = new Child();
console.log(child1.data.name);
child1.data.name = 'jw';
console.log(child2.data.name);
src/core/util/options.js:121data的合并策略
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) { // 组件在合并时并没有产生实例,所以会校验类型
if (childVal && typeof childVal !== 'function') {
process.env.NODE_ENV !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
function mergedInstanceDataFn () {
// instance merge
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData) // 合并两个对象
} else {
return defaultData
}
}src/core/vdom/patch.js`:404 比较两个虚拟节点 `patchChildren()
# 10.nextTick在哪里使用?原理是?
nextTick中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。- 可用于获取更新后的 DOM。
- Vue中数据更新是异步的,使用
nextTick方法可以保证用户定义的逻辑在更新之后执行。
src/core/util/nextTick.js:89
# 11.computed和watch区别
- computed和watch都是基于Watcher来实现的
- computed属性是具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会重新执行
- watch则是监控值的变化,当值发生变化时调用对应的回调函数
src/core/instance/state.js:58
src/core/instance/state.js:241` 计算属性取值函数
src/core/instance/state.js:345` watch的实现
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) { // 如果值是脏的 进行求值操作
watcher.evaluate()
}
if (Dep.target) { // 让计算属性所依赖的属性 收集渲染watcher
watcher.depend()
}
return watcher.value
}
}
}
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 || {}
options.user = true // 标记为用户watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
}
# 12.Vue.set方法是如何实现的
- 我们给对象和数组本身都增加了
dep属性 - 当给对象新增不存在的属性则触发对象依赖的watcher去更新
- 当修改数组索引时我们调用数组本身的splice方法去更新数组
// src/core/observer:45; 给对象增加dep属性// src/core/observer:201;set方法的定义
export function set (target: Array<any> | Object, key: any, val: any): any {
// 1.是开发环境 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.如果是数组 Vue.set(array,1,100); 调用我们重写的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时 报错,(更新_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)
// 通知视图更新
ob.dep.notify()
return val
}
# 13.Vue为什么需要虚拟DOM
- Virtual DOM就是用
js对象来描述真实DOM,是对真实DOM的抽象 - 由于直接操作DOM性能低但是
js层的操作效率高,可以将DOM操作转化成对象操作,最终通过diff算法比对差异进行更新DOM(减少了对真实DOM的操作)。 - 虚拟DOM不依赖真实平台环境从而也可以实现跨平台。
scr/core/vdom/create-element.js:28 src/core/vdom/vnode.js` 虚拟节点的实现
# 14.Vue中diff算法原理
- Vue的
diff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较。 - 1.先比较是否是相同节点 key tag
- 2.相同节点比较属性,并复用老节点
- 3.比较儿子节点,考虑老节点和新节点儿子的情况
- 4.优化比较:头头、尾尾、头尾、尾头
- 5.比对查找进行复用
- Vue3中采用最长递增子序列来实现
diff优化
src/core/vdom/patch.js:700src/core/vdom/patch.js
:501 比较两个虚拟节点patchVnode()src/core/vdom/patch.js
:404 比较两个虚拟节点patchChildren()

# 15.既然Vue通过数据劫持可以精准探测数据变化,为什么还需要虚拟DOM进行diff检测差异
- 响应式数据变化,Vue确实可以在数据发生变化时,响应式系统可以立刻得知。但是如果给每个属性都添加watcher用于更新的话,会产生大量的watcher从而降低性能。
- 而且粒度过细也会导致更新不精准的问题,所以vue采用了组件级的watcher配合diff来检测差异。
# 16.请说明Vue中key的作用和原理,谈谈你对它的理解
- Vue在patch过程中通过key可以判断两个虚拟节点是否是相同节点。 (可以复用老节点)
- 无key会导致更新的时候出问题
- 尽量不要采用索引作为key

# 17.谈一谈对Vue组件化的理解
- 组件化开发能大幅提高应用开发效率、测试性、复用性等;
- 常用的组件化技术:属性、自定义事件、插槽等
- 降低更新范围,只重新渲染变化的组件
- 组件的特点:高内聚、低耦合、单向数据流
# 18.Vue的组件渲染流程
- 产生组件虚拟节点 -> 创建组件的真实节点 -> 插入到页面中

# 19.Vue组件更新流程
属性更新时会触发
patchVnode方法 -> 组件虚拟节点会调用prepatch钩子 -> 更新属性 -> 组件更新

# 20.Vue中异步组件原理
- 默认渲染异步占位符节点 -> 组件加载完毕后调用
forceUpdate强制更新

# 21.函数组件的优势及原理
函数式组件的特性:无状态、无生命周期、无this。但是性能高 正常组件是一个类继承了Vue, 函数式组件就是普通的函数,没有new的过程,也没有 init、prepatch
src/vdom/create-component.js:163
if (isTrue(Ctor.options.functional)) { // 函数式组件
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on // 处理事件
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn // 处理原生事件
// install component management hooks onto the placeholder node
installComponentHooks(data) // 初始化组件钩子方法
# 22.Vue组件间传值的方式及之间区别
props和$emit父组件向子组件传递数据是通过prop传递的,子组件传递数据给父组件是通过$emit触发事件来做到的$parent,$children获取当前组件的父组件和当前组件的子组件$attrs和$listenersA->B->C。Vue 2.4 开始提供了$attrs和$listeners来解决这个问题父组件中通过
provide来提供变量,然后在子组件中通过inject来注入变量。$refs获取实例envetBus平级组件数据传递 这种情况下可以使用中央事件总线的方式vuex状态管理# 1).props实现原理
<my-component a="1" b="2" c="3" @xxx @qqq @click.native></my-component>
src\core\vdom\create-component.js:192
const vnode = new VNode( // 创建组件虚拟节点
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children }, // 包含组件的属性及事件
asyncFactory
)
src\core\instance\init.js:36
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData // 将属性添加到$options中
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
src\core\instance\state.js属性的初始化
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData // 将属性添加到$options中
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
src\core\instance\state.js属性的初始化
function initProps (vm: Component, propsOptions: Object) { // propsOptions 校验属性
const propsData = vm.$options.propsData || {} // 获取用户的数据
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) { // 如果时根元素,属性需要定义成响应式的
toggleObserving(false)
}
for (const key in propsOptions) {// 用户用户的 props:{}
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
defineReactive(props, key, value) // 定义到_props中
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key) // 将_props代理到实例上
}
}
toggleObserving(true)
}
# 2).$on , $emit
<my-component @change="fn" @change="fn" @change="fn"></my-component> // this.$on('change')
<script>
this.$emit('change')
</script>
opts._parentListeners = vnodeComponentOptions.listeners // 用户在组件上定义的事件
src\core\instance\events.js:12
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners) // 更新组件的事件
}
}
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm // 更新事件,采用add 、 remove方法
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
function add (event, fn) {
target.$on(event, fn)
}
function remove (event, fn) {
target.$off(event, fn)
}
内部采用的就是发布订阅模式来进行实
# 3).$parent,$children
src\core\instance\lifecycle.js:32
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) { // 排除抽象组件
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm) // 让父实例记住当前组件实例
}
vm.$parent = parent // 增加$parent属性 指向父实例
vm.$root = parent ? parent.$root : vm
// ...
}
# 4).$attrs, $listeners
<my-component a="1" b="2"></my-component> => $vnode.data.attrs = {a:1,b:2}
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree 获取占位符节点
// ...
const parentData = parentVnode && parentVnode.data // 占位符节点上的数据
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
# 5).provide & inject
src\core\instance\inject.js:7
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) { // 将用户定义的provide 挂载到_provided
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
export function initInjections (vm: Component) { // inject:[a,b,c]
const result = resolveInject(vm.$options.inject, vm) // 不停的向上查找 inject的属性
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
# 6).$ref
src\core\vdom\modules\ref.js:20
export function registerRef (vnode: VNodeWithData, isRemoval: ?boolean) {
const key = vnode.data.ref // 获取ref
if (!isDef(key)) return
const vm = vnode.context
const ref = vnode.componentInstance || vnode.elm // 当前组件的实例 或者 组件的真实节点
const refs = vm.$refs
if (isRemoval) { // 删除ref
if (Array.isArray(refs[key])) {
remove(refs[key], ref)
} else if (refs[key] === ref) {
refs[key] = undefined
}
} else {
if (vnode.data.refInFor) {
if (!Array.isArray(refs[key])) { // 在v-for中是数组
refs[key] = [ref]
} else if (refs[key].indexOf(ref) < 0) {
// $flow-disable-line
refs[key].push(ref)
}
} else {
refs[key] = ref
}
}
}
# 23.$attrs是为了解决什么问题出现的,provide和inject不能解决它能解决的问题吗? v-bind="$attrs" v-on="$listeners"
$attrs主要的作用就是实现批量传递数据。provide/inject更适合应用在插件中,主要是实现跨级数据传递
# 24.v-if和v-for哪个优先级更高?
- v-for和v-if不要在同一个标签中使用,因为解析时先解析v-for在解析v-if。如果遇到需要同时使用时可以考虑写成计算属性的方式。
src/compiler/index.js:19 src/compiler/codegen/index.js::56 解析v-if 和 v-for
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) { // 处理v-for
return genFor(el, state)
} else if (el.if && !el.ifProcessed) { // 处理v-if
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
}
# 25.v-if,v-model,v-for的实现原理
- v-for
实现原理src/compiler/codegen/index.js:187
export function genFor(
el: any,
state: CodegenState,
altGen ? : Function,
altHelper ? : string
): string {
const exp = el.for // 拿到表达式arr
const alias = el.alias
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
if (process.env.NODE_ENV !== 'production' &&
state.maybeComponent(el) && // slot 和 template不能进行v-for操作
el.tag !== 'slot' &&
el.tag !== 'template' &&
!el.key
) {
state.warn(
`<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
`v-for should have explicit keys. ` +
`See https://vuejs.org/guide/list.html#key for more info.`,
el.rawAttrsMap['v-for'],
true /* tip */
)
}
el.forProcessed = true // avoid recursion 生成循环函数
const r = `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
return r;
}
v-if
实现原理src/compiler/codegen/index.js:147
function genIfConditions(
conditions: ASTIfConditions,
state: CodegenState,
altGen ? : Function,
altEmpty ? : string
): string {
if (!conditions.length) {
return altEmpty || '_e()'
}
const condition = conditions.shift()
if (condition.exp) { // 如果有表达式
return `(${condition.exp})?${ // 将表达式拼接起来
genTernaryExp(condition.block)
}:${ // v-else-if
genIfConditions(conditions, state, altGen, altEmpty)
}`
} else {
return `${genTernaryExp(condition.block)}` // 没有表达式直接生成元素 像v-else
}
// v-if with v-once should generate code like (a)?_m(0):_m(1)
function genTernaryExp(el) {
return altGen ?
altGen(el, state) :
el.once ?
genOnce(el, state) :
genElement(el, state)
}
}
v-model实现原理
# 普通元素上的v-model指令
src/compiler/codegen/index.js:310
function genDirectives(el: ASTElement, state: CodegenState): string | void {
const dirs = el.directives // 获取所有指令
if (!dirs) return
let res = 'directives:['
let hasRuntime = false
let i, l, dir, needRuntime
for (i = 0, l = dirs.length; i < l; i++) {
dir = dirs[i]
needRuntime = true
const gen: DirectiveFunction = state.directives[dir.name]
if (gen) {
// compile-time directive that manipulates AST.
// returns true if it also needs a runtime counterpart.
needRuntime = !!gen(el, dir, state.warn) // 添加input事件 和 value属性
}
if (needRuntime) {
hasRuntime = true // 是否需要运行时
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
}${
dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''
}${
dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},`
}
}
if (hasRuntime) { // directives:[{name:"model",rawName:"v-model",value:(msg),expression:"msg"}] 生成对应指令
let result = res.slice(0, -1) + ']'
return result;
}
}
# 组件上的v-model指令

function transformModel (options, data: any) {
const prop = (options.model && options.model.prop) || 'value' // 默认采用value属性
const event = (options.model && options.model.event) || 'input' // 默认采用input事件
;(data.attrs || (data.attrs = {}))[prop] = data.model.value // 绑定属性
const on = data.on || (data.on = {}) // 绑定事件
const existing = on[event]
const callback = data.model.callback
if (isDef(existing)) {
if (
Array.isArray(existing)
? existing.indexOf(callback) === -1
: existing !== callback
) {
on[event] = [callback].concat(existing)
}
} else {
on[event] = callback
}
}
# 26.Vue中slot是如何实现的?什么时候使用它?

const templateCompiler = require('vue-template-compiler');
let r = templateCompiler.compile(`
<div>
<slot name="title"></slot>
<slot name="content"></slot>
</div>`);
// with(this){return _c('div',[_t("title"),_v(" "),_t("content")],2)}
console.log(r.render)
let r1 = templateCompiler.compile(`
<my>
<h1 slot="title">标题</h1>
<div slot="content">内容</div>
</my>`)
/**
with(this){
return _c('my',[
_c('h1',{attrs:{"slot":"title"},slot:"title"},[_v("标题")]),_v(" "),
_c('div',{attrs:{"slot":"content"},slot:"content"},[_v("内容")])
])
}
**/
console.log(r1.render)

let r3 = templateCompiler.compile(`
<div>
<slot :article="{title:'标题',content:'内容'}"></slot>
</div>`);
// with(this){return _c('div',[_t("default",null,{"article":{title:'标题',content:'内容'}})],2)}
console.log(r3.render)
let r4 = templateCompiler.compile(`
<my>
<template slot-scope="{article}">
<h1 slot="article.title">标题</h1>
<div slot="article.content">内容</div>
</template>
</my>`)
/**
with(this){return _c('my',
{scopedSlots:_u([
{key:"default",fn:function({article}){
return [
_c('h1',{attrs:{"slot":"article.title"},slot:"article.title"},[_v("标题")]),
_v(" "),
_c('div',{attrs:{"slot":"article.content"},slot:"article.content"},[_v("内容")])
]
}
}
])
})}
*/
console.log(r4.render)
# 27.Vue.use是干什么的?原理是什么?
Vue.use是用来使用插件的,我们可以在插件中扩展全局组件、指令、原型方法等。- 会调用插件的
install方法,将Vue的构造函数默认传入,这样在插件中可以使用Vue无需依赖Vue库
src/core/global-api/use.js
Vue.use = function (plugin: Function | Object) {
// 插件缓存
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) { // 如果已经有插件 直接返回
return this
}
// additional parameters
const args = toArray(arguments, 1) // 除了第一项其他的参数整合成数组
args.unshift(this) // 将Vue 放入到数组中
if (typeof plugin.install === 'function') { // 调用install方法
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') { // 直接调用方法
plugin.apply(null, args)
}
installedPlugins.push(plugin) // 缓存插件
return this
}
# 28.组件中写name选项有哪些好处及作用?
- 增加name选项会在
components属性中增加组件本身,实现组件的递归调用。 - 可以标识组件的具体名称方便调试和查找对应组件。
src/core/global-api/extend.js:67
Sub.options.components[name] = Sub
# 29.Vue事件修饰符有哪些?其实现原理是什么?
- .stop、.prevent、.capture!、.self、.once~、.passive&
src\compiler\helpers.js:69
export function addHandler (
el: ASTElement,
name: string,
value: string,
modifiers: ?ASTModifiers,
important?: boolean,
warn?: ?Function,
range?: Range,
dynamic?: boolean
) {
modifiers = modifiers || emptyObject
// warn prevent and passive modifier
/* istanbul ignore if */
if (
process.env.NODE_ENV !== 'production' && warn &&
modifiers.prevent && modifiers.passive
) {
warn(
'passive and prevent can\'t be used together. ' +
'Passive handler can\'t prevent default event.',
range
)
}
if (modifiers.right) {
if (dynamic) {
name = `(${name})==='click'?'contextmenu':(${name})`
} else if (name === 'click') {
name = 'contextmenu'
delete modifiers.right
}
} else if (modifiers.middle) {
if (dynamic) {
name = `(${name})==='click'?'mouseup':(${name})`
} else if (name === 'click') {
name = 'mouseup'
}
}
// check capture modifier
if (modifiers.capture) { // 如果capture 用!标记
delete modifiers.capture
name = prependModifierMarker('!', name, dynamic)
}
if (modifiers.once) { // 如果是once 用~ 标记
delete modifiers.once
name = prependModifierMarker('~', name, dynamic)
}
/* istanbul ignore if */
if (modifiers.passive) { // 如果是passive 用 &标记
delete modifiers.passive
name = prependModifierMarker('&', name, dynamic)
}
let events
if (modifiers.native) {
delete modifiers.native
events = el.nativeEvents || (el.nativeEvents = {})
} else {
events = el.events || (el.events = {})
}
const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
if (modifiers !== emptyObject) {
newHandler.modifiers = modifiers
}
const handlers = events[name]
/* istanbul ignore if */
if (Array.isArray(handlers)) {
important ? handlers.unshift(newHandler) : handlers.push(newHandler)
} else if (handlers) {
events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
} else {
events[name] = newHandler
}
el.plain = false
}
src\compiler\codegen\events.js:42
function genHandler (handler: ASTElementHandler | Array<ASTElementHandler>): string {
let code = ''
let genModifierCode = ''
const keys = []
for (const key in handler.modifiers) {
if (modifierCode[key]) {
genModifierCode += modifierCode[key]
// left/right
if (keyCodes[key]) {
keys.push(key)
}
} else if (key === 'exact') {
const modifiers: ASTModifiers = (handler.modifiers: any)
genModifierCode += genGuard(
['ctrl', 'shift', 'alt', 'meta']
.filter(keyModifier => !modifiers[keyModifier])
.map(keyModifier => `$event.${keyModifier}Key`)
.join('||')
)
} else {
keys.push(key) // modifiers中表达式存起来
}
}
if (keys.length) {
code += genKeyFilter(keys)
}
// Make sure modifiers like prevent and stop get executed after key filtering
if (genModifierCode) {
code += genModifierCode
}
const handlerCode = isMethodPath
? `return ${handler.value}.apply(null, arguments)`
: isFunctionExpression
? `return (${handler.value}).apply(null, arguments)`
: isFunctionInvocation
? `return ${handler.value}`
: handler.value
/* istanbul ignore if */
if (__WEEX__ && handler.params) {
return genWeexHandler(handler.params, code + handlerCode)
}
return `function($event){${code}${handlerCode}}`
}
# 30.Vue中.sync修饰符的作用,用法及实现原理
src\compiler\parser\index.js:798
if (modifiers.sync) {
syncGen = genAssignmentCode(value, `$event`) // 转.async 改成 ${value} = xxx
if (!isDynamic) {
addHandler( // 添加update事件
el,
`update:${camelize(name)}`,
syncGen,
null,
false,
warn,
list[i]
)
if (hyphenate(name) !== camelize(name)) {
addHandler(
el,
`update:${hyphenate(name)}`,
syncGen,
null,
false,
warn,
list[i]
)
}
} else {
// handler w/ dynamic event name
addHandler(
el,
`"update:"+(${name})`,
syncGen,
null,
false,
warn,
list[i],
true // dynamic
)
}
}
}
let r5 = templateCompiler.compile(`
<my :value.sync="xxxx"></my>
`);
// with(this){return _c('my',{attrs:{"value":xxxx},on:{"update:value":function($event){xxxx=$event}}})}
console.log(r5.render)
# 31.如何理解自定义指令
- 1.在生成
ast语法树时,遇到指令会给当前元素添加directives属性 - 2.通过
genDeirectives生成指令代码 - 3.在
patch前将指令的钩子提取到cbs中,在patch过程中调用对应的钩子 - 4.当执行
cbs对应的钩子时,调用对应指令定义的方法
src/vdom/patch.js:77提取钩子函数
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
const { modules, nodeOps } = backend
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]]); // 收集hook,patch过程中调用
// {create:[fn,fn],activate:[fn,fn]...}
}
}
}
src/vdom/modules/directives.js:7` 指令钩子
export default { // 指令的钩子, 在创建和更新过程中会调用 create、update、destroy钩子
create: updateDirectives,
update: updateDirectives,
destroy: function unbindDirectives (vnode: VNodeWithData) {
updateDirectives(vnode, emptyNode)
}
}
function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (oldVnode.data.directives || vnode.data.directives) { // 如果有指令
_update(oldVnode, vnode)
}
}
# 32.keep-alive平时在哪里使用?原理是?
- 使用keep-alive包裹动态组件时, 会对组件进行缓存。避免组件的重新创建
<keep-alive :include="whiteList" :exclude="blackList" :max="count">
<component :is="component"></component>
</keep-alive>
<keep-alive :include="whiteList" :exclude="blackList" :max="count">
<router-view></router-view>
</keep-alive>
实现原理
export default { name: 'keep-alive', abstract: true, // 不会放到对应的lifecycle props: { include: patternTypes, // 白名单 exclude: patternTypes, // 黑名单 max: [String, Number] // 缓存的最大个数 }, created () { this.cache = Object.create(null) // 缓存列表 this.keys = [] // 缓存的key列表 }, destroyed () { for (const key in this.cache) { // keep-alive销毁时 删除所有缓存 pruneCacheEntry(this.cache, key, this.keys) } }, mounted () { // 监控缓存列表 this.$watch('include', val => { pruneCache(this, name => matches(val, name)) }) this.$watch('exclude', val => { pruneCache(this, name => !matches(val, name)) }) }, render () { const slot = this.$slots.default const vnode: VNode = getFirstComponentChild(slot) 、// 获得第一个组件 const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions if (componentOptions) { // check pattern const name: ?string = getComponentName(componentOptions) const { include, exclude } = this if ( // 获取组件名 看是否需要缓存,不需要缓存则直接返回 // not included (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) { return vnode } const { cache, keys } = this const key: ?string = vnode.key == null // same constructor may get registered as different local components // so cid alone is not enough (#3269) ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key // 生成缓存的key if (cache[key]) { // 如果有key 将组件实例直接复用 vnode.componentInstance = cache[key].componentInstance // make current key freshest remove(keys, key) keys.push(key) // lru算法 } else { cache[key] = vnode // 缓存组件 keys.push(key) // prune oldest entry if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) // 超过最大限制删除第一个 } } vnode.data.keepAlive = true // 在firstComponent的vnode中增加keep-alive属性 } return vnode || (slot && slot[0]) } }
# # (opens new window)13.Vue-Router有几种钩子函数,具体是什么及执行流程是怎样的?
钩子函数的种类有:全局守卫、路由守卫、组件守卫
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave守卫。 - 调用全局的
beforeEach守卫。 - 在重用的组件里调用
beforeRouteUpdate守卫 (2.2+)。 - 在路由配置里调用
beforeEnter。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter。 - 调用全局的
beforeResolve守卫 (2.5+)。 - 导航被确认。
- 调用全局的
afterEach钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入。
const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated), // 离开钩子
// global before hooks
this.router.beforeHooks, // 全局before钩子
// in-component update hooks
extractUpdateHooks(updated), // 更新钩子 beforeRouteUpdate
// in-config enter guards
activated.map(m => m.beforeEnter), // beforeEnter钩子
// async components
resolveAsyncComponents(activated) // 异步组件
)
runQueue(queue, iterator, () => {
// wait until async components are resolved before
// extracting in-component enter guards
const enterGuards = extractEnterGuards(activated) // beforeRouteEnter
const queue = enterGuards.concat(this.router.resolveHooks) // beforeResolve
runQueue(queue, iterator, () => {
afterEachs.forEach(fn=>fn())
})
})
}
# 34.Vue-Router的两种模式的区别
Vue-Router有三种模式hash、history、abstractabstract模式是在不支持浏览器API环境使用,不依赖于浏览器历史hash模式:hash+popState/hashChange兼容性好但是不够美观,hash服务端无法获取。不利于seo优化history模式:historyApi+popState美观,刷新会出现404 -> CLI webpack history-fallback
# 35.谈一下你对vuex的个人理解
vuex是专门为vue提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部核心原理是通过创造一个全局实例 new Vue)

方法:
replaceState、subscribe、registerModule、namespace(modules)、辅助函数...
# 36.mutation和action的区别
mutation: 主要在于修改状态,必须同步执行action: 执行业务代码,方便复用,逻辑可以为异步,不能直接修改状态
function enableStrictMode (store) {
store._vm.$watch(function () { return this._data.$$state }, function () {
if ((process.env.NODE_ENV !== 'production')) {
assert(store._committing, "do not mutate vuex store state outside mutation handlers.");
}
}, { deep: true, sync: true }); // 同步watcher监控状态变化
}
# 37.Vue中的性能优化有哪些?
- 数据层级不易过深,合理设置响应式数据
- 使用数据时缓存值的结果,不频繁取值。
- 合理设置Key属性
- v-show和v-if的选取
- 控制组件粒度 -> Vue采用组件级更新
- 采用函数式组件 -> 函数式组件开销低
- 采用异步组件 -> 借助
webpack分包的能力 - 使用
keep-alive缓存组件 - 虚拟滚动、时间分片等策略...
- 打包优化
# 38.Vue中使用了哪些设计模式?
- 单例模式 - 单例模式就是整个程序有且仅有一个实例
export function install (_Vue) {
if (Vue && _Vue === Vue) {
if (__DEV__) {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue
applyMixin(Vue)
}
- 工厂模式 - 传入参数即可创建实例 (
createElement)
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// ...
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
vnode = createComponent(tag, data, context, children)
}
// ....
}
- 发布订阅模式 - 订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码。
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
观察者模式 -
watcher&dep的关系代理模式 - 代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。
_data属性、proxy、防抖、节流 let p = new Proxy
- 装饰模式 -
Vue2装饰器的用法 (对功能进行增强 @) - 中介者模式 - 中介者是一个行为设计模式,通过提供一个统一的接口让系统的不同部分进行通信。
Vuex - 策略模式 - 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案。 mergeOptions
- 外观模式 - 提供了统一的接口,用来访问子系统中的一群接口。
← vue中使用通信方式 Git 常见问题汇总 →