# v-show和v-if的区别

v-show:通过css display控制元素显示和隐藏,元素依然保留在DOM节点中

v-if:元素真正的渲染和销毁,会在DOM节点中把元素移除和新增,而不是显示和隐藏

频繁切换显示状态用v-show,否则用v-if,利于性能优化

# 为何在v-for中使用key

diff算法中通过tag和key来判断,是否是通一个节点,减少渲染次数,提升渲染性能

# 描述vue组件的生命周期(包括父子组件)

生命周期 描述
beforeCreate 在实例初始化之后,进行数据侦听和事件/侦听器的配置之前同步调用。
created 在实例创建完成后被立即同步调用。在这一步中,实例已完成对选项的处理,意味着以下内容已被配置完毕:数据侦听、计算属性、方法、事件/侦听器的回调函数。然而,挂载阶段还没开始,且 $el property 目前尚不可用。
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
mounted 实例被挂载后调用,这时 el 被新创建的 vm.$el 替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.$el 也在文档内。注意 mounted 不会保证所有的子组件也都被挂载完成。如果你希望等到整个视图都渲染完毕再执行某些操作,可以在 mounted 内部使用 vm.$nextTick
beforeUpdate 在数据发生改变后,DOM 被更新之前被调用。这里适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器。
updated 在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。注意,updated 不会保证所有的子组件也都被重新渲染完毕。如果你希望等到整个视图都渲染完毕,可以在 updated 里使用 vm.$nextTick
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
destroyed 实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。

父子组件生命周期关系:

// src/App.vue

<template>
  {{ count }}
  <child v-if="isShow" :count1="count1" />
</template>
<script>
import Child from "./Child.vue";

export default {
  components: { Child },
  data() {
    return {
      count: 1,
      count1: 2,
      isShow: true,
    };
  },
  beforeCreate() {
    console.log("parent ~ beforeCreate");
  },
  created() {
    console.log("parent ~ created");
  },
  beforeMount() {
    console.log("parent ~ beforeMount");
  },
  mounted() {
    console.log("parent ~ mounted");
    setTimeout(() => {
      this.count = 2;
    }, 3000);
    setTimeout(() => {
      this.count1 = 10;
    }, 6000);
    setTimeout(() => {
      this.isShow = false;
    }, 7000);
  },
  beforeUpdate() {
    console.log("parent ~ beforeUpdate");
  },
  updated() {
    console.log("parent ~ updated");
  },
  beforeDestroy() {
    console.log("parent ~ beforeDestroy");
  },
  destroyed() {
    console.log("parent ~ destroyed");
  },
};
</script>
<template>
  <div>child-{{ count }} || {{ count1 }}</div>
</template>
<script>
export default {
  name: "child",
  props: ["count1"],
  data() {
    return {
      count: 1,
    };
  },
  beforeCreate() {
    console.log("child ~ beforeCreate");
  },
  created() {
    console.log("child ~ created");
  },
  beforeMount() {
    console.log("child ~ beforeMount");
  },
  mounted() {
    console.log("child ~ mounted");
    setTimeout(() => {
      this.count = 2;
    }, 4000);
  },
  beforeUpdate() {
    console.log("child ~ beforeUpdate");
  },
  updated() {
    console.log("child ~ updated");
  },
  beforeDestroy() {
    console.log("parent ~ beforeDestroy");
  },
  destroyed() {
    console.log("parent ~ destroyed");
  },
};
</script>

首次加载时生命周期执行的顺序:

运行结果:

只是子组件中的数据更新:

只是父组件中的数据更新,不影响子组件:

父组件中的数据更新,影响到了子组件:

组件销毁:

# 错误捕获

# errorCaptured

详细含义请查阅官网 errorCaptured (opens new window)

在捕获一个来自后代组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播。

示例:

src/Child.vue

<template></template>
<script>
export default {
  mounted() {
    console.log(a);
  },
};
</script>

src/App.vue

<template>
  <child />
</template>
<script>
import Child from "./Child.vue";
export default {
  components: {
    Child,
  },
  errorCaptured(err, vm, info) {
    console.log(err, vm, info);
  },
};
</script>

运行代码后,可以看到控制台打印出了报错信息:

# errorHandler

errorHandler (opens new window) 指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和 Vue 实例。

还是上面的代码,我们把 App.vue 中捕获错误的代码删掉,在 main.js 中加入 errorHandler 方法,用于捕获错误。

import Vue from 'vue';
import App from './App.vue';

Vue.config.productionTip = false;

Vue.config.errorHandler = function(err, vm, info) {
	console.log(err, vm, info);
};
new Vue({
	render: (h) => h(App)
}).$mount('#app');

可以看到浏览器控制台打印出了捕获的错误信息:

细心的读者可能看到了,上面的代码我们只测试了同步代码报错,如果是异步代码呢?

src/Child.vue

<template></template>
<script>
export default {
  mounted() {
    setTimeout(() => {
      console.log(a);
    }, 3000);
  },
};
</script>

src/App.vue

<template>
  <child />
</template>
<script>
import Child from "./Child.vue";
export default {
  components: {
    Child,
  },
  errorCaptured(err, vm, info) {
    console.log(err, vm, info);
  },
};
</script>

src/main.js

import Vue from 'vue';
import App from './App.vue';

Vue.config.productionTip = false;

Vue.config.errorHandler = function(err, vm, info) {
	console.log(err, vm, info);
};
new Vue({
	render: (h) => h(App)
}).$mount('#app');

运行代码,可以看到,这两个方法都捕获不到定时器中的错误:

来看下接口请求:

<template>
  <div></div>
</template>
<script>
import axios from "axios";
export default {
  mounted() {
    axios
      .get("http://192.168.0.104:8083/index.html1")
      .then((res) => {
        console.log("res", res);
      })
      .catch((err) => {
        // console.log(err);
      });
  }
};
</script>

控制台输出:

可以看到捕获不到 Promise 里的报错。

改成 async 和 await 形式的接口请求:

<template>
  <div></div>
</template>
<script>
import axios from "axios";
export default {
  mounted() {
    const res = await axios.get("http://192.168.0.104:8083/index.html1");
    console.log("res", res);
  },
};
</script>

控制台输出结果如下:

可以看到可以捕获 async、await 方法里的报错。

errorHandler 从 2.6.0 起,这个钩子也会捕获 v-on DOM 监听器内部抛出的错误。另外,如果任何被覆盖的钩子或处理函数返回一个 Promise 链 (例如 async 函数),则来自其 Promise 链的错误也会被处理。

所以,如果我们要在代码中要上报项目错误的时候,不能完全依赖这两个函数,还需配合其他的方式,一起处理上报。

# 父组件怎么监听子组件生命周期

可以在调用的组件上添加 @hook:[生命周期方法]= “[回调函数]” 来实现,具体代码如下:

src/Child.vue

<template>
  <div>child</div>
</template>
<script>
export default {
  mounted() {},
};
</script>

src/App.vue

<template>
  <div>
    <child @hook:mounted="callback" />
  </div>
</template>
<script>
import Child from "./Child.vue";
export default {
  components: {
    Child,
  },
  methods: {
    callback() {
      console.log("子组件生命周期执行了");
    },
  },
};
</script>

可以看到控制台输出了:

# Vue组件如何通信

Vue 组件间的通信大概可以分为三类:

  • 父子组件通信

props、 $parent / $children、 provide / inject 、 ref 、$attrs / $listeners

  • 兄弟组件通信

eventBus ; vuex

  • 隔代组件通信

eventBus、Vuex;provide / inject 、$attrs / $listeners

下面我们来详细介绍下:

# props 和 $emit

父组件通过 props 将数组传给子组件;子组件通过 $emit 给父组件发送消息

父组件:

<template>
  <div>
    <child :name="name" @changeAge="changeAge" />
    {{ age }}
  </div>
</template>
<script>
import Child from "./Child.vue";
export default {
  components: {
    Child,
  },
  data() {
    return {
      name: "f",
      age: 17,
    };
  },
  methods: {
    changeAge(ev) {
      this.age = ev;
    },
  },
};
</script>

子组件:

<template>
  <div @click="changeAge">{{ name }}</div>
</template>
<script>
export default {
  props: ["name"],
  methods: {
    changeAge() {
      this.$emit("changeAge", 18);
    },
  },
};
</script>

# EventBus

事件总线的方式适合父子、兄弟、跨级组件的通信。

下面是一个简单的 demo:

父组件:

// src/App.vue

<template>
  <div>
    <child-1/>
    <child-2/>
  </div>
</template>
<script>
import Child1 from "./Child.vue";
import Child2 from "./Child2.vue";
export default {
  components: {
    Child1,
    Child2,
  }
};
</script>

子组件1:

// src/Child1.vue
<template>
  <div @click="send">child1:{{ info }}</div>
</template>
<script>
import EventBus from "./bus/index";
export default {
  data() {
    return {
      info: "",
    };
  },
  mounted() {
    EventBus.$on("receiveInfoChild2", (info) => (this.info = info));
  },
  methods: {
    send() {
      EventBus.$emit("receiveInfoChild1", "I love you");
    },
  },
};
</script>

子组件2:

// src/Child2.vue

<template>
  <div @click="send">child2: {{ info }}</div>
</template>
<script>
import EventBus from "./bus/index";
export default {
  data() {
    return {
      info: "",
    };
  },
  mounted() {
    EventBus.$on("receiveInfoChild1", (info) => (this.info = info));
  },
  methods: {
    send() {
      EventBus.$emit("receiveInfoChild2", "I love you, too.");
    },
  },
};
</script>

效果如下:

使用 EventBus 代码可读性、维护性都比较低。

# $attrs 和 $listeners

  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。
  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

demo 如下:

父组件:

// src/App.vue

<template>
  <div>
    <child
      :name="name"
      :age="age"
      :score="100"
      sex="man"
      @click="handleClick"
    />
  </div>
</template>
<script>
import Child from "./Child.vue";
export default {
  components: {
    Child,
  },
  data() {
    return {
      name: "f",
      age: 18,
      socre: 100,
    };
  },
  methods: {
    handleClick(value) {
      console.log("来自child2传递过来的数据:", value);
    },
  },
};
</script>

子组件1:

// src/Child.vue

<template>
  <div>
    <child2 v-bind="$attrs" v-on="$listeners" />
  </div>
</template>
<script>
import Child2 from "./Child2.vue";
export default {
  components: {
    Child2,
  },
  inheritAttrs: false,
  mounted() {
    console.log("child1 $attrs ~ ", this.$attrs);
    console.log("child1 $listeners ~ ", this.$listeners);
  },
};
</script>

子组件2:

// src/Child2.vue

<template>
  <div @click="handleClick">child2 {{ $attrs.name }}</div>
</template>
<script>
export default {
  props: ["sex"],
  inheritAttrs: false,
  mounted() {
    console.log("child2 $attrs ~ ", this.$attrs);
    console.log("child2 $listeners ~ ", this.$listeners);
  },
  methods: {
    handleClick() {
      this.$listeners.click("你好呀,爷爷组件");
    },
  },
};
</script>

运行效果如下:

从上图可以看到由于 child2 设置的 props 属性中含有 sex,所以 child2 并没有在控制台输出 sex,child1 输出了 sex ,这也就印证了 $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 。

# provide 和 inject

祖先组件中通过provider来提供变量,然后在子孙组件中通过inject来注入变量。示例代码如下:

父组件:

// src/App.vue
<template>
  <div>
    <child />
  </div>
</template>
<script>
import Child from "./Child.vue";
export default {
  components: {
    Child,
  },
  provide: {
    name: "fang",
  },
};
</script>

子组件1:

// src/Child.vue
<template>
  <div>
    <child2 />
  </div>
</template>
<script>
import Child2 from "./Child2.vue";
export default {
  components: {
    Child2,
  },
};
</script>

子组件2:

// src/Child2.vue
<template>
  <div>child2接收到的数据 - {{ name }}</div>
</template>
<script>
export default {
  inject: ["name"],
};
</script>

效果如下:

# $children 和 $parent

这个方法只能在父子组件中使用,且并不推荐使用。

父组件:

// src/App.vue

<template>
  <div><child ref="child1" /></div>
</template>
<script>
import Child from "./Child.vue";
export default {
  components: {
    Child,
  },
  data() {
    return {
      age: 18,
    };
  },
  mounted() {
    console.log("父组件获取子组件的name属性", this.$children[0].name);
    this.$children[0].handleClick();
  },
};
</script>

子组件:

// src/Child.vue

<template>
  <div></div>
</template>
<script>
export default {
  data() {
    return {
      name: "f",
    };
  },
  created() {
    console.log("子组件获取父组件的age属性", this.$parent.age);
  },
  methods: {
    handleClick() {
      console.log("父组件执行了子组件的方法");
    },
  },
};
</script>

运行效果如下:

# ref

ref 用在普通的 DOM 元素上,指代的是 DOM 节点;用在组件上,指代的是组件实例,这种方式一般也不推荐使用。

父组件:

// src/App.vue
<template>
  <div><child ref="child1" /></div>
</template>
<script>
import Child from "./Child.vue";
export default {
  components: {
    Child,
  },
  mounted() {
    // 获取子组件的方法和事件
    const child1 = this.$refs.child1;
    console.log("子组件的name属性值为", child1.name);
    child1.handleClick();
  },
};
</script>

子组件:

// src/Child.vue

<template>
  <div></div>
</template>
<script>
export default {
  data() {
    return {
      name: "f",
    };
  },
  methods: {
    handleClick() {
      console.log("执行了子组件的方法");
    },
  },
};
</script>

效果展示如下:

# Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

我们先来简要介绍下 Vuex 中核心概念:

  • state:数据来源
  • mutaions:改变 state 的唯一途径,不能用来处理异步操作
  • action:用来处理异步操作,如需修改状态,还是要在内部调用 mutation 才行,不能直接修改。
  • getter:类似 Vue 中的计算属性
  • module:相当于命名空间

Vuex 的具体使用可以查阅 官方文档 (opens new window) ,这里不再赘述。

下面我们来看下通过 Vuex 怎么实现组件间的通信,这里以兄弟组件为例:

父组件:

// src/App.vue
<template>
  <div>
    <child-1 />
    <child-2 />
  </div>
</template>
<script>
import Child1 from "./Child1.vue";
import Child2 from "./Child2.vue";
export default {
  components: {
    Child1,
    Child2,
  },
};
</script>

子组件1:

// src/Child1.vue
<template>
  <div>来自child2的打招呼: {{ $store.state.fromChild2Msg }}</div>
</template>
<script>
export default {
  mounted() {
    this.$store.commit("sayHiToChild2", "hi child2");
  },
};
</script>

子组件2:

// src/Child2.vue
<template>
  <div>来自child1的打招呼: {{ $store.state.fromChild1Msg }}</div>
</template>
<script>
export default {
  mounted() {
    this.$store.commit("sayHiToChild1", "hi child1");
  },
};
</script>

在 store 文件夹中新建 index.js 文件,输入以下内容:

// src/store/index.js

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const state = {
	fromChild1Msg: '',
	fromChild2Msg: ''
};

const mutations = {
	sayHiToChild1(state, payload) {
		state.fromChild2Msg = payload;
	},
	sayHiToChild2(state, payload) {
		state.fromChild1Msg = payload;
	}
};

export default new Vuex.Store({ state, mutations });

在 main.js 中引入 store:





 







 



// src/main.js

import Vue from 'vue';
import App from './App.vue';
import store from './store/index';

Vue.config.productionTip = false;

Vue.config.errorHandler = function(err, vm, info) {
	console.log(err, vm, info);
};
new Vue({
	store,
	render: (h) => h(App)
}).$mount('#app');

运行效果:

# 描述组件渲染和更新过程

# 虚拟DOM的优缺点

优点:

  • 极大的减少了对真实 DOM 的操作

框架会帮助我们更新视图,不用我们自己频繁操作 DOM,可以极大的提升开发效率。

  • 更好的跨平台

# 真实的 DOM 依赖于平台,而虚拟 DOM 的本质是 JS 对象,相对而言可以更方便的进行跨平台操作,如服务端渲染、混合开发等;

缺点:

虚拟 DOM 在某些极端情况下不一定比真实 DOM 快,举一个例子,就比如我就想在页面上写一个 hello world在,直接使用 document.write('hello world')即可,要是使用框架就繁琐了很多。既然它能够如此流行,对于大多数情况下表现肯定是不错的,能应对我们大多数的开发,个人感觉至于虚拟 DOM 的缺点一般情况根本不用关心。

# 如何将组件所有的props传递给子组件

通过$props

<Login v-bind="$props"/>

# 如何自己实现v-model

v-model 是一个语法糖,在我们使用一些表单输入元素的时候帮我们实现双向数据绑定。v-model 对不同的输入元素绑定不同的属性和方法:

  1. text 和 textarea 元素使用 value 属性和 input 事件;
  2. checkbox 和 radio 使用 checked 属性和 change 事件;
  3. select 字段将 value 作为 prop 并将 change 作为事件。

这里以 input 为例:

<template>
  <div>
    <input :value="value" @input="value = $event.target.value" />
    {{ value }}
  </div>
</template>
<script>
export default {
  data() {
    return {
      value: "",
    };
  },
};
</script>

展示效果:

# 多个组件有相同的逻辑,如何抽离

mixin

# 何时使用异步组件

加载大组件

路由异步加载

# 什么时候使用keep-alive

缓存组件, 不需要重复渲染

多个静态tab页的切换

优化性能

# 什么时候使用beforeDestory

解绑自定义事件event.$off

清除定时器

解绑自定义DOM事件,如window scroll等

# 什么是作用域插槽

# vuex中action和mutation的区别

action中可以处理异步,mutation中不可以

mutation一般做原子操作,就是每次做一个操作

action可以整合多个mutation

# Vue-router常用的路由模式

路由模式主要分为两类:hash 模式和 H5 history 模式。

hash 模式: 使用 URL hash 值来作路由,兼容性好;

history 模式: 依赖 HTML5 History API 和服务器配置,兼容性略差,但用户体验好点;

# hash 模式的实现原理

location.hash 的值就是 url 中 # 后面的值,它有以下特性:

  • hash 值的变化会在浏览器中增加访问记录,可以通过浏览器的前进、后退实现;
  • 可以通过 hashchange 事件监听 hash 值的变化,控制页面的渲染
  • hash 值的变化不会向服务器发送请求,不会刷新页面

# history 模式的实现原理

history.pushState() 和 history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。

# 如何配置vue-router异步加载

export default new VueRouter {
  routes: [
    {
      path: '/',
      component: ()=>import(/* webpackChunkName: "cart" */'./comoponents/Cart')
    }
  ]
}

# 用Vnode描述一个DOM结构

<div id="app" class="box">
  <p>hello</p>
  <ul style="color:red;">
    <li>f</li>
  </ul>
</div>

对应的Vnode

{
  tag: 'div'
  props: {
    className: 'box'
    id: 'app'
  },
  children: [
    {
      tag: 'p'
      children: 'hello'
    },
    {
      tag: 'ul',
      props: {
        style: 'color:red'
      },
      children: [
        {
          tag: 'li',
          children: 'f'
        }
      ]
    }
  ]
}

# 监听data变化的核心API是什么

Object.defineProperty

深度监听和监听数组

有何缺点

# Vue如何监听数组变化

vu2中重新定义原型,重写push、pop等方法,实现监听

vue3中Proxy可以原生监听数组变化

# 描述响应式原理

监听data变化

组件渲染和更新流程

# diff算法的时间复杂度

O(n)

在O(n^3)基础上做了一些调整

# 简述diff算法过程

# Vue为什么是异步渲染,$nextTick有什么用

异步渲染,以提高渲染性能

$nextTick在DOM更新后触发回调

# Vue常见性能优化

合理使用v-show和v-if

合理使用computed

v-for中使用key,避免和v-if同时使用,因为v-for的优先级高些,每次v-for的时候v-if都要重新计算一遍

自定义事件、DOM事件要及时销毁

合理使用异步组件

data层级不要太深

使用vue-loader在开发环境做模板编译

合理使用keep-alive

前端通用的一些性能优化,如图片懒加载等

webpack层面的一些优化

# 双向数据绑定v-model的实现原理

input元素 value=this.name

绑定input事件this.name=$event.target.value

data更新触发re-render

# 对MVVM的理解

很久以前就有了组件化的概念asp、jsp时就有了,传统组件只是静态渲染,更新还需要依赖于操作DOM。Vue通过MVVM实现数据驱动视图,React 使用 setState 来实现数据驱动视图。我们不在具体操作 DOM,直接修改 Vue中的数据就行了。Vue 框架本身根据数据重新渲染视图。使用 Vue 开发的时候让我们更加关注数据,更加关注业务逻辑。

MVVM分为Model、View、ViewModel三者。

  • Model:代表数据模型,数据和业务逻辑都在Model层中定义;
  • View:代表UI视图,负责数据的展示;
  • ViewModel:就是与界面(view)对应的Model。因为,数据库结构往往是不能直接跟界面控件一一对应上的,所以,需要再定义一个数据对象专门对应view上的控件。而 ViewModel 的职责就是把 model 对象封装成可以显示和接受输入的界面数据对象。

Model 和 View 并无直接关联,而是通过 ViewModel 来进行联系的,Model 和 ViewModel 之间有着双向数据绑定的联系。因此当 Model 中的数据改变时会触发 View 层的刷新,View 中由于用户交互操作而改变的数据也会在 Model 中同步。

简单的说,ViewModel 就是 View 与 Model 的连接器,View 与 Model 通过 ViewModel 实现双向绑定。View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

# computed有什么特点

具备缓存功能,只要当依赖的属性发生变化时才会重新计算有利于提高性能

# 为何data必须是一个函数

如果data是对象,每个组件的data 都会是内存的同一个地址,一个数据改变了其他也改变了; 如果data是一个函数时,每个组件实例都有自己的作用域,每个实例相互独立不会相互影响;

# ajax请求应该放在哪个生命周期

created 和 mounted 是在同一个 tick 中执行的,而ajax 请求的时间一定会超过一个 tick。所以即便ajax的请求耗时是 0ms, 那么也是在 nextTick 中更新数据到 DOM 中。所以说在不依赖 DOM 节点的情况下一点区别都没有

如果需要操作 DOM 则放在 mounted 中。

# 如何理解ref toRef 和 toRefs

# ref

生成值类型的响应数据

可用于模板和reactive

通过.value修改值

<template>
    <p>ref demo {{ageRef}} {{state.name}}</p>
</template>

<script>
import { ref, reactive } from 'vue'

export default {
    name: 'Ref',
    setup() {
        const ageRef = ref(20) // 值类型 响应式
        const nameRef = ref('fang')

        const state = reactive({
            name: nameRef
        })

        setTimeout(() => {
            console.log('ageRef', ageRef.value)

            ageRef.value = 25 // .value 修改值
            nameRef.value = 'fei'
        }, 1500);

        return {
            ageRef,
            state
        }
    }
}
</script>

模板引用

<template> 
  <div ref="root">This is a root element</div>
</template>

<script>
  import { ref, onMounted } from 'vue'

  export default {
    setup() {
      const root = ref(null)

      onMounted(() => {
        // DOM元素将在初始渲染后分配给ref
        console.log(root.value) // <div>这是根元素</div>
      })

      return {
        root
      }
    }
  }
</script>

# toRef和toRefs如何使用

针对一个响应式对象(reactive封装)的prop

创建一个ref,具有响应式

两者保持引用关系

<template>
    <p>toRef demo - {{ageRef}} - {{state.name}} {{state.age}}</p>
</template>

<script>
import { ref, toRef, reactive } from 'vue'

export default {
    name: 'ToRef',
    setup() {
        const state = reactive({
            age: 20,
            name: 'fang'
        })

        const age1 = computed(() => {
            return state.age + 1
        })

        // // toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式
        // const state = {
        //     age: 20,
        //     name: 'fei'
        // }

        const ageRef = toRef(state, 'age')

        setTimeout(() => {
            state.age = 25
        }, 1500)

        setTimeout(() => {
            ageRef.value = 30 // .value 修改值
        }, 3000)

        return {
            state,
            ageRef
        }
    }
}
</script>

toRefs

将响应式对象转换为普通对象

对象的每个props都对应ref

两者保持引用关系

<template>
    <p>toRefs demo {{age}} {{name}}</p>
</template>

<script>
import { ref, toRef, toRefs, reactive } from 'vue'

export default {
    name: 'ToRefs',
    setup() {
        const state = reactive({
            age: 20,
            name: '双越'
        })

        const stateAsRefs = toRefs(state) // 将响应式对象,变成普通对象

        // const { age: ageRef, name: nameRef } = stateAsRefs // 每个属性,都是 ref 对象
        // return {
        //     ageRef,
        //     nameRef
        // }

        setTimeout(() => {
            state.age = 25
        }, 1500)

        return stateAsRefs

        // 这样结构出来state的值,虽然模板中可以正常使用,但是失去了响应式
        // return {
        //   ...state
        // }
    }
}
</script>

# ref toRef和toRefs的最佳使用方式

function useFn () {
  const state = reactive({
    x: 1,
    y: 2
  })

  // 返回时转换为ref
  return toRefs(state)
}

export default {
  setup() {
    // 可以在不失去响应式的情况下进行结构
    const { x, y } = useFn()

    return {
      x,
      y
    }
  }
}

最佳使用方式:

  • 用reactive做对象的响应式, 用ref做值类型的响应式
  • setup中返回toRefs(state)或者toRef(state, key)
  • ref变量命名都用xxxRef的方式
  • 合成函数返回响应式对象时,使用toRefs

# 为什么需要用 ref

为什么需要ref?

返回值类型,会丢失响应式,如在setup、computed、合成函数,都有可能返回值类型。如果Vue不定义ref用户将自造ref,返回更混乱。

<template>
    <p>{{age}} {{state.age}} - {{age1}}</p>
</template>

<script>
import { ref } from 'vue'

export default {
    name: 'WhyRef',
    setup() {
        let age = 20

        setTimeout(() => {
            age = 30
        }, 1500)
        // Proxy不能设置值类型
        return {
          // 此时age并不具备响应式
          age
        }
    }
}
</script>

为什么需要.value?

ref是一个对象(不丢失响应式),value存储值,通过.value属性的get、set实现响应式,用于模板、reactive时不需要.value,其他情况都需要

为什么需要toRef和toRefs?

在不丢失响应式的情况下,把对象数据分解、扩散。前提是针对响应式对象非普通对象它们不创造响应式,而是延续响应式。

# vue3升级了哪些重要功能

createApp

// vue2
const app = new Vue({/*选项*/})

// vue3
const app = Vue.createApp({/*选项*/})

import { createApp } from 'vue'
import App from './App'
createApp(App).mount('#app')

// vue2
Vue.use()
Vue.mixin()
Vue.component()
Vue.directive()

// vue3
app.use()
app.mixin()
app.component()
app.directive()

emits属性

生命周期

多事件

<button @click="one(), two()"></button>

Fragment

// vue2
<template>
  <div>
    <h1>测试</h1>
    <p>测试结果</p>
  <div>
</template>

// vue3
<template>
  <h1>测试</h1>
  <p>测试结果</p>
</template>

移除.sync

// vu2
<MyCmp v-bind:title.sync="title"></MyCmp>

// vue3
<MyCmp v-model:title="title"></MyCmp>

异步组件的写法

new Vue({
  components: {
    'my-component': () => import('./home.vue')
  }
})


// vue3
import { createApp, defineAsyncComponent } from 'vue'
createApp({
  components: {
    AsyncComponent: defineAsyncComponent(() => import('./home.vue'))
  }
})

移除filter

// 在花括号中
{{message | capitalize}}

// 在v-bindz中
<div v-bind="rawId | formatId"></div>

Teleport

// 伪代码
<button @click="modalOpen = true"></button>
<teleport to="body">
  <div v-if="modalOpen" class="modal">
    <div>
      <button @click="modalOpen = false"></button>
    <div>
  </div>
</teleport>

Suspense


CompositionAPI

# Composition API 如何实现逻辑复用

抽离逻辑代码到一个函数

函数命名以use开头,如:useXXX

在setup中引用useXXX函数

useMousePosition.js

import { reactive, ref, onMounted, onUnmounted } from 'vue'

function useMousePosition() {
    const x = ref(0)
    const y = ref(0)

    function update(e) {
        x.value = e.pageX
        y.value = e.pageY
    }

    onMounted(() => {
        console.log('useMousePosition mounted')
        window.addEventListener('mousemove', update)
    })

    onUnmounted(() => {
        console.log('useMousePosition unMounted')
        window.removeEventListener('mousemove', update)
    })

    return {
        x,
        y
    }
}

export default useMousePosition

MousePosition.vue

<template>
    <p>mouse position {{x}} {{y}}</p>
</template>

<script>
import { reactive } from 'vue'
import useMousePosition from './useMousePosition'

export default {
    name: 'MousePosition',
    setup() {
        const { x, y } = useMousePosition()
        return {
            x,
            y
        }
    }
}
</script>

# Proxy 和 Object.defineProperty 对比

优点:

  • 可以直接实现对整个对象的监听,无需通过遍历监听每个属性;
  • 可以直接监听对象属性的新增、删除;
  • 可以直接监听数组的变化;
  • 提供了更多拦截方法,可以实现更多操作;

缺点:

兼容性不是很好,且不能通过 polyfill 抹平;

# Object.defineProperty

优点:

兼容性相对较好,Proxy 存在浏览器兼容问题,并且无法被 polyfill 抹平;

缺点:

  • 它只能劫持对象的属性,如果想实现对对象的监控,需要逐个遍历对象的属性;
  • 不能监听数组的变化,如果想实现对数组的监听,需要重写数组的方法,但即便重写后直接通过数组索引修改数组元素也检测不到;
  • 它不能对 Set、Map 进行监听;
  • 不能监听对象属性的新增和删除,只能通过 Vue.set 和 Vue.delete 实现监听;

# Vue3如何实现响应式

// const data = {
//     name: 'zhangsan',
//     age: 20,
// }
const data = ['a', 'b', 'c']

const proxyData = new Proxy(data, {
    get(target, key, receiver) {
        // 只处理本身(非原型的)属性
        const ownKeys = Reflect.ownKeys(target)
        if (ownKeys.includes(key)) {
            console.log('get', key) // 监听
        }

        const result = Reflect.get(target, key, receiver)
        return result // 返回结果
    },
    set(target, key, val, receiver) {
        // 重复的数据,不处理
        if (val === target[key]) {
            return true
        }

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

# Vite 为什么启动非常快

Vite是一个前端打包工具,Vue作者发起的项目,借助Vue的影响力,发展较快和Webpack竞争

优势:开发环境下无需打包,启动快

开发环境下使用ES6 Module,无需打包-非常快,无需像webpack打包成ES5再引入

生产环境使用rollup并不会快很多

浏览器中怎么直接使用ES Module呢?

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">app</div>

  <!-- 基本使用 -->
  <script type="module">
    import { add } from './index.js'
    console.log('基本使用', add(1, 2))
  </script>

  <!-- 外联的方式 -->
  <script type="module" src="./index.js"></script>

  <!-- 远程引入 -->
  <script type="module">
    import { createStore } from 'https://unpkg.com/redux@latest/es/redux.mjs'
    console.log('远程引入', createStore)
  </script>

  <!-- 动态引入 -->
  <script>
    app.onclick = async function() {
      const add = await import('./index.js')
      const multi = await import('./index.js')
      console.log('add', multi)
      console.log('动态引入', add.add(1, 2), multi.default(34, 45))
    }
  </script>
</body>
</html>

index.js

export function add(a, b) {
  return a + b
}

export default function multi(a, b) {
  return a * b
}

console.log('外链引入')

# watch和watchEffect的区别

两者都可以监听data属性变化

watch需要明确监听哪个属性

watchEffect会根据其中的属性,自动监听其变化

<template>
    <p>watch vs watchEffect</p>
    <p>{{numberRef}}</p>
    <p>{{name}} {{age}}</p>
</template>

<script>
import { reactive, ref, toRefs, watch, watchEffect } from 'vue'

export default {
    name: 'Watch',
    setup() {
        const numberRef = ref(100)
        const state = reactive({
            name: '双越',
            age: 20
        })

        watchEffect(() => {
            // 初始化时,一定会执行一次(收集要监听的数据)
            console.log('hello watchEffect')
        })
        watchEffect(() => {
            console.log('state.name', state.name)
        })
        watchEffect(() => {
            console.log('state.age', state.age)
        })
        watchEffect(() => {
            console.log('state.age', state.age)
            console.log('state.name', state.name)
        })
        setTimeout(() => {
            state.age = 25
        }, 1500)
        setTimeout(() => {
            state.name = '双越A'
        }, 3000)
        

        // watch(numberRef, (newNumber, oldNumber) => {
        //     console.log('ref watch', newNumber, oldNumber)
        // }
        // // , {
        // //     immediate: true // 初始化之前就监听,可选
        // // }
        // )

        // setTimeout(() => {
        //     numberRef.value = 200
        // }, 1500)

        // watch(
        //     // 第一个参数,确定要监听哪个属性
        //     () => state.age,

        //     // 第二个参数,回调函数
        //     (newAge, oldAge) => {
        //         console.log('state watch', newAge, oldAge)
        //     },

        //     // 第三个参数,配置项
        //     {
        //         immediate: true, // 初始化之前就监听,可选
        //         // deep: true // 深度监听
        //     }
        // )

        // setTimeout(() => {
        //     state.age = 25
        // }, 1500)
        // setTimeout(() => {
        //     state.name = '双越A'
        // }, 3000)

        return {
            numberRef,
            ...toRefs(state)
        }
    }
}
</script>

# setup中如何获取组件实例

在setup中和其他的Composition API中是没有this的,通过getCurrentInstance获取当前实例。如果使用的是Options API可照常使用this。

<template>
    <p>get instance</p>
</template>

<script>
import { onMounted, getCurrentInstance } from 'vue'

export default {
    name: 'GetInstance',
    data() {
        return {
            x: 1,
            y: 2
        }
    },
    setup() {
        console.log('this', this) // undefined

        onMounted(() => {
            console.log('this in onMounted', this) // undefined
            console.log('x', instance.data.x) // 1
        })

        const instance = getCurrentInstance()
        // setup是created和beforeCreate的合集,这个时候组件还没有正式的初始化,所以打印输出undefined
        console.log('x', instance.data.x) // undefined
    },
    mounted() {
        console.log('this2', this)
        console.log('y', this.y)
    }
}
</script>

# Composition API 和 React Hooks 的对比

前者setup只会被调用一次,后者函数(函数组件)会被调用多次

前者无需使用useMemo、useCallback,因为setup只调用一次

前者无需顾虑调用顺序,后者必须保证hooks顺序一致

前者reactive+ref比后者useState难理解

# Vue3为什么比Vue2快

Vue原理大体分为

组件化

响应式

vdom和diff

模板编译

渲染过程

前端路由

# 监听data变化的核心API是什么

组件data的数据一旦变化,立刻触发视图更新。

核心API Object.defineProperty

Proxy兼容性不太好,且无法polyfill

怎么监听数组,怎么监听复杂对象

Object.defineProperty的缺点:

深度监听需要一次性递归到底,一次性计算量大

无法监听新增属性/删除属性(Vue.set、Vue.delete)

无法原生监听数组,需要特殊处理

const data = {
    name: 'f',
    info: {
        name:''
    }
}

function updateView() {
    console.log('更新视图')
}

function defineReactive(target, key, value) {

    // 深度监听
    observe(value)
    
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                
                observe(value)

                value = newValue
                updateView()
            }
        }
    })
}

function observe(target) {
    if (target == null || typeof target !== 'object') return target

    for (let key in target)
        defineReactive(target, key, target[key])
}

observe(data)

data.name
// 赋值相同的值不会重新设置
data.name = 'f'
data.name = 'ff'
data.info.name = 1 // 深度监听

delete data.name //
data.age = 18

# vue如何监听数组变化

const data = {
    arr: []
}

function updateView() {
    console.log('更新视图')
}

const oldArrPrototype = Array.prototype;
const arrProto = Object.create(oldArrPrototype);

['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function () {
        updateView()
        oldArrPrototype[methodName].call(this, ...arguments)
    }
})

function defineReactive(target, key, value) {
    // 深度监听
    observe(value)

    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {

                observe(value)

                value = newValue
                updateView()
            }
        }
    })
}

function observe(target) {
    if (target == null || typeof target !== 'object') return target

    if (Array.isArray(target))
        target.__proto__ = arrProto

    for (let key in target)
        defineReactive(target, key, target[key])
}

observe(data)

data.arr.push(1)

# 如何用JS实现hash路由

url组成部分

以这个链接为例:http://192.168.0.103:8080/books/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BAVue.js/01.html?a=1&b=2#%E4%BB%80%E4%B9%88%E6%98%AFvue-js

location.protocol -> "http:"

location.hostname -> "192.168.0.103"

location.host -> "192.168.0.103:8080"

location.port -> "8080"

location.pathname -> "/books/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BAVue.js/01.html"

location.search -> "?a=1&b=2"

location.hash -> "#%E4%BB%80%E4%B9%88%E6%98%AFvue-js"

hash变化会触发页面跳转,即浏览器的前进、后退,hash变化不会刷新页面,SPA的特点不需要刷新页面,hash不会提交到server端

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>hash test</title>
</head>
<body>
    <p>hash test</p>
    <button id="btn1">修改 hash</button>

    <script>
        // hash 变化,包括:
        // a. JS 修改 url
        // b. 手动修改 url 的 hash
        // c. 浏览器前进、后退
        window.onhashchange = (event) => {
            console.log('old url', event.oldURL)
            console.log('new url', event.newURL)

            console.log('hash:', location.hash)
        }

        // 页面初次加载,获取 hash
        document.addEventListener('DOMContentLoaded', () => {
            console.log('hash:', location.hash)
        })

        // JS 修改 url
        document.getElementById('btn1').addEventListener('click', () => {
            location.href = '#/user'
        })
    </script>
</body>
</html>

hash变化,包括:

  1. JS修改url // location.href = "#/user"
  2. 手动修改url的hash //在浏览器地址栏手动修改hash,但是注意如果修改了url的其它部分可能会触发页面刷新,而且会触发前进后退
  3. 浏览器前进、后退 //在浏览器操作前进后退,会导致hash变化

# 如何用JS实现H5 history路由

url规范的路由,跳转时不刷新页面,主要通过history.pushState和window.onpopstate这两个方式实现

history需要后端支持

toB的情况推荐用hash,简单易用,对url规范不敏感,C端 不考虑 SEO ,搜索引擎优化的话,也可以不用 H5 history

popstate事件在浏览器的history对象的当前记录发生显式切换时触发。注意,调用history.pushState()history.replaceState(),并不会触发popstate事件。该事件只在用户在history记录之间显式切换时触发,比如鼠标点击“后退/前进”按钮,或者在脚本中调用history.back()history.forward()history.go()时触发。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>history API test</title>
</head>
<body>
    <p>history API test</p>
    <button id="btn1">修改 url</button>

    <script>
        // 页面初次加载,获取 path
        document.addEventListener('DOMContentLoaded', () => {
            console.log('load', location.pathname)
        })

        // 打开一个新的路由
        // 【注意】用 pushState 方式,浏览器不会刷新页面
        document.getElementById('btn1').addEventListener('click', () => {
            const state = { name: 'page1' }
            console.log('切换路由到', 'page1')
            history.pushState(state, '', 'page1') // 重要!!
        })

        // 监听浏览器前进、后退
        window.onpopstate = (event) => { // 重要!!
            console.log('onpopstate', event.state, location.pathname)
        }

        // 需要 server 端配合,可参考
        // https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90
    </script>
</body>
</html>

# vue组件是如何渲染和更新的

初次渲染过程

  1. 解析模板为render函数(开发环境已通过vue-loader完成)
  2. 触发响应式,监听data属性getter setter
  3. 执行render函数,生成vnode,执行patch

更新过程

  1. 修改data,触发setter
  2. 重新执行render函数,生成newVnode
  3. patch

render函数在生成vnode时会触发data里面的getter,并收集依赖,当data被修改时去通知watcher重新render

# vue组件异步渲染

汇集data的修改,一次性更新视图,减少DOM操作次数提高性能。

# Vdom和diff算法

vdom是实现Vue的重要基石,diff算法是vdom中最核心、最关键部分

DOM操作非常消耗性能,以前用JQ可以自行控制DOM操作的时机手动调整。

Vue、React都是数据驱动视图

vdom - 用JS模拟DOM结构,计算出最小的变更,操作DOM

树diff的时间复杂度为O(n^3) 1.遍历tree1 2.遍历tree2 3.排序

优化时间复杂度到O(n):

只比较同一层级,不跨级比较

tag不相同则直接删掉重建,不再深度比较

tag和key两者都相同,则认为是相同节点,不再深度比较。

# 模板编译前置知识点-with语法

vue template complier将模板编译成render函数,执行render函数生成vnode

使用with,改变{}内自由变量的查找方式,将{}内自由变量,当做obj的属性来查找,如果找不到匹配的属性就会报错 ,with要慎用,它打破了作用域规则,易读性变差。

let person = {
    name: 'f'
}

console.log(person.name) // f
console.log(person.age) // undefined

with(person) {
    console.log(name) // f
    console.log(age) // Uncaught ReferenceError: age is not defined
}

参考:

对MVVM的理解 (opens new window)

Ajax请求放在Vue哪个生命周期中 (opens new window)