作者: 文章来源:
本文原地址:https://www.mimiwuqi.com/webqianduan/196377.html
目录文章目录近期整理了一下高频的前端 Vue 面试题,分享给大家一起来学习。如有问题,欢迎指正!
当一个 Vue 实例创建时,Vue 会遍历data
中的属性,用 Object.defineProperty
(vue3.0 使用proxy
)将它们转为 getter/setter
,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher
程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter
被调用时,会通知watcher
重新计算,从而致使它关联的组件得以更新。
(2)mergeOptions 的执行过程
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) } } }
在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。 一般需要对 DOM 元素进行底层操作时使用,尽量只用来操作 DOM 展示,不修改内部的值。当使用自定义指令直接修改 value 值时绑定v-model
的值也不会同步更新;如必须修改可以在自定义指令中使用keydown
事件,在 vue 组件中使用 change
事件,回调中修改 vue 数据;
(1)自定义指令基本内容
Vue.directive("focus",{})
directives:{focus:{}}
bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。inSerted
:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。update
:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前调用。指令的值可能发生了改变,也可能没有。但是可以通过比较更新前后的值来忽略不必要的模板更新。ComponentUpdate
:指令所在组件的 VNode 及其子 VNode 全部更新后调用。unbind
:只调用一次,指令与元素解绑时调用。(2)使用场景
(3)使用案例
初级应用:
高级应用:
子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。如果这样做了,Vue 会在浏览器的控制台中发出警告。
Vue 提倡单向数据流,即父级 props 的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解,导致数据流混乱。如果破坏了单向数据流,当应用复杂时,debug 的成本会非常高。
只能通过 $emit
派发一个自定义事件,父组件接收到后,由父组件修改。
在初始化 Vue 的每个组件时,会对组件的 data 进行初始化,就会将由普通对象变成响应式对象,在这个过程中便会进行依赖收集的相关逻辑,如下所示∶
function defieneReactive (obj, key, val){ const dep = new Dep(); ... Object.defineProperty(obj, key, { ... get: function reactiveGetter () { if(Dep.target){ dep.depend(); ... } return val } ... }) }
以上只保留了关键代码,主要就是 const dep = new Dep()
实例化一个 Dep 的实例,然后在 get 函数中通过 dep.depend()
进行依赖收集。
(1)Dep Dep 是整个依赖收集的核心,其关键代码如下:
class Dep { static target; subs; constructor () { ... this.subs = []; } addSub (sub) { this.subs.push(sub) } removeSub (sub) { remove(this.sub, sub) } depend () { if(Dep.target){ Dep.target.addDep(this) } } notify () { const subs = this.subds.slice(); for(let i = 0;i < subs.length; i++){ subs[i].update() } } }
Dep 是一个 class ,其中有一个关 键的静态属性 static,它指向了一个全局唯一 Watcher
,保证了同一时间全局只有一个 watcher 被计算,另一个属性 subs 则是一个 Watcher
的数组,所以 Dep 实际上就是对 Watcher 的管理,再看看 Watcher
的相关代码∶
(2)Watcher
class Watcher { getter; ... constructor (vm, expression){ ... this.getter = expression; this.get(); } get () { pushTarget(this); value = this.getter.call(vm, vm) ... return value } addDep (dep){ ... dep.addSub(this) } ... } function pushTarget (_target) { Dep.target = _target }
Watcher
是一个 class,它定义了一些方法,其中和依赖收集相关的主要有 get、addDep 等。
(3)过程
在实例化 Vue 时,依赖收集的相关过程如下∶ 初 始 化 状 态 initState
, 这 中 间 便 会 通 过 defineReactive
将数据变成响应式对象,其中的 getter
部分便是用来依赖收集的。 初始化最终会走 mount 过程,其中会实例化 Watcher
,进入 Watcher
中,便会执行 this.get()
方法,
updateComponent = () => { vm._update(vm._render()) } new Watcher(vm, updateComponent)
get
方法中的 pushTarget
实际上就是把 Dep.target
赋值为当前的 watcher
。
this.getter.call(vm,vm)
,这里的 getter
会执行 vm._render()
方法,在这个过程中便会触发数据对象的 getter
。那么每个对象值的 getter
都持有一个 dep,在触发 getter
的时候会调用 dep.depend()
方法,也就会执行 Dep.target.addDep(this)
。刚才 Dep.target
已经被赋值为 watcher
,于是便会执行 addDep
方法,然后走到 dep.addSub()
方法,便将当前的 watcher
订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。所以在 vm._render()
过程中,会触发所有数据的 getter
,这样便已经完成了一个依赖收集的过程。
相似之处:
不同之处 :
1)数据流
Vue 默认支持数据双向绑定,而 React 一直提倡单向数据流
2)虚拟 DOM
Vue2.x 开始引入”Virtual DOM”,消除了和 React 在这方面的差异,但是在具体的细节还是有各自的特点。
3)组件化
React 与 Vue 最大的不同是模板的编写。
具体来讲:React 中 render 函数是支持闭包特性的,所以 import 的组件在 render 中可以直接调用。但是在 Vue 中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 一个组件完了之后,还需要在 components 中再声明下。 4)监听数据变化的实现原理不同
getter/setter
以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到很好的性能5)高阶组件
react 可以通过高阶组件(HOC)来扩展,而 Vue 需要通过 mixins 来扩展。
高阶组件就是高阶函数,而 React 的组件本身就是纯粹的函数,所以高阶函数对 React 来说易如反掌。相反 Vue.js 使用 HTML 模板创建视图组件,这时模板无法有效的编译,因此 Vue 不能采用 HOC 来实现。
6)构建工具
两者都有自己的构建工具:
7)跨平台
kb
;angular
的特点,在数据操作方面更为简单;react
的优点,实现了 html
的封装和重用,在构建单页面应用方面有着独特的优势;dom
操作是非常耗费性能的,不再使用原生的 dom
操作节点,极大解放 dom
操作,但具体操作的还是 dom
不过是换了另一种方式;react
而言,同样是操作虚拟 dom
,就性能而言, vue
存在很大的优势。相同点: assets
和 static
两个都是存放静态资源文件。项目中所需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件下,这是相同点
不相同点:assets
中存放的静态资源文件在项目打包时,也就是运行 npm run build
时会将 assets
中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化。而压缩后的静态资源文件最终也都会放置在 static
文件中跟着 index.html
一同上传至服务器。static
中放置的静态资源文件就不会要走打包压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是 static
中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于 assets
中打包后的文件提交较大点。在服务器中就会占据更大的空间。
建议: 将项目中 template
需要的样式文件 js 文件等都可以放置在 assets
中,走打包这一流程。减少体积。而项目中引入的第三方的资源文件如iconfoont.css
等文件可以放置在 static
中,因为这些引入的第三方文件已经经过处理,不再需要处理,直接上传。
delete
只是被删除的元素变成了 empty/undefined
其他的元素的键值还是不变。Vue.delete
直接删除了数组 改变了数组的键值。当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,这个时候,你会发现页面并没有更新。这是因为Object.defineProperty()
限制,监听不到变化。
解决方式:
this.$set
(你要改变的数组/对象,你要改变的位置/key,你要改成什么 value)
this.$set(this.arr, 0, "OBKoro1"); // 改变数组 this.$set(this.obj, "c", "OBKoro1"); // 改变对象
splice() push() pop() shift() unshift() sort() reverse()
vue 源码里缓存了 array 的原型链,然后重写了这几个方法,触发这几个方法的时候会 observer 数据,意思是使用这些方法不用再进行额外的操作,视图自动进行更新。 推荐使用splice
方法会比较好自定义,因为splice
可以在数组的任何位置进行删除/添加操作。
vm.$set
的实现原理是:
splice
方法触发相应式;defineReactive
方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty
动态添加 getter
和 setter
的功能所调用的方法)vue 中的模板 template 无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的 HTML 语法,所有需要将 template 转化成一个 JavaScript 函数,这样浏览器就可以执行这一个函数并渲染出对应的 HTML 元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。模板编译又分三个阶段,解析 parse,优化 optimize,生成 generate,最终生成可执行函数 render。
SSR 也就是服务端渲染,也就是将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端完成,然后再把 html 直接返回给客户端
SSR 的优势:
SSR 的缺点:
beforeCreate
和created
两个钩子;(1)编码阶段
getter
和setter
,会收集对应的watcher
。v-if
和v-for
不能连用v-for
给每项元素绑定事件时使用事件代理keep-alive
缓存组件v-if
替代v-show
(2)SEO 优化
(3)打包优化
(4)用户体验
SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。
优点:
缺点:
对于 runtime 来说,只需要保证组件存在 render 函数即可,而有了预编译之后,只需要保证构建过程中生成 render 函数就可以。在 webpack 中,使用vue-loader
编译.vue 文件,内部依赖的vue-template-compiler
模块,在 webpack 构建过程中,将 template 预编译成 render 函数。与 react 类似,在添加了 jsx 的语法糖解析器babel-plugin-transform-vue-jsx
之后,就可以直接手写 render 函数。
所以,template 和 jsx 的都是 render 的一种表现形式,不同的是:JSX 相对于 template 而言,具有更高的灵活性,在复杂的组件中,更具有优势,而 template 虽然显得有些呆滞。但是 template 在代码结构上更符合视图与逻辑分离的习惯,更简单、更直观、更好维护。
使用 vue 开发时,在 vue 初始化之前,由于 div 是不归 vue 管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于{{message}}
的字样,虽然一般情况下这个时间很短暂,但是还是有必要让解决这个问题的。
首先:在 css 里加上以下代码:
[v-cloak] {display: none;}
如果没有彻底解决问题,则在根元素加上style="display: none;" :style="{display: 'block'}"
这个 API 很少用到,作用是扩展组件生成一个构造器,通常会与 $mount
一起使用。
// 创建组件构造器 let Component = Vue.extend({ template: '<div>test</div>' }) // 挂载到 #app 上 new Component().$mount('#app') // 除了上面的方式,还可以用来扩展已有的组件 let SuperComponent = Vue.extend(Component) new SuperComponent({ created() { console.log(1) } }) new SuperComponent().$mount('#app')
mixin
用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。
Vue.mixin({ beforeCreate() { // ...逻辑 // 这种方式会影响到每个组件的 beforeCreate 钩子函数 } })
虽然文档不建议在应用中直接使用 mixin
,但是如果不滥用的话也是很有帮助的,比如可以全局混入封装好的 ajax
或者一些工具函数等等。
mixins
应该是最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 mixins
混入代码,比如上拉下拉加载数据这种逻辑等等。 另外需要注意的是 mixins
混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并。
优点:
缺点:
Vue 实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是 Vue 的⽣命周期。
$el
属性。this
仍能获取到实例。另外还有 keep-alive
独有的生命周期,分别为 activated
和 deactivated
。用 keep-alive
包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated
钩子函数,命中缓存渲染后会执行 activated
钩子函数。
加载渲染过程:
beforeCreate
created
beforeMount
beforeCreate
created
beforeMount
mounted
mounted
更新过程:
beforeUpdate
beforeUpdate
updated
updated
销毁过程:
beforeDestroy
beforeDestroy
destroyed
destoryed
created
: 在模板渲染成 html 前调用,即通常初始化某些属性值,然后再渲染成视图。mounted
: 在模板渲染成 html 后调用,通常是初始化页面完成后,再对 html 的 dom 节点进行一些需要的操作。我们可以在钩子函数 created
、beforeMount
、mounted
中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
推荐在 created
钩子函数中调用异步请求,因为在 created
钩子函数中调用异步请求有以下优点:
beforeMount
、mounted
钩子函数,放在 created
中有助于一致性。keep-alive
是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染 DOM。
如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated
、activated
。同时,beforeDestroy
和 destroyed
就不会再被触发了,因为组件不会被真正销毁。
当组件被换掉时,会被缓存到内存中、触发 deactivated
生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated
钩子函数。
组件通信的方式如下:
父组件通过props
向子组件传递数据,子组件通过$emit
和父组件通信
props
只能是父组件向子组件进行传值,props
使得父子组件之间形成了一个单向下行绑定。子组件的数据会随着父组件不断更新。props
可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以传递一个函数。props
属性名规则:若在props
中使用驼峰形式,模板中需要使用短横线的形式// 父组件 <template> <div id="father"> <son :msg="msgData" :fn="myFunction"></son> </div> </template> <script> import son from "./son.vue"; export default { name: father, data() { msgData: "父组件数据"; }, methods: { myFunction() { console.log("vue"); } }, components: { son } }; </script>
// 子组件 <template> <div id="son"> <p>{{msg}}</p> <button @click="fn">按钮</button> </div> </template> <script> export default { name: "son", props: ["msg", "fn"] }; </script>
$emit
绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过v-on
监听并接收参数。
// 父组件 <template> <div > <com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article> <p>{{currentIndex}}</p> </div> </template> <script> import comArticle from './test/article.vue' export default { name: 'comArticle', components: { comArticle }, data() { return { currentIndex: -1, articleList: ['红楼梦', '西游记', '三国演义'] } }, methods: { onEmitIndex(idx) { this.currentIndex = idx } } } </script>
//子组件 <template> <div> <div v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</div> </div> </template> <script> export default { props: ['articles'], methods: { emitIndex(index) { this.$emit('onEmitIndex', index) // 触发父组件的方法,并传递参数 index } } } </script>
eventBus
事件总线适用于父子组件、非父子组件等之间的通信,使用步骤如下:
// event-bus.js import Vue from 'vue' export const EventBus = new Vue()
firstCom
和secondCom
:
<template> <div> <first-com></first-com> <second-com></second-com> </div> </template> <script> import firstCom from './firstCom.vue' import secondCom from './secondCom.vue' export default { components: { firstCom, secondCom } } </script>
在 firstCom 组件中发送事件:
<template> <div> <button @click="add">加法</button> </div> </template> <script> import {EventBus} from './event-bus.js' // 引入事件中心 export default { data(){ return{ num:0 } }, methods:{ add(){ EventBus.$emit('addition', { num:this.num++ }) } } } </script>
<template> <div>求和: {{count}}</div> </template> <script> import { EventBus } from './event-bus.js' export default { data() { return { count: 0 } }, mounted() { EventBus.$on('addition', param => { this.count = this.count + param.num; }) } } </script>
在上述代码中,这就相当于将 num 值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。
虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。
这种方式就是 Vue 中的依赖注入,该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了。
provide / inject
是 Vue 提供的两个钩子,和data
、methods
是同级的。并且provide
的书写形式和data
一样。
provide
钩子用来发送数据或方法inject
钩子用来接收数据或方法在父组件中:
provide() { return { num: this.num }; }
在子组件中:
inject: ['num']
还可以这样写,这样写就可以访问父组件中的所有属性:
provide() { return { app: this }; } data() { return { num: 1 }; } inject: ['app'] console.log(this.app.num)
注意: 依赖注入所提供的属性是非响应式的。
这种方式也是实现父子组件之间的通信。
ref
: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。
在子组件中:
export default { data () { return { name: 'JavaScript' } }, methods: { sayHello () { console.log('hello') } } }
在父组件中:
<template> <child ref="child"></component-a> </template> <script> import child from './child.vue' export default { components: { child }, mounted () { console.log(this.$refs.child.name); // JavaScript this.$refs.child.sayHello(); // hello } } </script>
$parent
可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法)$children
可以让组件访问子组件的实例,但是,$children
并不能保证顺序,并且访问的数据也不是响应式的。在子组件中:
<template> <div> <span>{{message}}</span> <p>获取父组件的值为: {{parentVal}}</p> </div> </template> <script> export default { data() { return { message: 'Vue' } }, computed:{ parentVal(){ return this.$parent.msg; } } } </script>
在父组件中:
// 父组件中 <template> <div > <div>{{msg}}</div> <child></child> <button @click="change">点击改变子组件值</button> </div> </template> <script> import child from './child.vue' export default { components: { child }, data() { return { msg: 'Welcome' } }, methods: { change() { // 获取到子组件 this.$children[0].message = 'JavaScript' } } } </script>
在上面的代码中,子组件获取到了父组件的parentVal
值,父组件改变了子组件中message
的值。 需要注意:
$parent
访问到的是上一级父组件的实例,可以使用$root
来访问根组件的实例$children
拿到的是所有的子组件的实例,它是一个数组,并且是无序的#app
上拿$parent
得到的是new Vue()
的实例,在这实例上再拿$parent
得到的是undefined
,而在最底层的子组件拿$children
是个空数组$children
的值是数组,而$parent
是个对象考虑一种场景,如果 A 是 B 组件的父组件,B 是 C 组件的父组件。如果想要组件 A 给组件 C 传递数据,这种隔代的数据,该使用哪种方式呢?
如果是用props/$emit
来一级一级的传递,确实可以完成,但是比较复杂;如果使用事件总线,在多人开发或者项目较大的时候,维护起来很麻烦;如果使用 Vuex,的确也可以,但是如果仅仅是传递数据,那可能就有点浪费了。
针对上述情况,Vue 引入了$attrs / $listeners
,实现组件之间的跨代通信。
先来看一下inheritAttrs
,它的默认值 true,继承所有的父组件属性除props
之外的所有属性;inheritAttrs:false
只继承 class 属性 。
$attrs
:继承所有的父组件属性(除了 prop 传递的属性、class 和 style ),一般用在子组件的子元素上$listeners
:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合 v-on="$listeners"
将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)A 组件(APP.vue):
<template> <div id="app"> //此处监听了两个事件,可以在 B 组件或者 C 组件中直接触发 <child1 :p-child1="child1" :p-child2="child2" @test1="onTest1" @test2="onTest2"></child1> </div> </template> <script> import Child1 from './Child1.vue'; export default { components: { Child1 }, methods: { onTest1() { console.log('test1 running'); }, onTest2() { console.log('test2 running'); } } }; </script>
B 组件(Child1.vue):
<template> <div > <p>props: {{pChild1}}</p> <p>$attrs: {{$attrs}}</p> <child2 v-bind="$attrs" v-on="$listeners"></child2> </div> </template> <script> import Child2 from './Child2.vue'; export default { props: ['pChild1'], components: { Child2 }, inheritAttrs: false, mounted() { this.$emit('test1'); // 触发 APP.vue 中的 test1 方法 } }; </script>
C 组件 (Child2.vue):
<template> <div > <p>props: {{pChild2}}</p> <p>$attrs: {{$attrs}}</p> </div> </template> <script> export default { props: ['pChild2'], inheritAttrs: false, mounted() { this.$emit('test2');// 触发 APP.vue 中的 test2 方法 } }; </script>
在上述代码中:
$listeners
属性v-bind
绑定$attrs
属性,C 组件可以直接获取到 A 组件中传递下来的 props(除了 B 组件中 props 声明的)(1)父子组件间通信
$refs
组件名来获得子组件,子组件通过 $parent
获得父组件,这样也可以实现通信。provide/inject
,在父组件中通过 provide
提供变量,在子组件中通过 inject 来将变量注入到组件中。不论子组件有多深,只要调用了 inject
那么就可以注入 provide 中的数据。(2)兄弟组件间通信
eventBus
的方法,它的本质是通过创建一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现消息的传递。$parent/$refs
来获取到兄弟组件,也可以进行通信。(3)任意组件之间
eventBus
,其实就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候可以使用 vuex ,vuex 的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。
更多 web前端教程 请访问 https://www.mimiwuqi.com/webqianduan/