vue核心面试题汇总【查缺补漏】(一):https://developer.aliyun.com/article/1415695
三十、vue
中的原生事件
vue
中可以通过@
或者v-on
的方式绑定事件,也可为其添加修饰符。
new Vue({ el: '#app', template: `<div @click='divClick'><a @clickt='aClick' href=''>点击</a></div>`, methods: { divClick() { console.log('divClick') }, aClick() { console.log('aClick') }, } })
以上例子如果点击a
会触发其默认行为,如果href
不为空还会进行跳转。除此之外,点击还会继续触发div
上绑定的点击事件。
如果通过@click.stop.prevent='aClick'
的方式为a
标签的点击事件添加修饰符stop
和prevent
,那么就不会触发其a
的默认行为,即使href
不为空也不会进行跳转,同时,div
上的点击事件也不会进行触发。
模板的渲染一般分为编译生成render
函数、render
函数执行生成vNode
和patch
进行渲染。下面按照这步骤进行简单分析。
1、render
通过编译生成的render
函数:
with(this) { return _c('div', { on: { "click": divClick } }, [_c('a', { attrs: { "href": "http://www.baidu.com" }, on: { "click": function ($event) { $event.stopPropagation(); $event.preventDefault(); return aClick($event) } } }, [_v("点击")])]) }
其中div
的on
作为div
事件描述。a
标签的attrs
作为属性描述,on
作为事件描述,在描述中.stop
被编译成了$event.stopPropagation()
来阻止事件冒泡,.prevent
被编译成了$event.preventDefault()
用来阻止a
标签的默认行为。
2、vNode
通过执行Vue.prototype._render
将render
函数转换成vNode
。
3、patch
patch
的过程中,当完成$el
节点的渲染后会执行invokeCreateHooks(vnode, insertedVnodeQueue)
逻辑,其中,针对attrs
会将其设置为$el
的真实属性,当前例子中会为a
标签设置herf
属性。针对on
会通过target.addEventListener
的方式将其处理过的事件绑定到$el
上,当前例子中会分别对div
和a
中的click
进行处理,再通过addEventListener
的方式进行绑定。
小结
vue
中的事件,从编译生成render
再通过Vue.prototype._render
函数执行render
到生成vNode
,主要是通过on
作为描述。在patch
渲染阶段,将on
描述的事件进行处理再通过addEventListener
的方式绑定到$el
上。
三十一、常用修饰符
1、表单修饰符
(1).lazy
在默认情况下,v-model
在每次 input
事件触发后将输入框的值与数据进行同步 ,可以添加 lazy
修饰符,从而转为在 change
事件之后进行同步:
<input v-model.lazy="msg">
(2).number
如果想自动将用户的输入值转为数值类型,可以给 v-model
添加 number
修饰符:
<input v-model.number="age" type="number">
(3).trim
如果要自动过滤用户输入的首尾空白字符,可以给 v-model
添加 trim
修饰符:
<input v-model.trim="msg">
2、事件修饰符
(1).stop
阻止单击事件继续传播。
<!--这里只会触发a--> <div @click="divClick"><a v-on:click.stop="aClick">点击</a></div>
(2).prevent
阻止标签的默认行为。
<a href="http://www.baidu.com" v-on:click.prevent="aClick">点击</a>
(3).capture
事件先在有.capture
修饰符的节点上触发,然后在其包裹的内部节点中触发。
<!--这里先执行divClick事件,然后再执行aClick事件--> <div @click="divClick"><a v-on:click="aClick">点击</a></div>
(4).self
只当在 event.target 是当前元素自身时触发处理函数,即事件不是从内部元素触发的。
<!--在a标签上点击时只会触发aClick事件,只有点击phrase的时候才会触发divClick事件--> <div @click.self="divClick">phrase<a v-on:click="aClick">点击</a></div>
(5).once
不像其它只能对原生的 DOM 事件起作用的修饰符,.once
修饰符还能被用到自定义的组件事件上,表示当前事件只触发一次。
<a v-on:click.once="aClick">点击</a>
(6).passive
.passive
修饰符尤其能够提升移动端的性能
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 --> <!-- 而不会等待 `onScroll` 完成 --> <!-- 这其中包含 `event.preventDefault()` 的情况 --> <div v-on:scroll.passive="onScroll">...</div>
3、其他修饰符
除了表单和事件的修饰符,Vue
还提供了很多其他修饰符,在使用的时候可以查阅文档。
小结
Vue
中提供了很多好用的功能和api
,那么修饰符的出现就为功能和api
提供了更为丰富的扩展属性和更大的灵活度。
三十二、vue-router
vue
路由是单页面中视图切换的方案,有三种mode
:
- hash,#后的仅仅作为参数,不属于url部分
- history,路径作为请求url请求资源链接,如果找不到会出现404错误
- abstract,服务端渲染场景 hash场景下,会出现
url
链接,再修改其view-router中对应的值。
了解vue-router
的底层实现请参考vue2视图切换:vue-router
三十三、vuex
vuex
是状态管理仓库,一般使用的场景为:多个视图依赖于同一状态,来自不同视图的行为需要变更同一状态。其管理的状态是响应式的,修改也只能显式提交mutation
的方式修改。vuex
有state
、getter
、mutation
、action
和module
五个核心,并且通过module
实现了vuex
树的管理。
了解vuex
的底层实现请参考vue2状态管理:vuex
三十四、eventBus
使用场景:兄弟组件传参
const eventBus = new Vue(); const A = { template: `<div @click="send">component-a</div>`, methods: { send() { eventBus.$emit('sendData', 'data from A') } }, } const B = { template: `<div>component-b</div>`, created() { eventBus.$on('sendData', (args) => { console.log(args) }) }, } new Vue({ el: '#app', components: { A, B, }, template: `<div><A></A><B></B></div>`, })
在当前例子中,A
组件和B
组件称为兄弟组件,A
组件通过事件总线eventBus
中的$emit
分发事件,B
组件则通过$on
来监听事件。
实现原理:eventsMixin
export function eventsMixin (Vue: Class<Component>) { const hookRE = /^hook:/ 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) // optimize hook:event cost by using a boolean flag marked at registration // instead of a hash lookup if (hookRE.test(event)) { vm._hasHookEvent = true } } return vm } Vue.prototype.$once = function (event: string, fn: Function): Component { const vm: Component = this function on () { vm.$off(event, on) fn.apply(vm, arguments) } on.fn = fn vm.$on(event, on) return vm } Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component { const vm: Component = this // all if (!arguments.length) { vm._events = Object.create(null) return vm } // array of events if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { vm.$off(event[i], fn) } return vm } // specific event const cbs = vm._events[event] if (!cbs) { return vm } if (!fn) { vm._events[event] = null return vm } // specific handler let cb let i = cbs.length while (i--) { cb = cbs[i] if (cb === fn || cb.fn === fn) { cbs.splice(i, 1) break } } return vm } Vue.prototype.$emit = function (event: string): Component { const vm: Component = this if (process.env.NODE_ENV !== 'production') { const lowerCaseEvent = event.toLowerCase() if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) { tip( `Event "${lowerCaseEvent}" is emitted in component ` + `${formatComponentName(vm)} but the handler is registered for "${event}". ` + `Note that HTML attributes are case-insensitive and you cannot use ` + `v-on to listen to camelCase events when using in-DOM templates. ` + `You should probably use "${hyphenate(event)}" instead of "${event}".` ) } } 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 } }
在Vue
构造函数定义完执行的eventsMixin
函数中,在Vue.prototype
上分别定义了$on
、$emit
、$off
和$once
的方法易实现对事件的绑定、分发、取消和只执行一次的方法。eventBus
就是利用了当new Vue
实例化后实例上的$on
、$emit
、$off
和$once
进行数据传递。
三十五、ref
使用场景: 父组件获取子组件数据或者执行子组件方法
const A = { template: `<div>{{childData.age}}</div>`, data() { return { childData: { name: 'qb', age: 30 }, } }, methods: { increaseAge() { this.childData.age++; } } } new Vue({ el: '#app', components: { A, }, template: `<A ref='childRef' @click.native='changeChildData'></A>`, methods: { changeChildData() { // 执行子组件的方法 this.$refs.childRef.increaseAge() // 获取子组件的数据 console.log(this.$refs.childRef.childData); }, } })
在当前例子中,通过ref='childRef'
的方式在当前组件中定义一个ref
,可以通过this.$refs.childRef
的方式获取到子组件A
。可以通过this.$refs.childRef.increaseAge()
的方式执行子组件中age
增加的方法,也可以通过this.$refs.childRef.childData
的方式获取到子组件中的数据。
三十六、props
使用场景: 父子传参
const A = { template: `<div @click='emitData'>{{childData}}</div>`, props: ['childData'], methods: { emitData() { this.$emit('emitChildData', 'data from child') } }, } new Vue({ el: '#app', components: { A }, template: `<A :childData='parentData' @emitChildData='getChildData'></A>`, data() { return { parentData: 'data from parent' } }, methods: { getChildData(v) { console.log(v); } } })
从当前例子中可以看出,数据父传子是通过:childData='parentData'
的方式,数据子传父是通过this.$emit('emitChildData', 'data from child')
的方式,然后,父组件通过@emitChildData='getChildData'
的方式进行获取。
1、父组件render
函数
new Vue
中传入的模板template
经过遍历生成的render
函数如下:
with(this) { return _c('A', { attrs: { "childData": parentData }, on: { "emitChildData": getChildData } }) }
其中data
部分有attrs
和on
来描述属性和方法。
在通过createComponent
创建组件vnode
的过程中,会通过const propsData = extractPropsFromVNodeData(data, Ctor, tag)
的方式获取props
,通过const listeners = data.on
的方式获取listeners
,最后将其作为参数通过new VNode(options)
的方式实例化组件vnode
。
2、子组件渲染
在通过const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance )
创建组件实例的过程中,会执行到组件继承自Vue
的._init
方法,通过initEvents
将事件处理后存储到vm._events
中,通过initProps
将childData
赋值到子组件A
的vm
实例上,并进行响应式处理,让其可以通过vm.childData
的方式访问,并且数据发生变化时视图也可以发生改变。
组件模板编译后对应的render
函数是:
with(this) { return _c('div', { on: { "click": emitData } }, [_v(_s(childData))]) }
在createElm
完成节点的创建后,在invokeCreateHooks(vnode, insertedVnodeQueue)
阶段,给DOM
原生节点节点绑定emitData
。
3、this.$emit
在点击执行this.$emit
时,会通过var cbs = vm._events[event]
取出_events
中的事件进行执行。
至此,父组件中的传递的数据就在子组件中可以通过this.xxx
的方式获得,也可以通过this.$emit
的方式将子组件中的数据传递给父组件。
prop
数据发生改变引起视图变化的底层逻辑请参考vue2从数据变化到视图变化:props引起视图变化详解
三十七、$attrs
和$listeners
使用场景: 父子组件非props
属性和非native
方法传递
// main.js文件 import Vue from "vue"; const B = { template: `<div @click="emitData">{{ formParentData }}</div>`, data() { return { formParentData: '' } }, inheritAttrs: false, created() { this.formParentData = this.$attrs; console.log(this.$attrs, '--------------a-component-$attrs') console.log(this.$listeners, '--------------b-component-$listeners') }, methods: { emitData() { this.$emit('onFun', 'form B component') } }, } const A = { template: `<B v-bind='$attrs' v-on='$listeners'></B>`, components: { B, }, props: ['propData'], inheritAttrs: false, created() { console.log(this.$attrs, '--------------b-component-$attrs') console.log(this.$listeners, '--------------b-component-$listeners') } } new Vue({ el: '#app', components: { A, }, template: `<A :attrData='parentData' :propData='parentData' @click.native="nativeFun" @onFun="onFun"></A>`, data() { return { parentData: 'msg' } }, methods: { nativeFun() { console.log('方法A'); }, onFun(v) { console.log('方法B', v); }, } })
当前例子中,new Vue
的template
模板中有attrData
、propData
、click.native
和onFun
在进行传递。实际运行后,在A
组件中this.$attrs
为{attrData: 'msg'}
,this.$listeners
为{onFun:f(...)}
。在A
组件中通过v-bind='$attrs'
和v-on='$listeners'
的方式继续进行属性和方法的传递,在B
组件中就可以获取到A
组件中传入的$attrs
和$listeners
。
当前例子中完成了非props
属性和非native
方法的传递,并且通过v-bind='$attrs'
和v-on='$listeners'
的方式实现了属性和方法的跨层级传递。
同时通过this.$emit
的方法触发了根节点中onFun
事件。
关于例子中的inheritAttrs: false
,默认情况下父作用域的不被认作props
的attribute
绑定将会“回退”且作为普通的HTML
属性应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置inheritAttrs
到false
,这些默认行为将会被去掉。
三十八、$parent
和$children
使用场景: 利用父子关系进行数据的获取或者方法的调用
const A = { template: `<div @click="changeParentData">{{childRandom}}</div>`, data() { return { childRandom: Math.random() } }, mounted() { console.log(this.$parent.parentCount, '--child-created--'); // 获取父组件中的parentCount }, methods: { changeParentData() { console.log(this.$parent); // 打印当前实例的$parent this.$parent.changeParentData(); // 调用当前父级中的方法`changeParentData` }, changeChildData() { this.childRandom = Math.random(); } } } const B = { template: `<div>b-component</div>`, } new Vue({ el: '#app', components: { A, B, }, template: `<div><A></A><B></B><p>{{parentCount}}</p><button @click="changeChildrenData">修改子组件数据</button></div>`, data() { return { parentCount: 1 } }, mounted() { console.log(this.$children[0].childRandom, '--parent-created--'); // 获取第一个子组件中的childRandom }, methods: { changeParentData() { this.parentCount++; }, changeChildrenData() { console.log(this.$children); // 此时有两个子组件 this.$children[0].changeChildData(); // 调起第一个子组件中的'changeChildData'方法 } } })
在当前例子中,父组件可以通过this.$children
获取所有的子组件,这里有A
组件和B
组件,可以通过this.$children[0].childRandom
的方式获取子组件A
中的数据,也可以通过this.$children[0].changeChildData()
的方式调起子组件A
中的方法。
子组件可以通过this.$parent
的方式获取父组件,可以通过this.$parent.parentCount
获取父组件中的数据,也可以通过this.$parent.changeParentData()
的方式修改父组件中的数据。
Vue
中$parent
和$children
父子关系的底层构建请参考杂谈:������/parent/children的底层逻辑
三十九、inject
和provide
使用场景:嵌套组件多层级传参
const B = { template: `<div>{{parentData1}}{{parentData2}}</div>`, inject: ['parentData1', 'parentData2'], } const A = { template: `<B></B>`, components: { B, }, } new Vue({ el: '#app', components: { A, }, template: `<A></A>`, provide: { parentData1: { name: 'name-2', age: 30 }, parentData2: { name: 'name-2', age: 29 }, } })
例子中在new Vue
的时候通过provide
提供了两个数据来源parentData1
和parentData2
,然后跨了一个A
组件在B
组件中通过inject
注入了这两个数据。
1、initProvide
在执行组件内部的this._init
初始化方法时,会执行到initProvide
逻辑:
export function initProvide (vm: Component) { const provide = vm.$options.provide if (provide) { vm._provided = typeof provide === 'function' ? provide.call(vm) : provide } }
如果在当前vm.$options
中存在provide
,会将其执行结果赋值给vm._provided
。
2、initInjections
function initInjections (vm: Component) { const result = resolveInject(vm.$options.inject, vm) 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) } } function resolveInject (inject: any, vm: Component): ?Object { if (inject) { // inject is :any because flow is not smart enough to figure out cached const result = Object.create(null) const keys = hasSymbol ? Reflect.ownKeys(inject) : Object.keys(inject) for (let i = 0; i < keys.length; i++) { const key = keys[i] // #6574 in case the inject object is observed... if (key === '__ob__') continue const provideKey = inject[key].from let source = vm while (source) { if (source._provided && hasOwn(source._provided, provideKey)) { result[key] = source._provided[provideKey] break } source = source.$parent } if (!source) { if ('default' in inject[key]) { const provideDefault = inject[key].default result[key] = typeof provideDefault === 'function' ? provideDefault.call(vm) : provideDefault } else if (process.env.NODE_ENV !== 'production') { warn(`Injection "${key}" not found`, vm) } } } return result } }
如果当前组件中有选项inject
,会以while
循环的方式不断在source = source.$parent
中寻找_provided
,然后获取到祖先组件中提供的数据源,这是实现祖先组件向所有子孙后代注入依赖的核心。
四十、Vue
项目能做的性能优化
1、v-if
和v-show
- 频繁切换时使用
v-show
,利用其缓存特性 - 首屏渲染时使用
v-if
,如果为false
则不进行渲染
2、v-for
的key
- 列表变化时,循环时使用唯一不变的
key
,借助其本地复用策略 - 列表只进行一次渲染时,
key
可以采用循环的index
3、侦听器和计算属性
- 侦听器
watch
用于数据变化时引起其他行为 - 多使用
compouter
计算属性顾名思义就是新计算而来的属性,如果依赖的数据未发生变化,不会触发重新计算
4、合理使用生命周期
- 在
destroyed
阶段进行绑定事件或者定时器的销毁 - 使用动态组件的时候通过
keep-alive
包裹进行缓存处理,相关的操作可以在actived
阶段激活
5、数据响应式处理
- 不需要响应式处理的数据可以通过
Object.freeze
处理,或者直接通过this.xxx = xxx
的方式进行定义 - 需要响应式处理的属性可以通过
this.$set
的方式处理,而不是JSON.parse(JSON.stringify(XXX))
的方式
6、路由加载方式
- 页面组件可以采用异步加载的方式
7、插件引入
- 第三方插件可以采用按需加载的方式,比如
element-ui
。
8、减少代码量
- 采用
mixin
的方式抽离公共方法 - 抽离公共组件
- 定义公共方法至公共
js
中 - 抽离公共
css
9、编译方式
- 如果线上需要
template
的编译,可以采用完成版vue.esm.js
- 如果线上无需
template
的编译,可采用运行时版本vue.runtime.esm.js
,相比完整版体积要小大约30%
10、渲染方式
- 服务端渲染,如果是需要
SEO
的网站可以采用服务端渲染的方式 - 前端渲染,一些企业内部使用的后端管理系统可以采用前端渲染的方式
11、字体图标的使用
- 有些图片图标尽可能使用字体图标
四十一、Vue
项目白屏问题
- 1、开启
gzip
压缩减小文件体积。 - 2、
webpack
设置productionSourceMap:false
,不在线上环境打包.map
文件。 - 3、路由懒加载
- 4、异步组件的使用
- 5、静态资源使用
cdn
链接引入 - 6、采用
ssr
服务端渲染方案 - 7、骨架屏或者
loading
效果填充空白间隙 - 8、首次不渲染的隐藏采用
v-if
- 9、注重代码规范:抽取公共组件,公共js,公共css样式,减小代码体积。删除无用代码,减少非必要注释。防止写出死循环等等
- 10、删除辅助开发的
console.log
- 11、非
Vue
角度思考:非重要文件采用异步加载方式、css样式采用媒体查询、采用域名分片技术、http1升级成http2、如果是SSR项目考虑服务端渲染有没有可优化的点、请求头是否带了多余信息等思路
内容有些多,大体可以归类为从服务端拿到资源的速度、资源的体积和渲染是否阻塞的角度去作答。
四十二、从0
到1
构建一个Vue
项目需要注意什么
- 架子:选用合适的初始化脚手架(
vue-cli2.0
或者vue-cli3.0
) - 请求:数据
axios
请求的配置 - 登录:登录注册系统
- 路由:路由管理页面
- 数据:
vuex
全局数据管理 - 权限:权限管理系统
- 埋点:埋点系统
- 插件:第三方插件的选取以及引入方式
- 错误:错误页面
- 入口:前端资源直接当静态资源,或者服务端模板拉取
SEO
:如果考虑SEO
建议采用SSR
方案- 组件:基础组件/业务组件
- 样式:样式预处理起,公共样式抽取
- 方法:公共方法抽离
四十三、SSR
1、什么是服务端渲染(SSR)?
Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
2、为什么使用服务端渲染(SSR)?
与传统 SPA (单页应用程序 (Single-Page Application)) 相比,服务器端渲染 (SSR) 的优势主要在于:
- 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
- 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。
3、使用服务器端渲染 (SSR) 时需要考虑的问题?
使用服务器端渲染 (SSR) 时还需要有一些权衡之处
- 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。
- 涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
- 更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 (high traffic) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。
四十四、scoped
在Vue
项目开发的项目中如果样式中未使用scoped
,组件间的样式会出现覆盖的问题。
反例:
// app.vue文件 <template> <div> <h3 class="title">app-txt</h3> <child></child> </div> </template> <script> import child from "@/components/child"; export default { components: { child }, }; </script> <style> .title { color: red; } </style>
// child.vue文件 <template> <h3 class="title">child-txt</h3> </template> <style> .title { color: green; } </style>
父组件和子组件的样式颜色都为green
,子组件中的样式覆盖了父组件的样式。
正例:
<template> <h3 class="title">child-txt</h3> </template> <style scoped> .title { color: green; } </style>
此时,父组件中颜色为red
,子组件中颜色为green
。
主要原因:
例子中的DOM节点和CSS层叠样式中都被添加了data-v-xxx
来表示唯一,所以scoped
是给当前组件的节点和样式唯一标识为data-v-xxx
,避免了样式覆盖。
给大家推荐一个实用面试题库
1、前端面试题库 (面试必备) 推荐:★★★★★
地址:web前端面试题库