vue面试题(一)

8,173 阅读56分钟

VUE

Vue2和3对比

脚手架创建项目

之前有个国企,问到了怎么用脚手架创建vue项目。

2.x

npm install -g vue-cli (npm uninstall -g vue-cli 删)

vue init webpack "项目名称"

进入项目 npm run dev

3.x

npm install -g @vue/cli (npm uninstall -g @vue/cli 删)

或者npm install vue@next(最新最稳定版)

npm install -g @vue/cli-init

vue create 项目名称

进入项目 npm run serve

vue3新特性

  1. 支持碎片Fragments,模板可以有多个根元素
  2. 提供了composition api,更好的逻辑复用与代码组织
  3. 响应式数据声明方式改变
  4. 生命周期的改变
  5. 父子组件传参不同
  6. vue3 Teleport瞬移组件(类似于react的Portals传送门)
  7. vue3中v-for与v-if,只会把当前v-if当做v-for中的一个判断语句,不会相互冲突
  8. vue3中移除keyCode作为v-on的修饰符,当然也不支持config.keyCodes
  9. vue3中移除过滤器filter
  10. computed和watch变成组合式的
  11. 数据响应重新实现(ES6的proxy代替Es5的Object.defineProperty)
  12. 源码使用ts重写,更好的类型推导
  13. 虚拟DOM新算法(更快,更小)

一.响应原理对比

vue2使用Object.defineProperty方法实现响应式数据

缺点:

  • 无法检测到对象属性的动态添加和删除
  • 无法检测到数组的下标和length属性的变更
  • 深度监听需要递归到底,性能层面考虑不太好

解决方案:

  • vue2提供Vue.$set动态给对象添加属性
  • Vue.$delete动态删除对象属性
  • 也可以通过splice解决数组中的问题,object.assign解决批量添加对象属性的问题

Object.defineProperty还存在一个缺点:不能检测数组变化。2.x中是通过重写数组方法实现的对数组的监听。

vue3使用proxy实现响应式数据  

优点:

  • 可以检测到代理对象属性的动态新增和删除
  • 可以检测数组的下标和length属性的变化

缺点:

  • es6的proxy不支持低版本浏览器 IE11
  • 会针对IE11出一个特殊版本进行支持
  • 无法polyfill

二. Composition API

Vue2使用选项类型API(Options API):选项型API在代码里分割了不同的属性: data,computed属性,methods,等等。

Vue3使用合成型API(Composition API):新的合成型API能让我们用方法(function)来分割,相比于旧的API使用属性来分组,这样代码会更加简便和整洁

 // 2.0
export default {
    props: {
        title: String
    },
    data() {
        return {
            username: '',
            password: ''
        }
    },
    methods: {
        login() {
            // 登陆方法
        }
    },
    components: {
        "buttonComponent": btnComponent
    },
    computed: {
        fullName() {
            return this.firstName + " " + this.lastName;
        }
    }
}

// 3.0
export default {
    props: {
        title: String
    },

    setup() {
        const state = reactive({ //数据
            username: '',
            password: '',
            lowerCaseUsername: computed(() => state.username.toLowerCase()) //计算属性
        })
        //方法
        const login = () => {
            // 登陆方法
        }
        return {
            login,
            state
        }
    }
}
 
composition API 解决了什么问题

options API

  • 条例清晰:相同的放在相同的地方,比如方法都放在methods中,状态都在data中
  • 调用时使用this,逻辑过多时this指向不明确
  • 代码分散:一个功能的代码往往散落在不同的options种,比如data methods等等,这也导致新添加功能的时候需要在各种options中反复横跳,这时候如果代码行数较多那是要命的
  • 逻辑过于复杂的场景可以将某个功能代码抽象出mixin,但是这会导致数据来源不明确,在template中有个count变量,你会不知道他到底是来源于data还是mixin还是vue.prototype设置的全局变量。除此之外,如果存在多个mixin还可能存在同名变量被覆盖的问题。

composition API

  • 将一个功能的代码整合到一起,方便开发的同时也便于代码复用。总之就是更好的代码组织方式和更好的代码复用。
  • 没有对this的使用,避免了指向不明确的情况
  • 全部都是函数,更加方便类型推断

三.建立数据 data

VUE2.0中将数据放入到data属性中,在VUE3.0中使用setup()方法,此方法在组件初始化构造的时候触发。

使用以下三步来建立响应式数据:

  1. 从vue引入reactive
  2. 使用reactive()方法来声名我们的数据为响应性数据
  3. 使用setup()方法来返回我们的响应性数据,从而template可以获取这些响应性数据

可以通过state.username和state.password获得数据的值。

<template>
  <div>
    <h2> {{ state.username }} </h2>
  </div>
</template>

四.碎片

// 2.0
<template>
  <div class='form-element'>
      <h2> {{ title }} </h2>
  </div>
</template>

// 3.0
<template>
  <div class='form-element'>
  </div>
  <h2> {{ title }} </h2>
</template>

五.生命周期钩子函数改变

Vue2--------------vue3
beforeCreate  -> setup()开始创建组件之前,在beforeCreate和created之前执行。创建的是data和method
created       -> setup()
beforeMount   -> onBeforeMount 组件挂载到节点上之前执行的函数。
mounted       -> onMounted 组件挂载完成后执行的函数。
beforeUpdate  -> onBeforeUpdate 组件更新之前执行的函数。
updated       -> onUpdated 组件更新完成之后执行的函数。
beforeDestroy -> onBeforeUnmount 组件卸载之前执行的函数。
destroyed     -> onUnmounted 组件卸载完成后执行的函数
activated     -> onActivated 被包含在中的组件,会多出两个生命周期钩子函								数。被激活时执行 。
deactivated   -> onDeactivated 比如从 A组件,切换到 B 组件,A 组件消失									时执行。

六.父子传参

  1. setup 函数时,它将接受两个参数:(props、context(包含attrs、slots、emit))
  2. setup函数是处于生命周期函数 beforeCreate 和 Created 两个钩子函数之前的函数
  3. 执行 setup 时,组件实例尚未被创建(在 setup() 内部,this 不会是该活跃实例的引用,即不指向vue实例,Vue 为了避免我们错误的使用,直接将 setup函数中的this修改成了 undefined
  4. 与模板一起使用:需要返回一个对象 (在setup函数中定义的变量和方法最后都是需要 return 出去的不然无法在模板中使用)
  5. 使用渲染函数:可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态
  6. setup 函数中的 props 是响应式的,当传入新的 prop 时,它将被更新。但是,因为 props 是响应式的,你不能使用 ES6 解构,因为它会消除 prop 的响应性。如果需要解构 prop,可以通过使用 setup 函数中的toRefs 来完成此操作
父传子

所以当父组件向子组件中传递,和2.x版本中没有太多区别,但是如果需要从props中派生处数据时,需要从setup函数中接收。

// 父组件
<template>
  <Son :msg="state.msg"/>
</template>

<script>
import Son from "@/components/transParams/Son";
import {reactive} from "vue";
export default {
  name: "Parent",
  components: {Son},
  setup(){
    const state =reactive({
      msg:'父组件传递给子组件的参数'
    });
    return {
      state
    }
  }
}
</script>

<style scoped>

</style>

// 子组件
<template>
  <div>这里是子组件啦</div>
  <div>这里是父组件传递过来的值呀:{{msg}}</div>
</template>

<script>
export default {
  name: "Son",
  props:{
    msg:{
      type:String,
      default:''
    }
  },
  setup(props){
    console.log(props) //Proxy {msg: '父组件传递给子组件的参数'}
  }
}
</script>

<style scoped>

</style>
子传父(emit event)

子组件向父组件传递至差别比较大:

//父组件
<template>
  <div>这里是父组件啦</div>
  <div>这里是子组件传递过来的值呀:{{state.sonMsg}}</div>
  <hr/> 
  <!-- 子传父:定义子组件emit时的函数sonSendMsg,并且绑定到父组件中注册的receiveMessageFromSon函数 -->
  <Son @sonSendMsg="receiveMessageFromSon" :msg="state.msg"/>
</template>

<script>
import Son from "@/components/transParams/Son";
import {reactive} from "vue";

export default {
  name: "Parent",
  components: {Son},
  setup(){
    const state =reactive({
      msg:'父组件传递给子组件的参数',
      sonMsg:''
    });
    //子传父:定义接收函数
    const receiveMessageFromSon =(data)=>{
      state.sonMsg=data.sonMsg
    }
    return {
      state,
      receiveMessageFromSon
    }
  }
}
</script>

<style scoped>

</style>

// 子组件
<template>
  <div>这里是子组件啦</div>
  <button @click="sendMsgToParent">向父组件传递数据</button>
  <div>这里是父组件传递过来的值呀:{{msg}}</div>
</template>

<script>
export default {
  name: "Son",
  //差别一:子组件向父组件传值要注册emits
  emits:['sonSendMsg'],
  props:{
    msg:{
      type:String,
      default:''
    }
  },
  setup(props,{ attrs, slots, emit }){
    const sendMsgToParent =()=>{
      // 差别二:向父组件传递参数:不再通过this.$emit触发函数
      emit('sonSendMsg', {
        sonMsg:'子组件传递过来的数据'
      })
    };
    return {
      sendMsgToParent
    }
  }
}
</script>

<style scoped>

</style>

setup函数只能是同步的不能是异步的

七.vue3 Teleport瞬移组件

类比react中的传送门将组件挂载到想挂载的DOM上。

比如创建一个modal组件:

<template>
  <teleport to="#modal">
    <div id="center" v-if="isOpen">
      <h2><slot>this is a modal</slot></h2>
      <button @click="buttonClick">Close</button>
    </div>
  </teleport>
</template>

<script>
export default {
  name: "TeleportComponent",
  props: {
    isOpen: Boolean,
    closeModal:Function
  },
  emits: {
    'closeModal': null
  },
  setup(props, context) {
    const buttonClick = () => {
      context.emit('closeModal')
    }
    return {
      buttonClick
    }
  }
}
</script>

<style scoped>
#center {
  width: 200px;
  height: 200px;
  border: 2px solid black;
  background: white;
  position: fixed;
  left: 50%;
  top: 50%;
  margin-left: -100px;
  margin-top: -100px;
}
</style> 

使用方法如下:

<template>
  <div id="modal">这是modal即将挂载的元素</div>
  <button @click="showModal">点击我,打开Modal</button>
  <teleport-component :isOpen="isModalOpen" :closeModal="closeModal" />
</template>

<script>
import TeleportComponent from '@/components/TeleportComponent.vue';
import { ref } from 'vue'

export default {
  name: "TeleportPage",
  components:{TeleportComponent},
  setup(){
    const isModalOpen = ref(false)
    const closeModal = function(){
      isModalOpen.value=false
    };
    const showModal = function(){
      isModalOpen.value = true
    }
    return {
      closeModal,isModalOpen,showModal
    }
  }
}
</script>

<style scoped>

</style>

使用方式有两个,第一个是在app.vue中使用,但是这样存在的问题是:modal是在app的 DOM节点之下的,父节点的dom结构和css都会给modal产生影响

为了避免这个问题,我们可以在public文件夹下的index.html中增加一个节点<div id="modal"></div>,这样可以看到modal组件就是没有挂载在app下,不再受app组件的影响了

八.computed和watch

//2.0中
computed:{  //计算属性
            _suming(){
                return parseInt(this.one)+parseInt(this.two)
            },
            dataTimeing(){
                console.log("计算属性方法");
                // return "计算属性方法"+new Date()
                return "普通方法"+this.time
            }
        },

watch: {
            userName: {
                handler(val,res){
                    console.log(val);
                    console.log(res);
                },
                immediate:true,
                deep:true
            },
            
        }

//3.0中
<template>
    <section>my firstName is : {{ nameObj.firstName }}</section>
    <section>my lastName is : {{ nameObj.lastName }}</section>
    <section>my fullName is : {{ fullName }}</section>
    <p>my desc : {{ nameObj.desc }}</p>
    <button @click="changeLastName">click me to add a '!' to lastName</button>
    <p>打开控制台可以查看watch的情况</p>
    <button @click="changeHobby">修改hobby,测试深度监听</button>
</template>

<script>
import { computed, reactive, watch } from 'vue'

export default {
    name: 'ComputedAndWatch',
    setup() {
        const nameObj = reactive({
            firstName: 'Forever',
            lastName: 'Young',
            hobby: {
                ball: {
                    isLike: false
                }
            }
        });
        // 计算属性
        const fullName = computed(() => {
            return `${nameObj.firstName}_${nameObj.lastName}`
        });

        nameObj.desc = computed({
            get() {
                return `my name is ${nameObj.firstName}_${nameObj.lastName}`
            },
            // 此处的set好像没生效
            set(value) {
                console.log('set', value)
            }
        })
        const changeLastName = function () {
            nameObj.lastName = nameObj.lastName + '!'
        }
        // 监听属性
        // 可以深度监听到isLike属性的变化
        watch(nameObj, (newVal) => {
            console.info('watch nameObj 改变了', newVal)
        })
        // 只有更改nameObj.lastName会触发这个watch
        watch(()=>nameObj.lastName, (newVal) => {
            console.info('watch nameObj.lastName', newVal)
        })

        const changeHobby = function () {
            nameObj.hobby.ball.isLike = !nameObj.hobby.ball.isLike
        }
        return {
            nameObj,
            fullName,
            changeLastName,
            changeHobby
        }
    }

}
</script>

如果需要监听深度属性怎么办呢,我们都知道reactive是响应式数据属性,如果这个属性是对象,那么我们就可以开启深度监听

注意:我理解这不应该说成是深度监听,只能说是监听某个属性。由上述代码可以看出直接监听nameObj就可以监听到他内部isLike属性的变化,我觉得这已经足够深度监听了

//第一种
watch(()=> names.job.salary,(newValue,oldValue)=>{
  console.log('names改变了',newValue,oldValue)
})
//第二种
watch(()=> names.job,(newValue,oldValue)=>{
  console.log('names改变了',newValue,oldValue)
},{deep:true})

九.虚拟DOM算法差异

十.defineProperty实现响应式

// 触发更新视图
function updateView() {
    console.log('视图更新啦啦啦')
}

// -------------------------处理数组start-------------------
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新的对象,原型指向oldArrayProperty,在扩展新的方法不会影响原型
// Array.prototype.push=function(){} 会污染全局
const arrProto = Object.create(oldArrayProperty);
//重写数组方法,弥补不能监听数组变化的缺陷
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodsName => {
    arrProto[methodsName] = function () {
        updateView()//更新视图
        oldArrayProperty[methodsName].call(this, ...arguments)
    }
})
// -------------------------处理数组end-------------------


// 重新定义属性,监听起来
function defineReactive(target, key, value) {
    // 深度监听-递归处理,在性能上有影响
    observer(value)

    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newVal) {
            if (newVal !== value) {
                // 设置新值,深度监听
                observer(newVal)

                value = newVal
                // 触发视图更新
                updateView()
            }
        }
    })

}

// 监听对象属性
function observer(target) {
    
    if (typeof target !== 'object' || target === null) {
        // 不是对象或者数组
        return target
    }
    // -------------------------处理数组start-------------------
    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }
    // -------------------------处理数组end-------------------

    // 重新定义属性,for-in可以遍历数组
    for (let key in target) { 
        defineReactive(target, key, target[key])
    }

}

// 准备数据
const data = {
    name: 'jerry',
    age: 18,
    info: {
        address: 'beijing'//深度监听
    },
    nums: [1, 2, 3, 4]
}

// 监听数据
observer(data)

// 测试
// data.name = 'tom'
// console.log('更新后名字',data.name)
// data.age = 20

// // 深度监听
// data.info.address = '唐山'

// // 设置新值深度监听
// data.age = { num: 21 }
// data.age.num = 22

// delete data.name// 删除属性监听不到
// data.x = '新增属性'//新增属性监听不到

// 数组监听
data.nums.push(5)

十一.proxy实现响应式

function reactive(target = {}) {
    if (typeof target !== 'object' || target == null) {
        // 不是数组或者对象
        return target
    }

    // 代理配置
    const proxyConf = {
        get(target, key, receiver) {
            // 只处理非原型的属性
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
                console.log('get', key)
            }

            const result = Reflect.get(target, key, receiver)
            console.log('get', key)
            // return result //返回结果
            //深度监听
            return reactive(result)
        },
        set(target, key, val, receiver) {
            // 不重复修改数据
            const oldVal = target[key]
            if (oldVal === val) {
                return true
            }
            
            // 区别已有的key还是新增的key
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
                console.log('已有的key', key)
            } else {
                console.log('新增的key', key)
            }

            const result = Reflect.set(target, key, val, receiver)
            console.log('set', key, val)
            console.log('set-result', result)
            return result //是否设置成功
        },
        deleteProperty(target, key) {
            const result = Reflect.deleteProperty(target, key)
            console.log('deleteProperty', key)
            console.log('delete result', result)
            return result //是否删除成功
        }
    }

    // 生成代理对象
    const observed = new Proxy(target, proxyConf)
    return observed
}


// 测试数据
const data = {
    name: 'jerry',
    age: 18,
    info: {
        address: 'beijing'
    }
}
// const data = [1, 2, 3]

const proxyData = reactive(data)

参考链接:

blog.csdn.net/m0_64346035…

blog.csdn.net/duguxueao/a…

blog.csdn.net/weixin_4393…

vue3中ref与reactive

在Vue 3中,refreactive 是用来创建响应式数据的两种不同方式。

  1. ref:

    • ref 是一个函数,它接受一个参数,并返回一个带有 .value 属性的对象。
    • ref 主要用于创建单个基本数据类型(比如数字、字符串、布尔值等)的响应式数据。
    • ref 创建的响应式数据通过 .value 属性来访问和修改。
    • 示例:
    import { ref } from 'vue';
    
    const count = ref(0);
    
    console.log(count.value); // 输出 0
    count.value++; // count 的值现在变为 1
    
  2. reactive:

    • reactive 是一个函数,它接受一个普通对象并返回一个响应式代理对象。
    • reactive 主要用于创建复杂的对象、数组等数据结构的响应式数据。
    • reactive 创建的响应式数据可以直接访问和修改对象的属性,无需使用 .value
    • 示例:
    import { reactive } from 'vue';
    
    const state = reactive({
      count: 0,
      message: 'Hello'
    });
    
    console.log(state.count); // 输出 0
    state.count++; // state.count 的值现在变为 1
    

总结:

  • 使用 ref 当你需要创建单个基本数据类型的响应式数据时。
  • 使用 reactive 当你需要创建复杂的对象、数组等数据结构的响应式数据时。

Vue 3 中的响应式数据是通过 ES6 的 Proxy 对象实现的。refreactive 都使用了 Proxy 来实现数据的响应式。

ref 的底层实现原理:

当你调用 ref 函数时,它会创建一个包含 .value 属性的对象。在内部,Vue 使用 Proxy 将这个对象包装起来。当你访问或修改 ref 创建的对象的 .value 属性时,Proxy 会捕获这个操作并触发相应的响应式更新。

function ref(raw) {
  return new Proxy(
    {
      value: raw
    },
    {
      get(target, key) {
        // 当访问 .value 属性时返回原始值
        return target[key];
      },
      set(target, key, value) {
        // 当修改 .value 属性时触发响应式更新
        target[key] = value;
        // 触发响应式更新的逻辑...
      }
    }
  );
}

reactive 的底层实现原理:

当你调用 reactive 函数时,它会创建一个原始对象的响应式代理。在内部,Vue 使用 Proxy 包装了传入的对象,使其具有响应式能力。当你访问或修改代理对象的属性时,Proxy 会捕获这些操作并触发相应的响应式更新。

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      // 当访问属性时返回相应的值
      const value = target[key];
      // 如果值是对象,则递归地转换为响应式代理对象
      return typeof value === 'object' ? reactive(value) : value;
    },
    set(target, key, value) {
      // 当修改属性时触发响应式更新
      target[key] = value;
      // 触发响应式更新的逻辑...
    }
  });
}

这种基于 Proxy 的响应式实现使得 Vue 3 的响应式系统更加高效和灵活,能够准确地追踪数据的变化,并在数据发生变化时触发相关的更新。

$nextTick 实现原理

在 Vue 2 和 Vue 3 中,nextTick 的原理有一些不同。 nextTick 的目标是在 DOM 更新之后执行回调函数,以确保在更新之后获取到最新的 DOM。

Vue 2 的 nextTick 原理:

在 Vue 2 中,nextTick 使用了 microtasks,即微任务队列。Microtasks 是一种异步任务队列,它在事件循环的每个周期结束时执行,优先于其他异步任务(如宏任务,setTimeout 等)。这确保 nextTick 中的回调函数会在 DOM 更新之后执行。

具体来说,Vue 2 中使用了以下策略:

  1. 如果当前环境支持 Promise,则使用 Promise.resolve().then() 来创建微任务。
  2. 如果当前环境不支持 Promise,会降级为使用 MutationObserver 创建微任务。
  3. 如果都不支持,会降级为使用 setTimeout 创建宏任务。

Vue 3 的 nextTick 原理:

在 Vue 3 中,nextTick 的实现也使用了 microtasks,但通过 queueJobflushJobs 机制更为灵活和高效。

具体来说:

  1. 当数据发生变化时,Vue 3 使用 queueJob 将需要执行的回调函数加入到一个队列中。
  2. 在合适的时机(例如在异步操作结束后),使用 flushJobs 来执行队列中的回调函数。
  3. Vue 3 会使用 Promise 作为微任务的实现,但它还使用了更底层的 queueMicrotask 来保证兼容性。

Vue 3 的 nextTick 在性能上有一些优化,更好地利用了新的响应性系统和 Composition API。

总体而言,虽然 nextTick 的核心概念在 Vue 2 和 Vue 3 中都是使用 microtasks,但在具体的实现上有所不同,以适应各自版本的架构和性能优化。

import { ref, nextTick } from 'vue';

async function example() {
  // 创建响应式变量
  const data = ref(0);

  // 在数据变化之后执行回调
  data.value++;
  await nextTick();
  console.log('DOM 已经更新');
}

example();

v-for中没有Key和有key两种情况,diff算法怎么比较

diff

diff 算法主要进行vDOM的对比,找出差异,减少不必要的dom操作。

两个 js 对象或者两棵树都可以进行diff,但是两棵树的diff复杂度达到o(n^3),基本上不可用。

所以需要优化:

  • 只对比同一层的节点,不跨级比较
  • tag不同则删除重建,不进行深层
  • tag和key都相同,认为是同一节点,不深度比较

用到的重要函数

  • h函数,生成VNode
  • patch函数,接受两个参数,第一个是VNode或者element,第二个是VNode
  • sameVnode函数,接受两个vnode,判断两者的key和sel(即tag)是否相同,返回boolean值

patch函数

  • 第一个参数不是vnode, 创建一个空的vnode并关联到这个element
  • 两个参数都是vnode
    • 两个VNode相同(sameVnode返回true):patchVnode对比节点文本变化或子节点变化
    • 两个VNode不同:创建新的VNode,插入到VDom

pacthVnode

主要作用是对比两个VNode

  • old === new return
  • new.text === undefined (这基本上意味着新节点的children不为空
    • 旧节点有children:updateChildren
    • 旧节点没有children:旧节点的text设置为空,然后addNodes添加children
    • 旧节点有text:清空text
  • new.text !== undefined(新节点的children不存在)
    • old.text !== new.text 删除旧节点的children,设置新的节点的text

updateChildren

维持四个指针:

  • 新children的开始
  • 新children的结束
  • 旧children的开始
  • 旧children的结束

两两对比(sameVnode):

  • 新的开始和旧的开始
  • 新的开始和旧的结束
  • 新的结束和旧的结束
  • 新的结束和旧的开始
  • 以上四个判断命中的话:pacthVnode 并且移动指针
  • 以上四个均未命中:
    • 对比当前新节点的key是否对应旧节点的某个key
      • 没对应上:创建新的vnode并插入
      • 对应上了:找到对应旧节点,看sel是否相同
        • 不相同就创建新的vnode并且插入
        • 相同:patchVnode

在没有 Key 的情况下。sameVnode 下的 key 都是 undefined ,所以是相同的

答案

Vue 的 diff 算法(Virtual DOM diff算法)是一种用于比较虚拟DOM树的算法,以确定最小的更新量,从而提高性能。在 v-for 中涉及两种情况:有 key 和没有 key

1. 没有提供 key 的情况:

如果在 v-for 中没有提供 key,Vue 会使用默认的索引作为 key。在这种情况下,Vue 的 diff 算法会按照数组索引顺序依次比较新旧节点,尽量复用现有的DOM元素,避免不必要的DOM操作。

具体比较流程如下:

  • 对比新旧节点列表,从头开始逐个比较节点。
  • 如果新旧节点类型不同,直接替换旧节点为新节点。
  • 如果新旧节点类型相同,递归比较子节点。
  • 如果新旧节点类型相同且无子节点,判断节点内容是否相同,如果不同则更新为新节点。

这种情况下,Vue 的 diff 算法主要尝试通过最小的DOM操作来实现更新。

2. 提供了 key 的情况:

如果在 v-for 中提供了 key,Vue 会使用指定的 key 值来进行节点的比较和更新。在有 key 的情况下,Vue 的 diff 算法会按照以下步骤进行比较:

  • 对比新旧节点列表,通过 key 值映射旧节点,找到对应的节点。
  • 如果找到了对应的旧节点,比较新旧节点类型,递归比较子节点。
  • 如果找不到对应的旧节点,说明该节点是新增节点,直接在相应位置插入新节点。

使用 key 可以帮助Vue更准确地追踪每个节点的变化,确保在数据发生变化时,能够准确地进行比较和更新,而不是简单地根据顺序来操作DOM。

总之,v-for 中有无 key 直接影响了Vue的diff算法的实现方式。在大多数情况下,提供合适的 key 值可以帮助Vue的diff算法更准确、高效地更新DOM,提高应用性能。

keep-alive实现原理

用于保留组件状态或避免组件的多次渲染。其主要原理是将组件缓存起来,而不是销毁和重新创建,以便在组件切换时可以重用这些组件的状态和DOM结构。

keep-alive组件的props维持includes,excludes和max分别表示要缓存的组件,不缓存的组件和缓存的最大值。

在created里边初始化cache和keys来存储已经保存的vnode和响应的keys。

在mounted里边监听includes和excludes 来实时更新cache和keys。

渲染的时候首先会找到keep-alive第一个子组件对象以及他的name,然后检查:是否满足不在include或者在exclude中(也就是检查是否满足不需要缓存组件),如果满足的话直接返回vnode,不满足的话(需要缓存)根据tag和ID生成缓存key,检查是否已经被缓存过,如果已经缓存过那就取出缓存并且更新key在keys中的位置,以便达到max时做资源置换,如果没缓存过,那就缓存到cache中,然后检测已缓存的实例对象是否达到max,已经达到的话按照LRU置换策略舍弃掉最近最不常用的那个(index===0的那个),keep-alive设置为true。

vue-router和window.location跳转的区别(对浏览器来讲)

Vue Router 和 window.location 跳转都可以用来导航到不同的页面,但它们之间有很大的区别。

Vue Router 跳转:

  1. 单页面应用(SPA): Vue Router 通常用于单页面应用(SPA),其中所有的页面内容都加载在同一个 HTML 页面上。在SPA中,Vue Router 通过切换组件来实现页面的切换,而不是刷新整个页面。

  2. 组件导航: Vue Router 提供了 <router-link> 组件和 this.$router.push()this.$router.replace()this.$router.go() 等方法,用于在组件之间进行导航。它们会在不刷新页面的情况下,改变 URL 并更新视图。

    // 使用 <router-link> 组件
    <router-link to="/example">Example</router-link>
    
    // 使用编程式导航
    this.$router.push('/example');
    
  3. 路由守卫: Vue Router 允许你通过路由守卫来控制页面访问权限、重定向、页面加载前后的逻辑等,提供了更多的灵活性和控制权。

window.location 跳转:

  1. 整体页面跳转window.location 主要用于整体页面的跳转,它会加载一个新的 HTML 页面。每次使用 window.location 进行跳转时,页面都会被重新加载,所有的资源都会被重新请求,导致页面的刷新。

  2. 无法控制页面的局部更新: 使用 window.location 进行页面跳转会导致整个页面的刷新,无法做到局部的更新。如果你希望只更新页面的一部分内容而不刷新整个页面,你需要使用 Vue Router 或其他前端路由库。

总的来说,Vue Router 适用于单页面应用,能够实现组件级别的导航和视图更新,而 window.location 主要用于整体页面的跳转,会导致整个页面的刷新。在大多数单页面应用的场景下,推荐使用 Vue Router 进行页面导航,以提供更好的用户体验。

Vue如何进行组件封装

组件封装基本上需要考虑三个方面:

  1. props:外部传递给组件的数据
  2. 事件:组件触发外部的方法
  3. slot:外部注入到组件的视图

详情见

具体使用:

  1. 定义组件,像普通页面那样;

  2. 局部使用:在使用的页面中import,之后在components中注册;

    全局使用:在main.js中引入,然后使用Vue.component(组件名,组件)

双向数据绑定的机制

Vue的双向数据绑定是通过其特有的指令v-model来实现的,它基于以下原理:

  1. 数据劫持: Vue使用Object.defineProperty或者ES6的Proxy来劫持对象的属性。这意味着当访问或修改对象的属性时,Vue能够捕获这些操作,从而实现对数据的监听。

  2. getter和setter: 在数据劫持过程中,为对象的每个属性都定义了getter和setter。当访问属性时,会触发getter;当修改属性时,会触发setter。这就允许Vue在数据发生变化时进行通知和更新。

  3. v-model指令: v-model是Vue提供的一种双向数据绑定的语法糖,用于在表单元素(如input、textarea等)和组件上创建双向绑定。通过v-model,Vue实际上帮助我们自动处理了事件监听和数据更新。

具体流程如下:

  • 输入框初始化阶段:

    1. 当页面加载时,Vue会将数据与输入框的值进行绑定。
    2. 如果是使用v-model,Vue会监听输入框的input事件。
  • 输入框输入阶段:

    1. 用户在输入框中输入内容时,触发了input事件。
    2. Vue捕获到input事件,通过事件处理函数更新数据。
  • 数据更新阶段:

    1. 数据发生变化时,由于数据被劫持,setter被触发。
    2. 触发setter后,Vue会通知相关的观察者(Watcher)。
    3. 观察者收到通知后,会调用相应的更新函数,更新视图。

这样,无论是从输入框到数据的更新,还是从数据到输入框的更新,都能够保持同步。这就是Vue实现双向数据绑定的基本原理。值得注意的是,Vue 3 使用 Proxy 替代了 Object.defineProperty 作为数据劫持的手段,但整体的原理和流程是相似的。

一、vue-cli工程技术集合介绍

1. 构建的vue-cli工程都用到了哪些技术,它们的作用是什么?

vue.js:vue-cli工程的核心,主要特点是数据双向绑定和组件系统
vue-router:vue官方推荐使用的路由框架
vuex:专为Vue.js应用项目开发的状态管理器,主要用于维护vue组件间共用的一些变量和方
法。
asios:用于发起GETPOST等的http请求,基于Promise 设计
创建一个eenit.js文件,用于vue事件机制的管理
webpack:模块加载和vue-cli工程打包器

2. vue-cli工程中常用的npm命令有哪些?

下载node_modules资源包的命令:npm install

启动vue-cli开发环境的npm命令:npm run dev

vue-cli生成生产环境部署资源的npm命令:npm run build

用于查看vue-cli生产环境部署资源文件大小的npm命令:npm run build --report

会在浏览器上自动弹出一个展示vue-cli工程打包后app.js、manifest.js、vendor.js文件里面所包含代码的页面。可以具此优化vue-cli生产环境部署的静态资源,提升页面的加载速度。

二、vue-cli工程目录结构介绍

1.请说出vue-cli工程中每个文件夹和文件的用处

vue-cli目录解析:

  1. build 文件夹:用于存放 webpack 相关配置和脚本。开发中仅偶尔使用 到此文件夹下webpack.base.conf.js 用于配置 less、sass等css预编译库,或者配置一下 UI 库。
  2. config 文件夹:主要存放配置文件,用于区分开发环境、线上环境的不同。 常用到此文件夹下config.js 配置开发环境的端口号、是否开启热加载或者 设置生产环境的静态资源相对路径、是否开启gzip压缩、npm run build 命令打包生成静态资源的名称和路径等。
  3. dist 文件夹:默认 npm run build 命令打包生成的静态资源文件,用于生产部署。
  4. node_modules:存放npm命令下载的开发环境和生产环境的依赖包。
  5. src: 存放项目源码及需要引用的资源文件。
  6. src下assets:存放项目中需要用到的资源文件,css、js、images等。
  7. src下componets:存放vue开发中一些公共组件:header.vue、footer.vue等。
  8. src下emit:自己配置的vue集中式事件管理机制。
  9. src下router:vue-router vue路由的配置文件。
  10. src下service:自己配置的vue请求后台接口方法。
  11. src下page:存在vue页面组件的文件夹。
  12. src下util:存放vue开发过程中一些公共的.js方法。
  13. src下vuex:存放 vuex 为vue专门开发的状态管理器。
  14. src下app.vue:使用标签渲染整个工程的.vue组件。
  15. src下main.js:vue-cli工程的入口文件。
  16. index.html:设置项目的一些meta头信息和提供
    用于挂载 vue 节 点。
  17. package.json:用于 node_modules资源部 和 启动、打包项目的 npm 命令管理。

3. 请你详细介绍一些 package.json 里面的配置

  • scripts:npm run xxx 命令调用node执行的 .js 文件
  • dependencies:生产环境依赖包的名称和版本号,即这些依赖包都会打包进生产环境的JS文件里面
  • devDependencies:开发环境依赖包的名称和版本号,即这些依赖包只用于代码开发的时候,不会打包进生产环境js文件里面。

4. public和assets文件夹的区别

在Vue.js项目(以及大多数前端项目)中,publicassets 文件夹是两个不同的目录,用于存放不同类型的文件。

public 文件夹:

public 文件夹是在Vue CLI创建的项目中的一个特殊目录。在这个目录下的文件不会被webpack打包,而是会直接被复制到构建目录(如dist目录)下。通常情况下,public 文件夹用于存放不需要经过webpack处理的静态文件,比如一些第三方的 JavaScript 或 CSS 文件、网站图标(favicon.ico)等。这些文件会在构建时被直接复制到输出目录,不会被打包。

public 文件夹中的文件在HTML中的引用路径为根路径(例如,/favicon.ico),因此可以直接通过相对根路径进行访问。

assets 文件夹:

assets 文件夹是Vue.js项目中用于存放需要经过webpack处理的资源文件,比如图片、样式表、字体等。这些文件会被webpack打包,并且可以在Vue组件中通过相对路径引用。

在Vue组件中,你可以使用相对路径引用assets 文件夹中的资源,webpack会在构建时将这些资源打包到输出目录中,确保资源的引用路径是正确的。例如,如果在assets 文件夹中有一张图片logo.png,你可以在Vue组件中这样引用:

<template>
  <div>
    <img src="@/assets/logo.png" alt="Logo">
  </div>
</template>

上述代码中,@/assets/logo.png 使用了Vue CLI的别名,表示src/assets/logo.png。webpack会将该路径下的资源打包到输出目录,确保在构建后能够正确引用这个图片。

总结来说,public 文件夹用于存放不需要经过webpack处理的静态文件,而 assets 文件夹用于存放需要webpack处理的资源文件,这两者在处理和使用方式上有所不同。

三、Vue.js核心知识点高频试题一

1. Vue.js的两个核心是什么?

  1. 数据驱动,也叫双向数据绑定

    Vue.js 将视图(HTML模板)和数据(JavaScript对象)建立了关联。当数据发生变化时,相关的视图会自动更新,实现了数据和视图的自动同步。这种数据驱动的特性使得开发者可以专注于处理数据,而无需手动操作DOM,提高了开发效率。

  2. 组件系统

Vue.js 提供了强大的组件化系统,允许开发者将页面拆分成小的、独立的组件。每个组件包含了自己的模板、逻辑和样式,组件之间可以嵌套使用,形成复杂的界面。组件系统使得代码更具可维护性和复用性,加速了开发过程,并且使得应用的结构更加清晰和易于管理。Vue.js 的组件系统也支持自定义事件、插槽(slot)等功能,使得组件之间的通信更加灵活。

vue组件的核心选项:

  • 模板(template):模板声明了数据和最终展现给用户的DOM之间的映射关系
  • 初始数据(data):一个组件的初始数据状态。对于可复用的组件来说,这通常是私有的状态。
  • 接受的外部参数(props):组件之间通过参数来进行数据的传递和共享
  • 方法(methods):对数据的改动操作一般都在组建的方法内进行
  • 生命周期钩子函数(lifeCycle hooks):一个组件会出发多个生命周期钩子函数
  • 私有资源(assets):Vue.js当中经用户自定义的指令、过滤器、组件等统称为资源。一个组件可以声明自己的爱有资源。私有资源只有该组件和它的子组件可以调用

2.对于 Vue 是一套构建用户界面的渐进式框架的理解

Vue的核心的功能,是一个视图模板引擎,但这不是说Vue就不能成为一个框架。

在声明式渲染(视图模板引擎)的基础上,我们可以通过添加组件系统、客户端 路由、大规模状态管理来构建一个完整的框架。更重要的是,这些功能相互独立,你可以在核心功能的基础上任意选用其他的部件,不一定要全部整合在一起。可以看到,所说的“渐进式”,其实就是Vue的使用方式,同时也体现了Vue的设计的理念

渐进式代表的含义是:没有多做职责之外的事。

vue.js只提供了 vue-cli 生态中最核心的组件系统和双向数据绑定。

像vuex、vue-router都属于围绕 vue.js开发的库。

比如说,你要使用Angular,必须接受以下东西:

  • 必须使用它的模块机制
  • 必须使用它的依赖注入-
  • 必须使用它的特殊形式定义组件(这一点每个视图框架都有,难以避免)

所以Angular是带有比较强的排它性的,如果你的应用不是从头开始,而是要不断考虑是否跟其他东西集成,这些主张会带来一些困扰。

比如说,你要使用React,你必须理解:

  • 函数式编程的理念,
  • 需要知道什么是副作用,
  • 什么是纯函数,
  • 如何隔离副作用

它的侵入性看似没有Angular那么强,主要因为它是软性侵入。

Vue与React、Angular的不同是,但它是渐进的:

  • 你可以在原有大系统的上面,把一两个组件改用它实现,当jQuery用;
  • 也可以整个用它全家桶开发,当Angular用;
  • 还可以用它的视图,搭配你自己设计的整个下层用。
  • 你可以在底层数据逻辑的地方用OO和设计模式的那套理念,
  • 也可以函数式,都可以,它只是个轻量视图而已,只做了最核心的东西。

3.请说出vue几种常用的指令

  • v-if:根据表达式的值的真假条件渲染元素。在切换时元素及它的数据绑定 / 组件被销毁并重建。
  • v-show:根据表达式之真假值,切换元素的 display CSS 属性。
  • v-for:循环指令,基于一个数组或者对象渲染一个列表,vue 2.0以上必须需配合 key使用。
  • v-bind:动态地绑定一个或多个特性,或一个组件 prop 到表达式。
  • v-on:用于监听指定元素的DOM事件,比如点击事件。绑定事件监听器。
  • v-model:实现表单输入和应用状态之间的双向绑定
  • v-pre:在模板中跳过vue的编译,直接输出原始值。就是在标签中加入v-pre就不会输出vue中的data值了。跳过大量没有指令的节点会加快编译。比如<div v-pre>{{message}}</div>这时并不会输出我们的message值,而是直接在网页中显示{{message}}
  • v-once:只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。

4.请问 v-if 和 v-show 有什么区别

共同点:

v-if 和 v-show 都是动态显示DOM元素。

v-ifv-show 是 Vue.js 中用于条件渲染的两个指令,它们的主要区别在于元素的渲染和显示控制方式。

v-if 指令:

  • v-if 指令是真正的条件渲染指令。如果表达式的值为 true,则元素会被渲染到DOM中;如果表达式的值为 false,则元素不会被渲染到DOM中。当条件为 false 时,Vue.js 会销毁该元素及其内部的所有事件和子组件。
<template>
  <div v-if="isTrue">This element is rendered</div>
</template>

<script>
export default {
  data() {
    return {
      isTrue: true
    };
  }
};
</script>

在上面的例子中,如果 isTrue 的值为 true,那么<div>元素将被渲染到DOM中。

v-show 指令:

  • v-show 指令也用于条件渲染,但它的实现方式不同。无论条件是 true 还是 false,元素都会被渲染到DOM中,只是通过CSS的 display 属性来控制元素的显示与隐藏。当条件为 false 时,元素的 display 样式会被设置为 none,使其隐藏。
<template>
  <div v-show="isTrue">This element is shown or hidden using v-show</div>
</template>

<script>
export default {
  data() {
    return {
      isTrue: true
    };
  }
};
</script>

在上面的例子中,无论 isTrue 的值是 true 还是 false<div> 元素都会被渲染到DOM中。只是当 isTrue 的值为 false 时,该元素在页面上不可见,但它仍然存在于DOM中。

因此,选择使用 v-if 还是 v-show 取决于你的需求。如果你希望在条件为 false 时彻底从DOM中移除元素(比如减少内存占用),可以使用 v-if。如果你希望在条件为 false 时将元素保留在DOM中,只是不显示它,可以使用 v-show

5.vue常用的修饰符

Vue.js 提供了一些常用的修饰符,用于在事件处理、双向绑定、表单输入等方面增加一些特殊行为。以下是一些常用的 Vue 修饰符:

事件修饰符:

  1. .stop:阻止事件冒泡。

    <a @click.stop="doSomething"></a>
    
  2. .prevent:阻止事件的默认行为。

    <form @submit.prevent="onSubmit"></form>
    
  3. .capture:事件捕获,即在捕获阶段处理事件。

    <div @click.capture="doSomething"></div>
    
  4. .self:只当事件在该元素自身触发时触发处理函数。

    <div @click.self="doSomething"></div>
    
  5. .once:事件将只会触发一次。

    <button @click.once="doSomething"></button>
    

键盘修饰符:

  1. .enter:监听 Enter 键。

    <input @keyup.enter="submit">
    
  2. .tab:监听 Tab 键。

    <input @keyup.tab="nextField">
    
  3. .delete:监听 Delete 和 Backspace 键。

    <input @keyup.delete="deleteItem">
    

鼠标修饰符:

  1. .left:只当左键被点击时触发。

    <div @click.left="doSomething"></div>
    
  2. .right:只当右键被点击时触发。

    <div @click.right="doSomething"></div>
    

表单输入修饰符:

  1. .lazy:默认情况下,v-model 在 input 事件中同步输入框的值,使用 .lazy 修饰符,它会在 change 事件中同步。

    <input v-model.lazy="message">
    
  2. .number:将用户的输入值转为数值类型。

    <input v-model.number="age" type="number">
    
  3. .trim:自动过滤用户输入的首尾空白字符。

    <input v-model.trim="username">
    

这些修饰符可以帮助你更灵活地处理事件和表单输入。具体使用哪些修饰符取决于你的需求。

6.v-on可以监听多个方法吗?

v-on可以监听多个方法,例如:

<input type="text" :value="name" @input="onInput" @focus="onFocus"
@blur="onBlur" />

但是同一种事件类型的方法,vue-cli工程会报错,例如:

<a href="javascript:;" @click="methodsOne" @click="methodsTwo"></a>

7.vue中 key 值的作用

在Vue.js中,key 是用于帮助Vue识别VNodes的一个特殊属性。当Vue更新渲染列表时(例如使用v-for指令),它会尽量复用相同key的元素而不是重新渲染它们,以提高性能。

2.2.0+ 的版本里,当在组件中使用 v-for 时,key 现在是必须的。

例如,如果你允许用户在不同的登录方式之间切换:

<template v-if="loginType === 'username'">
    <label>Username</label>
    <input placeholder="Enter your username">
</template>

<template v-else>
    <label>Email</label>
    <input placeholder="Enter your email address">
</template>

那么在上面的代码中切换loginType 将不会清除用户已经输入的内容。因为两个模板使用了相同的元素,</ input>不会被替换掉,仅仅是替换了它的placeholder。

这样也不总是符合实际需求,所以Vue为你提供了一种方式来表达这两个元素是完全独立的,不要复用它们。只需添加一个具有唯一值的 key 属性即可:

<template v-if="loginType === 'username'">
    <label>Username</label>
    <input placeholder="Enter your username" key="username-input">
</template>

<template v-else>
    <label>Email</label>
    <input placeholder="Enter your email address" key="email-input">
</template>

现在,每次切换时,输入框都将被重新渲染。

有key性能一定好吗

使用 key 是有助于 Vue 优化列表性能的一种方式,但并不是绝对的。在某些情况下,key 的使用可能并不会对性能产生显著影响,或者甚至可能引入不必要的复杂性。

key 的主要优势在于帮助 Vue 更精确地识别和追踪每个列表项的变化。当列表数据动态变化时,Vue 可以根据 key 的唯一性来决定是复用、更新还是销毁重新创建列表项,这在某些场景下可以提高性能。

但是,key 并不是一个“性能增强剂”或者“一切问题的解决方案”。它在以下情况下特别有用:

  1. 列表顺序会变化:如果你的列表项的顺序可能会发生变化,使用 key 可以确保在列表项重新排序时正确地更新DOM,而不是简单地改变位置。

  2. 列表项可能被增删:如果列表项可能会被动态添加或移除,使用 key 可以帮助 Vue 正确地识别新的项、销毁旧的项,避免不必要的DOM操作。

  3. 列表项可能是动态组件:如果列表项是动态组件(即使用 is 特性进行切换),使用 key 可以确保正确匹配新旧组件实例。

在某些情况下,特别是对于静态列表(不会发生顺序变化、不会增删项)或者简单的列表渲染,不使用 key 并不会引发严重性能问题。使用 key 主要是为了处理复杂的、动态变化的列表结构时提供更精确的DOM更新控制。

在实际开发中,建议根据具体的业务需求和性能测试结果来决定是否使用 key。不是每个列表都需要 key,而是根据列表的特性来决定是否需要引入 key 进行优化。

8.vue事件中如何使用event对象?

如果直接传递具体的值。可以像如下:

<button @click="event('123')">修饰符</button>

event(message){
    console.log(message); //123
}

如果需要访问原始的DOM事件,可以使用特殊变量 $event,使用方法如下:

<button @click="event($event)">修饰符</button>

event(e){
    console(e); //MouseEvent事件
}
<button data-id='event' @click="event($event)">修饰符</button>

event(e){
    console.log(e.srcElement.dataset.id); //event
}

ref的使用:

<div ref="name">
    <button data-id='event' @click="event($event)">修饰符</button>
</div>
 
event(){
    console.log(this.$refs.name)
}

9.什么是$nextTick?

简单回答:因为Vue的异步更新队列,$nextTick是用来知道什么时候DOM更新完成的。

异步更新队列:指的是当状态发生变化时,Vue异步执行DOM更新。

DOM的异步更新:

<template>
    <div>
        <div ref="test">{{test}}</div>
        <button @click="handleClick">tet</button>
    </div>
</template>
export default {
    data () {
        return {
            test: 'begin'
        };
    },
    methods () {
        handleClick () {
            this.test = 'end';
            console.log(this.$refs.test.innerText);//打印“begin”
        }
    }
}

vue官方文档中是这样说的:

可能你还没有注意到,Vue异步执行DOM更新。只要观察到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变化。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作上非常重要。然后,在下一个事件循环在“tick”中,Vue刷新队列并执行实际(已去重)的工作

简而言之,就是在一个事件循环中发生的所有数据改变都会在下一个事件循环的Tick中来触发视图更新,这也是一个“批处理”的过程。(注意下一个事件循环的Tick有可能是在当前的Tick微任务执行阶段执行,也可能是在下一个Tick执行,主要取决于nextTick函数到底是使用Promise/MutationObserver还是setTimeout)

为什么要异步更新视图

看下面的代码:

<template>
    <div>
        <div>{{test}}</div>
    </div>
</template>

export default {
    data () {
        return {
            test: 0
        };
    },
    mounted () {
        for(let i = 0; i < 1000; i++) {
            this.test++;
        }
    }
}

现在有这样的一种情况,mounted的时候test的值会被++循环执行 1000 次。 每次++时,都会根据响应式触发setter->Dep->Watcher->update->run。 如果这时候没有异步更新视图,那么每次++都会直接操作DOM更新视图,这是非常消耗性能的。

所以Vue实现了一个queue队列,在下一个Tick(或者是当前Tick的微任务阶段)的时候会统一执行queue中Watcher的run。同时,拥有相同id的Watcher不会被重复加入到该queue中去,所以不会执行 1000 次Watcher的run。最终更新视图只会直接将test对应的DOM的 0 变成 1000 。 保证更新视图操作DOM的动作是在当前栈执行完以后下一个Tick(或者是当前Tick的微任务阶段)的时候调用,大大优化了性能。

Vue会根据当前浏览器环境优先使用原生的Promise.then和MutationObservery以及setImmediate,如果都不支持,就会采用setTimeout代替。

应用场景

在操作DOM节点无效的时候,就要考虑操作的实际DOM节点是否存在,或者相应的DOM是否被更新完毕。

  • 在created钩子中涉及DOM节点的操作肯定是无效的,因为此时还没有完成相关DOM的挂载。

    解决的方法就是在nextTick函数中去处理DOM,这样才能保证DOM被成功挂载而有效操作。

  • 在数据变化之后要执行某个操作,而这个操作需要使用随数据改变而改变的DOM时,这个操作应该放进Vue.nextTick。

  • 获取this.$refs.refName 返回undefined时,证明组件可能尚未存在,这时候可以在Vue.nextTick获取。

10.Vue 组件中 data 为什么必须是函数

因为一个组件是可以共享的,但它们的data是私有的,所以每个组件都return一个新的data对象,返回一个唯一的对象,不要和其他组件共用一个对象。

在Vue的源码中,当创建组件实例时,会调用initState(vm)函数来初始化组件的状态。在initState函数内部,会调用initData(vm)函数来初始化数据。

function initState(vm: Component) {
  // ...
  if (opts.data) {
    initData(vm);
  }
  // ...
}

initData函数中,会检查组件配置中的data选项,如果data是一个函数,会调用该函数获取数据对象,否则会使用一个空对象。

function initData(vm: Component) {
  let data = vm.$options.data;
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
  // ...
}

getData函数用来获取data选项返回的数据对象。

function getData(data: Function, vm: Component): any {
  // ...
  try {
    return data.call(vm, vm);
  } catch (e) {
    handleError(e, vm, `data()`);
    return {};
  }
  // ...
}

从上述源码可以看出,Vue在初始化组件实例时,会调用data选项所指定的函数(如果有)来获取数据对象,并将这个数据对象赋值给组件实例的_data属性。由于data是一个函数,每次调用组件构造函数创建新的实例时,都会执行一次data函数,从而得到独立的数据对象。

这种机制保证了每个组件实例都有独立的数据对象,避免了数据共享的问题,确保了组件状态的独立性和封装性。因此,在Vue的组件中,data必须是一个函数,而不是一个普通的对象。

11.v-for 与 v-if 的优先级

当它们处于同一节点,v-for的优先级比v-if更高,这意味着 v-if将分别重复运行于每个 v-for循环中。当你想为仅有的一些项渲染节点时,这种优先级的机制会十分有用,如下:

<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo }}
</li>

而如果你的目的是有条件地跳过循环的执行,那么可以将 v-if置于外层元素 (或 < template>)上。如:

<ul v-if="todos.length">
    <li v-for="todo in todos">
    {{ todo }}
    </li>
</ul>
<p v-else>No todos left!</p>

注意:当你只想条件性的渲染列表中的某些项的时候建议使用computed进行过滤后使用v-for进行渲染,而不是v-for和v-if同时使用。

四、Vue.js核心知识点高频试题二

4.1 组件传参

props/$emit

组件是Vue.js中最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用。一般来说,组件可以有以下几种关系:

  • 父子关系
  • 兄弟关系
  • 个贷关系
父组件向子组件传值

子组件:

<template>
    <div>
        <ul>
            //遍历传递过来的值,然后呈现到页面
            <li v-for="user in users" :key="user">{{user}}</li>
        </ul>
    </div>
</template>
<script>
export default {
    name: "zizujian",
    props: {
        users: { //这个就是父组件中子标签自定义名字
            type: Array,
            require: true
        }
    }
};
</script>

父组件

<template>
    <div>
        <User :users="users"></User>
    </div>
</template>
<script>
import User from "../zizujian";
export default {
    name: "fuzujian",
    components: { User },
    data() {
        return {
            users: ["ma", "nan", "nan"]
        };
    }
};

总结:父组件通过 props向下传递数据给子组件。

注:组件中的数据共有三种形式: data 、 props、 computed

子组件向父组件传值(通过事件形式)

子组件

<template>
    <header>
        <h1 @click="changeTitle">{{title}}</h1>
    </header>
</template>
<script>
export default {
    name: "zizujian",
    data() {
        return {
            title: "Vue.js Demo"
        };
    },
    methods: {
        changeTitle() {
            //自定义事件,传递值“子组件向父组件传值”
            this.$emit("titleChanged", "子组件向父组件传值"); 
        }
    }
}

父组件

<template>
    <div>
        <Header @titleChanged="updateTitle"></Header>//与子组件titleChanged自定义事件保持一致
        <h2>{{title}}</h2>
    </div>
</template>
<script>
import Header from "../zizujian";
export default {
    name: "fuzujian",
    components: { Header },
    data() {
        return {
            title: "传递的是一个值"
        };
    },
    methods: {
        updateTitle(e) {
            this.title = e;
        }
    }
};

总结:子组件通过 event给父组件发送消息,实际上就是子组件把自己的数据发送到父组件。

$emit/$on

这种方式通过一个空的Vue实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。

当我们的项目比较大时,可以选择更好的转台管理解决方案Vuex

具体实现:

var Event=new Vue();

// 发送数据
Event.$emit(事件名,数据);

// 接收数据
Event.$on(事件名,data => {});

vuex

vuex适用于一个状态需要在多个组件中同时使用的场景。避免传来传去逻辑复杂难以维护。

$attrs/$listeners

多级组件嵌套传递数据,仅仅是传递数据,不做中间处理

$attrs:包含了父作用域中不被 prop所识别(且获取)的特性绑定。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定,并且可以通过 v-bind="$attrs" 传入内部组件。通常配合inheritAttrs选项一起使用。

$listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

// index.vue
<template>
    <div>
        <h2>浪里行舟</h2>
        <child-com
        :foo="foo"
        :boo="boo"
        :coo="coo"
        :doo="doo"
        title="前端工匠"
        ></child-com1>
    </div>
</template>

<script>
const childCom1 = () => import("./childCom1.vue");
export default {
    components: { childCom1 },
    data() {
        return {
            foo: "Javascript",
            boo: "Html",
            coo: "CSS",
            doo: "Vue"
        };
    }
};
</script>
// childCom1.vue
<template class="border">
    <div>
        <p>foo: {{ foo }}</p>
        <p>childCom1的$attrs: {{ $attrs }}</p>
        <child-com2 v-bind="$attrs"></child-com2>
    </div>
</template>

<script>
const childCom2 = () => import("./childCom2.vue");
export default {
    components: {childCom},
    inheritAttrs: false, // 可以关闭自动挂载到组件根元素上的没有在props声明的属性
    props: {
        foo: String // foo作为props属性绑定
    },
    created() {
        console.log(this.$attrs);
        // { "boo": "Html", "coo": "CSS", "doo":"Vue", "title": "前端工匠" }
    }
};
</script>
// childCom2.vue
<template>
  <div class="border">
    <p>boo: {{ boo }}</p>
    <p>childCom2: {{ $attrs }}</p>
    <child-com3 v-bind="$attrs"></child-com3>
  </div>
</template>

<script>
const childCom3 = () => import("./childCom3.vue");
export default {
    components: {childCom},
    inheritAttrs: false,
    props: {
        boo: String
    },
    created() {
        console.log(this.$attrs); 
        // { "coo": "CSS", "doo": "Vue", "title":"前端工匠" }
    }
};
</script>
// childCom3.vue
<template>
    <div class="border">
    <p>childCom3: {{ $attrs }}</p>
    </div>
</template>

<script>
export default {
    props: {
        coo: String,
        title: String
    }
};
</script>

Vue2.4提供了$attrs/$listeners 来传递数据与事件,跨级组件之间的通讯变得更简单。

简单来说:$attrs与$listeners 是两个对象,$attrs 里存放的是父组件中绑定的非 Props 属性,$listeners里存放的是父组件中绑定的非原生事件。

provide/inject

Vue2.2.0新增API,这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。一言而蔽之:祖先组件中通过provider来提供变量,然后在子孙组件中通过inject来注入变量。

provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

假设有两个组件: A.vue 和 B.vue,B 是 A 的子组件

// A.vue
export default {
    provide: {
        name: '浪里行舟'
    }
}

// B.vue
export default {
    inject: ['name'],
    mounted () {
        console.log(this.name); // 浪里行舟
    }
}

需要注意的是:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的

所以,上面 A.vue 的 name 如果改变了,B.vue 的 this.name 是不会改变的,仍然是"浪里行舟"。

provide与 inject如何实现数据响应式

一般来说,有两种办法:

  • provide祖先组件的实例,然后在子孙组件中注入依赖,这样就可以在子孙组件中直接修改祖先组件的实例的属性,不过这种方法有个缺点就是这个实例上挂载很多没有必要的东西比如props,methods
  • 使用2.6最新API Vue.observable 优化响应式 provide(推荐)

我们来看个例子:孙组件D、E和F获取A组件传递过来的color值,并能实现数据响应式变化,即A组件的color变化后,组件D、E、F会跟着变(核心代码如下:)

//A 组件
<div>
    <h1>A 组件</h1>
    <button @click="() => changeColor()">改变color</button>
    <ChildrenB />
    <ChildrenC />
</div>
......
data() {
    return {
        color: "blue"
    };
},
// provide() {
//  return {
//      theme: {
//          color: this.color //这种方式绑定的数据并不是可响应的
//      } // 即A组件的color变化后,组件D、E、F不会跟着变
//  };
// },

provide() {
    return {
        theme: this//方法一:提供祖先组件的实例
    };
},
methods: {
    changeColor(color) {
        if (color) {
            this.color = color;
        } else {
            this.color = this.color === "blue"? "red" : "blue";
        }
    }
}

// 方法二:使用2.6最新API Vue.observable 优化响应式 provide
 provide() {
     this.theme = Vue.observable({
        color: "blue"
     });
     return {
        theme: this.theme
     };
 },
 methods: {
     changeColor(color) {
         if (color) {
            this.theme.color = color;
         } else {
            this.theme.color = this.theme.color === "blue"? "red" :"blue";
         }
     }
 }
 
// F 组件
<template functional>
    <div class="border2">
        <h3 :style="{ color: injections.theme.color }">F 组件</h3>
      </div>
</template>
<script>
export default {
  inject: {
    theme: {
      //函数式组件取值不一样
      default: () => ({})
    }
} };
</script>

虽说provide 和 inject 主要为高阶插件/组件库提供用例,但如果你能在业务中熟练运用,可以达到事半功倍的效果!

$parent / $children与 ref

  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例

  • $parent / $children:访问父 / 子实例

需要注意的是:这两种都是直接得到组件实例,使用后可以直接调用组件的方法或访问数据。我们先来看个用 ref来访问组件的例子:

// component-a 子组件
export default {
    data () {
        return {
            title: 'Vue.js'
        }
    },
    methods: {
        sayHello () {
            window.alert('Hello');
        }
    }
}

// 父组件
<template>
    <component-a ref="comA"></component-a>
</template>
<script>
export default {
    mounted () {
        const comA = this.$refs.comA;
        console.log(comA.title); // Vue.js
        comA.sayHello(); // 弹窗
    }
}
</script>

不过,这两种方法的弊端是,无法在跨级或兄弟间通信。

// parent.vue
<component-a></component-a>
<component-b></component-b>
<component-b></component-b>

我们想在 component-a 中,访问到引用它的页面中(这里就是parent.vue)的两个 component-b组件,那这种情况下,就得配置额外的插件或工具了,比如 Vuex 和 Bus 的解决方案。

组件传参场景总结

常见使用场景可以分为三类:

  • 父子通信: 父向子传递数据是通过 props,子向父是通过 events($emit);通过父链 / 子链也可以通信($parent / $children);ref 也可以访问组件实例;provide / inject API;$attrs/$listeners
  • 兄弟通信: Bus;Vuex
  • 跨级通信: Bus;Vuex;provide / inject API、$attrs/$listeners

4.2 vue中 keep-alive 组件的作用

keep-alive:主要是用于保留组件状态和避免重新渲染

其是一个抽象组件(或称为功能性组件),实际上不会被渲染在DOM树中。它的作用是在内存中缓存组件(不让组件销毁),等到下次在渲染的时候,还会保持其中的所有状态,并且会触发 activated钩子函数

属性:(属性表示要缓存的组件名,即组件定义时的name属性)

  • include:字符串或正则表达式,只有匹配的组件会被缓存
  • exclude:字符串或正则表达式,任何匹配的组件都不会被缓存

被包含在 keep-alive中创建的组件,会多出两个生命周期的钩子: activated与 decativated

  • activated:在组件被激活时调用,在组件第一次渲染时也会被调用,之后每次 keep-alive激活时被调用
  • decativated:在组件被停用时调用。

注意:只有组件被 keep-alive 包裹时,这两个生命周期才会被调用,如果作为正常组件使用,是不会被调用,以及在 2.1.0 版本之后,使用 exclude 排除之后,就算被包裹在 keep-alive中,这两个钩子依然不会被调用!另外在服务端渲染时此钩子也不会被调用的。

实际应用:(下面的写法是需要将整个路由页面缓存下来的写法)

在App.vue中修改为如下:

<keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>

<router-view v-if="!$route.meta.keepAlive"></router-view>

在router/index.js中为每一个路由添加:

meta:{
    keepAlive:true/false //true代表这个页面(组件)需要缓存,false代表不需要
}

实际应用:(下面的写法是将组件在内存中进行缓存--没有设么意义)

<!-- 基本 -->
<keep-alive>
    <component :is="view"></component>
</keep-alive>

<!-- 多个条件判断的子组件 -->
<keep-alive>
    <comp-a v-if="a > 1"></comp-a>
    <comp-b v-else></comp-b>
</keep-alive>

<!-- 和 `<transition>` 一起使用 -->
<transition>
    <keep-alive>
        <component :is="view"></component>
    </keep-alive>
</transition>

注意:<keep-alive>是用在其一个直属的子组件被开关的情形。如果你在其中有 v-for 则不会工作。如果有上述的多个条件性的子元素,<keep-alive> 要求同时只有一个子元素被渲染

include 和 exclude 属性的使用

include 和 exclude 属性允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:

<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
    <component :is="view"></component>
</keep-alive>


<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
    <component :is="view"></component>
</keep-alive>


<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
    <component :is="view"></component>
</keep-alive>

匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件components 选项的键值)。匿名组件不能被匹配。 不会在函数式组件中正常工作,因为它们没有缓存实例。

4.3 vue中如何编写可复用的组件?

在编写组件的时候,时刻考虑组件是否可复用是有好处的。一次性组件跟其他组件紧密耦合没关系,但是可复用组件一定要定义一个清晰的公开接口。

Vue.js组件 API 来自 三部分:prop、事件、slot:

  • prop 允许外部环境传递数据给组件,在vue-cli工程中也可以使用vuex等传递数据。
  • 事件允许组件触发外部环境的 action
  • slot 允许外部环境将内容插入到组件的视图结构内。

4.4 vue生命周期

1. 什么是vue生命周期和生命周期钩子函数?

Vue 的生命周期是指一个组件从创建到销毁的整个过程,而生命周期钩子函数则是在组件不同阶段会被自动调用的函数。

生命周期钩子函数允许你在组件生命周期的不同阶段执行自定义逻辑。例如,在 created 钩子函数中,你可以进行一些初始化的操作,而在 mounted 钩子函数中,你可以访问组件的DOM元素,进行DOM操作或发起异步请求。

2. vue生命周期钩子函数有哪些?

Vue.js 的生命周期钩子函数提供了一种在组件生命周期不同阶段执行代码的方式,它们允许你在组件的生命周期中执行特定的逻辑和操作。以下是每个生命周期钩子函数的主要用途和可以进行的操作:

  1. beforeCreate:

    • 在实例初始化之后,数据观测(data observation)和事件配置之前被调用。
    • 用途:通常用于初始化数据,data 和 methods 函数中的数据在此时不可用。
  2. created:

    • 实例已经创建完成之后被调用。
    • 用途:通常用于数据的初始化、异步请求、订阅事件等。可以访问到实例的 data 和 methods。
  3. beforeMount:

    • 在挂载开始之前被调用,相关的 render 函数首次被调用。
    • 用途:通常用于在渲染前获取 DOM 元素的信息或进行其他准备工作。
  4. mounted:

    • el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子函数。
    • 用途:通常用于进行 DOM 操作、调用第三方库初始化、发起异步请求等。可以访问到实例的 $el,表示组件已经挂载到 DOM 中。
  5. beforeUpdate:

    • 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
    • 用途:通常用于在数据更新前获取更新前的状态,比如记录某些数据的值。
  6. updated:

    • 数据更改导致的虚拟 DOM 重新渲染和打补丁后调用。
    • 用途:通常用于 DOM 操作,因为此时组件的数据已经更新,可以执行基于数据变化的操作。
  7. beforeDestroy:

    • 实例销毁之前调用。在这一步,实例仍然完全可用。
    • 用途:通常用于清理定时器、取消订阅事件、解绑自定义事件等,确保组件销毁前的清理工作。
  8. destroyed:

    • 实例销毁之后调用。在这一步,Vue 实例的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
    • 用途:通常用于释放组件所占用的资源,以及执行其他的清理工作。

当一个组件被 <keep-alive> 包裹时,它会多出两个生命周期钩子函数:activateddeactivated

  1. activated 钩子函数

    • 当包含被 <keep-alive> 缓存的组件被切换到可视状态时,activated 钩子函数会被调用。
    • 用途:通常用于需要在组件被激活时执行的逻辑,比如发起网络请求或执行一些动画效果。
  2. deactivated 钩子函数

  • 当包含被 <keep-alive> 缓存的组件被切换到不可视状态时,deactivated 钩子函数会被调用。
  • 用途:通常用于需要在组件被停用时执行的逻辑,比如清理工作或取消网络请求。

这两个钩子函数的存在是为了让开发者能够在组件被缓存和激活时执行特定的逻辑。然而,在 Vue.js 2.x 版本的非 <keep-alive> 情况下,并没有这两个特定的钩子函数,所以在普通情况下是不会出现 activateddeactivated 这两个生命周期钩子函数的。

需要注意的是,在 Vue.js 3.x 版本中,activateddeactivated 这两个生命周期钩子函数依然存在,并且仍然是 <keep-alive> 组件的一部分。

这些生命周期钩子函数为开发者提供了在组件生命周期中执行操作的时机,使得我们能够更好地掌握组件的状态和行为。根据需求,在这些钩子函数中可以进行各种操作,如数据初始化、异步请求、DOM 操作、事件订阅、资源释放等。

注意:

  1. mounted、updated不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用vm.$nextTick 替换掉mounted、updated:
  2. http请求建议在created 生命周期内发出

4.5 vue如何监听键盘事件中的按键

在监听键盘事件时,我们经常需要检查常见的键值。Vue允许为 v-on在监听键盘事件时添加按键修饰符:

<!-- 只有在 `keyCode` 是 13 时调用 `vm.submit()` -->
<input v-on:keyup.13="submit">

记住所有的 keyCode 比较困难,所以 Vue 为最常用的按键提供了别名:

<input v-on:keyup.enter="submit">
<!-- 缩写语法 -->
<input @keyup.enter="submit">

全部的按键别名:

.enter
.tab
.delete (捕获“删除”和“退格”键)
.esc
.space
.up
.down
.left
.right

可以通过全局 config.keyCodes 对象自定义按键修饰符别名:

// 可以使用 `v-on:keyup.f1`
Vue.config.keyCodes.f1 = 112

鼠标按钮修饰符:2.2.0 新增

.left
.right
.middle

这些修饰符会限制处理函数仅响应特定的鼠标按钮。

4.6 vue更新数组时触发视图更新的方法

Vue 包含一组观察数组的变异方法,所以它们也将会触发视图更新。这些方法如下:

push()
pop()
shift()
unshift()
splice()
sort()
reverse()

替换数组:

例如:filter(), concat()和 slice() 。这些不会改变原始数组,但总是返回一个新数组。当使用这些非变异方法时,可以用新数组替换旧数组:

example1.items = example1.items.filter(function (item) {
    return item.message.match(/Foo/)
})

你可能认为这将导致 Vue 丢弃现有 DOM 并重新渲染整个列表。幸运的是,事实并非如此。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的、启发式的方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。

注意事项:

由于 JavaScript 的限制,Vue 不能检测以下变动的数组:

  1. 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

举个例子:

var vm = new Vue({
    data: {
        items: ['a', 'b', 'c']
    }
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的

为了解决第一类问题,以下两种方式都可以实现和vm.items[indexOfItem] = newValue 相同的效果,同时也将触发状态更新:

// Vue.set
Vue.set(vm.items, indexOfItem, newValue)

// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)

你也可以使用vm.$set实例方法,该方法是全局方法 Vue.set 的一个别名:

vm.$set(vm.items, indexOfItem, newValue)

为了解决第二类问题,你可以使用 splice:

vm.items.splice(newLength)

4.7 vue中对象更改检测的注意事项

由于 JavaScript 的限制,Vue 不能检测对象属性的添加或删除:

var vm = new Vue({
    data: {
        a: 1
    }
})
// `vm.a` 现在是响应式的

vm.b = 2
// `vm.b` 不是响应式的

对于已经创建的实例,Vue 不能动态添加根级别的响应式属性。但是,可以使用 Vue.set(object,key, value)方法向嵌套对象添加响应式属性。例如,对于:

var vm = new Vue({
    data: {
        userProfile: {
            name: 'Anika'
        }
    }
})

你可以添加一个新的 age 属性到嵌套的 userProfile对象:

Vue.set(vm.userProfile, 'age', 27)

你还可以使用 vm.$set实例方法,它只是全局Vue.set 的别名:

vm.$set(vm.userProfile, 'age', 27)

有时你可能需要为已有对象赋予多个新属性,比如使用 Object.assign()或 _.extend()。在这种情况下,你应该用两个对象的属性创建一个新的对象。所以,如果你想添加新的响应式属性,不要像这样:

Object.assign(vm.userProfile, {
    age: 27,
    favoriteColor: 'Vue Green'
})

应该这样做:

vm.userProfile = Object.assign({}, vm.userProfile, {
    age: 27,
    favoriteColor: 'Vue Green'
})

4.8 如何解决非工程化项目,网速慢时初始化页面闪动问题?

使用 v-cloak指令,v-cloak不需要表达式,它会在Vue实例结束编译时从绑定的HTML元素上移除,经常和CSS的display:none配合使用。

<div id="app" v-cloak>
    {{message}}
</div>
<script>
var app = new Vue({
    el:"#app",
    data:{
        message:"这是一段文本"
    }
})
</script>

这时虽然已经加了指令v-cloak,但其实并没有起到任何作用,当网速较慢、Vue.js 文件还没加载完时,在页面上会显示{{message}}的字样,直到Vue创建实例、编译模版时,DOM才会被替换,所以这个过程屏幕是有闪动的。只要加一句CSS就可以解决这个问题了:

<!--属性选择器-->
[v-cloak]{
    display:none;
}

在一般情况下,v-cloak是一个解决初始化慢导致页面闪动的最佳实践,对于简单的项目很实用。

4.9 v-for产生的列表,如何实现active样式的切换?

通过设置当前 currentIndex + 动态样式实现:

<template>
    <div class="toggleClassWrap">
        <ul>
            <li @click="currentIndex = index" v-bind:class="
            {clicked: index === currentIndex}" v-for="(item, index) in desc"
            :key="index">
                <a href="javascript:;">{{item.ctrlValue}}</a>
            </li>
        </ul>
    </div>
</template>

<script type="text/javascript">
export default{
    data () {
        return {
            desc:[{
                ctrlValue:"test1"
                },{
                ctrlValue:"test2"
                },{
                ctrlValue:"test3"
                },{
                ctrlValue:"test4"
            }],
        
            currentIndex:0
        }
    }
}
</script>
<style type="text/css" lang="less">
.toggleClassWrap{
    .clicked{
        color:red;
    }
}
</style>

4.10 v-model语法糖的使用

使用v-model来进行双向数据绑定的时:

<input v-model="something">

仅仅是一个语法糖:

<input v-bind:value="something" v-on:input="something =
$event.target.value">

所以要组件的v-model生效,它必须:

  • 接受一个value属性
  • 在有新的value时触发input事件

五、Vue.js核心知识点 高频试题三

5.1 vue-cli工作中如何自定义一个过滤器?

在src目录下创建一个filter目录,在创建一个index.js文件,将过滤器们放在filter/index.js中

这个文件主要是写了过滤器实现的方法,然后export进行导出。

function filterOne(n){
    return n + 10;
}
function filterTwo(n){
    return n + 5;
}
export{
    filterOne,
    filterTwo
}

之后在main.js中:

import * as filters from './filter/filter.js'

//遍历所有导出的过滤器并添加到全局过滤器
Object.keys(filters).forEach((key) => {
    Vue.filter(key, filters[key]);
})

在.vue组件中使用:

{{test | filterOne}}

5.2 vue-cli工作中如何自定义一个指令

1. 什么是vue.js中的自定义指令

在Vue.js中,自定义指令(custom directives)允许你直接操作DOM,可以用于添加特定的行为或者响应用户交互。

2. 自定义指令的几个钩子函数

在Vue.js中,自定义指令(custom directives)可以包含一些生命周期钩子函数,用于在指令绑定(bind)到元素、更新(update)元素时执行特定逻辑。以下是自定义指令的生命周期钩子函数:

  1. bind

    • 在指令第一次绑定到元素时调用。只调用一次,用于初始化操作。
    • 接收参数:el - 指令所绑定的元素,binding - 一个对象,包含了指令的信息。
    bind(el, binding) {
      // 指令绑定到元素时执行的逻辑
    }
    
  2. inserted

    • 当被绑定元素插入到DOM中时调用。如果指令作用于一个组件的根节点,该钩子将在组件的 mounted 钩子之后调用。
    • 接收参数:el - 指令所绑定的元素,binding - 一个对象,包含了指令的信息。
    inserted(el, binding) {
      // 元素插入到DOM时执行的逻辑
    }
    
  3. update

    • 在被绑定元素所在的模板更新时调用,而不论指令的值是否发生变化。但是,参数包含了指令的当前值和之前的值。
    • 接收参数:el - 指令所绑定的元素,binding - 一个对象,包含了指令的信息,vnode - Vue 编译生成的虚拟节点。
    update(el, binding, vnode) {
      // 指令所在组件的 VNode 更新时执行的逻辑
    }
    
  4. componentUpdated

    • 在被绑定元素所在模板完成一次更新周期时调用。如果指令作用于一个组件的根节点,该钩子将在组件的 updated 钩子之后调用。
    • 接收参数:el - 指令所绑定的元素,binding - 一个对象,包含了指令的信息,vnode - Vue 编译生成的虚拟节点。
    componentUpdated(el, binding, vnode) {
      // 指令所在组件的 VNode 及其子 VNode 全部更新后执行的逻辑
    }
    
  5. unbind

    • 只调用一次,在指令与元素解绑时调用。在这里可以进行清理操作。
    • 接收参数:el - 指令所绑定的元素。
    unbind(el) {
      // 指令与元素解绑时执行的逻辑
    }
    

这些生命周期钩子函数允许你在自定义指令的不同生命周期阶段执行特定的逻辑。你可以根据需求选择合适的钩子函数来实现你的自定义指令逻辑。

3. 钩子函数的参数

除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。

指令钩子函数会被传入以下参数:

  • el:指令所绑定的元素,可以用来直接操作 DOM 。
  • binding:一个对象,包含以下属性:
    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
    • oldValue:指令绑定的前一个值,仅在update和 componentUpdated钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
    • arg:传给指令的参数,可选。例如 v-my-directive:foo中,参数为 "foo"。
    • modiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为{ foo: true, bar: true }。
  • vnode:Vue 编译生成的虚拟节点。
  • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

4. 如何在vue-cli中使用自定义指令

文件结构:

├── src
│ ├── directives
│ │ ├── index.js
│ │ ├── modules
│ └── main.js
└── ...

在modules下新建foucs.js下

// 聚焦指令
export default {
    bind (el, binding, vnode) {},
    inserted (el, binding, vnode) {
        el.focus()
    },
    update (el, binding, vnode) {},
    componentUpdated (el, binding, vnode) {},
    unbind (el, binding, vnode) {}
    }

在src/directives/index.js下

import focus from './modules/focus'
export {focus}

在src/main.js下,使用directives自定义指令

//引入自定义指令
import * as directives from './directives'
//注册指令(全局注册)
Object.keys(directives).forEach(k => Vue.directive(k, directives[k]));

在.vue组件中使用

<input v-focus type="text" />

5.3 vue等单页面应用及其优缺点

单页面应用(SPA)是一种在加载页面后,通过JavaScript、AJAX等技术在同一个页面内切换内容的Web应用程序。与传统的多页面应用程序(MPA)不同,SPA不会在用户操作时进行整页的刷新,而是动态地更新页面的局部内容,提供更加流畅的用户体验。下面是单页面应用的优缺点:

优点:

  1. 更快的用户体验:由于SPA只需要加载一次页面,并在后续的操作中更新局部内容,因此用户在浏览应用时感受到更快的页面加载速度,减少了页面切换的等待时间。

  2. 减轻服务器负担:SPA通过AJAX等技术获取数据,减少了对服务器的请求次数,减轻了服务器的负担。一旦页面加载完成,后续的数据获取通常是异步的,不需要刷新整个页面,提高了服务器的性能。

  3. 更接近原生应用:SPA提供了类似原生应用的交互体验,通过前端路由实现页面切换,用户可以在不重新加载整个页面的情况下浏览应用。

  4. 良好的前后端分离:SPA通常采用前后端分离的架构,前端负责页面渲染和用户交互,后端负责数据接口的提供,提高了代码的可维护性和可扩展性。

缺点:

  1. 首屏加载较慢:由于SPA需要在首次加载时下载所有必需的HTML、CSS、JavaScript文件,以及可能的数据,因此首屏加载时间可能会较长,特别是在网络条件较差的情况下。

  2. SEO优化难度较大:由于搜索引擎爬虫通常需要直接从HTML文件中获取页面内容,而SPA在首屏加载时通常只包含基础的HTML骨架,页面内容大多依赖于JavaScript的异步加载,这使得搜索引擎难以获取到完整的页面内容,对SEO优化造成挑战。

  3. 前端路由管理复杂:SPA通常使用前端路由来管理页面切换,这要求前端开发者具备良好的路由管理能力,对路由的处理和状态管理较为复杂,特别是在大型应用中。

  4. 内存占用较大:在SPA中,一旦页面加载完成,所有的页面内容和状态都保存在内存中,如果页面过于复杂或用户长时间停留在页面上,可能导致内存占用较大,影响设备性能。

综上所述,SPA适合需要提供良好用户体验、对前后端分离要求较高、交互复杂的应用场景。但在一些对SEO友好、首屏加载速度要求较高、不需要复杂交互的应用场景,传统的多页面应用可能更为适合。在选择SPA或MPA时,需要根据具体项目需求权衡它们的优劣势。

5.4 什么是vue的计算属性(computed)和侦听属性(watch)

Vue.js中的计算属性(computed properties)和侦听属性(watchers)是两种用于响应数据变化的技术。

计算属性(Computed Properties):

计算属性是指在Vue实例中声明的属性,它的值是由其他属性计算得出的。计算属性的值会根据依赖的属性的变化而自动更新,只有在依赖属性发生变化时,计算属性的值才会重新计算。计算属性可以帮助你将模板中的逻辑抽离出来,使得模板更加简洁和易读。

new Vue({
  data: {
    radius: 5
  },
  computed: {
    area: function() {
      return Math.PI * this.radius * this.radius;
    }
  }
});

在上面的例子中,area 是一个计算属性,它依赖于 radius。当 radius 发生变化时,area 的值会自动更新。

侦听属性(Watchers):

侦听属性是指在Vue实例中声明的一个对象,它包含了一个或多个属性的名称,每个属性都有一个对应的处理函数。当依赖的属性发生变化时,侦听属性中对应的处理函数就会被调用。侦听属性常用于需要在数据变化时执行异步或开销较大的操作。

new Vue({
  data: {
    radius: 5
  },
  watch: {
    radius: function(newRadius, oldRadius) {
      // 当 radius 发生变化时,执行处理函数
      console.log('New Radius: ' + newRadius + ', Old Radius: ' + oldRadius);
    }
  }
});

在上面的例子中,radius 是一个侦听属性,当 radius 发生变化时,侦听函数会被触发,可以在函数内执行相应的操作。

总的来说,计算属性用于派生出一个新的属性,而侦听属性则用于在数据变化时执行特定的逻辑。你可以根据具体的需求选择使用哪种方式来响应数据的变化。

计算属性VS普通属性:

可以像绑定普通属性一样在模板中绑定计算属性,在定义上的区别:计算属性的属性值必须是一个函数,函数必须有返回值,属性值就是函数的返回值

计算属性VS方法:

  1. 计算属性必须返回结果
  2. 计算属性是基于它的依赖缓存的。一个计算属性所依赖的数据发生变化时,它才会重新取值。
  3. 使用计算属性还是methods取决于是否需要缓存,当遍历大数组和做大量计算时,应当使用计算属性,除非你不希望得到缓存。
  4. 计算属性是根据依赖自动执行的,methods需要事件调用。

两者最主要的区别:computed 是可以缓存的,methods 不能缓存;只要相关依赖没有改变,多次访问计算属性得到的值是之前缓存的计算结果,不会多次执行。

网上有种说法就是方法可以传参,而计算属性不能,其实并不准确,计算属性可以通过闭包来实现传参:

:data="closure(item, itemName, blablaParams)"
computed: {
    closure () {
        return function (a, b, c) {
            /** do something */
            return data
        }
    }
}

两者之间的对比

  • watch:监测的是属性值,只要属性值发生变化,其都会触发执行回调函数来执行一系列操作。

  • computed:监测的是依赖值,依赖值不变的情况下其会直接读取缓存进行复用,变化的情况下才会重新计算。

  • computed:一个数据受多个数据影响

  • watch:一个数据影响多个数据

除此之外,有点很重要的区别是:计算属性不能执行异步任务,计算属性必须同步执行。也就是说计算属性不能向服务器请求或者执行异步任务。如果遇到异步任务,就交给侦听属性。watch也可以检测computed属性。

总结

  • 计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。

  • computed能做的,watch都能做,反之则不行。

  • 能用computed的尽量用computed

5.5 vue-cli开发环境使用全局常量

1.如何在组件中使用全局常量

第一步,在 src 下新建 const 文件夹下 新建 const.js

.
├── src
│ ├── const
│ │ ├── const.js
│ │
│ └── main.js
└── ...

第二步,在 const.js 文件下,设置常量

export default {   
    install(Vue,options){
        Vue.prototype.global = {
            title:'全局',
            isBack: true,
            isAdd: false,
        };
    }
}

第三步,在 main.js 下全局引入:

import constant from './const/const.js'
Vue.use(constant);

第四步,即可在 .vue 组件中使用:

//通过js方式使用:
this.global.title
//或在 html 结构中使用
{{global.title}}

2.在JS中使用常量

第一步,在 src 下新建 const 文件夹下 新建 type.js

.
├── src
│ ├── const
│ │ ├── type.js
│ │
│ └── main.js
└── ...

第二步,在 type.js 文件下,设置常量

export const TEST_INCREMENT='TEST_INCREMENT'
export const TEST_DEREMENT='TEST_DEREMENT'

第三步,在其他 .js 文件下引入并使用:

//以对象的形式引入:
import * as types from '../types'
//使用:
types.TEST_INCREMENT

5.6 父组件如何异步获取动态数据之后传递给子组件

  • v-if:开始的时候让子组件隐藏,然后等数据返回的时候,让子组件显示。(不过这种方式存在问题,当接口返回数据很慢的时候子组件延迟很久显示,这并不符合逻辑)

  • vuex

    • 利用vuex的辅助函数(mapState,mapMutations)mapState是将state里面的数据映射到计算中(computed),mapMutations也是类似,把vuex中mutations的方法映射到组件里面,就可以在组件里面直接使用方法了,在vuex中使用异步(actions)去掉用接口,然后在接口成功的函数里面取触发同步(mutations)里面的方法,把得到数据传给mutations里面的方法里并且给state里面的属性赋值,然后就可以在子组件中使用computed计算中去获取数据并且渲染到页面上