• vue核心面试题汇总【查缺补漏】


     给大家推荐一个实用面试题库

    1、前端面试题库 (面试必备)            推荐:★★★★★

    地址:web前端面试题库

    很喜欢‘万变不离其宗’这句话,希望在不断的思考和总结中找到Vue中的,来解答面试官抛出的各种Vue问题,一起加油~

    一、MVVM原理

    Vue2官方文档中没有找到VueMVVM的直接证据,但文档有提到:虽然没有完全遵循MVVM模型,但是 Vue 的设计也受到了它的启发,因此在文档中经常会使用vm(ViewModel 的缩写) 这个变量名表示 Vue 实例。

    为了感受MVVM模型的启发,我简单列举下其概念。

    MVVM是Model-View-ViewModel的简写,由三部分构成:

    • Model: 模型持有所有的数据、状态和程序逻辑
    • View: 负责界面的布局和显示
    • ViewModel:负责模型和界面之间的交互,是Model和View的桥梁

    二、SPA单页面应用

    单页Web应用(single page web application,SPA),就是只有一张Web页面的应用,是加载单个HTML页面并在用户与应用程序交互时动态更新该页面的Web应用程序。我们开发的Vue项目大多是借助个官方的CLI脚手架,快速搭建项目,直接通过new Vue构建一个实例,并将el:'#app'挂载参数传入,最后通过npm run build的方式打包后生成一个index.html,称这种只有一个HTML的页面为单页面应用。

    当然,vue也可以像jq一样引入,作为多页面应用的基础框架。

    三、Vue的特点

    • 清晰的官方文档和好用的api,比较容易上手。
    • 是一套用于构建用户界面的渐进式框架,将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库。
    • 使用 Virtual DOM
    • 提供了响应式 (Reactive) 和组件化 (Composable) 的视图组件。

    四、Vue的构建入口

    vue使用过程中可以采用以下两种方式:

    • 在vue脚手架中直接使用,参考文档:https://cn.vuejs.org/v2/guide/installation.html
    • 或者在html文件的头部通过静态文件的方式引入: 

    那么问题来了,使用的或者引入的到底是什么?
    答:引入的是已经打包好的vue.js文件,通过rollup构建打包所得。

    构建入口在哪里?
    答:在vue源码的package.json文件中:

    1. "scripts": {
    2. // ...
    3. "build": "node scripts/build.js",
    4. "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
    5. "build:weex": "npm run build -- weex",
    6. // ...
    7. },

    通过执行npm run build的时候,会进行scripts/build.js文件的执行,npm run build:ssr和npm run build:weex的时候,将ssr和weex作为参数传入,按照参数构建出不一样的vue.js打包文件。

    所以说,vue中的package.json文件就是构建的入口,具体构建流程可以参考vue2入口:构建入口

    五、对import Vue from "vue"的理解

    在使用脚手架开发项目时,会有一行代码import Vue from "vue",那么这个Vue指的是什么。
    答:一个构造函数。

    1. function Vue (options) {
    2. if (process.env.NODE_ENV !== 'production' &&
    3. !(this instanceof Vue)
    4. ) {
    5. warn('Vue is a constructor and should be called with the `new` keyword')
    6. }
    7. this._init(options)
    8. }
    9. initMixin(Vue)
    10. stateMixin(Vue)
    11. eventsMixin(Vue)
    12. lifecycleMixin(Vue)
    13. renderMixin(Vue)

    我们开发中引入的Vue其实就是这个构造函数,而且这个构造函数只能通过new Vue的方式进行使用,否则会在控制台打印警告信息。定义完后,还会通过initMixin(Vue)stateMixin(Vue)eventsMixin(Vue)lifecycleMixin(Vue)renderMixin(Vue)的方式为Vue原型中混入方法。我们通过import Vue from "Vue"引入的本质上就是一个原型上挂在了好多方法的构造函数。

    六、对new Vue的理解

    1. // main.js文件
    2. import Vue from "vue";
    3. var app = new Vue({
    4. el: '#app',
    5. data() {
    6. return {
    7. msg: 'hello Vue~'
    8. }
    9. },
    10. template: `
      {{msg}}
      `
      ,
    11. })
    12. console.log(app);

    new Vue就是对构造函数Vue进行实例化,执行结果如下:

    image.png

    可以看出实例化后的实例中包含了很多属性,用来对当前app进行描述,当然复杂的Vue项目这个app将会是一个树结构,通过$parent$children维护父子关系。

    new Vue的过程中还会执行this._init方法进行初始化处理。

    七、编译

    虚拟DOM的生成必须通过render函数实现,render函数的产生是在编译阶段完成,核心代码如下:

    1. export const createCompiler = createCompilerCreator(function baseCompile (
    2. template: string,
    3. options: CompilerOptions
    4. ): CompiledResult {
    5. const ast = parse(template.trim(), options)
    6. if (options.optimize !== false) {
    7. optimize(ast, options)
    8. }
    9. const code = generate(ast, options)
    10. return {
    11. ast,
    12. render: code.render,
    13. staticRenderFns: code.staticRenderFns
    14. }
    15. })

    主要完成的功能是:

    • 通过const ast = parse(template.trim(), options)template转换成ast
    • 通过optimize(ast, options)ast进行优化
    • 通过const code = generate(ast, options)将优化后的ast转换成包含render字符串的code对象,最终render字符串通过new Function转换为可执行的render函数

    模板编译的真实入口可以参考vue2从template到render:模板编译入口
    parse可以参考vue2从template到render:AST
    optimize可以参考vue2从template到render:optimize
    generate可以参考vue2从template到render:code

    八、虚拟DOM

    先看浏览器对HTML的理解

    1. <h1>My titleh1>
    2. Some text content
  • 当浏览器读到这些代码时,它会建立一个DOM树来保持追踪所有内容,如同你会画一张家谱树来追踪家庭成员的发展一样。 上述 HTML 对应的 DOM 节点树如下图所示:

    544ef95bdd7c96a19d700ce613ab425a_dom-tree.png

     每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。

    再看VueHTML template的理解

    Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。

    简言之,浏览器对HTML的理解是DOM树,Vue对HTML的理解是虚拟DOM,最后在patch阶段通过DOM操作的api将其渲染成真实的DOM节点。

    九、模板或者组件渲染

    Vue中的编译会执行到逻辑vm._update(vm._render(), hydrating),其中的vm._render执行会获取到vNodevm._update就会对vNode进行patch的处理,又分为模板渲染和组件渲染。

    十、数据响应式处理

    Vue的数据响应式处理的核心是Object.defineProperty,在递归响应式处理对象的过程中,为每一个属性定义了一个发布者dep,当进行_render函数执行时会访问到当前值,在get中通过dep.depend进行当前Watcher的收集,当数据发生变化时会在set中通过dep.notify进行Watcher的更新。

    数据响应式处理以及发布订阅者模式的关系请参考vue2从数据变化到视图变化:发布订阅模式

    十一、this.$set

    1. const app = new Vue({
    2. el: "#app",
    3. data() {
    4. return {
    5. obj: {
    6. name: "name-1"
    7. }
    8. };
    9. },
    10. template: `
      {{obj.name}}的年龄是{{obj.age}}
      `
      ,
    11. methods: {
    12. change() {
    13. this.obj.name = 'name-2';
    14. this.obj.age = 30;
    15. }
    16. }
    17. });

    以上例子执行的结果是:
    name-1的年龄是
    当点击后依然是:
    name-2的年龄是
    可以看出点击后,objname属性变化得到了视图更新,而age属性并未进行变化。

    name属性响应式的过程中锁定了一个发布者dep,在当前视图渲染时在发布者depsubs中做了记录,一旦其发生改变,就会触发set方法中的dep.notify,继而执行视图的重新渲染。然而,age属性并未进行响应式的处理,当其改变时就不能进行视图渲染。

    十二、组件注册

    组件的使用是先注册后使用,又分为:

    • 全局注册:可以直接在页面中使用
    • 局部注册:使用时需要通过import xxx from xxx的方式引入,并且在当前组件的选项components中增加局部组件的名称。

    十三、异步组件

    Vue单页面应用中一个页面只有一个

    承载所有节点,因此复杂项目可能会出现首屏加载白屏等问题,Vue异步组件就很好的处理了这问题。

    十四、this.$nextTick

    因为通过new实例化构造函数Vue的时候会执行初始化方法this._init,其中涉及到的方法大多都是同步执行。nextTick在vue中是一个很重要的方法,在new Vue实例化的同步过程中将一些需要异步处理的函数推到异步队列中去,可以等new Vue所有的同步任务执行完后,再执行异步队列中的函数。

    nextTick的实现可以参考 vue2从数据变化到视图变化:nextTick

    十五、keep-alive内置组件

    vue中支持组件化,并且也有用于缓存的内置组件keep-alive可直接使用,使用场景为路由组件动态组件

    • activated表示进入组件的生命周期,deactivated表示离开组件的生命周期
    • include表示匹配到的才缓存,exclude表示匹配到的都不缓存
    • max表示最多可以缓存多少组件

    keep-alive的具体实现请参考 vue中的keep-alive(源码分析)

    十六、生命周期

    vue中的生命周期有哪些?
    答案:11个,分别为beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedactivateddeactivatedbeforeDestroydestroyederrorCaptured

    具体实现请参考 vue生命周期

    十七、v-showv-if的区别

    先看v-ifv-show的使用场景:

    (1)v-if更多的使用在需要考虑白屏时间或者切换次数很少的场景
    (2)v-show更多使用在transition控制的动画或者需要非常频繁地切换的场景

    再从底层实现思路上分析:

    (1)v-if条件为false时,会生成空的占位注释节点,那么在考虑首页白屏时间时,选用v-if比较合适。条件从false变化为true的话会从空的注释节点变成真实节点,条件再变为false时真实节点又会变成注释节点,如果切换次数比较多,那么开销会比较大,频繁切换场景不建议使用v-if
    (2)v-show条件为false时,会生成真实的节点,只是为当前节点增加了display:none来控制其隐藏,相比v-if生成空的注释节点其首次渲染开销是比较大的,所以不建议用在考虑首屏白屏时间的场景。如果我们频繁切换v-show的值,从display:nonedisplay:block之间的切换比起空的注释节点和真实节点的开销要小很多,这种场景就建议使用v-show

    可以通过vue中v-if和v-show的区别(源码分析)了解v-ifv-show详细过程。

    十八、v-forkey的作用

    v-for进行循环展示过程中,当数据发生变化进行渲染的过程中,会进行新旧节点列表的比对。首先新旧vnode列表首先通过首首尾尾首尾尾首的方式进行比对,如果key相同则采取原地复用的策略进行节点的移动。

    如果首尾两两比对的方式找不到对应关系,继续通过keyvnode的对应关系进行寻找。

    如果keyvnode对应关系中找不到,继续通过sameVnode的方式在未比对的节点中进行寻找。

    如果都找不到,则将其按照新vnode进行createElm的方式进行创建,这种方式是比节点移动的方式计算量更大。

    最后将旧的vnode列表中没有进行匹配的vnode中的vnode.elm在父节点中移除。

    简单总结就是,新的vnode列表在旧的vnode列表中去寻找具有相同的key的节点进行原地复用,如果找不到则通过创建的方式createElm去创建一个,如果旧的vnode列表中没有进行匹配则在父节点中移除其vnode.elm。这就是原地复用逻辑的大体实现。

    具体keydiff算法的关系可以参考vue2从数据变化到视图变化:diff算法图解

    十九、v-forv-if能同时使用吗

    答案是:用了也能出来预期的效果,但是会有性能浪费。

    同时包含v-forv-iftemplate模板在编辑阶段会执行v-forv-if优先级更高的编译流程;在生成vnode的阶段,会包含属性isCommenttrue的空白占位vnode;在patch阶段,会生成真实的占位节点。虽然一个空的占位节点无妨,但是如果数据量比较大的话,也是一个性能问题。

    当然,可以在获取到数据(一般是在beforeCreate或者created阶段)时进行过滤处理,也可以通过计算属性对其进行处理。

    可以通过v-for和v-if可以一起使用吗?了解v-forv-if的详细过程。

    二十、vue中的data为什么是函数

    答案是:是不是一定是函数,得看场景。并且,也无需担心什么时候该将data写为函数还是对象,因为vue内部已经做了处理,并在控制台输出错误信息。

    场景一new Vue({data: ...})
    这种场景主要为项目入口或者多个html页面各实例化一个Vue时,这里的data即可用对象的形式,也可用工厂函数返回对象的形式。因为,这里的data只会出现一次,不存在重复引用而引起的数据污染问题。

    场景二:组件场景中的选项
    在生成组件vnode的过程中,组件会在生成构造函数的过程中执行合并策略:

    1. // data合并策略
    2. strats.data = function (
    3. parentVal,
    4. childVal,
    5. vm
    6. ) {
    7. if (!vm) {
    8. if (childVal && typeof childVal !== 'function') {
    9. process.env.NODE_ENV !== 'production' && warn(
    10. 'The "data" option should be a function ' +
    11. 'that returns a per-instance value in component ' +
    12. 'definitions.',
    13. vm
    14. );
    15. return parentVal
    16. }
    17. return mergeDataOrFn(parentVal, childVal)
    18. }
    19. return mergeDataOrFn(parentVal, childVal, vm)
    20. };

    如果合并过程中发现子组件的数据不是函数,即typeof childVal !== 'function'成立,进而在开发环境会在控制台输出警告并且直接返回parentVal,说明这里压根就没有把childVal中的任何data信息合并到options中去。

    可以通过vue中的data为什么是函数?了解详细过程。

    二十一、this.$watch

    使用场景:用来监听数据的变化,当数据发生变化的时候,可以做一些业务逻辑的处理。

    配置参数:

    • deep:监听数据的深层变化
    • immediate:立即触发回调函数

    实现思路: Vue构造函数定义完成以后,在执行stateMixin(Vue)时为Vue.prototype上定义$watch。该方法通过const watcher = new Watcher(vm, expOrFn, cb, options)进行Watcher的实例化,将options中的user属性设置为true。并且,$watch逻辑结束的会返回函数function unwatchFn () { watcher.teardown() },用来取消侦听的函数。

    可以通过watch选项和$watch方法的区别vue中的watch和$watch监听的事件,执行几次?来了解详细过程。

    二十二、计算属性和侦听属性的区别

    相同点: 两者都是Watcher实例化过程中的产物

    计算属性:

    • 使用场景:模板内的表达式主要用于简单运算,对于复杂的计算逻辑可以用计算属性
    • 计算属性是基于它们的响应式依赖进行缓存的,当依赖的数据未发生变化时,多次调用无需重复执行函数
    • 计算属性计算结果依赖于data中的值
    • 同步操作,不支持异步

    侦听属性:

    • 使用场景:当需要在数据变化时执行异步或开销较大的操作时,可以用侦听属性
    • 可配置参数:可以通过配置immediatedeep来控制立即执行和深度监听的行为
    • 侦听属性侦听的是data中定义的

    计算属性请参考vue2从数据变化到视图变化:计算属性
    侦听属性请参考vue2从数据变化到视图变化:侦听器

    二十三、v-model

    1. // main.js
    2. new Vue({
    3. el: "#app",
    4. data() {
    5. return {
    6. msg: ""
    7. };
    8. },
    9. template: `
    10. msg is: {{ msg }}

    11. `
  • });
  • 普通input:input中的v-model,最终通过target.addEventListener处理成在节点上监听input事件function($event){msg=$event.target.value}}的形式,当input值变化时msg也跟着改变。

    1. // main.js
    2. const inputBox = {
    3. template: ``,
    4. };
    5. new Vue({
    6. el: "#app",
    7. template: `
    8. {{msg}}

    9. `,
  • components: {
  • inputBox
  • },
  • data() {
  • return {
  • msg: 'hello world!'
  • };
  • },
  • });
  • 组件v-model在组件中则通过给点击事件绑定原生事件,当触发到$emit的时候,再进行回调函数ƒunction input($$v) {msg=$$v}的执行,进而达到子组件修改父组件中数据msg的目的。

    二十四、v-slot

    v-slot产生的主要目的是,在组件的使用过程中可以让父组件有修改子组件内容的能力,就像在子组件里面放了个插槽,让父组件往插槽内塞入父组件中的楔子;并且,父组件在子组件中嵌入的楔子也可以访问子组件中的数据。v-slot的产生让组件的应用更加灵活。

    1、具名插槽

    1. let baseLayout = {
    2. template: `
    3. `,
  • data() {
  • return {
  • url: ""
  • };
  • }
  • };
  • new Vue({
  • el: "#app",
  • template: `
  • paragraph-1-txt

  • paragraph-2-txt

  • `,
  • components: {
  • baseLayout
  • }
  • });
  • 引入的组件baseLayout中的template被添加了属性v-slot:headerv-slot:footer,子组件中定义了对应的插槽被添加了属性name="header"name="footer",未被进行插槽标识的内容被插入到了匿名的中。

    2、作用域插槽

    1. let currentUser = {
    2. template: `
    3. {{childData.firstName}}
    4. `,
    5. data() {
    6. return {
    7. childData: {
    8. firstName: "first",
    9. lastName: "last"
    10. }
    11. };
    12. }
    13. };
    14. new Vue({
    15. el: "#app",
    16. template: `
    17. `,
    18. components: {
    19. currentUser
    20. }
    21. });

    当前例子中作用域插槽通过v-bind:userData="childData"的方式,将childData作为参数,父组件中通过v-slot:user="slotProps"的方式进行接收,为父组件使用子组件中的数据提供了可能。

    v-slot的底层实现请参考vue中的v-slot(源码分析)

    二十五、Vue.filters

    filters类似于管道流可以将上一个过滤函数的结果作为下一个过滤函数的第一个参数,又可以在其中传递参数让过滤器更灵活。

    1. // main.js文件
    2. import Vue from "vue";
    3. Vue.filter("filterEmpty", function(val) {
    4. return val || "";
    5. });
    6. Vue.filter("filterA", function(val) {
    7. return val + "平时周末的";
    8. });
    9. Vue.filter("filterB", function(val, info, fn) {
    10. return val + info + fn;
    11. });
    12. new Vue({
    13. el: "#app",
    14. template: `
      {{msg | filterEmpty | filterA | filterB('爱好是', transformHobby('chess'))}}
      `
      ,
    15. data() {
    16. return {
    17. msg: "张三"
    18. };
    19. },
    20. methods: {
    21. transformHobby(type) {
    22. const map = {
    23. bike: "骑行",
    24. chess: "象棋",
    25. game: "游戏",
    26. swimming: "游泳"
    27. };
    28. return map[type] || "未知";
    29. }
    30. }
    31. });

    其中我们对msg通过filterEmptyfilterAfilterB('爱好是', transformHobby('chess'))}进行三层过滤。

    Vue.filters的底层实现请查看vue中的filters(源码分析)

    二十六、Vue.use

    二十七、Vue.extend和选项extends

    1、Vue.extend

    Vue.extend使用基础Vue构造器创建一个“子类”,参数是一个包含组件选项的对象,实例化的过程中可以修改其中的选项,为实现功能的继承提供了思路。

    1. new Vue({
    2. el: "#app",
    3. template: `
      `
      ,
    4. mounted() {
    5. // 定义子类构造函数
    6. var Profile = Vue.extend({
    7. template: '

      {{name}} 喜欢 {{fruit}}

      '
      ,
    8. data: function () {
    9. return {
    10. name: '张三',
    11. fruit: '苹果'
    12. }
    13. },
    14. methods: {
    15. showInfo() {
    16. console.log(`${this.name}喜欢${this.fruit}`)
    17. }
    18. }
    19. })
    20. // 实例化1,挂载到`#person1`上
    21. new Profile().$mount('#person1')
    22. // 实例化2,并修改其`data`选项,挂载到`#person2`上
    23. new Profile({
    24. data: function () {
    25. return {
    26. name: '李四',
    27. fruit: '香蕉'
    28. }
    29. },
    30. }).$mount('#person2')
    31. },
    32. });

    在当前例子中,通过Vue.extend构建了子类构造函数Profile,可以通过new Profile的方式实例化无数个vm实例。我们定义初始的templatedatamethodsvm进行使用,如果有变化,在实例的过程中传入新的选项参数即可,比如例子中实例化第二个vm的时候就对data进行了调整。

    2、选项extends

    extends允许声明扩展另一个组件 (可以是一个简单的选项对象或构造函数),而无需使用 Vue.extend。这主要是为了便于扩展单文件组件,以实现组件继承的目的。

    1. const common = {
    2. template: `
      {{name}}
      `
      ,
    3. data() {
    4. return {
    5. name: '表单'
    6. }
    7. }
    8. }
    9. const create = {
    10. extends: common,
    11. data() {
    12. return {
    13. name: '新增表单'
    14. }
    15. }
    16. }
    17. const edit = {
    18. extends: common,
    19. data() {
    20. return {
    21. name: '编辑表单'
    22. }
    23. }
    24. }
    25. new Vue({
    26. el: "#app",
    27. template: `
    28. `,
    29. components: {
    30. create,
    31. edit,
    32. }
    33. });

    当前极简demo中定义了公共的表单common,然后又在新增表单组件create和编辑表单组件edit中扩展了common

    二十八、Vue.mixin和选项mixins

    全局混入和局部混入视情况而定,主要区别在全局混入是通过Vue.mixin的方式将选项混入到了Vue.options中,在所有获取子组件构建函数的时候都将其进行了合并,是一种影响全部组件的混入策略。

    而局部混入是将选项通过配置mixins选项的方式合并到当前的子组件中,只有配置了mixins选项的组件才会受到混入影响,是一种局部的混入策略。

    二十九、Vue.directivedirectives

    1、使用场景

    主要用于对于DOM的操作,比如:文本框聚焦,节点位置控制、防抖节流、权限管理、复制操作等功能

    2、钩子函数

    3、钩子函数参数

    4、动态指令参数

    指令的参数可以是动态的。例如,在 v-mydirective:[argument]="value" 中,argument 参数可以根据组件实例数据进行更新!这使得自定义指令可以在应用中被灵活使用。

    三十、vue中的原生事件

    vue中可以通过@或者v-on的方式绑定事件,也可为其添加修饰符。

    1. new Vue({
    2. el: '#app',
    3. template: ``,
    4. methods: {
    5. divClick() {
    6. console.log('divClick')
    7. },
    8. aClick() {
    9. console.log('aClick')
    10. },
    11. }
    12. })

    以上例子如果点击a会触发其默认行为,如果href不为空还会进行跳转。除此之外,点击还会继续触发div上绑定的点击事件。

    如果通过@click.stop.prevent='aClick'的方式为a标签的点击事件添加修饰符stopprevent,那么就不会触发其a的默认行为,即使href不为空也不会进行跳转,同时,div上的点击事件也不会进行触发。

    模板的渲染一般分为编译生成render函数、render函数执行生成vNodepatch进行渲染。下面按照这步骤进行简单分析。

    1、render

    通过编译生成的render函数:

    1. with(this) {
    2. return _c('div', {
    3. on: {
    4. "click": divClick
    5. }
    6. }, [_c('a', {
    7. attrs: {
    8. "href": "http://www.baidu.com"
    9. },
    10. on: {
    11. "click": function ($event) {
    12. $event.stopPropagation();
    13. $event.preventDefault();
    14. return aClick($event)
    15. }
    16. }
    17. }, [_v("点击")])])
    18. }

    其中divon作为div事件描述。a标签的attrs作为属性描述,on作为事件描述,在描述中.stop被编译成了$event.stopPropagation()来阻止事件冒泡,.prevent被编译成了$event.preventDefault()用来阻止a标签的默认行为。

    2、vNode

    通过执行Vue.prototype._renderrender函数转换成vNode

    3、patch

    patch的过程中,当完成$el节点的渲染后会执行invokeCreateHooks(vnode, insertedVnodeQueue)逻辑,其中,针对attrs会将其设置为$el的真实属性,当前例子中会为a标签设置herf属性。针对on会通过target.addEventListener的方式将其处理过的事件绑定到$el上,当前例子中会分别对diva中的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

    阻止单击事件继续传播。

    1. <div @click="divClick"><a v-on:click.stop="aClick">点击a>div>
    (2).prevent

    阻止标签的默认行为。

    "http://www.baidu.com" v-on:click.prevent="aClick">点击
    
    (3).capture

    事件先在有.capture修饰符的节点上触发,然后在其包裹的内部节点中触发。

    1. <div @click="divClick"><a v-on:click="aClick">点击a>div>
    (4).self

    只当在 event.target 是当前元素自身时触发处理函数,即事件不是从内部元素触发的。

    1. <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 修饰符尤其能够提升移动端的性能

    1. <div v-on:scroll.passive="onScroll">...div>

    3、其他修饰符

    除了表单和事件的修饰符,Vue还提供了很多其他修饰符,在使用的时候可以查阅文档。

    小结

    Vue中提供了很多好用的功能和api,那么修饰符的出现就为功能和api提供了更为丰富的扩展属性和更大的灵活度。

    三十二、vue-router

    vue路由是单页面中视图切换的方案,有三种mode:

    了解vue-router的底层实现请参考vue2视图切换:vue-router

    三十三、vuex

    vuex是状态管理仓库,一般使用的场景为:多个视图依赖于同一状态,来自不同视图的行为需要变更同一状态。其管理的状态是响应式的,修改也只能显式提交mutation的方式修改。vuexstategettermutationactionmodule五个核心,并且通过module实现了vuex树的管理。

    了解vuex的底层实现请参考vue2状态管理:vuex

    三十四、eventBus

    使用场景:兄弟组件传参

    1. const eventBus = new Vue();
    2. const A = {
    3. template: `
      component-a
      `
      ,
    4. methods: {
    5. send() {
    6. eventBus.$emit('sendData', 'data from A')
    7. }
    8. },
    9. }
    10. const B = {
    11. template: `
      component-b
      `
      ,
    12. created() {
    13. eventBus.$on('sendData', (args) => {
    14. console.log(args)
    15. })
    16. },
    17. }
    18. new Vue({
    19. el: '#app',
    20. components: {
    21. A,
    22. B,
    23. },
    24. template: `
      `
      ,
    25. })

    在当前例子中,A组件和B组件称为兄弟组件,A组件通过事件总线eventBus中的$emit分发事件,B组件则通过$on来监听事件。

    实现原理:eventsMixin

    1. export function eventsMixin (Vue: Class<Component>) {
    2. const hookRE = /^hook:/
    3. Vue.prototype.$on = function (event: string | Array, fn: Function): Component {
    4. const vm: Component = this
    5. if (Array.isArray(event)) {
    6. for (let i = 0, l = event.length; i < l; i++) {
    7. vm.$on(event[i], fn)
    8. }
    9. } else {
    10. (vm._events[event] || (vm._events[event] = [])).push(fn)
    11. // optimize hook:event cost by using a boolean flag marked at registration
    12. // instead of a hash lookup
    13. if (hookRE.test(event)) {
    14. vm._hasHookEvent = true
    15. }
    16. }
    17. return vm
    18. }
    19. Vue.prototype.$once = function (event: string, fn: Function): Component {
    20. const vm: Component = this
    21. function on () {
    22. vm.$off(event, on)
    23. fn.apply(vm, arguments)
    24. }
    25. on.fn = fn
    26. vm.$on(event, on)
    27. return vm
    28. }
    29. Vue.prototype.$off = function (event?: string | Array, fn?: Function): Component {
    30. const vm: Component = this
    31. // all
    32. if (!arguments.length) {
    33. vm._events = Object.create(null)
    34. return vm
    35. }
    36. // array of events
    37. if (Array.isArray(event)) {
    38. for (let i = 0, l = event.length; i < l; i++) {
    39. vm.$off(event[i], fn)
    40. }
    41. return vm
    42. }
    43. // specific event
    44. const cbs = vm._events[event]
    45. if (!cbs) {
    46. return vm
    47. }
    48. if (!fn) {
    49. vm._events[event] = null
    50. return vm
    51. }
    52. // specific handler
    53. let cb
    54. let i = cbs.length
    55. while (i--) {
    56. cb = cbs[i]
    57. if (cb === fn || cb.fn === fn) {
    58. cbs.splice(i, 1)
    59. break
    60. }
    61. }
    62. return vm
    63. }
    64. Vue.prototype.$emit = function (event: string): Component {
    65. const vm: Component = this
    66. if (process.env.NODE_ENV !== 'production') {
    67. const lowerCaseEvent = event.toLowerCase()
    68. if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
    69. tip(
    70. `Event "${lowerCaseEvent}" is emitted in component ` +
    71. `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
    72. `Note that HTML attributes are case-insensitive and you cannot use ` +
    73. `v-on to listen to camelCase events when using in-DOM templates. ` +
    74. `You should probably use "${hyphenate(event)}" instead of "${event}".`
    75. )
    76. }
    77. }
    78. let cbs = vm._events[event]
    79. if (cbs) {
    80. cbs = cbs.length > 1 ? toArray(cbs) : cbs
    81. const args = toArray(arguments, 1)
    82. const info = `event handler for "${event}"`
    83. for (let i = 0, l = cbs.length; i < l; i++) {
    84. invokeWithErrorHandling(cbs[i], vm, args, vm, info)
    85. }
    86. }
    87. return vm
    88. }
    89. }

    Vue构造函数定义完执行的eventsMixin函数中,在Vue.prototype上分别定义了$on$emit$off$once的方法易实现对事件的绑定、分发、取消和只执行一次的方法。eventBus就是利用了当new Vue实例化后实例上的$on$emit$off$once进行数据传递。

    三十五、ref

    使用场景: 父组件获取子组件数据或者执行子组件方法

    1. const A = {
    2. template: `
      {{childData.age}}
      `
      ,
    3. data() {
    4. return {
    5. childData: {
    6. name: 'qb',
    7. age: 30
    8. },
    9. }
    10. },
    11. methods: {
    12. increaseAge() {
    13. this.childData.age++;
    14. }
    15. }
    16. }
    17. new Vue({
    18. el: '#app',
    19. components: {
    20. A,
    21. },
    22. template: ``,
    23. methods: {
    24. changeChildData() {
    25. // 执行子组件的方法
    26. this.$refs.childRef.increaseAge()
    27. // 获取子组件的数据
    28. console.log(this.$refs.childRef.childData);
    29. },
    30. }
    31. })

    在当前例子中,通过ref='childRef'的方式在当前组件中定义一个ref,可以通过this.$refs.childRef的方式获取到子组件A。可以通过this.$refs.childRef.increaseAge()的方式执行子组件中age增加的方法,也可以通过this.$refs.childRef.childData的方式获取到子组件中的数据。

    三十六、props

    使用场景: 父子传参

    1. const A = {
    2. template: `
      {{childData}}
      `
      ,
    3. props: ['childData'],
    4. methods: {
    5. emitData() {
    6. this.$emit('emitChildData', 'data from child')
    7. }
    8. },
    9. }
    10. new Vue({
    11. el: '#app',
    12. components: {
    13. A
    14. },
    15. template: ``,
    16. data() {
    17. return {
    18. parentData: 'data from parent'
    19. }
    20. },
    21. methods: {
    22. getChildData(v) {
    23. console.log(v);
    24. }
    25. }
    26. })

    从当前例子中可以看出,数据父传子是通过:childData='parentData'的方式,数据子传父是通过this.$emit('emitChildData', 'data from child')的方式,然后,父组件通过@emitChildData='getChildData'的方式进行获取。

    1、父组件render函数

    new Vue中传入的模板template经过遍历生成的render函数如下:

    1. with(this) {
    2. return _c('A', {
    3. attrs: {
    4. "childData": parentData
    5. },
    6. on: {
    7. "emitChildData": getChildData
    8. }
    9. })
    10. }

    其中data部分有attrson来描述属性和方法。

    在通过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中,通过initPropschildData赋值到子组件Avm实例上,并进行响应式处理,让其可以通过vm.childData的方式访问,并且数据发生变化时视图也可以发生改变。

    组件模板编译后对应的render函数是:

    1. with(this) {
    2. return _c('div', {
    3. on: {
    4. "click": emitData
    5. }
    6. }, [_v(_s(childData))])
    7. }

    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方法传递

    1. // main.js文件
    2. import Vue from "vue";
    3. const B = {
    4. template: `
      {{ formParentData }}
      `
      ,
    5. data() {
    6. return {
    7. formParentData: ''
    8. }
    9. },
    10. inheritAttrs: false,
    11. created() {
    12. this.formParentData = this.$attrs;
    13. console.log(this.$attrs, '--------------a-component-$attrs')
    14. console.log(this.$listeners, '--------------b-component-$listeners')
    15. },
    16. methods: {
    17. emitData() {
    18. this.$emit('onFun', 'form B component')
    19. }
    20. },
    21. }
    22. const A = {
    23. template: ``,
    24. components: {
    25. B,
    26. },
    27. props: ['propData'],
    28. inheritAttrs: false,
    29. created() {
    30. console.log(this.$attrs, '--------------b-component-$attrs')
    31. console.log(this.$listeners, '--------------b-component-$listeners')
    32. }
    33. }
    34. new Vue({
    35. el: '#app',
    36. components: {
    37. A,
    38. },
    39. template: ``,
    40. data() {
    41. return {
    42. parentData: 'msg'
    43. }
    44. },
    45. methods: {
    46. nativeFun() {
    47. console.log('方法A');
    48. },
    49. onFun(v) {
    50. console.log('方法B', v);
    51. },
    52. }
    53. })

    当前例子中,new Vuetemplate模板中有attrDatapropDataclick.nativeonFun在进行传递。实际运行后,在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,默认情况下父作用域的不被认作propsattribute绑定将会“回退”且作为普通的HTML属性应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置inheritAttrsfalse,这些默认行为将会被去掉。

    三十八、$parent$children

    使用场景: 利用父子关系进行数据的获取或者方法的调用

    1. const A = {
    2. template: `
      {{childRandom}}
      `
      ,
    3. data() {
    4. return {
    5. childRandom: Math.random()
    6. }
    7. },
    8. mounted() {
    9. console.log(this.$parent.parentCount, '--child-created--'); // 获取父组件中的parentCount
    10. },
    11. methods: {
    12. changeParentData() {
    13. console.log(this.$parent); // 打印当前实例的$parent
    14. this.$parent.changeParentData(); // 调用当前父级中的方法`changeParentData`
    15. },
    16. changeChildData() {
    17. this.childRandom = Math.random();
    18. }
    19. }
    20. }
    21. const B = {
    22. template: `
      b-component
      `
      ,
    23. }
    24. new Vue({
    25. el: '#app',
    26. components: {
    27. A,
    28. B,
    29. },
    30. template: `

      {{parentCount}}

      `
      ,
    31. data() {
    32. return {
    33. parentCount: 1
    34. }
    35. },
    36. mounted() {
    37. console.log(this.$children[0].childRandom, '--parent-created--'); // 获取第一个子组件中的childRandom
    38. },
    39. methods: {
    40. changeParentData() {
    41. this.parentCount++;
    42. },
    43. changeChildrenData() {
    44. console.log(this.$children); // 此时有两个子组件
    45. this.$children[0].changeChildData(); // 调起第一个子组件中的'changeChildData'方法
    46. }
    47. }
    48. })

    在当前例子中,父组件可以通过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的底层逻辑

    三十九、injectprovide

    使用场景:嵌套组件多层级传参

    1. const B = {
    2. template: `
      {{parentData1}}{{parentData2}}
      `
      ,
    3. inject: ['parentData1', 'parentData2'],
    4. }
    5. const A = {
    6. template: ``,
    7. components: {
    8. B,
    9. },
    10. }
    11. new Vue({
    12. el: '#app',
    13. components: {
    14. A,
    15. },
    16. template: ``,
    17. provide: {
    18. parentData1: {
    19. name: 'name-2',
    20. age: 30
    21. },
    22. parentData2: {
    23. name: 'name-2',
    24. age: 29
    25. },
    26. }
    27. })

    例子中在new Vue的时候通过provide提供了两个数据来源parentData1parentData2,然后跨了一个A组件在B组件中通过inject注入了这两个数据。

    1、initProvide

    在执行组件内部的this._init初始化方法时,会执行到initProvide逻辑:

    1. export function initProvide (vm: Component) {
    2. const provide = vm.$options.provide
    3. if (provide) {
    4. vm._provided = typeof provide === 'function'
    5. ? provide.call(vm)
    6. : provide
    7. }
    8. }

    如果在当前vm.$options中存在provide,会将其执行结果赋值给vm._provided

    2、initInjections

    1. function initInjections (vm: Component) {
    2. const result = resolveInject(vm.$options.inject, vm)
    3. if (result) {
    4. toggleObserving(false)
    5. Object.keys(result).forEach(key => {
    6. /* istanbul ignore else */
    7. if (process.env.NODE_ENV !== 'production') {
    8. defineReactive(vm, key, result[key], () => {
    9. warn(
    10. `Avoid mutating an injected value directly since the changes will be ` +
    11. `overwritten whenever the provided component re-renders. ` +
    12. `injection being mutated: "${key}"`,
    13. vm
    14. )
    15. })
    16. } else {
    17. defineReactive(vm, key, result[key])
    18. }
    19. })
    20. toggleObserving(true)
    21. }
    22. }
    23. function resolveInject (inject: any, vm: Component): ?Object {
    24. if (inject) {
    25. // inject is :any because flow is not smart enough to figure out cached
    26. const result = Object.create(null)
    27. const keys = hasSymbol
    28. ? Reflect.ownKeys(inject)
    29. : Object.keys(inject)
    30. for (let i = 0; i < keys.length; i++) {
    31. const key = keys[i]
    32. // #6574 in case the inject object is observed...
    33. if (key === '__ob__') continue
    34. const provideKey = inject[key].from
    35. let source = vm
    36. while (source) {
    37. if (source._provided && hasOwn(source._provided, provideKey)) {
    38. result[key] = source._provided[provideKey]
    39. break
    40. }
    41. source = source.$parent
    42. }
    43. if (!source) {
    44. if ('default' in inject[key]) {
    45. const provideDefault = inject[key].default
    46. result[key] = typeof provideDefault === 'function'
    47. ? provideDefault.call(vm)
    48. : provideDefault
    49. } else if (process.env.NODE_ENV !== 'production') {
    50. warn(`Injection "${key}" not found`, vm)
    51. }
    52. }
    53. }
    54. return result
    55. }
    56. }

    如果当前组件中有选项inject,会以while循环的方式不断在source = source.$parent中寻找_provided,然后获取到祖先组件中提供的数据源,这是实现祖先组件向所有子孙后代注入依赖的核心。

    四十、Vue项目能做的性能优化

    1、v-ifv-show

    2、v-forkey

    3、侦听器和计算属性

    4、合理使用生命周期

    5、数据响应式处理

    6、路由加载方式

    7、插件引入

    8、减少代码量

    9、编译方式

    10、渲染方式

    11、字体图标的使用

    四十一、Vue项目白屏问题

    内容有些多,大体可以归类为从服务端拿到资源的速度、资源的体积和渲染是否阻塞的角度去作答。

    四十二、从01构建一个Vue项目需要注意什么

    四十三、SSR

    1、什么是服务端渲染(SSR)?

    Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。

    2、为什么使用服务端渲染(SSR)?

    与传统 SPA (单页应用程序 (Single-Page Application)) 相比,服务器端渲染 (SSR) 的优势主要在于:

    3、使用服务器端渲染 (SSR) 时需要考虑的问题?

    使用服务器端渲染 (SSR) 时还需要有一些权衡之处

    四十四、scoped

    Vue项目开发的项目中如果样式中未使用scoped,组件间的样式会出现覆盖的问题。

    反例:

    1. // app.vue文件
    2. <template>
    3. <div>
    4. <h3 class="title">app-txth3>
    5. <child>child>
    6. div>
    7. template>
    8. <script>
    9. import child from "@/components/child";
    10. export default {
    11. components: { child },
    12. };
    13. script>
    14. <style>
    15. .title {
    16. color: red;
    17. }
    18. style>
    1. // child.vue文件
    2. <template>
    3. <h3 class="title">child-txth3>
    4. template>
    5. <style>
    6. .title {
    7. color: green;
    8. }
    9. style>

    父组件和子组件的样式颜色都为green,子组件中的样式覆盖了父组件的样式。

    正例:

    1. <template>
    2. <h3 class="title">child-txth3>
    3. template>
    4. <style scoped>
    5. .title {
    6. color: green;
    7. }
    8. style>

    此时,父组件中颜色为red,子组件中颜色为green

    主要原因:

    image.png

    image.png

     例子中的DOM节点和CSS层叠样式中都被添加了data-v-xxx来表示唯一,所以scoped是给当前组件的节点和样式唯一标识为data-v-xxx,避免了样式覆盖。

     给大家推荐一个实用面试题库

    1、前端面试题库 (面试必备)            推荐:★★★★★

    地址:web前端面试题库

  • 相关阅读:
    Linux中jar包实现自动重启、开机自启方案
    大数据运维实战第七课 通过 Ambari工具自动化构建 Hadoop 大数据平台和外围应用(上)
    最具有中国特色的微服务组件,阿里新一代SpringCloud学习指南
    神经网络的图像识别技术,人工神经网络图像识别
    2023最新SSM计算机毕业设计选题大全(附源码+LW)之java教学信息管理辅助系统jszpb
    (数据科学学习手札134)pyjanitor:为pandas补充更多功能
    GaussDB数据库SQL系列-自定义函数
    【冒泡排序设计】
    学习Java的第二十三天。。。(集合)
    Python(八)——列表的详细使用
  • 原文地址:https://blog.csdn.net/weixin_42981560/article/details/134082695