一. $nextTick使用以及原理

A: 使用: 它可以在DOM更新完毕之后执行一个回调;
原理: 先来了解下,浏览器的机制宏任务微任务

宏任务(Macrotask):
setTimeout,setInterval ,script(整体代码块) ,MessageChannel
I/O,事件队列
微任务(Microtask) :MutationObserver(浏览器环境),Promise.[ then/catch/finally ], process.nextTick(Node环境)

1.1、$nextTick有什么用?

Vue是异步渲染的框架。
data改变之后,DOM不会立刻渲染,因为dom更新是异步方法中执行的nextTick。
连续多次的异步渲染,$nextTick只会执行最后一次渲染后的结果。

1.2、$nextTick的原理

代码执行步骤:

//data数据更新 => 生成新的虚拟dom =>  diff算法比较新旧虚拟dom => patch更新变化的虚拟dom到真实的dom上 =>  触发更新回调

<div class="submit-btn" ref="btn" @click="submit">下一步{{time}}</div>
js:
submit(){
	this.time = Math.random()*10;
    console.log(this.$refs.btn)
    console.log(this.$refs.btn.innerText)
    this.$nextTick(() => {
                console.log("dom更新了的1")
            })
    this.$nextTick(() => {
                console.log("dom更新了的2")
            })
	}
    

this.time = Math.random()*10,数据改变时,调用了watcher观察者中的update,update方法通过调用nextTick方法 ,把更新dom的方法加入到异步任务队列中。
自己页面上用的this. n e x t T i c k 方法,也会把回调方法加入到异步队列中。如果自己的页面多次调用 t h i s . nextTick方法,也会把回调方法加入到异步队列中。 如果自己的页面多次调用this. nextTick方法,也会把回调方法加入到异步队列中。如果自己的页面多次调用this.nextTick方法,也会加入到异步队列中。
加入到队列中的异步方法,会依次执行:DOM更新的回调—自己页面的this.$nextTick回调函数;

中间涉及到根据浏览器的环境去判断,这些回调方法,加入哪个异步方法中去执行?
nextTick并不是浏览器本身提供的一个异步API,而是Vue中,用过由浏览器本身提供的原生异步API封装而成的一个异步封装方法。
它对于浏览器异步API的选用规则如下,Promise存在取由Promise.then,不存在Promise则取MutationObserver,MutationObserver不存在setImmediate,setImmediate不存在最后取setTimeout来实现。
nextTick即有可能是微任务,也有可能是宏任务,从优先去Promise和MutationObserver可以看出nextTick优先微任务,其次是setImmediate和setTimeout宏任务。

同步代码执行完毕之后,优先执行微任务,其次才会执行宏任

1.3、循环调用的话nextTick里面有容错机制吗?

多次调用 nextTick 会将方法存入队列 callbacks 中,通过这个异步方法清空当前队列。

二. VUE3中watch与watchEffect

2.1、 watchEffect
立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

watchEffect的一些特点:

不需要手动传入依赖(不用指定监听对象)
无法获取原始值,只能获取更新后的值
立即执行(在onMounted前调用)
一些异步操作放里面更加的合适
watchEffect第一个参数是一个箭头函数(是一个副作用函数),里面用来获取侦听到的新值以及用来进行其它操作(比如清除副作用等)

import { watchEffect } from 'vue'
const count = ref(0)

watchEffect(() =>{
 	console.log(count.value)
 	// -> logs 0
})

setTimeout(() => {
  count.value++
  // -> logs 1
}, 1000)

进入页面,直接打印结果:1

watch监听的一些特点:
监听单一数据,监听多个数据,监听基本数据,监听对象属性;
watch监听一般数据:

  data(){
        return {
            title:'标题',
        }
    }, 
watch:{
        // 监听基本数据类型
        title(oldval, newval){
            console.log('oldval', oldval)
            console.log('newval', newval)
        },
}

watch监听对象:

<script>
export default {
    data(){
        return {
            obj:{
                a:'对象中的属性'
            }
        }
    },
    watch:{
        // 监听对象--这样子是监听不到的
        obj(oldval, newval){
             console.log('对象oldval', oldval)
            console.log('对象newval', newval)
        },
        // 深度监听方法1-- 使用handler,deep
        obj:{
            handler(newval){
            console.log('对象深度newval', newval)
            },
            deep: true,
        },
        // 深度监听方法2-- 直接取用对象的属性
        'obj.a' (val){
            console.log('val', val)
        }
    }
}
</script>

VUE3中watch与watchEffect —— 全网最详细系列

三. Ref reactive区别,底层怎么实现响应式的

reactive与ref对比
从定义数据角度对比:

  • ref用来定义:任意数据类型
  • reactive用来定义:对象(或数组)类型数据

如何选择 ref 和 reactive?

  • 基础类型值(String,Number,Boolean,Symbol) 或单值对象(类似{ count: 1 }这样只有一个属性值的对象) 使用 ref
  • 引用类型值(Object、Array)使用 reactive

从原理角度对比:

ref通过Object.defineProperty()的get和set来实现响应式(数据劫持)。

reactive通过使用Proxy来实现响应式(数据劫持),并通过Reflect操作源对象内部的数据

从使用角度对比:

  • ref定义的数据:访问或更改数据需要.value
  • reactive定义的数据:操作数据与读取数据均不需要.value。

toRef
针对一个响应式对象(reactive封装)的prop(属性)创建一个ref,且保持响应式

两者保持引用关系

语法:const 属性名= toRef(对象,‘属性名’)

toRefs
toRefs 是一种用于破坏响应式对象并将其所有属性转换为 ref 的实用方法

将响应式对象(reactive封装)转成普通对象

对象的每个属性(Prop)都是对应的ref

两者保持引用关系

语法:const 属性名= toRefs(对象,‘属性名’)

四. vue监听数组

Vue2为什么不能监测数组的变化
首先从表象上来看,Vue2对数组的响应式实现是有些不足的:

  • 无法监测数组的新增
  • 无法监测用索引改变数组的操作
    先来简单分析下为什么会存在上述问题:
    我们知道Vue2是通过Object.defineProperty方法来进行数据监测的。
    从上述结果可以看出,Object.defineProperty是可以对数组进行监测的,但是Vue2为什么没用呢,其实是出于性能的考虑,数据一般会被频繁的改动,每次的改动都需要遍历整个数组,给数组属性重新observe,这样会极大的消耗性能,因此在Vue2中hack了Array上的一些方法。

1、首先还是定义一组数据用于展示,hobbys 为字符串数组,friends 为对象数组

const vm = new Vue({
  el: '#root',
  data() {
    return {
      hobbys: ['抽烟', '喝酒', '烫头'],
      firends: [
        { name: 'al', age: 20 },
        { name: 'hj', age: 22 }
      ]
    }
  },
})

2、展示完了之后,我们在控制台上查看 vm.data 发现 两个数组中的值,并没有像对象中的值一样,绑定一个 get 和 set 方法。
在这里插入图片描述

这也就是意味着,如果我通过 vm._data.hobbys 去修改 hobbys 的第一个值的时候,Vue内的数据是会修改的,但是,因为没有 set 函数,无法监听到数据的改变,所以页面上是不会重新渲染修改后的属性值的。例如下图
在这里插入图片描述
Vue-数组变更方法
Vue 收集了我们平时操作数组的方法,发现 push、pop、shift、unshift,splice,sort、reserve这七个操作数组的方法,都会对原数组产生影响,反之像 map、filter、some、every等数组遍历的方法都只是会产生一个新的数组,并不会对原数组产生影响。

所以 Vue 将这7个方法进行了处理,并规定如果调用了这七个操作数组的方法,那我的 Vue 才会去监听数据的改变,如果是map等方法,那你原数据都没有改变,我也不用监听了。所以,当我想去操作数组中的属性的时候,我们不能直接 通过序号来操作,而是需要调用数组的这七个方法。
那么 Vue 包装之后的这些方法里面又是怎么做的来让Vue实现对数据监听的呢?我们还是以push为例,当调用 vm中数组原型上的 push 方法之后,分别作了如下操作

    1、调用原生的 push方法,改变数组数据

    2、生成新的虚拟DOM,新旧虚拟DOM对比,模板编译、页面重新渲染

总结:
1、Vue 监听数组改变,使用的是 数组的变更方法,包括:push、pop、shift、unshift、splice、sort、reserve,因为这七个方法操作数组之后,会改变原数组

2、这七个方法不是Array.prototype 上的方法,而是经过 Vue 的包装处理之后,挂载到Vue 的 Array.prototype 上的

3、同时也可以使用 set 方法,以此来改变当前数组,且Vue还能监听到,不过基本不这么使用

VUE3检测数组:
Vue3是用Proxy来进行对象、数组的代理,其实只要理解了Proxy就明白Vue3为什么会抛弃Object.defineProperty了:

Object.defineProperty只能劫持对象的属性,而Proxy是直接代理对象。
由于 Object.defineProperty 只能对属性进行劫持,需要遍历对象的每个属性,如果属性值也是对象,则需要深度遍历。而 Proxy 直接代理对象,不需要遍历操作。
Object.defineProperty对新增属性需要手动进行Observe。
由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持。
Proxy 具有更多的拦截支持,可以做的更精细化的控制

五. vue. s e t , v u e . set, vue. set,vue.delete

this.$set的使用

就是当data中包含声明且已赋值的对象或者数组(数组包对象)时,我们要向当前的这个对象中添加一个新的属性并且更新,结果发现并不会更新视图,

受 ES5 的限制,Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue.js 转换它,才能让它是响应的。

所以在data中注册的对象是响应式的,而后来添加的属性不是响应式的,只是个普通的属性,为了避免上面出现的问题,我们使用this.$set来向响应式对象中添加响应式的新属性

语法:this.$set(target,key,value)

<script>
export default {
 data() {
   return {
     student: {
       name: '张三',
     }
   }
 },
 methods: {
   setMessage() {
     this.$set(this.student, 'age', 15)
     console.log(this.student)
   }
 }
}
</script>

delete与$delete

var a=[1,2,3,4]
var b=[1,2,3,4]
delete a[1]
console.log(a) //134 length4 (变为undefined)
this.$delete(b,1)
console.log(b) //134 length3 (完成删除)
let obj={a:1,b:2}

六. Vue页面优化

在vue中,可以通过路由配个路由占位符来完成单页面应用的实现,其原理时通过对路由占位符的更新来完成单页面应用的实现。
单页面应用的优点在于页面的切换不会导致整个页面的刷新,而是对路由占位符的更新,比起传统的,单页面应用切换页面速度更快、用户体验更好,代码的样式及标准更好控制,程序员的工作量更少。缺点在于单页面的首屏加载速度较慢,SEO不友好。
1. 缩小项目体积:
原理:体积越小,加载越快。
方法:

通过webpack对项目体积进行压缩,开启gzip压缩文件
通过对css3、js文件的合并,如在两不同组件中,拥有相同的样式,可通过全局css文件中设置。在js文件上,将相同的方法封装合并成一个方法,如API请求。
减小图片体积,图标可通过矢量图来代替。

2. 减少加载模块:

原理:单页面应用的首屏加载较慢主要是因为单页面应用在首屏时,无论是否需要都会加载所有的模块,可通过按需加载、路由懒加载来优化。
方法:

按需加载,通过对路由文件的配置,来对相关模块划分区间,如登录界面可以和首页、主页面划分一块,在进入首屏时,只对首屏所在的区块进行加载。通过require.ensure()来将多个相同类的组件打包成一个文件。如示例代码,打包时,将两个组件打包成一个js文件,文件名为good。

{
     path: '/goodList',	//path路径	
     name: 'goodList',	//组件名
     component: r => require.ensure([], () => 	r(require('../components/goodList')), 'good')	//good类型的组件
},
{
     path: '/goodOrder',
     name: 'goodOrder',
     component: r => require.ensure([], () => r(require('../components/goodOrder')), 'good')//good类型的组件
}

动态加载,通过import来实现路由的动态加载,这个时候对于路由的加载是动态的,用到再加载。

{
     path: '/goodList',	//path路径	
     name: 'goodList',	//组件名
     component: () => import('../components/goodList')
},
{
     path: '/goodOrder',
     name: 'goodOrder',
     component: () => import('../components/goodOrder'),
}

3. 代码层面的优化
1 v-for 遍历为 item 添加 key
在列表数据进行遍历渲染时,需要为每一项 item 设置唯一 key 值,方便 Vue.js 内部机制精准找到该条列表数据。

2 v-for 遍历避免同时使用 v-if
v-for 比 v-if 优先级高,这意味着 v-if 将分别重复运行于每个 v-for 循环中,将会影响速度。建议替换成 computed 属性。

3 v-if 和 v-show 区分使用场景
v-if 是真正的条件渲染,也是惰性的:如果在初始渲染时条件为假,则什么也不做,直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多,不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display 属性进行样式切换。
v-if 适用于很少改变条件,不需要频繁切换的场景;v-show 则适用于需要非常频繁切换的场景。

4 computed 和 watch 区分使用场景
computed 计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch 更多的是观察的作用,每当监听的数据变化时都会执行相关回调。

当需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
当需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,watch 允许我们执行异步操作。
5 keep-alive
利用keep-alive包裹,将不活动的组件缓存起来。
在组件切换过程中将状态保留在内存中,防止重复渲染dom,减少加载时间及性能消耗,提高用户体验。

6 图片资源懒加载(使用插件)
对于图片过多的页面,为了加快页面加载速度,可以将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件。
四.Webpack 层面的优化

1 Webpack 对图片进行压缩
对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader 来压缩图片

2 模板预编译
打包时,直接把组件中的模板转换为render函数,这叫做模板预编译。这样一来,运行时就不再需要编译模板了,提高了运行效率。

五. 基础的 Web 技术优化化

1 浏览器缓存
为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的。

七. Vue2 vue3怎么父子传参

vue2: 父传子: props
子传父: $emit
vue3:
父传子:子组件依旧通过 props 来接收并且在模版中使用
setup函数接收props作为其第一个参数,props对象是响应式的(单向的父—>子),watchEffect或watch会观察和响应props的更新。不要对props对象进行解构,那样会失去响应性。

// 子组件
<template>
  <div class="children">
    <!-- 3、在子组件模版中使用 -->
    {{sonData}}
  </div>
</template>
<script>
export default {
name:"Children",
// 2、子组件通过 props 来接收
  props:["sonData"],
  setup(props){
    function childrenbut(){
      // props.sonData  可以访问到父组件传递的数据
      console.log(props.sonData);
    }
    return {
      childrenbut
    }
  }
}
</script>

子传父:
1、子组件触发事件通过 setup 的第二个参数 context.emit 来实现子传父

context 上下文对象或者context 也可以使用结构的形式这样写{emit, attrs, slots}

// 子组件
<template>
  <div class="children">
    {{sonData}}
    <!-- 1、触发事件 -->
    <button @click="childrenbut">子组件按钮</button>
  </div>
</template>
<script>
export default {
name:"Children",
  setup(props,context){
    function childrenbut(){
      console.log(context);
      // 2、通过context.emit 实现子传父
      // 第一个参数为 方法名,第二个参数为 需要传递的数据
      context.emit("change",'world')
    }
    return {
      childrenbut,
    }
  }
}
</script>

或者这样写:

// 子组件
<script>
export default {
name:"Children",
  // 通过结构 之后直接写 emit {emit}
  setup(props,{emit}){
    function childrenbut(){
      // 省去 context.emit
      emit("change",'world')
    }
    return {
      childrenbut,
    }
  }
}
</script>

兄弟间传参:bus总线传值的总路线及方法

//bus.js 
import Vue from 'vue';
export default new Vue;
//使用 兄弟A 传值
import bus from '路径'
bus.$emit('自定义事件名称sendMsg',输出数据)
//使用 兄弟B 接受传过来的值,res
import bus from '路径'
bus.$on('自定义事件名sendMsg',(res)=>{console.log(res})

看代码:
在项目中创建一个单独的eventBus.js文件

// 暴露一个vue实例
import Vue from 'Vue'
export default new Vue;

创建相关的组件: 父组件index_05.vue,两个兄弟组件one.vue, two.vue.
父组件index_05.vue代码:

<template>
    <div class="wrapper">
        <h2>bus传值</h2>
        <one></one>
        <two></two>
    </div>
</template>
<script>
import one from './one'
import two from './two'
export default {
    name:'index_05',
    components:{
        one,
        two,
    }
    
}
</script>

子组件one.vue 传点击事件给two.vue

<template>
    <div class="one">
        <button @click="sendMessage">点我减---{{message}}</button>

    </div>
</template>
<script>
import bus from '../assets/js/eventBus'
export default {
    data(){
        return{
            message:10
        }
    },
    methods:{
        sendMessage(){
            --this.message;
            bus.$emit('sendByBus',this.message)
        }
    }
    
}
</script>

two.vue

<template>
    <button>{{message}}</button>
</template>
<script>
import bus from '../assets/js/eventBus'
export default {
     data(){
        return{
            message:''
        }
    },
    mounted(){
        bus.$on('sendByBus', data =>{
            console.log('data--', data)
            this.message = data
            console.log('this.message--',this.message)
        })
    },
}
</script>

八. VUE2与VUE3自定义指令

Vue中内置了很多的指令,如v-model、v-show、v-html等,但是有时候这些指令并不能满足我们,或者说我们想为元素附加一些特别的功能,这时候,我们就需要用到vue中一个很强大的功能了—自定义指令。

在开始之前,我们需要明确一点,自定义指令解决的问题或者说使用场景是对普通 DOM 元素进行底层操作,所以我们不能盲目的胡乱的使用自定义指令。

VUE2自定义指令:
自定义指令的 钩子函数
上面我们也介绍了,自定义指令一共有5个钩子函数,他们分别是: bind、inserted、update、componentUpdate和unbind

一个指令定义对象可以提供如下几个钩子函数 (均为可选):

bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。。

componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

unbind:只调用一次,指令与元素解绑时调用。

指令钩子函数会被传入以下参数:
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”。

modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
vnode: Vue编译生成的虚拟节点。移步 VNode API 来了解更多详情。
oldVnode: 上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

介绍下inserted的使用:
directives/indx.js文件:

//改变文字颜色指令
export const fontColor={
	//dom元素节点 options包含传入的属性对象
	inserted(dom,options){
        console.log(dom);  // 获取到span元素<span v-fontColor="color" v-fontSize class="name">改变文字颜色和大小</span>
		console.log(options); 
        dom.style.background=options.value
	}
}
// 改变文字大小指令
export const fontSize = {
  inserted(dom) {
    dom.style.fontSize = '24px'
  }
}
// 改变innerHtml
export const innerText = {
    inserted(dom) {
      dom.innerText = '我成功了没'
    }
  }


main.js文件全局注册:

import * as directives from '@/directives' //import * as 变量  得到的是一个对象{ 变量1:对象1,变量2: 对象2 }
// Object.keys 方法返回一个对象键名的数组
Object.keys(directives).forEach(key => {
  // 注册自定义指令
  Vue.directive(key, directives[key])
})

directive.vue文件使用:

<template>
  <div class="box">
    <!-- 自定义指令
1.在src/directives/index.js文件中定义几个指令
2.在main.js中完成全局注册
3. 当前页面使用
 -->
    <span v-fontColor="color" v-fontSize v-innerText class="name"
      >改变文字颜色和大小</span>
  </div>
</template>
<script>
export default {
  data() {
    return {
      n: 1,
      color: 'yellow',
    }
  },
</script>



VUE3的自定义指令
VUE3指令的钩子函数发生了变化:
一个指令定义的对象,Vue提供了如下的几个钩子函数

created:在绑定元素的 attribute 或事件监听器被应用之前调用;
beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;
mounted:在绑定元素的父组件被挂载后调用;
beforeUpdate:在更新包含组件的 VNode 之前调用;
updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用;
beforeUnmount:在卸载绑定元素的父组件之前调用;
unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次;
自定义一个 v-focus 的全局指令:

 app.directive("focus", {
   mounted(el, bindings, vnode, preVnode) {
     console.log("focus mounted");
     el.focus();
   }
 })

可以掌握下vue原生默认实现focus的方法:

<template>
  <div>
    <input type="text" ref="input" />
  </div>
</template>

<script>
import { ref, onMounted } from "vue";

export default {
  setup () {
    const input = ref(null);

    onMounted(() => {
      input.value.focus();
    })

    return {
      input
    }
  }
}
</script>

<style scoped>
</style>

九. Mixin的使用以及哪些不足?

mixin: 把多个组件共同的配置,抽取出来成一个混入对象;
看代码:
mixinx/mixin.js

let mixin = {
  created() {
    console.log("我是mixin里面的created!")
  },
  methods: {
    hello() {
      console.log("hello from mixin!")
    }
  }
}
 
export default mixin

home.vue文件中使用:

<template>
  <div class="home">
    <span>This is a Home page</span>
  </div>
</template>
 
<script>
import myMixins from "../mixins/mixin.js";   //导入混入(mixin)
export default {
  name: 'Home',
  mixins: [myMixins]     //使用混入(mixin)
}
</script>

真实项目中使用:是因为这个接口好多个页面使用,所以做了mixins处理:
dictData.js:

**在这里插入代码片let dictData = {
    data() {
        return {
            dictStatusList:[],//状态字典列表
        }
    },
    methods: {
        getDictData(dictType) {
            this.$API.system.dict.data.list.post({
                dictType
            }).then(res => {
                if(res.code == "2000") {
                    this[`${dictType}List`] = res.data;
                }
            })
        },
    },
};
export default dictData;

home.vue文件使用:

import dictData from "@/mixins/dictData";
export default {
    mixins:[dictData],
      created() {
        //获取状态字典
        this.getDictData("dictStatus");
        //获取是否字典
        this.getDictData("dictYesNo");
    },
}

比较需要注意的是mixin文件中定义字段/方法/钩子函数与引入到组件相应方法/字段/钩子函数的优先级顺序:

1.对于created,mounted 等生命周期函数 mixin文件中的代码先执行,组件中的后执行。
2.对于data中定义的字段,组件中定义组件覆盖mixin中同名字段。
3.对于 method中的同名方法,组件内的同名方法覆盖mixin中的方法。

十. vue2 vue3的区别以及性能优化有哪些?

1、监测机制的改变:用Proxy替换Object.defineProperty

Proxy 是什么
Proxy为 构造函数,用来生成 Proxy实例;

var proxy = new Proxy(target, handler)
//target 表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数)
// handler通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为

关于handler拦截属性,有如下13种

1、get(对象属性的读取)、
2、set(对象属性的设置)、
3、apply(对函数的调用call、apply操作进行拦截)、
4、has( 用于拦截hasProperty操作,返回一个布尔值)、
5、deleteProperty (拦截delete操作,返回一个布尔值)
6、defineProperty ( 拦截Object.defineProperty操作)
7、getOwnPropertyDescriptor (用于拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined)
8、getPrototypeOf (用于拦截获取对象的原型)
9、isExtensible (拦截Object.isExtensible()操作)
10、ownKeys (用于拦截对象自身属性的读取操作,比如:Object.keys ()、Object.getOwnPropertySymbols ()、Object.getOwnPropertyNames ())
11、preventExtensions (该方法拦截 Object.preventExtensions () ,该方法必须返回个布尔值)
12、setPrototypeOf ( Object.setPrototypeOf 方法 )
13、construct (拦截 new 操作符命令)

Vue3.0 里为什么要用 Proxy API 替代 defineProperty API?
1、defineProperty API 的局限性,就是它只能针对单个属性做监听。
Vue2.x 中的响应式实现正是基于 defineProperty ,对 data 中的属性做了遍 历 + 递归,为每个属性设置了 getter、setter。 这也就是为什么 Vue 只能对 data 中预定义过的属性做出响应的原因。

defineProperty的局限性
对于数组来说,通过下标的方式直接修改属性的值,或者添加一个预先不存在的属性,是无法监听到数据变化的。
对于对象来说,只能劫持对象的属性,从而需要对每个对象的属性进行遍历,如果,属性值是对象,还需要深度遍历。
这就是 defineProperty 的局限性

2、Proxy API 的监听是针对一个对象的,那么对这个对象的所有操作都会进入监听操作,这就完全可以代理所有属性,将会带来很大的性能提升和代码的优化。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须 先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

总结:
在 Vue.js 2.x 中,对于一个深层属性嵌套的对象,要劫持它内部深层次的变化,就需要 递归遍历这个对象,执行 Object.defineProperty 把每一层对象数据都变成响应式的,这 无疑会有很大的性能消耗。
在 Vue.js 3.0 中,使用 Proxy API 并不能监听到对象内部深层次的属性变化,因此它的 处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部属性才会变成响 应式,简单的可以说是按需实现响应式,减少性能消耗。

Proxy 区别于 Object.definedProperty?
是否可以监听属性的读写、删除、方法的调用?
Object.defineProperty 只能监听到属性的读写,而 Proxy 除读写外,还可以监听属性的删除,方法的调用等。

是否可以监听数组、对象的变化?
Object.defineProperty 无法监测数组、对象的变化。而 Proxy 可以直接监视数组、对象的变化

大白话总结:
Object.defineProperty 监听对象,使用的遍历+递归处理子集对象中的对象;
Proxy可以直接监听对象,提升性能;
Object.defineProperty 无法监测数组、对象的变化。
Proxy 可以直接监视数组、对象的变化。

2、作用域插槽方面

模板方面没有大的变更,只改了作用域插槽,
2.x 的机制导致作用域插槽变了,父组件也会重新渲染,
3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。

3、对象式的组件声明方式

Vue2.x 中的组件是通过声明的方式传入一系列 option
Vue3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易

4、Vue3的虚拟dom

传统Vdom的缺点

虽然∨ue能够保证进行最小化的更新,节约浏览器的渲染性能,但却新增了新旧Vdom对比的性能消耗。因此, vdom的大小就直接影响了对比的快慢,而vdom中有一部分是静态标签,不会改变,但在对比时,仍会进行遍历,因此就会增加性能的消耗。 所以在vue3.

5、非兼容变更

1、模板指令
组件上 v-model 用法已更改
和 非 v-for节点上key用法已更改
在同一元素上使用的 v-if 和 v-for 优先级已更改
v-bind=“object” 现在排序敏感
v-for 中的 ref 不再注册 ref 数组
2、组件
只能使用普通函数创建功能组件
异步组件现在需要 defineAsyncComponent 方法来创建

十一、 keep-alive 的作用是什么;

作用:
在组件切换过程中将状态保留在内存中,防止重复渲染DOM,减少加载时间及性能消耗,提高用户体验性。

原理:
在 created 函数调用时将需要缓存的 VNode 节点保存在 this.cache 中/在 render(页面渲染) 时,如果 VNode 的 name 符合缓存条件(可以用 include 以及 exclude 控制),则会从 this.cache 中取出之前缓存的 VNode 实例进行渲染。

include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
max - 数字。最多可以缓存多少组件实例。

生命周期函数

**activated:**在 keep-alive 组件激活时调用
该钩子函数在服务器端渲染期间不被调用

deactivated: 在 keep-alive 组件停用时调用
该钩子在服务器端渲染期间不被调用

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

使用 keep-alive 会将数据保留在内存中,如果要在每次进入页面的时候获取最新的数据,需要在 activated 阶段获取数据,承担原来 created 钩子函数中获取数据的任务。

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

<router-view v-slot="{ Component }">
 <keep-alive :include="this.$store.state.keepAlive.keepLiveRoute">
	<component :is="Component" :key="$route.fullPath" v-if="$store.state.keepAlive.routeShow"/>
 </keep-alive>
</router-view>

十二. hash和history:

两种路由模式原因— 对于 Vue 这类渐进式前端开发框架,
为了构建 SPA(单页面应用),
需要引入前端路由系统,
这也就是 Vue-Router 存在的意义。
改变视图但不重复请求页面是前端路由的核心。
因此有两种路由:hash 模式和 history 模式,
这两种都属于浏览器自身的特性,Vue-Router 只是利用了这两个特性
(通过调用浏览器提供的接口)来实现前端路由。
1】. vue-router(前端路由)有两种模式,hash模式和history模式
1.hash 就是指 url 后面的 # 号以及后面的字符,history没有带#,外观上比hash 模式好看些
2.原理的区别(原理)
3. hash 能兼容到IE8, history 只能兼容到 IE10;
4.由于 hash 值变化不会导致浏览器向服务器发出请求,而且 hash 改变会触发 hashchange 事件(hashchange只能改变 # 后面的url片段);虽然hash路径出现在URL中,但是不会出现在HTTP请求中,对后端完全没有影响,因此改变hash值不会重新加载页面,基本都是使用 hash 来实现前端路由的。
2】原理:
1.hash通过监听浏览器的onhashchange()事件变化,查找对应的路由规则

2.history原理: 利用H5的 history中新增的两个API pushState() 和 replaceState() 和一个事件onpopstate监听URL变化

history模式URL就要和后端进行一致,所以要改为history也需要后端的配合,否则会报错。
所以hash模式在每次刷新页面时是直接更改“#”后的东西,history每次刷新会重新像后端请求整个网址,也就是重新请求服务器。如果后端没有及时响应,就会报错404!。history的好处是可以进行修改历史记录,并且不会立刻像后端发起请求。不过如果对于项目没有硬性标准要求,我们可以直接使用hash模式开发。

十三. 路由守卫

Vue-Router路由钩子函数(导航守卫)
路由钩子函数有三种:
1:全局钩子: beforeEach、 afterEach
2:单个路由里面的钩子: beforeEnter、 beforeLeave
3:组件路由:beforeRouteEnter、 beforeRouteUpdate、 beforeRouteLeave。
全局守卫
router.beforeEach() 进入之前触发

router.afterEach() 进入之后触发
每个守卫方法接收三个参数:

①to: Route: 即将要进入的目标路由对象(to是一个对象,是将要进入的路由对象,可以用to.path调用路由对象中的属性)

②from: Route: 当前导航正要离开的路由

③next: Function: 这是一个必须需要调用的方法,执行效果依赖 next 方法的调用参数。

十四. 跨域:proxy

vue中使用proxy代理的方法处理跨域:


  devServer: {
    proxy:{
      "/api": {
          target: "http://www.xiongmaoyouxuan.com", // 需要代理的域名
           ws: false, // 是否启用websockets
          changeOrigin: true, //开启代理:在本地会创建一个虚拟服务端,然后发送请求的数据,并同时接收请求							的数据,这样服务端和服务端进行数据的交互就不会有跨域问题
          pathRewrite: {  //重写匹配的字段,如果不需要在请求路径上,重写为""
              "^/api": "/api"
          }
      },
   
  },

其他方法: Jsonp, cors

十五、虚拟DOM循环算法

diff算法就是用于比较新旧两个虚拟dom之间差异的一种算法。

下面我们总结下虚拟dom生成的过程。

首先,代码初次运行,会走生命周期,当生命周期走到created到beforeMount之间的时候,会编译template模板成render函数。然后当render函数运行时,h函数被调用,而h函数内调用了vnode函数生成虚拟dom,并返回生成结果。故虚拟dom首次生成。
之后,当数据发生变化时会重新编译生成一个新vdom。再后面就等待新 旧两个vdom进行对比吧。我们后面就继续说对比的事情。

Diff 比较规则

1、diff 比较两个虚拟dom只会在同层级之间进行比较,不会跨层级进行比较。而用来判断是否是同层级的标准就是

是否在同一层
是否有相同的父级
下面,我们来一张图,就很好理解了(盗用网上一张很经典的图)
2、diff是采用先序深度优先遍历得方式进行节点比较的,即,当比较某个节点时,如果该节点存在子节点,那么会优先比较他的子节点,直到所有子节点全部比较完成,才会开始去比较改节点的下一个同层级节点。

注意:
首先开始比较两个vdom时,这两个vdom肯定是都有各自的根节点的,且根节点必定是一个元素,不可能存在多个。我们首先要比较的肯定是根节点,那我们都知道根节点只有一个,就可以直接比较了

十六. 打包:打包loader plug-in,热加载

webpack打包:
Entry: 入口文件,默认: ./src/index.js
Output: 出口文件,打包后生成的文件,默认dist
Loader: 转换器, saa/scss----> css, ts—>js, img-loader图片打包
plugins: 扩展插件: 打包,压缩,优化
clean-Webpack-plugin: 清空之前的dist文件

webpack打包的原理:
入口文件,分析依赖,loader,plugin处理,出口文件;
按需加载:import
打包过程中webpack通过code spliting(代码分割)功能将文件分为多个chunks,将重复的部分单独提取出来作为commonchunks, 从而实现按需加载。 将所有的依赖包打包成一个bundle.js文件,通过代码进行按需加载。

vue中使用vue-cli,在vue.config.js文件做配置:
文件压缩: CompressWebpackPlugin
清除文件:cleanWebpackPlugin
splitchunk(包分离):node-modules打包成一个文件

热加载
使用vue的热加载可以实现局部内容的刷新,更改数据后,不会刷新整个页面。
在vur.config.js文件配置:

module.exports = {
    devServer : {
        hot:true,
        open : true,
        port : 8080,
        host : "127.0.0.1"
    }
}
注:hot:true为热更新选项,如不需要,删掉即可。

打包多页面:
在入口Entry中配置:

module.exports = {
    entry: {    // 指定入口文件
        'home': "./src/index.js",   
        'other': "./src/other.js"
    }, 
    output: { // 配置文件默认是dist目录下的main.js文件
        filename: "[name].js", // 出口文件的文件名
        path: path.resolve(__dirname, "build"), //出口文件的路径,注意一定要是绝对路径
    },
}

通过上面的代码,我们可以看到,entry 属性的值由字符串变为了对象。在该对象中,我们配置了两个属性,这就表明,我们准备使用webpack来构建 两个 单页面应用,分别是 home 应用和 other 应用。他们都有一个对应的入口文件,webpack在打包编译的时候,就是从他们的入口文件触发,分别构建对应的单页面应用。

同时我们还修改了 output.filename 这个属性,不再是固定的 index.js,而是 [name].js 。这样就可以通过 entry对象 的两个属性(即home和other)来动态生成对应的 .js 文件。

现在你可以在 src文件夹 下创建两个入口文件 index.js 和 other.js ,在里面写对应的代码,然后执行webpack的 build 命令,就可以进行打包编译,最终会生成两个文件,即 index.js 和 other.js ,到这一步多页面应用的 .js 文件已经构建完成了。

如何在 .html 文件中,引入指定的 .js 文件
现在多页面应用的 .js 文件已经构建完成,但是我们还需要在 .html 文件中引入 .js 文件,多页面应用才算是正式完成,所以这一节我们介绍如何在 .html 文件中引入 .js 文件。

十七. 打包:get, post, option请求的区别

http/1.1协议中共定义了八种请求方式来表明requeset-url不同的操作,分别是get、post、put、delete、options、trace、head、connect。

Get
get请求是用来获取数据的,只是用来查询数据,不对服务器的数据做任何的修改,新增,删除等操作。

特点:

get请求会把请求的参数附加在URL后面,这样会产生安全问题,如果是系统的登陆接口采用的get请求,需要对请求的参数做一个加密。

get请求其实本身HTTP协议并没有限制它的URL大小,但是不同的浏览器对其有不同的大小长度限制

POST
post请求一般是对服务器的数据做改变,常用来数据的提交,新增操作。

特点:

post请求的请求参数都是请求体中

post请求本身HTTP协议也是没有限制大小的,限制它的是服务器的处理能力

OPTIONS
options请求属于浏览器的预检请求,查看服务器是否接受请求,预检通过后,浏览器才会去发get,post,put,delete等请求。

使用场景: 跨域,复杂请求;

至于什么情况下浏览器会发预检请求,浏览器会会将请求分为两类,简单请求与非简单请求, 非简单请求会产生预检options请求:它用于获取当前URL所支持的方法。若请求成功,则它会在HTTP头中包含一个名为“Allow”的头,值是所支持的方法,如“GET, POST”。

TRACE
TRACE请求服务器回显其收到的请求信息,该方法主要用于HTTP请求的测试或诊断。

十八 vue修饰符

.stop 阻止事件继续传播
.prevent 阻止标签默认行为
.capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理
.self 只当在 event.target 是当前元素自身时触发处理函数
.once 事件将只会触发一次
.passive 告诉浏览器你不想阻止事件的默认行为
v.trim: 清除输入时前后的空格
v.number: 输入的类型全部是数字

十九 js算法

先说说什么是算法?
算法是解决编程问题的代码实现过程,比如将最大的数从数组中取出来,给数组排序,甚至简单到将两个数组连接,将两个数字相加。换句话说,平时我们为了实现数据上变化,所写的函数或者逻辑,都能称之为算法,算法不是什么高大上的东西,编程人员基本天天与之接触。这也体现了它的重要性,别说前端开发只是实现页面布局,不需要学算法。

时间复杂度和空间复杂度是考量算法性能的重要指标,尤其是时间复杂度,因为计算时间的快慢,直接影响到用户的体验,而占用的内存可以在物理形式上解决。
时间复杂度的时间量级单位有哪些?
O(1) < O(logN) < O(N) < O(NlogN) < O(N^2)

用大O表示法来表示某一算法的时间复杂度。

O(1)示例
所有未达到n级的时间复杂度都可以视作1,尽管test里面可能还含有很多步操作,但都是可以计数数出来的,十几步和几步对于计算机来说都是1。

function test(){
	let a = 1;
	let b = 1;
	console.log(a + b)
	console.log('dx, 18')
	...
	return a + b;
}

O(NlogN)示例
这个test函数实现了循环嵌套,外层循环n次内层循环logn次,所以是nlogn

function test(n) {
	// 外层实现n次
	for(let i = 0; i < n.length; i++) {
		// 内层实现logn次
		for(let j = n.length - 1; j >= 0; j = Math.floor( j / 2)) {
			console.log(n[j])
		}
	}
}

O(N^2)示例
在排序算法中,用冒泡排序的时间复杂度就是O(N^2),同样是一个循环嵌套,外层循环n次,内层同样循环n次

function bubbleSort ( data ) {
    var temp = 0;
    for ( var i = data.length ; i > 0 ; i -- ){
        for( var j = 0 ; j < i - 1 ; j++){
           if( data[j] > data[j + 1] ){
               temp = data[j];
               data[j] = data [j+1];
               data[j+1] = temp;
           }
        }
    }
    return data;
}

二十、Http

1.定义:
HTTP 协议(Hypertext Transfer Protocol,超文本传输协议),是一个客户端请求和响应的标准协议,这个协议详细规定了浏览器和万维网服务器之间互相通信的规则。
用户输入地址和端口号之后就可以从服务器上取得所需要的网页信息。

2.http URL:
URL作用:HTTP协议工作于客户端-服务端架构上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。

3. HTTP协议格式:
HTTP请求由三部分组成,分别是:请求行、请求头(请求报头)、请求正文(请求体)。
格式:
在这里插入图片描述
请求头常见内容:

Content-Type: 数据类型(text/html等): application/json
Content-Length: Body的长度
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
User-Agent: 声明用户的操作系统和浏览器版本信息;
referer: 当前页面是从哪个页面跳转过来的;
location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能.

浏览器不是根据资源的后缀判断资源的类型,而是通过Content-Type来判断资源的类型。

响应:
在接收和解释请求消息后,服务器返回一个HTTP响应消息。
HTTP响应也是由三个部分组成,分别是:状态行、消息报头、响应正文。
格式
在这里插入图片描述
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(BadGatewa
在这里插入图片描述
4. HTTP工作流程
一次HTTP操作称为一个事务,其工作过程可分为四步:

1)首先客户机与服务器需要建立连接。只要单击某个超级链接,HTTP的工作开始。

2)建立连接后,客户机发送一个请求给服务器,请求方式的格式为:统一资源标识符(URL)、协议版本号,后边是MIME信息包括请求修饰符、客户机信息和可能的内容。

3)服务器接到请求后,给予相应的响应信息,其格式为一个状态行,包括信息的协议版本号、一个成功或错误的代码,后边是MIME信息包括服务器信息、实体信息和可能的内容。

4)客户端接收服务器所返回的信息通过浏览器显示在用户的显示屏上,然后客户机与服务器断开连接。

如果在以上过程中的某一步出现错误,那么产生错误的信息将返回到客户端,有显示屏输出。对于用户来说,这些过程是由HTTP自己完成的,用户只要用鼠标点击,等待信息显示就可以了。

二十一、网络请求协议

一、IP协议
数据包在传输之前,会被附加上双方主机的IP地址信息。是通过向数据包上附加IP头的方式实现的(IP头中包含双方主机的IP信息和一些其他信息)。

二、UDP协议
数据包在传输之前,会被附加上双方的端口信息。是通过向数据包上附加UDP头的方式实现的(UDP头中包含双方的端口信息和一些其他信息)。

在使用 UDP 发送数据时,有各种因素会导致数据包出错,虽然 UDP 可以校验数据是否正确,但是对于错误的数据包,UDP 并不提供重发机制,只是丢弃当前的包,而且 UDP 在发送之后也无法知道是否能达到目的地。

虽说 UDP 不能保证数据可靠性,但是传输速度却非常快,所以 UDP 会应用在一些关注速度、但不那么严格要求数据完整性的领域,如在线视频、互动游戏等。

三、IP/UDP
上层把数据给传输层,传输层会给数据包加上UDP头,然后给网络层,网络层则给数据包添加IP头,最后底层将被附加了IP/UDP头的数据传输给主机B,主机B在网络层解开IP头获取IP数据,传输层解开UDP头取出端口数据,最后数据交给了上层。

四、TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。

通过 TCP 头的信息保证了一块大的数据传输的完整性。

对于数据包丢失的情况,TCP 提供重传机制;
TCP 引入了数据包排序机制,用来保证把乱序的数据包组合成一个完整的文件。
类似于UDP,在传输层时数据包会被附加上TCP头,TCP 头除了包含了目标端口和本机端口号外,还提供了用于排序的序列号,以便接收端通过序号来重排数据包。
六、HTTP协议
HTTP 是一种允许浏览器向服务器获取资源的协议,是 Web 的基础。通常由浏览器发起请求,用来获取不同类型的文件,例如 HTML 文件、CSS 文件、JavaScript 文件、图片、视频等。

二十二、组件化思想

组件化思想能开发我们自己的组件库方便我们开发,也可以拿别人的组件库去开发,一个同样配置的元素多次使用时就要组件化,减少代码杂乱看着也简洁

组件化能提高开发效率,组件化可以增加代码的复用性、可维护性和可测试性,简化调试步骤,提升项目可维护性,便于多人协同开发 。

使用场景
什么时候使用组件?以下分类可作为参考:

通用组件:实现最基本的功能,具有通用性、复用性,例如按钮组件、输入框组件、布局组件等。
业务组件:它们完成具体业务,具有一定的复用性,例如登录组件、轮播图组件。
页面组件:组织应用各部分独立内容,需要时在不同页面组件间切换,例如列表页、详情页组件

Watch监听的原理,
Template一个跟标签

路由守卫

Vue3的vuex
History模式刷新后,404怎么办

Logo

前往低代码交流专区

更多推荐