0


2024年前端真实面试题集合(Vue篇02)

第一章 v-if和v-for的优先级是什么?

一、作用

v-if

指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回

true

值的时候被渲染

v-for

指令基于一个数组来渲染一个列表。

v-for

指令需要使用

item in items

形式的特殊语法,其中

items

是源数据数组或者对象,而

item

则是被迭代的数组元素的别名

v-for

的时候,建议设置

key

值,并且保证每个

key

值是独一无二的,这便于

diff

算法进行优化

两者在用法上

<Modal v-if="isShow" />

<li v-for="item in items" :key="item.id">
    {{ item.label }}
</li>

二、优先级

v-if

v-for

都是

vue

模板系统中的指令

vue

模板编译的时候,会将指令系统转化成可执行的

render

函数

示例

编写一个

p

标签,同时使用

v-if

v-for
<div id="app">
    <p v-if="isShow" v-for="item in items">
        {{ item.title }}
    </p>
</div>

创建

vue

实例,存放

isShow

items

数据

const app = new Vue({
  el: "#app",
  data() {
    return {
      items: [
        { title: "foo" },
        { title: "baz" }]
    }
  },
  computed: {
    isShow() {
      return this.items && this.items.length > 0
    }
  }
})

模板指令的代码都会生成在

render

函数中,通过

app.$options.render

就能得到渲染函数

ƒ anonymous() {
  with (this) { return 
    _c('div', { attrs: { "id": "app" } }, 
    _l((items), function (item) 
    { return (isShow) ? _c('p', [_v("\n" + _s(item.title) + "\n")]) : _e() }), 0) }
}
_l

vue

的列表渲染函数,函数内部都会进行一次

if

判断

初步得到结论:

v-for

优先级是比

v-if

再将

v-for

v-if

置于不同标签

<div id="app">
    <template v-if="isShow">
        <p v-for="item in items">{{item.title}}</p>
    </template>
</div>

再输出下

render

函数

ƒ anonymous() {
  with(this){return 
    _c('div',{attrs:{"id":"app"}},
    [(isShow)?[_v("\n"),
    _l((items),function(item){return _c('p',[_v(_s(item.title))])})]:_e()],2)}
}

这时候我们可以看到,

v-for

v-if

作用在不同标签时候,是先进行判断,再进行列表的渲染

我们再在查看下

vue

源码

源码位置:

\vue-dev\src\compiler\codegen\index.js
export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }
  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) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    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)
  } else {
    // component or element
    ...
}

在进行

if

判断的时候,

v-for

是比

v-if

先进行判断

最终结论:

v-for

优先级比

v-if

三、注意事项

  1. 永远不要把 v-ifv-for 同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)
  2. 如果避免出现这种情况,则在外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环
<template v-if="isShow">
    <p v-for="item in items">
</template>

如果条件出现在循环内部,可通过计算属性

computed

提前过滤掉那些不需要显示的项

computed: {
    items: function() {
      return this.list.filter(function (item) {
        return item.isShow
      })
    }
}

第二章 SPA首屏加载速度慢的怎么解决?

image.png

一、什么是首屏加载

    首屏时间(First Contentful Paint),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容

首屏加载可以说是用户体验中最重要的环节

关于计算首屏时间

利用

performance.timing

提供的数据:

image.png

通过

DOMContentLoad

或者

performance

来计算出首屏时间

// 方案一:
document.addEventListener('DOMContentLoaded', (event) => {
    console.log('first contentful painting');
});
// 方案二:
performance.getEntriesByName("first-contentful-paint")[0].startTime

// performance.getEntriesByName("first-contentful-paint")[0]
// 会返回一个 PerformancePaintTiming的实例,结构如下:
{
  name: "first-contentful-paint",
  entryType: "paint",
  startTime: 507.80000002123415,
  duration: 0,
};

二、加载慢的原因

在页面渲染的过程,导致加载速度慢的因素可能如下:

  • 网络延时问题
  • 资源文件体积是否过大
  • 资源是否重复发送请求去加载了
  • 加载脚本的时候,渲染内容堵塞了

三、解决方案

常见的几种SPA首屏优化方式

  • 减小入口文件积
  • 静态资源本地缓存
  • UI框架按需加载
  • 图片资源的压缩
  • 组件重复打包
  • 开启GZip压缩
  • 使用SSR

减小入口文件体积

常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加

image.png

vue-router

配置路由的时候,采用动态加载路由的形式

routes:[ 
    path: 'Blogs',
    name: 'ShowBlogs',
    component: () => import('./components/ShowBlogs.vue')
]

以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件

静态资源本地缓存

后端返回资源问题:

  • 采用HTTP缓存,设置Cache-ControlLast-ModifiedEtag等响应头
  • 采用Service Worker离线缓存

前端合理利用

localStorage

UI框架按需加载

在日常使用

UI

框架,例如

element-UI

、或者

antd

,我们经常性直接引用整个

UI

import ElementUI from 'element-ui'
Vue.use(ElementUI)

但实际上我用到的组件只有按钮,分页,表格,输入与警告 所以我们要按需引用

import { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element-ui';
Vue.use(Button)
Vue.use(Input)
Vue.use(Pagination)

组件重复打包

假设

A.js

文件是一个常用的库,现在有多个路由使用了

A.js

文件,这就造成了重复下载

解决方案:在

webpack

config

文件中,修改

CommonsChunkPlugin

的配置

minChunks: 3
minChunks

为3表示会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件

图片资源的压缩

图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素,对于所有的图片资源,我们可以进行适当的压缩,对页面上使用到的

icon

,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻

http

请求压力。

开启GZip压缩

拆完包之后,我们再用

gzip

做一下压缩 安装

compression-webpack-plugin
cnmp i compression-webpack-plugin -D

vue.congig.js

中引入并修改

webpack

配置

const CompressionPlugin = require('compression-webpack-plugin')

configureWebpack: (config) => {
        if (process.env.NODE_ENV === 'production') {
            // 为生产环境修改配置...
            config.mode = 'production'
            return {
                plugins: [new CompressionPlugin({
                    test: /\.js$|\.html$|\.css/, //匹配文件名
                    threshold: 10240, //对超过10k的数据进行压缩
                    deleteOriginalAssets: false //是否删除原文件
                })]
            }
        }

在服务器我们也要做相应的配置 如果发送请求的浏览器支持

gzip

,就发送给它

gzip

格式的文件 我的服务器是用

express

框架搭建的 只要安装一下

compression

就能使用

const compression = require('compression')
app.use(compression())  // 在其他中间件使用之前调用

使用SSR

SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览器

从头搭建一个服务端渲染是很复杂的,

vue

应用建议使用

Nuxt.js

实现服务端渲染

小结:

减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化 和 页面渲染优化

下图是更为全面的首屏优化的方案

image.png

大家可以根据自己项目的情况选择各种方式进行首屏渲染的优化

第三章 为什么data属性是一个函数而不是一个对象?

一、实例和组件定义data的区别

vue

实例的时候定义

data

属性既可以是一个对象,也可以是一个函数

const app = new Vue({
    el:"#app",
    // 对象格式
    data:{
        foo:"foo"
    },
    // 函数格式
    data(){
        return {
             foo:"foo"
        }
    }
})

组件中定义

data

属性,只能是一个函数

如果为组件

data

直接定义为一个对象

Vue.component('component1',{
    template:`<div>组件</div>`,
    data:{
        foo:"foo"
    }
})

则会得到警告信息

警告说明:返回的

data

应该是一个函数在每一个组件实例中

二、组件data定义函数与对象的区别

上面讲到组件

data

必须是一个函数,不知道大家有没有思考过这是为什么呢?

在我们定义好一个组件的时候,

vue

最终都会通过

Vue.extend()

构成组件实例

这里我们模仿组件构造函数,定义

data

属性,采用对象的形式

function Component(){
 
}
Component.prototype.data = {
    count : 0
}

创建两个组件实例

const componentA = new Component()
const componentB = new Component()

修改

componentA

组件

data

属性的值,

componentB

中的值也发生了改变

console.log(componentB.data.count)  // 0
componentA.data.count = 1
console.log(componentB.data.count)  // 1

产生这样的原因这是两者共用了同一个内存地址,

componentA

修改的内容,同样对

componentB

产生了影响

如果我们采用函数的形式,则不会出现这种情况(函数返回的对象内存地址并不相同)

function Component(){
    this.data = this.data()
}
Component.prototype.data = function (){
    return {
           count : 0
    }
}

修改

componentA

组件

data

属性的值,

componentB

中的值不受影响

console.log(componentB.data.count)  // 0
componentA.data.count = 1
console.log(componentB.data.count)  // 0
vue

组件可能会有很多个实例,采用函数返回一个全新

data

形式,使每个实例对象的数据不会受到其他实例对象数据的污染

三、原理分析

首先可以看看

vue

初始化

data

的代码,

data

的定义可以是函数也可以是对象

源码位置:

/vue-dev/src/core/instance/state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
    ...
}
data

既能是

object

也能是

function

,那为什么还会出现上文警告呢?

别急,继续看下文

组件在创建的时候,会进行选项的合并

源码位置:

/vue-dev/src/core/util/options.js

自定义组件会进入

mergeOptions

进行选项合并

Vue.prototype._init = function (options?: Object) {
    ...
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    ...
  }

定义

data

会进行数据校验

源码位置:

/vue-dev/src/core/instance/init.js

这时候

vm

实例为

undefined

,进入

if

判断,若

data

类型不是

function

,则出现警告提示

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);
};

四、结论

  • 根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况
  • 组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象

第四章 动态给vue的data添加一个新的属性时会发生什么?怎样解决?

image.png

一、直接添加属性的问题

我们从一个例子开始

定义一个

p

标签,通过

v-for

指令进行遍历

然后给

botton

标签绑定点击事件,我们预期点击按钮时,数据新增一个属性,界面也 新增一行

<p v-for="(value,key) in item" :key="key">
    {{ value }}
</p>
<button @click="addProperty">动态添加新属性</button>

实例化一个

vue

实例,定义

data

属性和

methods

方法

const app = new Vue({
    el:"#app",
       data:()=>{
           item:{
            oldProperty:"旧属性"
        }
    },
    methods:{
        addProperty(){
            this.items.newProperty = "新属性"  // 为items添加新属性
            console.log(this.items)  // 输出带有newProperty的items
        }
    }
})

点击按钮,发现结果不及预期,数据虽然更新了(

console

打印出了新属性),但页面并没有更新

二、原理分析

为什么产生上面的情况呢?

下面来分析一下

vue2

是用过

Object.defineProperty

实现数据响应式

const obj = {}
Object.defineProperty(obj, 'foo', {
        get() {
            console.log(`get foo:${val}`);
            return val
        },
        set(newVal) {
            if (newVal !== val) {
                console.log(`set foo:${newVal}`);
                val = newVal
            }
        }
    })
}

当我们访问

foo

属性或者设置

foo

值的时候都能够触发

setter

getter
obj.foo   
obj.foo = 'new'

但是我们为

obj

添加新属性的时候,却无法触发事件属性的拦截

obj.bar  = '新属性'

原因是一开始

obj

foo

属性被设成了响应式数据,而

bar

是后面新增的属性,并没有通过

Object.defineProperty

设置成响应式数据

三、解决方案

Vue

不允许在已经创建的实例上动态添加新的响应式属性

若想实现数据与视图同步更新,可采取下面三种解决方案:

  • Vue.set()
  • Object.assign()
  • $forcecUpdated()
Vue.set()

Vue.set( target, propertyName/index, value )

参数

  • {Object | Array} target
  • {string | number} propertyName/index
  • {any} value

返回值:设置的值

通过

Vue.set

向响应式对象中添加一个

property

,并确保这个新

property

同样是响应式的,且触发视图更新

关于

Vue.set

源码(省略了很多与本节不相关的代码)

源码位置:

src\core\observer\index.js
function set (target: Array<any> | Object, key: any, val: any): any {
  ...
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

这里无非再次调用

defineReactive

方法,实现新增属性的响应式

关于

defineReactive

方法,内部还是通过

Object.defineProperty

实现属性拦截

大致代码如下:

function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(`get ${key}:${val}`);
            return val
        },
        set(newVal) {
            if (newVal !== val) {
                console.log(`set ${key}:${newVal}`);
                val = newVal
            }
        }
    })
}
Object.assign()

直接使用

Object.assign()

添加到对象的新属性不会触发更新

应创建一个新的对象,合并原对象和混入对象的属性

this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})
$forceUpdate

如果你发现你自己需要在

Vue

中做一次强制更新,99.9% 的情况,是你在某个地方做错了事

$forceUpdate

迫使

Vue

实例重新渲染

PS:仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。

小结

  • 如果为对象添加少量的新属性,可以直接采用Vue.set()
  • 如果需要为新对象添加大量的新属性,则通过Object.assign()创建新对象
  • 如果你实在不知道怎么操作时,可采取$forceUpdate()进行强制刷新 (不建议)

PS:

vue3

是用过

proxy

实现数据响应式的,直接动态添加新属性仍可以实现数据响应式

第五章 Vue中组件和插件有什么区别?

image.png

一、组件是什么

回顾以前对组件的定义:

组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在

Vue

中每一个

.vue

文件都可以视为一个组件

组件的优势

  • 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现
  • 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单
  • 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级

二、插件是什么

插件通常用来为

Vue

添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:

  • 添加全局方法或者属性。如: vue-custom-element
  • 添加全局资源:指令/过滤器/过渡等。如 vue-touch
  • 通过全局混入来添加一些组件选项。如vue-router
  • 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
  • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如vue-router

三、两者的区别

两者的区别主要表现在以下几个方面:

  • 编写形式
  • 注册形式
  • 使用场景

编写形式

编写组件

编写一个组件,可以有很多方式,我们最常见的就是

vue

单文件的这种格式,每一个

.vue

文件我们都可以看成是一个组件

vue

文件标准格式

<template>
</template>
<script>
export default{ 
    ...
}
</script>
<style>
</style>

我们还可以通过

template

属性来编写一个组件,如果组件内容多,我们可以在外部定义

template

组件内容,如果组件内容并不多,我们可直接写在

template

属性上

<template id="testComponent">     // 组件显示的内容
    <div>component!</div>   
</template>

Vue.component('componentA',{ 
    template: '#testComponent'  
    template: `<div>component</div>`  // 组件内容少可以通过这种形式
})

编写插件

vue

插件的实现应该暴露一个

install

方法。这个方法的第一个参数是

Vue

构造器,第二个参数是一个可选的选项对象

MyPlugin.install = function (Vue, options) {
  // 1. 添加全局方法或 property
  Vue.myGlobalMethod = function () {
    // 逻辑...
  }

  // 2. 添加全局资源
  Vue.directive('my-directive', {
    bind (el, binding, vnode, oldVnode) {
      // 逻辑...
    }
    ...
  })

  // 3. 注入组件选项
  Vue.mixin({
    created: function () {
      // 逻辑...
    }
    ...
  })

  // 4. 添加实例方法
  Vue.prototype.$myMethod = function (methodOptions) {
    // 逻辑...
  }
}

注册形式

组件注册
vue

组件注册主要分为全局注册与局部注册

全局注册通过

Vue.component

方法,第一个参数为组件的名称,第二个参数为传入的配置项

Vue.component('my-component-name', { /* ... */ })

局部注册只需在用到的地方通过

components

属性注册一个组件

const component1 = {...} // 定义一个组件

export default {
    components:{
        component1   // 局部注册
    }
}

插件注册

插件的注册通过

Vue.use()

的方式进行注册(安装),第一个参数为插件的名字,第二个参数是可选择的配置项

Vue.use(插件名字,{ /* ... */} )

注意的是:

注册插件的时候,需要在调用

new Vue()

启动应用之前完成

Vue.use

会自动阻止多次注册相同插件,只会注册一次

使用场景

具体的其实在插件是什么章节已经表述了,这里在总结一下

组件

(Component)

是用来构成你的

App

的业务模块,它的目标是

App.vue

插件

(Plugin)

是用来增强你的技术栈的功能模块,它的目标是

Vue

本身

简单来说,插件就是指对

Vue

的功能的增强或补充


本文转载自: https://blog.csdn.net/2401_82552544/article/details/141059104
版权归原作者 泛微集团小王 所有, 如有侵权,请联系我们删除。

“2024年前端真实面试题集合(Vue篇02)”的评论:

还没有评论