.jpg)
# 1.谈一下你对MVVM原理的理解

- 传统的 MVC 指的是,用户操作会请求服务端路由,路由会调用对应的控制器来处理,控制器会获取数 据。将结果返回给前端,页面重新渲染
- MVVM :传统的前端会将数据手动渲染到页面上, MVVM 模式不需要用户手动操作 dom 元素,将数据绑 定到 viewModel 层上,会自动将数据渲染到页面中,视图变化会通知 viewModel层 更新数据。 ViewModel 就是我们 MVVM 模式中的桥梁
# 2.请说一下响应式数据的原理?
- 核心点: Object.defineProperty
- 默认 Vue 在初始化数据时,会给 data 中的属性使用 Object.defineProperty 重新定义所有属性,当页面取到对应属性时。会进行依赖收集(收集当前组件的watcher) 如果属性发生变化会通知相关依赖进行更新操作。

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
}
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])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
# 3.Vue中是如何检测数组变化?
- 使用函数劫持的方式,重写了数组的方法
- Vue 将 data 中的数组,进行了原型链重写。指向了自己定义的数组原型方法,这样当调用数组api 时,可以通知依赖更新.如果数组中包含着对象。会对数组中的对象再次进行监控。

onst 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
})
})
this.observeArray(value) // 进行深度监控 因为数组中也可能有对象
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
} //必须是对象类型才会被观测 observe方法中有判断
# 4.为何Vue采用异步渲染?
vue是组件级更新
因为如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染.所以为了性能考虑。 Vue会在本轮数据更新后,再去异步更新视图
因为vue是组件级更新视图,每一次update都要渲染整个组件,为了提高性能,采用了队列的形式,存储同一个watcher的所有data属性变化,然后统一调用nextTick 方法进行更新渲染(有且只调用一次)。
依次调用watcher的update并不会立即让watch执行而是放到一个队列里
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(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)
}
}
}
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
# 5.nextTick实现原理?
nextTick 方法主要是使用了宏任务和微任务,定义了一个异步方法.多次调用 nextTick 会将方法存入队列中,通过这个异步方法清空当前队列。 所以这个 nextTick 方法就是异步方法
默认内部会先flushSchedulerQueue使用的是nextTick,我们自己用nextTick写的cb会放到flushSchedulerQueue的后面,会保证当前视图渲染完成

# 6.Vue中Computed的特点
computed watch methods区别 默认 computed 也是一个 watcher 是具备缓存的,只要当依赖的属性发生变化时才会更新视图
- methods和computed:只要把方法用在模板上了,每次一变化就会重新渲染,性能开销较大, computed是具有缓存的,渲染的时候依赖的属性没有发生变化就不会重新执行
- computed和watch:computed和watch都是使用watcher实现的,每一不同的是computed具备缓存功能
做了一个dirty实现缓存机制
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
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 //{ lazy: true }默认是一个懒watcher不会执行
)
}
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)
}
}
}
}
------------------------------------------------------
Class Watcher里面
this.dirty = this.lazy //所以默认dirty是true
this.value = this.lazy
? undefined //默认是不会执行计算属性的 取值的时候才执行
: this.get()
---------------------------------------------
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) {
if (watcher.dirty) { //做了一个dirty实现缓存机制
watcher.evaluate() //dirty为true的时候求值
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
Class Watcher
evaluate () {
this.value = this.get()
this.dirty = false
}
get () {
pushTarget(this)//把watcher放到全局上
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
}
update () {
/* istanbul ignore else */
if (this.lazy) { //计算属性的watcher
this.dirty = true //依赖的数据发生变化了会让计算属性的watcher的dirty变为true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)//普通的watcher
}
}
# 7.Watch中的deep:true 是如何实现的
当用户指定了 watch 中的deep属性为 true 时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前 watcher 存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新
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)
}
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
// vm.$watch('msg',()=>{}) msg=>expOrFn
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()
}
}
}
------------------------------------------------
class Watcher
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
)
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn) //如果expOrFn是字符串会包装成函数
this.value = this.lazy //lazy是false
? undefined
: this.get()
get () {
pushTarget(this)//把watcher放到全局上
let value
const vm = this.vm
try {//默认将msg进行取值 computed为什么没有deep true
//computed 用在模板里{{xxx}}模板中的数据会调用Json.Strinify()会对里的所有属性进行取值
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
}
# 8.Vue组件的生命周期
要掌握每个生命周期什么时候被调用
- beforeCreate 在实例初始化之后,数据观测(data observer) 之前被调用。
- created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el
- beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
- mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。
- beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
- updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
- beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
- destroyed Vue 实例销毁后调用。调用后, Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
要掌握每个生命周期内部可以做什么事
- created 实例已经创建完成,因为它是最早触发的原因可以进行一些数据,资源的请求。
- mounted 实例已经挂载完成,可以进行一些DOM操作
- beforeUpdate 可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
- updated 可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。
- beforedestroy可以执行一些优化操作,清空定时器,解除绑定事件

# 9.ajax请求放在哪个生命周期中
- 在created的时候,视图中的 dom 并没有渲染出来,所以此时如果直接去操 dom 节点,无法找到相关的元素
- 在mounted中,由于此时 dom 已经渲染出来了,所以可以直接操作 dom 节点
一般情况下都放到 mounted 中,保证逻辑的统一性,因为生命周期是同步执行的, ajax 是异步执行的
服务端渲染不支持mounted方法,所以在服务端渲染的情况下统一放到created中
# 10.何时需要使用beforeDestroy
- 可能在当前页面中使用了 $on 方法,那需要在组件销毁前解绑。
- 清除自己定义的定时器
- 解除事件的绑定 scroll mousemove ....
# 11.Vue中模板编译原理
将 template 转化成 render 函数
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.优化树
}
const code = generate(ast, options)// 3.生成树 将ast树生成代码
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
...........
...........
...........
主要是正则匹配一点一点转化 最后的ast结果是这样的
//root
{
tag:'div',
type:1,
children:[
{
tag:'p',
type:1,
children:[Array],
attrs:[],
parent;[circular]
}
],
attrs:[{name:'id',value:'container'}],
parrent:null
}
// 根据语法树生成新的代码
let code = generate(root);
let render = `with(this){return ${code}}`;//with解决作用域
// 包装成函数
let renderFn = new Function(render); //模板引擎实现原理
console.log(renderFn.toString());
# 12.Vue中v-if和v-show的区别
- v-if 如果条件不成立不会渲染当前指令所在节点的 dom 元素
- v-show 只是切换当前 dom 的显示或者隐藏
通过模板编译的方式来看: v-if
const VueTemplateCompiler = require('vue-template-compiler');
//VueTemplateCompiler相当于compile
//把模板编译成render方法
let r1 = VueTemplateCompiler.compile(`<div v-if="true"><span v-for="i in 3">hello</span></div>`);
//r1
with(this) { return (true) ? _c('div', _l((3), function (i) { return _c('span', [_v("hello")]) }), 0) : _e() }
//不满足返回_e (createEmptyVNode) 所以dom不会渲染
v-show
const VueTemplateCompiler = require('vue-template-compiler');
let r2 = VueTemplateCompiler.compile(`<div v-show="true"></div>`);
//编译出来一个指令directives 在运行的阶段会处理这个指令
with(this) { return _c('div', {
directives: [{
name: "show",
rawName: "v-show",
value: (true),
expression: "true" }] }) }
// v-show 操作的是样式 定义在platforms/web/runtime/directives/show.js
//指令的编写
bind (el: any, { value }: VNodeDirective, vnode: VNodeWithData) {
vnode = locateNode(vnode)
const transition = vnode.data && vnode.data.transition
const originalDisplay = el.__vOriginalDisplay =
el.style.display === 'none' ? '' : el.style.display
if (value && transition) {
vnode.data.show = true
enter(vnode, () => {
el.style.display = originalDisplay
})
} else {
el.style.display = value ? originalDisplay : 'none'
}
},
不同点一个在编译阶段变成了指令,另一个变成了三元运算符
# 13.为什么V-for和v-if不能连用
v-for 会比 v-if 的优先级高一些,如果连用的话会把 v-if 给每个元素都添加一下,会造成性能问题
const VueTemplateCompiler = require('vue-template-compiler');
let r1 = VueTemplateCompiler.compile(`<div v-if="false" v-for="i in 3">hello</div>`);
//渲染成的是 先循环3次
//一般这样使用<template v-if="false"><div v-for = "i in 3">
with(this) { return _l((3), function (i) { return (false) ? _c('div', [_v("hello")]) : _e() }) }
console.log(r1.render)
# 14.用vnode来描述一个DOM结构
虚拟节点就是用一个对象来描述真实的 dom 元素
和AST很像,但AST是从语法角度一行行分析的,而vnode不是直接转换成的是通过方法
let obj = {
tag:'div',
data:{
id:'container'
},
children:[
{
tag:'p',
data:{},
children:[]
}
]
}
render(){
return _c('div',{id:'container'},_c('p',{}))
}
//js语法转换成obj 而不是直接把字符串转成一个树
function createElement(tag,data,...children){
let key = data.key;
delete data.key;
children = children.map(child=>{
if(typeof child === 'object')
//如果是对象说明又用了createElement方法
{ return child }
else
{return vnode(undefined,undefined,undefined,undefined,child) } })
return vnode(tag,data,key,children);
}
function vnode(tag,data,key,children,text){
return {
tag, // 表示的是当前的标签名
data, // 表示的是当前标签上的属性
key, // 唯一表示用户可能传递
children,
text }
}
createElement('div',{id:'container'},createElement('p',{},'hello'),'zzzz')
tag: "div"
data: {id: "container"}
key: undefined
children:
{tag: "p", data: {…}, key: undefined, children: Array(1), text: undefined}
{tag: undefined, data: undefined, key: undefined, children: undefined, text: "zzzz"}
text: undefined
# 15.diff算法的时间复杂度
两个树的完全的 diff 算法是一个时间复杂度为 O(n3) , Vue 进行了优化·O(n3) 复杂度的问题转换成 O(n) 复杂度的问题(只比较同级不考虑跨级问题) 在前端当中, 你很少会跨越层级地移动Dom元素。 所 以 Virtual Dom只会对同一个层级的元素进行对比。
# 16.简述Vue中diff算法原理
- 先同级比较,在比较子节点
- 先判断一方有儿子一方没儿子的情况
- 比较都有儿子的情况
- 递归比较子节点
# 17.v-for中为什么要用key
使用v-for更新已渲染的元素列表时,默认用就地复用策略;列表数据修改的时候,他会根据key值去判断某个值是否修改,如果修改,则重新渲染这一项,否则复用之前的元素;

归根结底还是diff算法
# 18.描述组件渲染和更新过程
渲染组件时,会通过 Vue.extend 方法构建子组件的构造函数,并进行实例化。最终手动调用
$mount() 进行挂载。更新组件时会进行 patchVnode 流程.核心就是diff算法

# 19.组件中的 data为什么是一个函数?
function VueComponent(){}
VueComponent.prototype.$options = { data:{name:'zf'} }
let vc1 = new VueComponent();
vc1.$options.data = 'jw';
let vc2 = new VueComponent();
console.log(vc2.$options.data);
同一个组件被复用多次,会创建多个实例。这些实例用的是同一个构造函数,如果 data 是一个对象的 话。那么所有组件都共享了同一个对象。为了保证组件的数据独立性要求每个组件必须通过 data 函数 返回一个对象作为组件的状态。
Vue.extend //Vue.extend创建一个子类
const Sub = function VueComponent (options) {
Sub.options = mergeOptions(
Super.options,//合并父类的选项和自己的选项
extendOptions//自己的选项中就包含着data
)
function mergeOptions (
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
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
)
//合并时会判断子类的data必须是一个函数
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
Vue.extend 通过一个对象创建了一个构造函数,但这个构造函数并没有父类上的东西,所以需要merge
为什么new Vue({data:{}})不用是对象因为创建的实例不会被公用,只会new这一次
# 20.Vue中事件绑定的原理
Vue 的事件绑定分为两种一种是原生的事件绑定,还有一种是组件的事件绑定
- 1.原生 dom 事件的绑定,采用的是 addEventListener 实现
- 2.组件绑定事件采用的是 $on 方法
let compiler = require('vue-template-compiler'); //vue-loader里用的就是这个包
let r1 = compiler.compile('<div @click="fn()"></div>');
let r2 = compiler.compile('<my-component @click.native="fn" @click="fn1"></my- component>'); console.log(r1); // {on:{click}}
console.log(r2); // {nativeOnOn:{click},on:{click}}
//组件中的nativeOn等价于普通元素的 on 组件的on会单独处理
# 21.v-model中的实现原理及如何自定义v-model
v-model 可以看成是 value+input方法 的语法糖 input v-model (checkbox v-model select v-model不同)
- 组件的v-model 就是value+input的语法糖
<el-checkbox :value="" @input=""></el-checkbox><el-checkbox v-model="check"></el-checkbox>
function transformModel (options, data: any) {
const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || '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
}
}
可以自己重新定义 v-model 的含义
Vue.component('el-checkbox',{
template:`<input type="checkbox" :checked="check" @change="$emit('change',$event.target.checked)">`,
model:{
prop:'check', // 更改默认的value的名字
event:'change' // 更改默认的方法名
},
props: { check: Boolean },
})
- 原生的 v-model ,会根据标签的不同生成不同的事件和属性
const VueTemplateCompiler = require('vue-template-compiler');
const ele = VueTemplateCompiler.compile('<input v-model="value"/>');
with(this) {
return _c('input', {
directives: [{ //多了个指令
//运行时:会对元素处理一些关于输入法的问题
name: "model",
rawName: "v-model",
value: (value),
expression: "value"
}],
domProps: { "value": (value) },
on: {
"input": function ($event) {
if ($event.target.composing) return;
value = $event.target.value } } }) }*/
编译时:不同的标签解析出的内容不一样platforms/web/compiler/directives/model.js
if (el.component) {
genComponentModel(el, value, modifiers)
// component v-model doesn't need extra runtime
return false
} else if (tag === 'select') {
genSelect(el, value, modifiers)
} else if (tag === 'input' && type === 'checkbox') {
genCheckboxModel(el, value, modifiers)
} else if (tag === 'input' && type === 'radio') {
genRadioModel(el, value, modifiers)
} else if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers)
} else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers)
// component v-model doesn't need extra runtime
return false
} else if (process.env.NODE_ENV !== 'production') {
warn(
`<${el.tag} v-model="${value}">: ` +
`v-model is not supported on this element type. ` +
'If you are working with contenteditable, it\'s recommended to ' +
'wrap a library dedicated for that purpose inside a custom component.',
el.rawAttrsMap['v-model']
)
}
// ensure runtime directive metadata
return true
}
运行时:会对元素处理一些关于输入法的问题 platforms/web/runtime/directives/model.js
inserted (el, binding, vnode, oldVnode) {
if (vnode.tag === 'select') {
// #6903
if (oldVnode.elm && !oldVnode.elm._vOptions) {
mergeVNodeHook(vnode, 'postpatch', () => {ccc
directive.componentUpdated(el, binding, vnode)
})
} else {
setSelected(el, binding, vnode.context)
}
el._vOptions = [].map.call(el.options, getValue)
} else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
el._vModifiers = binding.modifiers
if (!binding.modifiers.lazy) {
el.addEventListener('compositionstart', onCompositionStart)
el.addEventListener('compositionend', onCompositionEnd)
// Safari < 10.2 & UIWebView doesn't fire compositionend when
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
el.addEventListener('change', onCompositionEnd)
/* istanbul ignore if */
if (isIE9) {
el.vmodel = true
}
}
}
}
原生的v-model除了value和input还会解析出来一个指令
# 22. Vue中v-html会导致哪些问题?
- 可能会导致 xss 攻击
<input type="text" v-model="msg">
<div v-html="msg"></div>//用户输入都是不可信的 img onerror
- v-html 会替换掉标签内部的子元素
let template = require('vue-template-compiler');
let r = template.compile(`<div v-html="'<span>hello</span>'"></div>`)
with(this){
return _c('div',{domProps: {"innerHTML":_s('<span>hello</span>')}})}
// _c 定义在core/instance/render.js
// _s 定义在core/instance/render-helpers/index,js
if (key === 'textContent' || key === 'innerHTML') {
if (vnode.children) vnode.children.length = 0 //换掉标签内部的子元素
if (cur === oldProps[key]) continue
if (elm.childNodes.length === 1) { elm.removeChild(elm.childNodes[0]) } }
# 23. Vue父子组件生命周期调用顺序
- 加载渲染过程
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted - 子组件更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated - 销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
# 24. Vue组件如何通信? 单向数据流
- 父子间通信 父->子通过 props 、子-> 父 $on、$emit (发布订阅)
- 获取父子组件实例的方式 $parent、$children
- 在父组件中提供数据子组件进行消费 Provide、inject 插件
- dispatch和broadcast
- Ref 获取实例的方式调用组件的属性或者方法
- Event Bus 实现跨组件通信 Vue.prototype.$bus = new Vue
- Vuex 状态管理实现通信 $attrs $listeners
# 25.Vue中相同逻辑如何抽离?
Vue.mixin 用法 给组件每个生命周期,函数等都混入一些公共逻辑
//定义一个 js 文件(mixin.js)
export default {
data() {
return {
name: 'mixin'
}
},
created() {
console.log('mixin...', this.name);
},
mounted() {},
methods: {}
}
import mixin from '@/mixin'; // 引入mixin文件
export default {
mixins: [mixin]
}
- 数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先
- 同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用
- 值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对
原理
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin); // 将当前定义的属性合并到每个组件中
return this
}
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)
const extendsFrom = child.extends
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm)
}
//// 遍历mixins,parent 先和 mixins 合并,然后在和 child 合并
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
// 先处理 parent 的 key,
for (key in parent) {
mergeField(key)
}
// // 遍历 child 的key ,排除已经处理过的 parent 中的key
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
// 拿到相应类型的合并函数,进行合并字段,strats 请看下面
function mergeField (key) {
// strats 保存着各种字段的处理函数,否则使用默认处理
const strat = strats[key] || defaultStrat
// 相应的字段处理完成之后,会完成合并的选项
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
# 26.为什么要使用异步组件?
如果组件功能多打包出的结果会变大,我可以采用异步的方式来加载组件。主要依赖 import() 这 个语法,可以实现文件的分割加载。
components:{ AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([]) }
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
//异步组件一定是一个函数 新版本提供了返回对象的写法
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
//asyncFactory即import("../components/AddCustomer")
//不会马上执行就没有返回值所以是undefined,返回的是一个Promise
// 默认调用此函数时返回 undefiend
// 第二次渲染时Ctor不为undefined
if (Ctor === undefined) {
return createAsyncPlaceholder( //渲染占位符 空虚拟节点
asyncFactory,
data,
context,
children,
tag
)
}
export function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>
): Class<Component> | void {
if (isDef(factory.resolved)) {
return factory.resolved// 3.再次渲染时可以拿到获取的最新组件
}
const resolve = once((res: Object | Class<Component>) => {
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender(true)//2. 强制更新视图重新渲染
} else {
owners.length = 0
}
})
const reject = once(reason => {
if (isDef(factory.errorComp)) {
factory.error = true
forceRender(true)
}
})
const res = factory(resolve, reject)
// 1.将resolve方法和reject方法传入,用户调用 resolve方法后
sync = false
return factory.resolved
# 27.什么是作用域插槽?
- 插槽
<app><div slot="a">xxxx</div><div slot="b">xxxx</div></app>
//渲染app外层组件时会将xxx渲染成虚拟节点留起来,并不是在app组件里渲染的
<slot name="a">< slot name="b">
- 创建组件虚拟节点时,会将组件的儿子的虚拟节点保存起来。当初始化组件时,通过插槽属性将儿 子进行分类 {a:[vnode],b[vnode]}
- 渲染组件时会拿对应的slot属性的节点进行替换操作。(插槽的作用域为父组件)
const VueTemplateCompiler = require('vue-template-compiler');
let ele = VueTemplateCompiler.compile(`
<my-component>
<div slot="header">node</div>
<div>react</div>
<div slot="footer">vue</div>
</my-component> `)
with(this) { return _c('my-component',
[_c('div', {
attrs: {
"slot": "header"
},
slot: "header"
}, [_v("node")] // _V createTextVnode 文本节点 把里面的内存存起来了 ),
_v(" "), _c('div', [_v("react")]), _v(" "), _c('div', { attrs: { "slot": "footer" },slot: "footer" }, [_v("vue")])]) }
const VueTemplateCompiler = require('vue-template-compiler');
let ele = VueTemplateCompiler.compile(`
<div>
<slot name="header"></slot>
<slot name="footer"></slot>
<slot></slot>
</div> `);
with(this) { return _c('div', [_t("haeder"), _v(" "), _t("footer"), _v(" "), _t("default")], 2)//_t renderSlot 进行替换
return _c('div', [_v("node"), _v(" "), _t(_v("vue")])]), _v(" "), _t("default")], 2)
- 作用域插槽 作用域插槽在解析的时候,不会作为组件的孩子节点。会解析成函数,当子组件渲染时,会调用此 函数进行渲染。(插槽的作用域为子组件)
let ele = VueTemplateCompiler.compile(`
<app>
<div slot-scope="msg" slot="footer">{{msg.a}}</div>
</app> `);
with(this) { return _c('app', {
scopedSlots: _u([{ // 作用域插槽的内容会被渲染成一个函数
key: "footer",
fn: function (msg) { return _c('div', {}, [_v(_s(msg.a))]) } }]) })} }
VueTemplateCompiler.compile(`
<div>
<slot name="footer" a="1" b="2"></slot>
</div> `);
with(this) { return _c('div', [_t("footer", null, { "a": "1", "b": "2" })], 2) }**/
# 28.谈谈你对 keep-alive 的了解?
keep-alive 可以实现组件的缓存,当组件切换时不会对当前组件进行卸载,常用的2个属性
include / exclude ,2个生命周期 activated , deactivated
LRU算法(最近最少使用)
在 2.5.0 版本中,keep-alive 新增了 max 属性,用于最多可以缓存多少组件实例,一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉,看,这里就应用了 LRU 算法。即在 keep-alive 中缓存达到 max,新增缓存实例会优先淘汰最近没有被访问到的实例

# 29.Vue中常见性能优化
- 编码优化:
- 不要将所有的数据都放在data中,data中的数据都会增加getter和setter,会收集对应的 watcher
- vue 在 v-for 时给每项元素绑定事件需要用事件代理
- SPA 页面采用keep-alive缓存组件
- 拆分组件( 提高复用性、增加代码的可维护性,减少不必要的渲染 )
- v-if 当值为false时内部指令不会执行,具有阻断功能,很多情况下使用v-if替代v-show
- key 保证唯一性 ( 默认 vue 会采用就地复用策略 )
- Object.freeze 冻结数据
- 合理使用路由懒加载、异步组件
- 尽量采用runtime运行时版本
- 数据持久化的问题 (防抖、节流)
- Vue 加载性能优化
- 第三方模块按需导入 ( babel-plugin-component )
- 滚动到可视区域动态加载 ( https://tangbc.github.io/vue-virtual-scroll-list )
- 图片懒加载 (https://github.com/hilongjw/vue-lazyload.git)
- 用户体验
- app-skeleton 骨架屏
- hell app壳
- pwa serviceworker
- SEO 优化
- 预渲染插件 prerender-spa-plugin
- 服务端渲染 ssr
- 打包优化
- 使用 cdn 的方式加载第三方模块
- 多线程打包 happypack splitChunks 抽离公共文件
- sourceMap 生成
- 缓存,压缩
- 客户端缓存、服务端缓存
- 服务端 gzip 压缩
# 30.Vue3.0你知道有哪些改进?
- Vue3 采用了TS来编写
- 支持 Composition API
- Vue3 中响应式数据原理改成 proxy
- vdom 的对比算法更新,只更新 vdom 的绑定了动态数据的部分
# 31.实现hash路由和history路由
- onhashchange #
- history.pushState h5 api
# 32.Vue-Router中导航守卫有哪些?
runQuene
- 导航被触发。
- 在失活的组件里调用离开守卫。
- 调用全局的 beforeEach 守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
- 在路由配置里调用 beforeEnter 。
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter 。
- 调用全局的 beforeResolve 守卫 (2.5+)。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 触发 DOM 更新。
- 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数
# 33.action 和 mutation区别
mutation 是同步更新数据(内部会进行是否为异步方式更新数据的检测) $watch 严格模式下会报 错action 异步操作,可以获取数据后调佣 mutation 提交最终数据