写点什么

19 道高频 vue 面试题,顺便写一下自己的答案

作者:bb_xiaxia1998
  • 2022 年 9 月 24 日
    浙江
  • 本文字数:43949 字

    阅读完需:约 144 分钟

Vue 路由 hash 模式和 history 模式

1. hash模式


早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL# 后面的内容。比如下面这个网站,它的 location.hash 的值为 '#search'


https://interview2.poetries.top#search
复制代码


hash 路由模式的实现主要是基于下面几个特性


  • URLhash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;

  • hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制 hash 的切换;

  • 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URLhash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URLhash 值;

  • 我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)


window.addEventListener("hashchange", funcRef, false);
复制代码


每一次改变 hashwindow.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了


特点 :兼容性好但是不美观


2. history模式


history采用HTML5的新特性;且提供了两个新方法: pushState()replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更


window.history.pushState(null, null, path);window.history.replaceState(null, null, path);
复制代码


这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。


history 路由模式的实现主要基于存在下面几个特性:


  • pushStaterepalceState 两个 API 来操作实现 URL 的变化 ;

  • 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);

  • history.pushState()history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。


特点 :虽然美观,但是刷新会出现 404 需要后端进行配置

Vue.set 的实现原理

  • 给对应和数组本身都增加了dep属性

  • 当给对象新增不存在的属性则触发对象依赖的watcher去更新

  • 当修改数组索引时,我们调用数组本身的splice去更新数组(数组的响应式原理就是重新了splice等方法,调用splice就会触发视图更新)


基本使用


以下方法调用会改变原始数组:push(), pop(), shift(), unshift(), splice(), sort(), reverse(),Vue.set( target, key, value )


  • 调用方法:Vue.set(target, key, value )

  • target:要更改的数据源(可以是对象或者数组)

  • key:要更改的具体数据

  • value :重新赋的值


<div id="app">{{user.name}} {{user.age}}</div><div id="app"></div><script>    // 1. 依赖收集的特点:给每个属性都增加一个dep属性,dep属性会进行收集,收集的是watcher    // 2. vue会给每个对象也增加一个dep属性    const vm = new Vue({        el: '#app',        data: { // vm._data              user: {name:'poetry'}        }    });    // 对象的话:调用defineReactive在user对象上定义一个age属性,增加到响应式数据中,触发对象本身的watcher,ob.dep.notify()更新     // 如果是数组 通过调用 splice方法,触发视图更新    vm.$set(vm.user, 'age', 20); // 不能给根属性添加,因为给根添加属性 性能消耗太大,需要做很多处理
// 修改肯定是同步的 -> 更新都是一步的 queuewatcher</script>
复制代码


相关源码


// src/core/observer/index.js 44export class Observer { // new Observer(value)  value: any;  dep: Dep;  vmCount: number; // number of vms that have this object as root $data
constructor (value: any) { this.value = value this.dep = new Dep() // 给所有对象类型增加dep属性 }}
复制代码


// src/core/observer/index.js 201export function set (target: Array<any> | Object, key: any, val: any): any {  // 1.是开发环境 target 没定义或者是基础类型则报错  if (process.env.NODE_ENV !== 'production' &&    (isUndef(target) || isPrimitive(target))  ) {    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)  }  // 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)  if (Array.isArray(target) && isValidArrayIndex(key)) {    target.length = Math.max(target.length, key)    // 利用数组的splice变异方法触发响应式      target.splice(key, 1, val)    return val  }  // 3.如果是对象本身的属性,则直接添加即可  if (key in target && !(key in Object.prototype)) {    target[key] = val // 直接修改属性值      return val  }  // 4.如果是Vue实例 或 根数据data时 报错,(更新_data 无意义)  const ob = (target: any).__ob__  if (target._isVue || (ob && ob.vmCount)) {    process.env.NODE_ENV !== 'production' && warn(      'Avoid adding reactive properties to a Vue instance or its root $data ' +      'at runtime - declare it upfront in the data option.'    )    return val  }  // 5.如果不是响应式的也不需要将其定义成响应式属性  if (!ob) {    target[key] = val    return val  }  // 6.将属性定义成响应式的  defineReactive(ob.value, key, val)  // 通知视图更新  ob.dep.notify()  return val}
复制代码


我们阅读以上源码可知,vm.$set 的实现原理是:


  • 如果目标是数组 ,直接使用数组的 splice 方法触发相应式;

  • 如果目标是对象 ,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 gettersetter 的功能所调用的方法)


前端vue面试题详细解答

Vue 组件之间通信方式有哪些

Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。 Vue 组件间通信只要指以下 3 类通信父子组件通信隔代组件通信兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信


组件传参的各种方式



组件通信常用方式有以下几种


  • props / $emit 适用 父子组件通信

  • 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的

  • ref$parent / $children(vue3废弃) 适用 父子组件通信

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

  • $parent / $children:访问访问父组件的属性或方法 / 访问子组件的属性或方法

  • EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信

  • 这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件

  • $attrs / $listeners(vue3废弃) 适用于 隔代组件通信

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

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

  • provide / inject 适用于 隔代组件通信

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

  • $root 适用于 隔代组件通信 访问根组件中的属性或方法,是根组件,不是父组件。$root只对根组件有用

  • Vuex 适用于 父子、隔代、兄弟组件通信

  • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。


根据组件之间关系讨论组件通信最为清晰有效


  • 父子组件:props/$emit/$parent/ref

  • 兄弟组件:$parent/eventbus/vuex

  • 跨层级关系:eventbus/vuex/provide+inject/$attrs + $listeners/$root


下面演示组件之间通讯三种情况: 父传子、子传父、兄弟组件之间的通讯


1. 父子组件通信


使用props,父组件可以使用props向子组件传递数据。


父组件vue模板father.vue:


<template>  <child :msg="message"></child></template>
<script>import child from './child.vue';export default { components: { child }, data () { return { message: 'father message'; } }}</script>
复制代码


子组件vue模板child.vue:


<template>    <div>{{msg}}</div></template>
<script>export default { props: { msg: { type: String, required: true } }}</script>
复制代码


回调函数(callBack)


父传子:将父组件里定义的method作为props传入子组件


// 父组件Parent.vue:<Child :changeMsgFn="changeMessage">methods: {    changeMessage(){        this.message = 'test'    }}
复制代码


// 子组件Child.vue:<button @click="changeMsgFn">props:['changeMsgFn']
复制代码


子组件向父组件通信


父组件向子组件传递事件方法,子组件通过$emit触发事件,回调给父组件


父组件vue模板father.vue:


<template>    <child @msgFunc="func"></child></template>
<script>import child from './child.vue';export default { components: { child }, methods: { func (msg) { console.log(msg); } }}</script>
复制代码


子组件vue模板child.vue:


<template>    <button @click="handleClick">点我</button></template>
<script>export default { props: { msg: { type: String, required: true } }, methods () { handleClick () { //........ this.$emit('msgFunc'); } }}</script>
复制代码


2. provide / inject 跨级访问祖先组件的数据


父组件通过使用provide(){return{}}提供需要传递的数据


export default {  data() {    return {      title: '我是父组件',      name: 'poetry'    }  },  methods: {    say() {      alert(1)    }  },  // provide属性 能够为后面的后代组件/嵌套的组件提供所需要的变量和方法  provide() {    return {      message: '我是祖先组件提供的数据',      name: this.name, // 传递属性      say: this.say    }  }}
复制代码


子组件通过使用inject:[“参数1”,”参数2”,…]接收父组件传递的参数


<template>  <p>曾孙组件</p>  <p>{{message}}</p></template><script>export default {  // inject 注入/接收祖先组件传递的所需要的数据即可   //接收到的数据 变量 跟data里面的变量一样 可以直接绑定到页面 {{}}  inject: [ "message","say"],  mounted() {    this.say();  },};</script>
复制代码


3. children 获取父组件实例和子组件实例的集合


  • this.$parent 可以直接访问该组件的父实例或组件

  • 父组件也可以通过 this.$children 访问它所有的子组件;需要注意 $children 并不保证顺序,也不是响应式的


<!-- parent.vue --><template><div>  <child1></child1>     <child2></child2>   <button @click="clickChild">$children方式获取子组件值</button></div></template><script>import child1 from './child1'import child2 from './child2'export default {  data(){    return {      total: 108    }  },  components: {    child1,    child2    },  methods: {    funa(e){      console.log("index",e)    },    clickChild(){      console.log(this.$children[0].msg);      console.log(this.$children[1].msg);    }  }}</script>
复制代码


<!-- child1.vue --><template>  <div>    <button @click="parentClick">点击访问父组件</button>  </div></template><script>export default {  data(){    return {      msg:"child1"    }  },  methods: {    // 访问父组件数据    parentClick(){      this.$parent.funa("xx")      console.log(this.$parent.total);    }  }}</script>
复制代码


<!-- child2.vue --><template>  <div>    child2  </div></template><script>export default {  data(){    return {     msg: 'child2'    }  }}</script>
复制代码


4. listeners 多级组件通信


$attrs 包含了从父组件传过来的所有props属性


// 父组件Parent.vue:<Child :name="name" :age="age"/>
// 子组件Child.vue:<GrandChild v-bind="$attrs" />
// 孙子组件GrandChild<p>姓名:{{$attrs.name}}</p><p>年龄:{{$attrs.age}}</p>
复制代码


$listeners包含了父组件监听的所有事件


// 父组件Parent.vue:<Child :name="name" :age="age" @changeNameFn="changeName"/>
// 子组件Child.vue:<button @click="$listeners.changeNameFn"></button>
复制代码


5. ref 父子组件通信


// 父组件Parent.vue:<Child ref="childComp"/><button @click="changeName"></button>changeName(){    console.log(this.$refs.childComp.age);    this.$refs.childComp.changeAge()}
// 子组件Child.vue:data(){ return{ age:20 }},methods(){ changeAge(){ this.age=15 }}
复制代码


6. 非父子, 兄弟组件之间通信


vue2中废弃了broadcast广播和分发事件的方法。父子组件中可以用props$emit()。如何实现非父子组件间的通信,可以通过实例一个vue实例Bus作为媒介,要相互通信的兄弟组件之中,都引入Bus,然后通过分别调用 Bus 事件触发和监听来实现通信和参数传递。Bus.js可以是这样:


// Bus.js
// 创建一个中央时间总线类 class Bus { constructor() { this.callbacks = {}; // 存放事件的名字 } $on(name, fn) { this.callbacks[name] = this.callbacks[name] || []; this.callbacks[name].push(fn); } $emit(name, args) { if (this.callbacks[name]) { this.callbacks[name].forEach((cb) => cb(args)); } } }
// main.js Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上 // 另一种方式 Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能
复制代码


<template>    <button @click="toBus">子组件传给兄弟组件</button></template>
<script>export default{ methods: { toBus () { this.$bus.$emit('foo', '来自兄弟组件') } }}</script>
复制代码


另一个组件也在钩子函数中监听on事件


export default {  data() {    return {      message: ''    }  },  mounted() {    this.$bus.$on('foo', (msg) => {      this.message = msg    })  }}
复制代码


7. $root 访问根组件中的属性或方法


  • 作用:访问根组件中的属性或方法

  • 注意:是根组件,不是父组件。$root只对根组件有用


var vm = new Vue({  el: "#app",  data() {    return {      rootInfo:"我是根元素的属性"    }  },  methods: {    alerts() {      alert(111)    }  },  components: {    com1: {      data() {        return {          info: "组件1"        }      },      template: "<p>{{ info }} <com2></com2></p>",      components: {        com2: {          template: "<p>我是组件1的子组件</p>",          created() {            this.$root.alerts()// 根组件方法            console.log(this.$root.rootInfo)// 我是根元素的属性          }        }      }    }  }});
复制代码


8. vuex


  • 适用场景: 复杂关系的组件数据传递

  • Vuex 作用相当于一个用来存储共享变量的容器



  • state用来存放共享变量的地方

  • getter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的值

  • mutations用来存放修改state的方法。

  • actions也是用来存放修改 state 的方法,不过action是在mutations的基础上进行。常用来做一些异步操作


小结


  • 父子关系的组件数据传递选择 props$emit进行传递,也可选择ref

  • 兄弟关系的组件数据传递可选择$bus,其次可以选择$parent进行传递

  • 祖先与后代组件数据传递可选择attrslisteners或者 ProvideInject

  • 复杂关系的组件数据传递可以通过vuex存放共享的变量

Vue-router 路由模式有几种

vue-router3 种路由模式:hashhistoryabstract,对应的源码如下所示


switch (mode) {    case 'history':    this.history = new HTML5History(this, options.base)    break    case 'hash':    this.history = new HashHistory(this, options.base, this.fallback)      break    case 'abstract':    this.history = new AbstractHistory(this, options.base)      break  default:    if (process.env.NODE_ENV !== 'production') {      assert(false, `invalid mode: ${mode}`)    }}
复制代码


其中,3 种路由模式的说明如下:


  • hash: 使用 URL hash 值来作路由,支持所有浏览器

  • history : 依赖 HTML5 History API 和服务器配置

  • abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

vue-router 守卫

导航守卫 router.beforeEach 全局前置守卫


  • to: Route: 即将要进入的目标(路由对象)

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

  • next: Function: 一定要调用该方法来 resolve 这个钩子。(一定要用这个函数才能去到下一个路由,如果不用就拦截)

  • 执行效果依赖 next 方法的调用参数。

  • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。

  • next(false):取消进入路由,url 地址重置为 from 路由地址(也就是将要离开的路由地址)


// main.js 入口文件import router from './router'; // 引入路由router.beforeEach((to, from, next) => {   next();});router.beforeResolve((to, from, next) => {  next();});router.afterEach((to, from) => {  console.log('afterEach 全局后置钩子');});
复制代码


路由独享的守卫 你可以在路由配置上直接定义 beforeEnter 守卫


const router = new VueRouter({  routes: [    {      path: '/foo',      component: Foo,      beforeEnter: (to, from, next) => {        // ...      }    }  ]})
复制代码


组件内的守卫你可以在路由组件内直接定义以下路由导航守卫


const Foo = {  template: `...`,  beforeRouteEnter (to, from, next) {    // 在渲染该组件的对应路由被 confirm 前调用    // 不!能!获取组件实例 `this`    // 因为当守卫执行前,组件实例还没被创建  },  beforeRouteUpdate (to, from, next) {    // 在当前路由改变,但是该组件被复用时调用    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。    // 可以访问组件实例 `this`  },  beforeRouteLeave (to, from, next) {    // 导航离开该组件的对应路由时调用,我们用它来禁止用户离开    // 可以访问组件实例 `this`    // 比如还未保存草稿,或者在用户离开前,    将setInterval销毁,防止离开之后,定时器还在调用。  }}
复制代码

Vue 要做权限管理该怎么做?控制到按钮级别的权限怎么做?

分析


  • 综合实践题目,实际开发中经常需要面临权限管理的需求,考查实际应用能力。

  • 权限管理一般需求是两个:页面权限和按钮权限,从这两个方面论述即可。



思路


  • 权限管理需求分析:页面和按钮权限

  • 权限管理的实现方案:分后端方案和前端方案阐述

  • 说说各自的优缺点


回答范例


  1. 权限管理一般需求是页面权限和按钮权限的管理

  2. 具体实现的时候分后端和前端两种方案:


  • 前端方案 会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个asyncRoutes数组,需要认证的页面在其路由的meta中添加一个roles字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过router.addRoutes(accessRoutes)方式动态添加路由即可

  • 后端方案 会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过addRoutes动态添加路由信息

  • 按钮权限的控制通常会实现一个指令,例如v-permission,将按钮要求角色通过值传给 v-permission指令,在指令的moutned钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮


  1. 纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!


可能的追问


  1. 类似Tabs这类组件能不能使用v-permission指令实现按钮权限控制?


<el-tabs>   <el-tab-pane label="⽤户管理" name="first">⽤户管理</el-tab-pane>     <el-tab-pane label="⻆⾊管理" name="third">⻆⾊管理</el-tab-pane></el-tabs>
复制代码


  1. 服务端返回的路由信息如何添加到路由器中?


// 前端组件名和组件映射表const map = {  //xx: require('@/views/xx.vue').default // 同步的⽅式  xx: () => import('@/views/xx.vue') // 异步的⽅式}// 服务端返回的asyncRoutesconst asyncRoutes = [  { path: '/xx', component: 'xx',... }]// 遍历asyncRoutes,将component替换为map[component]function mapComponent(asyncRoutes) {  asyncRoutes.forEach(route => {    route.component = map[route.component];    if(route.children) {      route.children.map(child => mapComponent(child))    }    })}mapComponent(asyncRoutes)
复制代码

你知道哪些 Vue3 新特性?

官网列举的最值得注意的新特性:v3-migration.vuejs.org(opens new window)



  • Composition API

  • SFC Composition API语法糖

  • Teleport传送门

  • Fragments片段

  • Emits选项

  • 自定义渲染器

  • SFC CSS变量

  • Suspense


以上这些是 api 相关,另外还有很多框架特性也不能落掉


回答范例


  1. api层面Vue3新特性主要包括:Composition APISFC Composition API语法糖、Teleport传送门、Fragments 片段、Emits选项、自定义渲染器、SFC CSS变量、Suspense

  2. 另外,Vue3.0在框架层面也有很多亮眼的改进:


  • 更快

  • 虚拟DOM重写,diff算法优化

  • 编译器优化:静态提升、patchFlags(静态标记)、事件监听缓存

  • 基于Proxy的响应式系统

  • SSR优化

  • 更小 :更好的摇树优化 tree shakingVue3移除一些不常用的 API

  • 更友好vue3在兼顾vue2options API的同时还推出了composition API,大大增加了代码的逻辑组织和代码复用能力

  • 更容易维护TypeScript + 模块化

  • 更容易扩展

  • 独立的响应化模块

  • 自定义渲染器

Vue3 有了解过吗?能说说跟 vue2 的区别吗?

1. 哪些变化



从上图中,我们可以概览Vue3的新特性,如下:


  • 速度更快

  • 体积减少

  • 更易维护

  • 更接近原生

  • 更易使用


1.1 速度更快


vue3相比vue2


  • 重写了虚拟Dom实现

  • 编译模板的优化

  • 更高效的组件初始化

  • undate性能提高 1.3~2 倍

  • SSR速度提高了 2~3 倍



1.2 体积更小


通过webpacktree-shaking功能,可以将无用模块“剪辑”,仅打包需要的


能够tree-shaking,有两大好处:


  • 对开发人员,能够对vue实现更多其他的功能,而不必担忧整体体积过大

  • 对使用者,打包出来的包体积变小了


vue可以开发出更多其他的功能,而不必担忧vue打包出来的整体体积过多



1.3 更易维护


compositon Api


  • 可与现有的Options API一起使用

  • 灵活的逻辑组合与复用

  • Vue3模块可以和其他框架搭配使用



更好的 Typescript 支持


VUE3是基于typescipt编写的,可以享受到自动的类型定义提示



1.4 编译器重写



1.5 更接近原生


可以自定义渲染 API



1.6 更易使用


响应式 Api 暴露出来



轻松识别组件重新渲染原因



2. Vue3 新增特性


Vue 3 中需要关注的一些新功能包括:


  • framents

  • Teleport

  • composition Api

  • createRenderer


2.1 framents


Vue3.x 中,组件现在支持有多个根节点


<!-- Layout.vue --><template>  <header>...</header>  <main v-bind="$attrs">...</main>  <footer>...</footer></template>
复制代码


2.2 Teleport


Teleport 是一种能够将我们的模板移动到 DOMVue app 之外的其他位置的技术,就有点像哆啦 A 梦的“任意门”


vue2中,像 modals,toast 等这样的元素,如果我们嵌套在 Vue 的某个组件内部,那么处理嵌套组件的定位、z-index 和样式就会变得很困难


通过Teleport,我们可以在组件的逻辑位置写模板代码,然后在 Vue 应用范围之外渲染它


<button @click="showToast" class="btn">打开 toast</button><!-- to 属性就是目标位置 --><teleport to="#teleport-target">    <div v-if="visible" class="toast-wrap">        <div class="toast-msg">我是一个 Toast 文案</div>    </div></teleport>
复制代码


2.3 createRenderer


通过createRenderer,我们能够构建自定义渲染器,我们能够将 vue 的开发模型扩展到其他平台


我们可以将其生成在canvas画布上



关于createRenderer,我们了解下基本使用,就不展开讲述了


import { createRenderer } from '@vue/runtime-core'
const { render, createApp } = createRenderer({ patchProp, insert, remove, createElement, // ...})
export { render, createApp }
export * from '@vue/runtime-core'
复制代码


2.4 composition Api


composition Api,也就是组合式api,通过这种形式,我们能够更加容易维护我们的代码,将相同功能的变量进行一个集中式的管理



关于compositon api的使用,这里以下图展开



简单使用:


export default {    setup() {        const count = ref(0)        const double = computed(() => count.value * 2)        function increment() {            count.value++        }        onMounted(() => console.log('component mounted!'))        return {            count,            double,            increment        }    }}
复制代码


3. 非兼容变更


3.1 Global API


  • 全局 Vue API 已更改为使用应用程序实例

  • 全局和内部 API 已经被重构为可 tree-shakable


3.2 模板指令


  • 组件上 v-model 用法已更改

  • <template v-for>和 非 v-for节点上key用法已更改

  • 在同一元素上使用的 v-ifv-for 优先级已更改

  • v-bind="object" 现在排序敏感

  • v-for 中的 ref 不再注册 ref 数组


3.3 组件


  • 只能使用普通函数创建功能组件

  • functional 属性在单文件组件 (SFC)

  • 异步组件现在需要 defineAsyncComponent 方法来创建


3.4 渲染函数


  • 渲染函数API改变

  • $scopedSlots property 已删除,所有插槽都通过 $slots 作为函数暴露

  • 自定义指令 API 已更改为与组件生命周期一致

  • 一些转换class被重命名了:

  • v-enter -> v-enter-from

  • v-leave -> v-leave-from

  • 组件 watch 选项和实例方法 $watch不再支持点分隔字符串路径,请改用计算函数作为参数

  • Vue 2.x 中,应用根容器的 outerHTML 将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。VUE3.x 现在使用应用程序容器的 innerHTML


3.5 其他小改变


  • destroyed 生命周期选项被重命名为 unmounted

  • beforeDestroy 生命周期选项被重命名为 beforeUnmount

  • [prop default工厂函数不再有权访问 this 是上下文

  • 自定义指令 API 已更改为与组件生命周期一致

  • data 应始终声明为函数

  • 来自 mixindata 选项现在可简单地合并

  • attribute 强制策略已更改

  • 一些过渡 class 被重命名

  • 组建 watch 选项和实例方法 $watch不再支持以点分隔的字符串路径。请改用计算属性函数作为参数。

  • <template> 没有特殊指令的标记 (v-if/else-if/elsev-forv-slot) 现在被视为普通元素,并将生成原生的 <template> 元素,而不是渲染其内部内容。

  • Vue 2.x 中,应用根容器的 outerHTML 将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。Vue 3.x 现在使用应用容器的 innerHTML,这意味着容器本身不再被视为模板的一部分。


3.6 移除 API


  • keyCode 支持作为 v-on 的修饰符

  • $on$off$once 实例方法

  • 过滤filter

  • 内联模板 attribute

  • $destroy 实例方法。用户不应再手动管理单个Vue 组件的生命周期。

watch 原理

watch 本质上是为每个监听属性 setter 创建了一个 watcher,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deepimmediate,对应原理如下


  • deep:深度监听对象,为对象的每一个属性创建一个 watcher,从而确保对象的每一个属性更新时都会触发传入的回调函数。主要原因在于对象属于引用类型,单个属性的更新并不会触发对象 setter,因此引入 deep 能够很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,避免性能浪费。

  • immediate:在初始化时直接调用回调函数,可以通过在 created 阶段手动调用回调函数实现相同的效果

谈一谈对 Vue 组件化的理解

  • 组件化开发能大幅提高开发效率、测试性、复用性等

  • 常用的组件化技术:属性、自定义事件、插槽

  • 降低更新频率,只重新渲染变化的组件

  • 组件的特点:高内聚、低耦合、单向数据流

说说你对 slot 的理解?slot 使用场景有哪些

一、slot 是什么

在 HTML 中 slot 元素 ,作为 Web Components 技术套件的一部分,是 Web 组件内的一个占位符


该占位符可以在后期使用自己的标记语言填充


举个栗子


<template id="element-details-template">  <slot name="element-name">Slot template</slot></template><element-details>  <span slot="element-name">1</span></element-details><element-details>  <span slot="element-name">2</span></element-details>
复制代码


template不会展示到页面中,需要用先获取它的引用,然后添加到DOM中,


customElements.define('element-details',  class extends HTMLElement {    constructor() {      super();      const template = document        .getElementById('element-details-template')        .content;      const shadowRoot = this.attachShadow({mode: 'open'})        .appendChild(template.cloneNode(true));  }})
复制代码


Vue中的概念也是如此


Slot 艺名插槽,花名“占坑”,我们可以理解为solt在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置),作为承载分发内容的出口

二、使用场景

通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理


如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情


通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用


比如布局组件、表格列、下拉选、弹框显示内容等

你觉得 vuex 有什么缺点

分析


相较于reduxvuex已经相当简便好用了。但模块的使用比较繁琐,对ts支持也不好。


体验


使用模块:用起来比较繁琐,使用模式也不统一,基本上得不到类型系统的任何支持


const store = createStore({  modules: {    a: moduleA  }})store.state.a // -> 要带上 moduleA 的key,内嵌模块的话会很长,不得不配合mapState使用store.getters.c // -> moduleA里的getters,没有namespaced时又变成了全局的store.getters['a/c'] // -> 有namespaced时要加path,使用模式又和state不一样store.commit('d') // -> 没有namespaced时变成了全局的,能同时触发多个子模块中同名mutationstore.commit('a/d') // -> 有namespaced时要加path,配合mapMutations使用感觉也没简化
复制代码


回答范例


  1. vuex利用响应式,使用起来已经相当方便快捷了。但是在使用过程中感觉模块化这一块做的过于复杂,用的时候容易出错,还要经常查看文档

  2. 比如:访问state时要带上模块key,内嵌模块的话会很长,不得不配合mapState使用,加不加namespaced区别也很大,gettersmutationsactions这些默认是全局,加上之后必须用字符串类型的 path 来匹配,使用模式不统一,容易出错;对 ts 的支持也不友好,在使用模块时没有代码提示。

  3. 之前Vue2项目中用过vuex-module-decorators的解决方案,虽然类型支持上有所改善,但又要学一套新东西,增加了学习成本。pinia出现之后使用体验好了很多,Vue3 + pinia会是更好的组合


原理


下面我们来看看vuexstore.state.x.y这种嵌套的路径是怎么搞出来的


首先是子模块安装过程:父模块状态parentState上面设置了子模块名称moduleName,值为当前模块state对象。放在上面的例子中相当于:store.state['x'] = moduleX.state。此过程是递归的,那么store.state.x.y安装时就是:store.state['x']['y'] = moduleY.state


//源码位置 https://github1s.com/vuejs/vuex/blob/HEAD/src/store-util.js#L102-L115if (!isRoot && !hot) {    // 获取父模块state    const parentState = getNestedState(rootState, path.slice(0, -1))    // 获取子模块名称    const moduleName = path[path.length - 1]    store._withCommit(() => {        // 把子模块state设置到父模块上        parentState[moduleName] = module.state    })}
复制代码

Vuex 为什么要分模块并且加命名空间

  • 模块 : 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 statemutationactiongetter、甚至是嵌套子模块

  • 命名空间 :默认情况下,模块内部的 actionmutationgetter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutationaction 作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getteractionmutation 都会自动根据模块注册的路径调整命名

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

  • vue.jsvue-cli工程的核心,主要特点是 双向数据绑定 和 组件系统。

  • vue-routervue官方推荐使用的路由框架。

  • vuex:专为 Vue.js 应用项目开发的状态管理器,主要用于维护vue组件间共用的一些 变量 和 方法。

  • axios( 或者 fetchajax ):用于发起 GET 、或 POSThttp请求,基于 Promise 设计。

  • vuex等:一个专为vue设计的移动端 UI 组件库。

  • 创建一个emit.js文件,用于vue事件机制的管理。

  • webpack:模块加载和vue-cli工程打包器。

Vue3.2 setup 语法糖汇总

提示:vue3.2 版本开始才能使用语法糖!


Vue3.0 中变量必须 return 出来, template 中才能使用;而在 Vue3.2 中只需要在 script 标签上加上 setup 属性,无需 returntemplate 便可直接使用,非常的香啊!


1. 如何使用 setup 语法糖


只需在 script 标签上写上 setup


<template></template><script setup></script><style scoped lang="less"></style>
复制代码


2. data 数据的使用


由于 setup 不需写 return ,所以直接声明数据即可


<script setup>import {  ref,  reactive,  toRefs,} from 'vue'
const data = reactive({ patternVisible: false, debugVisible: false, aboutExeVisible: false,})
const content = ref('content')//使用toRefs解构const { patternVisible, debugVisible, aboutExeVisible } = toRefs(data)</script>
复制代码


3. method 方法的使用


<template >  <button @click="onClickHelp">帮助</button></template><script setup>import {reactive} from 'vue'
const data = reactive({ aboutExeVisible: false,})// 点击帮助const onClickHelp = () => { console.log(`帮助`) data.aboutExeVisible = true}</script>
复制代码


4. watchEffect 的使用


<script setup>import {  ref,  watchEffect,} from 'vue'
let sum = ref(0)
watchEffect(()=>{ const x1 = sum.value console.log('watchEffect所指定的回调执行了')})</script>
复制代码


5. watch 的使用


<script setup>import {  reactive,  watch,} from 'vue'//数据let sum = ref(0)let msg = ref('hello')let person = reactive({  name:'张三',  age:18,  job:{    j1:{      salary:20    }  }})// 两种监听格式watch([sum,msg],(newValue,oldValue)=>{    console.log('sum或msg变了',newValue,oldValue)  },  {immediate:true})
watch(()=>person.job,(newValue,oldValue)=>{ console.log('person的job变化了',newValue,oldValue)},{deep:true})
</script>
复制代码


6. computed 计算属性的使用


computed 计算属性有两种写法(简写和考虑读写的完整写法)


<script setup>import {  reactive,  computed,} from 'vue'
// 数据let person = reactive({ firstName:'poetry', lastName:'x'})
// 计算属性简写person.fullName = computed(()=>{ return person.firstName + '-' + person.lastName})
// 完整写法person.fullName = computed({ get(){ return person.firstName + '-' + person.lastName }, set(value){ const nameArr = value.split('-') person.firstName = nameArr[0] person.lastName = nameArr[1] }})</script>
复制代码


7. props 父子传值的使用


父组件代码如下(示例):


<template>  <child :name='name'/>  </template>
<script setup> import {ref} from 'vue' // 引入子组件 import child from './child.vue' let name= ref('poetry')</script>
复制代码


子组件代码如下(示例):


<template>  <span>{{props.name}}</span></template>
<script setup>import { defineProps } from 'vue'// 声明propsconst props = defineProps({ name: { type: String, default: 'poetries' }}) // 或者//const props = defineProps(['name'])</script>
复制代码


8. emit 子父传值的使用


父组件代码如下(示例):


<template>  <AdoutExe @aboutExeVisible="aboutExeHandleCancel" /></template><script setup>import { reactive } from 'vue'// 导入子组件import AdoutExe from '../components/AdoutExeCom'
const data = reactive({ aboutExeVisible: false, })// content组件ref
// 关于系统隐藏const aboutExeHandleCancel = () => { data.aboutExeVisible = false}</script>
复制代码


子组件代码如下(示例):


<template>  <a-button @click="isOk">    确定  </a-button></template><script setup>import { defineEmits } from 'vue';
// emitconst emit = defineEmits(['aboutExeVisible'])/** * 方法 */// 点击确定按钮const isOk = () => { emit('aboutExeVisible');}</script>
复制代码


9. 获取子组件 ref 变量和 defineExpose 暴露


vue2中的获取子组件的ref,直接在父组件中控制子组件方法和变量的方法


父组件代码如下(示例):


<template>  <button @click="onClickSetUp">点击</button>  <Content ref="content" /></template>
<script setup>import {ref} from 'vue'
// content组件refconst content = ref('content')// 点击设置const onClickSetUp = ({ key }) => { content.value.modelVisible = true}</script><style scoped lang="less"></style>
复制代码


子组件代码如下(示例):


<template>   <p>{{data }}</p></template>
<script setup>import { reactive, toRefs} from 'vue'
/** * 数据部分* */const data = reactive({ modelVisible: false, historyVisible: false, reportVisible: false, })
defineExpose({ ...toRefs(data),})</script>
复制代码


10. 路由 useRoute 和 useRouter 的使用


<script setup>  import { useRoute, useRouter } from 'vue-router'
// 声明 const route = useRoute() const router = useRouter()
// 获取query console.log(route.query) // 获取params console.log(route.params)
// 路由跳转 router.push({ path: `/index` })</script>
复制代码


11. store 仓库的使用


<script setup>  import { useStore } from 'vuex'  import { num } from '../store/index'
const store = useStore(num)
// 获取Vuex的state console.log(store.state.number) // 获取Vuex的getters console.log(store.state.getNumber)
// 提交mutations store.commit('fnName')
// 分发actions的方法 store.dispatch('fnName')</script>
复制代码


12. await 的支持


setup语法糖中可直接使用await,不需要写asyncsetup会自动变成async setup


<script setup>  import api from '../api/Api'  const data = await Api.getData()  console.log(data)</script>
复制代码


13. provide 和 inject 祖孙传值


父组件代码如下(示例):


<template>  <AdoutExe /></template>
<script setup> import { ref,provide } from 'vue' import AdoutExe from '@/components/AdoutExeCom'
let name = ref('py') // 使用provide provide('provideState', { name, changeName: () => { name.value = 'poetries' } })</script>
复制代码


子组件代码如下(示例):


<script setup>  import { inject } from 'vue'  const provideState = inject('provideState')
provideState.changeName()</script>
复制代码

Vue-router 跳转和 location.href 有什么区别

  • 使用 location.href= /url 来跳转,简单方便,但是刷新了页面;

  • 使用 history.pushState( /url ) ,无刷新页面,静态跳转;

  • 引进 router ,然后使用 router.push( /url ) 来跳转,使用了 diff 算法,实现了按需加载,减少了 dom 的消耗。其实使用 router 跳转和使用 history.pushState() 没什么差别的,因为 vue-router 就是用了 history.pushState() ,尤其是在 history 模式下。

delete 和 Vue.delete 删除数组的区别?

  • delete只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。

  • Vue.delete直接删除了数组 改变了数组的键值。


var a=[1,2,3,4]var b=[1,2,3,4]delete a[0]console.log(a)  //[empty,2,3,4]this.$delete(b,0)console.log(b)  //[2,3,4]
复制代码

vue-loader 是什么?它有什么作用?

回答范例


  1. vue-loader是用于处理单文件组件(SFCSingle-File Component)的webpack loader

  2. 因为有了vue-loader,我们就可以在项目中编写SFC格式的Vue组件,我们可以把代码分割为<template><script><style>,代码会异常清晰。结合其他loader我们还可以用Pug编写<template>,用SASS编写<style>,用TS编写<script>。我们的<style>还可以单独作用当前组件

  3. webpack打包时,会以loader的方式调用vue-loader

  4. vue-loader被执行时,它会对SFC中的每个语言块用单独的loader链处理。最后将这些单独的块装配成最终的组件模块


原理


vue-loader会调用@vue/compiler-sfc模块解析SFC源码为一个描述符(Descriptor),然后为每个语言块生成import代码,返回的代码类似下面


// source.vue被vue-loader处理之后返回的代码// import the <template> blockimport render from 'source.vue?vue&type=template'// import the <script> blockimport script from 'source.vue?vue&type=script'export * from 'source.vue?vue&type=script'// import <style> blocksimport 'source.vue?vue&type=style&index=1'script.render = renderexport default script
复制代码


我们想要script块中的内容被作为js处理(当然如果是<script lang="ts">被作为ts理),这样我们想要webpack把配置中跟.js匹配的规则都应用到形如source.vue?vue&type=script的这个请求上。例如我们对所有*.js配置了babel-loader,这个规则将被克隆并应用到所在Vue SFC


import script from 'source.vue?vue&type=script
复制代码


将被展开为:


import script from 'babel-loader!vue-loader!source.vue?vue&type=script'
复制代码


类似的,如果我们对.sass文件配置了style-loader + css-loader + sass-loader,对下面的代码


<style scoped lang="scss">
复制代码


vue-loader将会返回给我们下面结果:


import 'source.vue?vue&type=style&index=1&scoped&lang=scss'
复制代码


然后webpack会展开如下:


import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
复制代码


  • 当处理展开请求时,vue-loader将被再次调用。这次,loader将会关注那些有查询串的请求,且仅针对特定块,它会选中特定块内部的内容并传递给后面匹配的loader

  • 对于<script>块,处理到这就可以了,但是<template><style>还有一些额外任务要做,比如

  • 需要用 Vue 模板编译器编译template,从而得到render函数

  • 需要对 <style scoped>中的CSS做后处理(post-process),该操作在css-loader之后但在style-loader之前


实现上这些附加的loader需要被注入到已经展开的loader链上,最终的请求会像下面这样:


// <template lang="pug">import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'// <style scoped lang="scss">import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
复制代码

Vue 中的过滤器了解吗?过滤器的应用场景有哪些?

过滤器实质不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理,我们也可以理解其为一个纯函数


Vue 允许你自定义过滤器,可被用于一些常见的文本格式化


ps: Vue3中已废弃filter

如何用

vue 中的过滤器可以用在两个地方:双花括号插值和 v-bind 表达式,过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:


<!-- 在双花括号中 -->{ message | capitalize }
<!-- 在 `v-bind` 中 --><div v-bind:id="rawId | formatId"></div>
复制代码

定义 filter

在组件的选项中定义本地的过滤器


filters: {  capitalize: function (value) {    if (!value) return ''    value = value.toString()    return value.charAt(0).toUpperCase() + value.slice(1)  }}
复制代码


定义全局过滤器:


Vue.filter('capitalize', function (value) {  if (!value) return ''  value = value.toString()  return value.charAt(0).toUpperCase() + value.slice(1)})
new Vue({ // ...})
复制代码


注意:当全局过滤器和局部过滤器重名时,会采用局部过滤器


过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。在上述例子中,capitalize 过滤器函数将会收到 message 的值作为第一个参数


过滤器可以串联:


{ message | filterA | filterB }
复制代码


在这个例子中,filterA 被定义为接收单个参数的过滤器函数,表达式 message 的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 filterB,将 filterA 的结果传递到 filterB 中。


过滤器是 JavaScript函数,因此可以接收参数:


{{ message | filterA('arg1', arg2) }}
复制代码


这里,filterA 被定义为接收三个参数的过滤器函数。


其中 message 的值作为第一个参数,普通字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数


举个例子:


<div id="app">  <p>{{ msg | msgFormat('疯狂','--')}}</p></div>
<script> // 定义一个 Vue 全局的过滤器,名字叫做 msgFormat Vue.filter('msgFormat', function(msg, arg, arg2) { // 字符串的 replace 方法,第一个参数,除了可写一个 字符串之外,还可以定义一个正则 return msg.replace(/单纯/g, arg+arg2) })</script>
复制代码


小结:


  • 部过滤器优先于全局过滤器被调用

  • 一个表达式可以使用多个过滤器。过滤器之间需要用管道符“|”隔开。其执行顺序从左往右

应用场景

平时开发中,需要用到过滤器的地方有很多,比如单位转换数字打点文本格式化时间格式化之类的等


比如我们要实现将30000 => 30,000,这时候我们就需要使用过滤器


Vue.filter('toThousandFilter', function (value) {  if (!value) return ''  value = value.toString()  return .replace(str.indexOf('.') > -1 ? /(\d)(?=(\d{3})+\.)/g : /(\d)(?=(?:\d{3})+$)/g, '$1,')})
复制代码

原理分析

使用过滤器


{{ message | capitalize }}
复制代码


在模板编译阶段过滤器表达式将会被编译为过滤器函数,主要是用过parseFilters,我们放到最后讲


_s(_f('filterFormat')(message))
复制代码


首先分析一下_f


_f 函数全名是:resolveFilter,这个函数的作用是从this.$options.filters中找出注册的过滤器并返回


// 变为this.$options.filters['filterFormat'](message) // message为参数
复制代码


关于resolveFilter


import { indentity,resolveAsset } from 'core/util/index' 
export function resolveFilter(id){ return resolveAsset(this.$options,'filters',id,true) || identity}
复制代码


内部直接调用resolveAsset,将option对象,类型,过滤器id,以及一个触发警告的标志作为参数传递,如果找到,则返回过滤器;


resolveAsset的代码如下:


export function resolveAsset(options,type,id,warnMissing){ // 因为我们找的是过滤器,所以在 resolveFilter函数中调用时 type 的值直接给的 'filters',实际这个函数还可以拿到其他很多东西  if(typeof id !== 'string'){ // 判断传递的过滤器id 是不是字符串,不是则直接返回      return   }  const assets = options[type]  // 将我们注册的所有过滤器保存在变量中  // 接下来的逻辑便是判断id是否在assets中存在,即进行匹配  if(hasOwn(assets,id)) return assets[id] // 如找到,直接返回过滤器  // 没有找到,代码继续执行  const camelizedId  = camelize(id) // 万一你是驼峰的呢  if(hasOwn(assets,camelizedId)) return assets[camelizedId]  // 没找到,继续执行  const PascalCaseId = capitalize(camelizedId) // 万一你是首字母大写的驼峰呢  if(hasOwn(assets,PascalCaseId)) return assets[PascalCaseId]  // 如果还是没找到,则检查原型链(即访问属性)  const result = assets[id] || assets[camelizedId] || assets[PascalCaseId]  // 如果依然没找到,则在非生产环境的控制台打印警告  if(process.env.NODE_ENV !== 'production' && warnMissing && !result){    warn('Failed to resolve ' + type.slice(0,-1) + ': ' + id, options)  }  // 无论是否找到,都返回查找结果  return result}
复制代码


下面再来分析一下_s


_s 函数的全称是 toString,过滤器处理后的结果会当作参数传递给 toString函数,最终 toString函数执行后的结果会保存到Vnode中的 text 属性中,渲染到视图中


function toString(value){  return value == null  ? ''  : typeof value === 'object'    ? JSON.stringify(value,null,2)// JSON.stringify()第三个参数可用来控制字符串里面的间距    : String(value)}
复制代码


最后,在分析下parseFilters,在模板编译阶段使用该函数阶段将模板过滤器解析为过滤器函数调用表达式


function parseFilters (filter) {    let filters = filter.split('|')    let expression = filters.shift().trim() // shift()删除数组第一个元素并将其返回,该方法会更改原数组    let i    if (filters) {        for(i = 0;i < filters.length;i++){            experssion = warpFilter(expression,filters[i].trim()) // 这里传进去的expression实际上是管道符号前面的字符串,即过滤器的第一个参数        }    }    return expression}// warpFilter函数实现function warpFilter(exp,filter){    // 首先判断过滤器是否有其他参数    const i = filter.indexof('(')    if(i<0){ // 不含其他参数,直接进行过滤器表达式字符串的拼接        return `_f("${filter}")(${exp})`    }else{        const name = filter.slice(0,i) // 过滤器名称        const args = filter.slice(i+1) // 参数,但还多了 ‘)’        return `_f('${name}')(${exp},${args}` // 注意这一步少给了一个 ')'    }}
复制代码


小结:


  • 在编译阶段通过parseFilters将过滤器编译成函数调用(串联过滤器则是一个嵌套的函数调用,前一个过滤器执行的结果是后一个过滤器函数的参数)

  • 编译后通过调用resolveFilter函数找到对应过滤器并返回结果

  • 执行结果作为参数传递给toString函数,而toString执行后,其结果会保存在Vnodetext属性中,渲染到视图

Vue3 速度快的原因

Vue3.0 性能提升体现在哪些方面


  • 代码层面性能优化主要体现在全新响应式API,基于Proxy实现,初始化时间和内存占用均大幅改进;

  • 编译层面做了更多编译优化处理,比如静态标记pachFlagdiff算法增加了一个静态标记,只对比有标记的dom元素)、事件增加缓存静态提升(对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用)等,可以有效跳过大量diff过程;

  • 打包时更好的支持tree-shaking,因此整体体积更小,加载更快

  • ssr渲染以字符串方式渲染


一、编译阶段


试想一下,一个组件结构如下图


<template>    <div id="content">        <p class="text">静态文本</p>        <p class="text">静态文本</p>        <p class="text">{ message }</p>        <p class="text">静态文本</p>        ...        <p class="text">静态文本</p>    </div></template>
复制代码


可以看到,组件内部只有一个动态节点,剩余一堆都是静态节点,所以这里很多 diff 和遍历其实都是不需要的,造成性能浪费


因此,Vue3 在编译阶段,做了进一步优化。主要有如下:


  • diff算法优化

  • 静态提升

  • 事件监听缓存

  • SSR优化


1. diff 算法优化


  • Vue 2x 中的虚拟 dom 是进行全量的对比。

  • Vue 3x 中新增了静态标记(PatchFlag):在与上次虚拟结点进行对比的时候,值对比 带有 patch flag 的节点,并且可以通过 flag 的信息得知当前节点要对比的具体内容化


Vue2.x 的 diff 算法


vue2.xdiff算法叫做全量比较,顾名思义,就是当数据改变的时候,会从头到尾的进行vDom对比,即使有些内容是永恒固定不变的



Vue3.0 的 diff 算法


vue3.0diff算法有个叫静态标记(PatchFlag)的小玩意,啥是静态标记呢?简单点说,就是如果你的内容会变,我会给你一个flag,下次数据更新的时候我直接来对比你,我就不对比那些没有标记的了



已经标记静态节点的p标签在diff过程中则不会比较,把性能进一步提高


export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [  _createVNode("p", null, "'HelloWorld'"),  _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)                        //上面这个1就是静态标记 ]))}
复制代码


关于静态类型枚举如下


TEXT = 1 // 动态文本节点CLASS=1<<1,1 // 2//动态classSTYLE=1<<2,// 4 //动态stylePROPS=1<<3,// 8 //动态属性,但不包含类名和样式FULLPR0PS=1<<4,// 16 //具有动态key属性,当key改变时,需要进行完整的diff比较。HYDRATE_ EVENTS = 1 << 5,// 32 //带有监听事件的节点STABLE FRAGMENT = 1 << 6, // 64 //一个不会改变子节点顺序的fragmentKEYED_ FRAGMENT = 1 << 7, // 128 //带有key属性的fragment 或部分子字节有keyUNKEYED FRAGMENT = 1<< 8, // 256 //子节点没有key 的fragmentNEED PATCH = 1 << 9, // 512 //一个节点只会进行非props比较DYNAMIC_SLOTS = 1 << 10 // 1024 // 动态slotHOISTED = -1 // 静态节点// 指示在diff算法中退出优化模式BALL = -2
复制代码


2. hoistStatic 静态提升


  • Vue 2x : 无论元素是否参与更新,每次都会重新创建。

  • Vue 3x : 对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用。这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用


<p>HelloWorld</p><p>HelloWorld</p>
<p>{ message }</p>
复制代码


开启静态提升前


export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [  _createVNode("p", null, "'HelloWorld'"),  _createVNode("p", null, "'HelloWorld'"),  _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */) ]))}
复制代码


开启静态提升后编译结果


const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */)const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [ _hoisted_1, _hoisted_2, _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */) ]))}
复制代码


可以看到开启了静态提升后,直接将那两个内容为helloworldp标签声明在外面了,直接就拿来用了。同时 _hoisted_1_hoisted_2 被打上了 PatchFlag ,静态标记值为 -1 ,特殊标志是负整数表示永远不会用于 Diff


3. cacheHandlers 事件监听缓存


  • 默认情况下 绑定事件会被视为动态绑定 ,所以每次都会去追踪它的变化

  • 但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可


<div> <button @click = 'onClick'>点我</button></div>
复制代码


开启事件侦听器缓存之前:


export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [  _createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])                       // PROPS=1<<3,// 8 //动态属性,但不包含类名和样式 ]))})
复制代码


这里有一个8,表示着这个节点有了静态标记,有静态标记就会进行diff算法对比差异,所以会浪费时间


开启事件侦听器缓存之后:


export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [  _createVNode("button", {   onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))  }, "点我") ]))}
复制代码


上述发现开启了缓存后,没有了静态标记。也就是说下次diff算法的时候直接使用


4. SSR 优化


当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染


<div>    <div>        <span>你好</span>    </div>    ...  // 很多个静态属性    <div>        <span>{{ message }}</span>    </div></div>
复制代码


编译后


import { mergeProps as _mergeProps } from "vue"import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"
export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) { const _cssVars = { style: { color: _ctx.color }} _push(`<div${ _ssrRenderAttrs(_mergeProps(_attrs, _cssVars)) }><div><span>你好</span>...<div><span>你好</span><div><span>${ _ssrInterpolate(_ctx.message) }</span></div></div>`)}
复制代码


二、源码体积


相比Vue2Vue3整体体积变小了,除了移出一些不常用的API,再重要的是Tree shanking


任何一个函数,如refreactivecomputed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小


import { computed, defineComponent, ref } from 'vue';export default defineComponent({  setup(props, context) {    const age = ref(18)
let state = reactive({ name: 'test' })
const readOnlyAge = computed(() => age.value++) // 19
return { age, state, readOnlyAge } }});
复制代码


三、响应式系统


vue2中采用 defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加gettersetter,实现响应式


vue3采用proxy重写了响应式系统,因为proxy可以对整个对象进行监听,所以不需要深度遍历


  • 可以监听动态属性的添加

  • 可以监听到数组的索引和数组length属性

  • 可以监听删除属性


Vue 的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)


相关代码如下


export function callHook(vm, hook) {  // 依次执行生命周期对应的方法  const handlers = vm.$options[hook];  if (handlers) {    for (let i = 0; i < handlers.length; i++) {      handlers[i].call(vm); //生命周期里面的this指向当前实例    }  }}
// 调用的时候Vue.prototype._init = function (options) { const vm = this; vm.$options = mergeOptions(vm.constructor.options, options); callHook(vm, "beforeCreate"); //初始化数据之前 // 初始化状态 initState(vm); callHook(vm, "created"); //初始化数据之后 if (vm.$options.el) { vm.$mount(vm.$options.el); }};
复制代码

Vue.set 的实现原理

  • 给对应和数组本身都增加了dep属性

  • 当给对象新增不存在的属性则触发对象依赖的watcher去更新

  • 当修改数组索引时,我们调用数组本身的splice去更新数组(数组的响应式原理就是重新了splice等方法,调用splice就会触发视图更新)


基本使用


以下方法调用会改变原始数组:push(), pop(), shift(), unshift(), splice(), sort(), reverse(),Vue.set( target, key, value )


  • 调用方法:Vue.set(target, key, value )

  • target:要更改的数据源(可以是对象或者数组)

  • key:要更改的具体数据

  • value :重新赋的值


<div id="app">{{user.name}} {{user.age}}</div><div id="app"></div><script>    // 1. 依赖收集的特点:给每个属性都增加一个dep属性,dep属性会进行收集,收集的是watcher    // 2. vue会给每个对象也增加一个dep属性    const vm = new Vue({        el: '#app',        data: { // vm._data              user: {name:'poetry'}        }    });    // 对象的话:调用defineReactive在user对象上定义一个age属性,增加到响应式数据中,触发对象本身的watcher,ob.dep.notify()更新     // 如果是数组 通过调用 splice方法,触发视图更新    vm.$set(vm.user, 'age', 20); // 不能给根属性添加,因为给根添加属性 性能消耗太大,需要做很多处理
// 修改肯定是同步的 -> 更新都是一步的 queuewatcher</script>
复制代码


相关源码


// src/core/observer/index.js 44export class Observer { // new Observer(value)  value: any;  dep: Dep;  vmCount: number; // number of vms that have this object as root $data
constructor (value: any) { this.value = value this.dep = new Dep() // 给所有对象类型增加dep属性 }}
复制代码


// src/core/observer/index.js 201export function set (target: Array<any> | Object, key: any, val: any): any {  // 1.是开发环境 target 没定义或者是基础类型则报错  if (process.env.NODE_ENV !== 'production' &&    (isUndef(target) || isPrimitive(target))  ) {    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)  }  // 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)  if (Array.isArray(target) && isValidArrayIndex(key)) {    target.length = Math.max(target.length, key)    // 利用数组的splice变异方法触发响应式      target.splice(key, 1, val)    return val  }  // 3.如果是对象本身的属性,则直接添加即可  if (key in target && !(key in Object.prototype)) {    target[key] = val // 直接修改属性值      return val  }  // 4.如果是Vue实例 或 根数据data时 报错,(更新_data 无意义)  const ob = (target: any).__ob__  if (target._isVue || (ob && ob.vmCount)) {    process.env.NODE_ENV !== 'production' && warn(      'Avoid adding reactive properties to a Vue instance or its root $data ' +      'at runtime - declare it upfront in the data option.'    )    return val  }  // 5.如果不是响应式的也不需要将其定义成响应式属性  if (!ob) {    target[key] = val    return val  }  // 6.将属性定义成响应式的  defineReactive(ob.value, key, val)  // 通知视图更新  ob.dep.notify()  return val}
复制代码


我们阅读以上源码可知,vm.$set 的实现原理是:


  • 如果目标是数组 ,直接使用数组的 splice 方法触发相应式;

  • 如果目标是对象 ,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 gettersetter 的功能所调用的方法)


前端vue面试题详细解答

nextTick 使用场景和原理

nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法


相关代码如下


let callbacks = [];let pending = false;function flushCallbacks() {  pending = false; //把标志还原为false  // 依次执行回调  for (let i = 0; i < callbacks.length; i++) {    callbacks[i]();  }}let timerFunc; //定义异步方法  采用优雅降级if (typeof Promise !== "undefined") {  // 如果支持promise  const p = Promise.resolve();  timerFunc = () => {    p.then(flushCallbacks);  };} else if (typeof MutationObserver !== "undefined") {  // MutationObserver 主要是监听dom变化 也是一个异步方法  let counter = 1;  const observer = new MutationObserver(flushCallbacks);  const textNode = document.createTextNode(String(counter));  observer.observe(textNode, {    characterData: true,  });  timerFunc = () => {    counter = (counter + 1) % 2;    textNode.data = String(counter);  };} else if (typeof setImmediate !== "undefined") {  // 如果前面都不支持 判断setImmediate  timerFunc = () => {    setImmediate(flushCallbacks);  };} else {  // 最后降级采用setTimeout  timerFunc = () => {    setTimeout(flushCallbacks, 0);  };}
export function nextTick(cb) { // 除了渲染watcher 还有用户自己手动调用的nextTick 一起被收集到数组 callbacks.push(cb); if (!pending) { // 如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为false pending = true; timerFunc(); }}
复制代码

computed 的实现原理

computed 本质是一个惰性求值的观察者。


computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。


其内部通过 this.dirty 属性标记计算属性是否需要重新求值。


当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,


computed watcher 通过 this.dep.subs.length 判断有没有订阅者,


有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)


没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)

Vue 为什么没有类似于 React 中 shouldComponentUpdate 的生命周期

  • 考点: Vue的变化侦测原理

  • 前置知识: 依赖收集、虚拟DOM、响应式系统


根本原因是VueReact的变化侦测方式有所不同


  • 当 React 知道发生变化后,会使用Virtual Dom Diff进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要 shouldComponentUpdate 进行手动操作来减少diff,从而提高程序整体的性能

  • Vue在一开始就知道那个组件发生了变化,不需要手动控制diff,而组件内部采用的diff方式实际上是可以引入类似于shouldComponentUpdate相关生命周期的,但是通常合理大小的组件不会有过量的 diff,手动优化的价值有限,因此目前Vue并没有考虑引入shouldComponentUpdate这种手动优化的生命周期

Vue 模版编译原理知道吗,能简单说一下吗?

简单说,Vue 的编译过程就是将template转化为render函数的过程。会经历以下阶段:


  • 生成 AST 树

  • 优化

  • codegen


首先解析模版,生成AST语法树(一种用 JavaScript 对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。


Vue 的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的 DOM 也不会变化。那么优化过程就是深度遍历 AST 树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用。


编译的最后一步是将优化后的AST树转换为可执行的代码

谈谈你对 SPA 单页面的理解

SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTMLJavaScriptCSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载


优点:


  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;

  • 基于上面一点,SPA 相对对服务器压力小;

  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理


缺点:


  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScriptCSS 统一加载,部分页面按需加载;

  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;

  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势


单页应用与多页应用的区别



实现一个 SPA


  • 监听地址栏中hash变化驱动界面变化

  • pushsate记录浏览器的历史,驱动界面发送变化



  1. hash 模式 :核心通过监听url中的hash来进行路由跳转


// 定义 Router  class Router {      constructor () {          this.routes = {}; // 存放路由path及callback          this.currentUrl = '';  
// 监听路由change调用相对应的路由回调 window.addEventListener('load', this.refresh, false); window.addEventListener('hashchange', this.refresh, false); }
route(path, callback){ this.routes[path] = callback; }
push(path) { this.routes[path] && this.routes[path]() } }
// 使用 router window.miniRouter = new Router(); miniRouter.route('/', () => console.log('page1')) miniRouter.route('/page2', () => console.log('page2'))
miniRouter.push('/') // page1 miniRouter.push('/page2') // page2
复制代码


  1. history 模式history 模式核心借用 HTML5 history apiapi 提供了丰富的 router 相关属性先了解一个几个相关的 api


  • history.pushState 浏览器历史纪录添加记录

  • history.replaceState修改浏览器历史纪录中当前纪录

  • history.popStatehistory 发生变化时触发


// 定义 Router  class Router {      constructor () {          this.routes = {};          this.listerPopState()      }  
init(path) { history.replaceState({path: path}, null, path); this.routes[path] && this.routes[path](); }
route(path, callback){ this.routes[path] = callback; }
push(path) { history.pushState({path: path}, null, path); this.routes[path] && this.routes[path](); }
listerPopState () { window.addEventListener('popstate' , e => { const path = e.state && e.state.path; this.routers[path] && this.routers[path]() }) } }
// 使用 Router
window.miniRouter = new Router(); miniRouter.route('/', ()=> console.log('page1')) miniRouter.route('/page2', ()=> console.log('page2'))
// 跳转 miniRouter.push('/page2') // page2
复制代码


题外话:如何给 SPA 做 SEO


  1. SSR 服务端渲染


将组件或页面通过服务器生成html,再返回给浏览器,如nuxt.js


  1. 静态化


目前主流的静态化主要有两种:


  • 一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中

  • 另外一种是通过 WEB 服务器的 URL Rewrite的方式,它的原理是通过 web 服务器内部模块按一定规则将外部的 URL 请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现 URL 静态化的效果


  1. 使用Phantomjs针对爬虫处理


原理是通过Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。下面是大致流程图


Vue 中封装的数组方法有哪些,其如何实现页面更新

在 Vue 中,对响应式处理利用的是 Object.defineProperty 对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行 hack,让 Vue 能监听到其中的变化。 那 Vue 是如何实现让这些数组方法实现元素的实时更新的呢,下面是 Vue 中对这些方法的封装:


// 缓存数组原型const arrayProto = Array.prototype;// 实现 arrayMethods.__proto__ === Array.prototypeexport const arrayMethods = Object.create(arrayProto);// 需要进行功能拓展的方法const methodsToPatch = [  "push",  "pop",  "shift",  "unshift",  "splice",  "sort",  "reverse"];
/** * Intercept mutating methods and emit events */methodsToPatch.forEach(function(method) { // 缓存原生数组方法 const original = arrayProto[method]; def(arrayMethods, method, function mutator(...args) { // 执行并缓存原生数组功能 const result = original.apply(this, args); // 响应式处理 const ob = this.__ob__; let inserted; switch (method) { // push、unshift会新增索引,所以要手动observer case "push": case "unshift": inserted = args; break; // splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。 case "splice": inserted = args.slice(2); break; } // if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听 // notify change ob.dep.notify();// 通知依赖更新 // 返回原生数组方法的执行结果 return result; });});
复制代码


简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的__ob__,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 继续对新的值观察变化(也就是通过target__proto__ == arrayMethods来改变了数组实例的型),然后手动调用 notify,通知渲染 watcher,执行 update。

子组件可以直接改变父组件的数据么,说明原因

这是一个实践知识点,组件化开发过程中有个单项数据流原则,不在子组件中修改父组件是个常识问题


思路


  • 讲讲单项数据流原则,表明为何不能这么做

  • 举几个常见场景的例子说说解决方案

  • 结合实践讲讲如果需要修改父组件状态应该如何做


回答范例


  1. 所有的 prop 都使得其父子之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。另外,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器控制台中发出警告


const props = defineProps(['foo'])// ❌ 下面行为会被警告, props是只读的!props.foo = 'bar'
复制代码


  1. 实际开发过程中有两个场景会想要修改一个属性:


这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data,并将这个 prop 用作其初始值:


const props = defineProps(['initialCounter'])const counter = ref(props.initialCounter)
复制代码


这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性:


const props = defineProps(['size'])// prop变化,计算属性自动更新const normalizedSize = computed(() => props.size.trim().toLowerCase())
复制代码


  1. 实践中如果确实想要改变父组件属性应该emit一个事件让父组件去做这个变更。注意虽然我们不能直接修改一个传入的对象或者数组类型的prop,但是我们还是能够直接改内嵌的对象或属性

delete 和 Vue.delete 删除数组的区别

  • delete 只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。

  • Vue.delete 直接删除了数组 改变了数组的键值。

Vue 的 diff 算法详细分析

1. 是什么


diff 算法是一种通过同层的树节点进行比较的高效算法


其有两个特点:


  • 比较只会在同层级进行, 不会跨层级比较

  • 在 diff 比较的过程中,循环从两边向中间比较


diff 算法在很多场景下都有应用,在 vue 中,作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较


2. 比较方式


diff整体策略为:深度优先,同层比较


  1. 比较只会在同层级进行, 不会跨层级比较



  1. 比较的过程中,循环从两边向中间收拢



下面举个vue通过diff算法更新的例子:


新旧VNode节点如下图所示:



第一次循环后,发现旧节点 D 与新节点 D 相同,直接复用旧节点 D 作为diff后的第一个真实节点,同时旧节点endIndex移动到 C,新节点的 startIndex 移动到了 C



第二次循环后,同样是旧节点的末尾和新节点的开头(都是 C)相同,同理,diff 后创建了 C 的真实节点插入到第一次创建的 D 节点后面。同时旧节点的 endIndex 移动到了 B,新节点的 startIndex 移动到了 E



第三次循环中,发现 E 没有找到,这时候只能直接创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 startIndex 移动到了 A。旧节点的 startIndexendIndex 都保持不动



第四次循环中,发现了新旧节点的开头(都是 A)相同,于是 diff 后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 startIndex 移动到了 B,新节点的startIndex 移动到了 B



第五次循环中,情形同第四次循环一样,因此 diff 后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex移动到了 C,新节点的 startIndex 移动到了 F



新节点的 startIndex 已经大于 endIndex 了,需要创建 newStartIdxnewEndIdx 之间的所有节点,也就是节点 F,直接创建 F 节点对应的真实节点放到 B 节点后面



3. 原理分析


当数据发生改变时,set方法会调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁,更新相应的视图


源码位置:src/core/vdom/patch.js


function patch(oldVnode, vnode, hydrating, removeOnly) {    if (isUndef(vnode)) { // 没有新节点,直接执行destory钩子函数        if (isDef(oldVnode)) invokeDestroyHook(oldVnode)        return    }
let isInitialPatch = false const insertedVnodeQueue = []
if (isUndef(oldVnode)) { isInitialPatch = true createElm(vnode, insertedVnodeQueue) // 没有旧节点,直接用新节点生成dom元素 } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // 判断旧节点和新节点自身一样,一致执行patchVnode patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { // 否则直接销毁及旧节点,根据新节点生成dom元素 if (isRealElement) {
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } } oldVnode = emptyNodeAt(oldVnode) } return vnode.elm } }}
复制代码


patch函数前两个参数位为oldVnodeVnode ,分别代表新的节点和之前的旧节点,主要做了四个判断:


  • 没有新节点,直接触发旧节点的destory钩子

  • 没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用 createElm

  • 旧节点和新节点自身一样,通过 sameVnode 判断节点是否一样,一样时,直接调用 patchVnode去处理这两个节点

  • 旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点


下面主要讲的是patchVnode部分


function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {    // 如果新旧节点一致,什么都不做    if (oldVnode === vnode) {      return    }
// 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化 const elm = vnode.elm = oldVnode.elm
// 异步占位符 if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } // 如果新旧都是静态节点,并且具有相同的key // 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上 // 也不用再有其他操作 if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance return }
let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) }
const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } // 如果vnode不是文本节点或者注释节点 if (isUndef(vnode.text)) { // 并且都有子节点 if (isDef(oldCh) && isDef(ch)) { // 并且子节点不完全一致,则调用updateChildren if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
// 如果只有新的vnode有子节点 } else if (isDef(ch)) { if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') // elm已经引用了老的dom节点,在老的dom节点上添加子节点 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh } else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1)
// 如果老节点是文本节点 } else if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, '') }
// 如果新vnode和老vnode是文本节点或注释节点 // 但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以 } else if (oldVnode.text !== vnode.text) { nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }
复制代码


patchVnode主要做了几个判断:


  • 新节点是否是文本节点,如果是,则直接更新dom的文本内容为新节点的文本内容

  • 新节点和旧节点如果都有子节点,则处理比较更新子节点

  • 只有新节点有子节点,旧节点没有,那么不用比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新DOM,并且添加进父节点

  • 只有旧节点有子节点而新节点没有,说明更新后的页面,旧节点全部都不见了,那么要做的,就是把所有的旧节点删除,也就是直接把DOM 删除


子节点不完全一致,则调用updateChildren


function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {    let oldStartIdx = 0 // 旧头索引    let newStartIdx = 0 // 新头索引    let oldEndIdx = oldCh.length - 1 // 旧尾索引    let newEndIdx = newCh.length - 1 // 新尾索引    let oldStartVnode = oldCh[0] // oldVnode的第一个child    let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child    let newStartVnode = newCh[0] // newVnode的第一个child    let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child    let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group> // to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly
// 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 如果oldVnode的第一个child不存在 if (isUndef(oldStartVnode)) { // oldStart索引右移 oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
// 如果oldVnode的最后一个child不存在 } else if (isUndef(oldEndVnode)) { // oldEnd索引左移 oldEndVnode = oldCh[--oldEndIdx]
// oldStartVnode和newStartVnode是同一个节点 } else if (sameVnode(oldStartVnode, newStartVnode)) { // patch oldStartVnode和newStartVnode, 索引左移,继续循环 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx]
// oldEndVnode和newEndVnode是同一个节点 } else if (sameVnode(oldEndVnode, newEndVnode)) { // patch oldEndVnode和newEndVnode,索引右移,继续循环 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx]
// oldStartVnode和newEndVnode是同一个节点 } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right // patch oldStartVnode和newEndVnode patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) // 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后 canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) // oldStart索引右移,newEnd索引左移 oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx]
// 如果oldEndVnode和newStartVnode是同一个节点 } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left // patch oldEndVnode和newStartVnode patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) // 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前 canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) // oldEnd索引左移,newStart索引右移 oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx]
// 如果都不匹配 } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果未找到,说明newStartVnode是一个新的节点 if (isUndef(idxInOld)) { // New element // 创建一个新Vnode createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
// 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove } else { vnodeToMove = oldCh[idxInOld] /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !vnodeToMove) { warn( 'It seems there are duplicate keys that is causing an update error. ' + 'Make sure each v-for item has a unique key.' ) }
// 比较两个具有相同的key的新节点是否是同一个节点 //不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。 if (sameVnode(vnodeToMove, newStartVnode)) { // patch vnodeToMove和newStartVnode patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue) // 清除 oldCh[idxInOld] = undefined // 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm // 移动到oldStartVnode.elm之前 canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
// 如果key相同,但是节点不相同,则创建一个新的节点 } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) } }
// 右移 newStartVnode = newCh[++newStartIdx] } }
复制代码


while循环主要处理了以下五种情景:


  • 当新老 VNode 节点的 start 相同时,直接 patchVnode ,同时新老 VNode 节点的开始索引都加 1

  • 当新老 VNode 节点的 end相同时,同样直接 patchVnode ,同时新老 VNode 节点的结束索引都减 1

  • 当老 VNode 节点的 start 和新 VNode 节点的 end 相同时,这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldEndVnode 的后面,同时老 VNode 节点开始索引加 1,新 VNode 节点的结束索引减 1

  • 当老 VNode 节点的 end 和新 VNode 节点的 start 相同时,这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldStartVnode 的前面,同时老 VNode 节点结束索引减 1,新 VNode 节点的开始索引加 1

  • 如果都不满足以上四种情形,那说明没有相同的节点可以复用,则会分为以下两种情况:

  • 从旧的 VNodekey 值,对应 index 序列为 value 值的哈希表中找到与 newStartVnode 一致 key 的旧的 VNode 节点,再进行patchVnode,同时将这个真实 dom移动到 oldStartVnode 对应的真实 dom 的前面

  • 调用 createElm 创建一个新的 dom 节点放到当前 newStartIdx 的位置


小结


  • 当数据发生改变时,订阅者watcher就会调用patch给真实的DOM打补丁

  • 通过isSameVnode进行判断,相同则调用patchVnode方法

  • patchVnode做了以下操作:

  • 找到对应的真实dom,称为el

  • 如果都有都有文本节点且不相等,将el文本节点设置为Vnode的文本节点

  • 如果oldVnode有子节点而VNode没有,则删除el子节点

  • 如果oldVnode没有子节点而VNode有,则将VNode的子节点真实化后添加到el

  • 如果两者都有子节点,则执行updateChildren函数比较子节点

  • updateChildren主要做了以下操作:

  • 设置新旧VNode的头尾指针

  • 新旧头尾指针进行比较,循环向中间靠拢,根据情况调用patchVnode进行patch重复流程、调用createElem创建一个新节点,从哈希表寻找 key一致的VNode 节点再分情况操作

简述 mixin、extends 的覆盖逻辑

(1)mixin 和 extends mixin 和 extends 均是用于合并、拓展组件的,两者均通过 mergeOptions 方法实现合并。


  • mixins 接收一个混入对象的数组,其中混入对象可以像正常的实例对象一样包含实例选项,这些选项会被合并到最终的选项中。Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。

  • extends 主要是为了便于扩展单文件组件,接收一个对象或构造函数。


(2)mergeOptions 的执行过程


  • 规范化选项(normalizeProps、normalizelnject、normalizeDirectives)

  • 对未合并的选项,进行判断


if (!child._base) {  if (child.extends) {    parent = mergeOptions(parent, child.extends, vm);  }  if (child.mixins) {    for (let i = 0, l = child.mixins.length; i < l; i++) {      parent = mergeOptions(parent, child.mixins[i], vm);    }  }}
复制代码


  • 合并处理。根据一个通用 Vue 实例所包含的选项进行分类逐一判断合并,如 props、data、 methods、watch、computed、生命周期等,将合并结果存储在新定义的 options 对象里。

  • 返回合并结果 options。

Vue 中修饰符.sync 与 v-model 的区别

sync的作用


  • .sync修饰符可以实现父子组件之间的双向绑定,并且可以实现子组件同步修改父组件的值,相比较与v-model来说,sync修饰符就简单很多了

  • 一个组件上可以有多个.sync修饰符


<!-- 正常父传子 --><Son :a="num" :b="num2" />
<!-- 加上sync之后的父传子 --><Son :a.sync="num" :b.sync="num2" />
<!-- 它等价于 --><Son :a="num" :b="num2" @update:a="val=>num=val" @update:b="val=>num2=val" />
<!-- 相当于多了一个事件监听,事件名是update:a, --><!-- 回调函数中,会把接收到的值赋值给属性绑定的数据项中。 -->
复制代码



v-model的工作原理


<com1 v-model="num"></com1><!-- 等价于 --><com1 :value="num" @input="(val)=>num=val"></com1>
复制代码


  • 相同点

  • 都是语法糖,都可以实现父子组件中的数据的双向通信

  • 区别点

  • 格式不同:v-model="num", :num.sync="num"

  • v-model: @input + value

  • :num.sync: @update:num

  • v-model只能用一次;.sync可以有多个

什么是 MVVM?

Model–View–ViewModel (MVVM) 是一个软件架构设计模式,由微软 WPF 和 Silverlight 的架构师 Ken Cooper 和 Ted Peters 开发,是一种简化用户界面的事件驱动编程方式。由 John Gossman(同样也是 WPF 和 Silverlight 的架构师)于 2005 年在他的博客上发表


MVVM 源自于经典的 Model–View–Controller(MVC)模式 ,MVVM 的出现促进了前端开发与后端业务逻辑的分离,极大地提高了前端开发效率,MVVM 的核心是 ViewModel 层,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与 Model 层通过接口请求进行数据交互,起呈上启下作用


(1)View 层


View 是视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。


(2)Model 层


Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。


(3)ViewModel 层


ViewModel 是由前端开发人员组织生成和维护的视图数据层。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。需要注意的是 ViewModel 所封装出来的数据模型包括视图的状态和行为两部分,而 Model 层的数据模型是只包含状态的,比如页面的这一块展示什么,而页面加载进来时发生什么,点击这一块发生什么,这一块滚动时发生什么这些都属于视图行为(交互),视图状态和行为都封装在了 ViewModel 里。这样的封装使得 ViewModel 可以完整地去描述 View 层。


MVVM 框架实现了双向绑定,这样 ViewModel 的内容会实时展现在 View 层,前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们开发者只需要处理和维护 ViewModel,更新数据视图就会自动得到相应更新。这样 View 层展现的不是 Model 层的数据,而是 ViewModel 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要的,它是前后端分离方案实施的重要一环。


我们以下通过一个 Vue 实例来说明 MVVM 的具体实现,有 Vue 开发经验的同学应该一目了然:


(1)View 层


<div id="app">    <p>{{message}}</p>    <button v-on:click="showMessage()">Click me</button></div>
复制代码


(2)ViewModel 层


var app = new Vue({    el: '#app',    data: {  // 用于描述视图状态           message: 'Hello Vue!',     },    methods: {  // 用于描述视图行为          showMessage(){            let vm = this;            alert(vm.message);        }    },    created(){        let vm = this;        // Ajax 获取 Model 层的数据        ajax({            url: '/your/server/data/api',            success(res){                vm.message = res;            }        });    }})
复制代码


(3) Model 层


{    "url": "/your/server/data/api",    "res": {        "success": true,        "name": "IoveC",        "domain": "www.cnblogs.com"    }}
复制代码

为什么要使用异步组件

  1. 节省打包出的结果,异步组件分开打包,采用jsonp的方式进行加载,有效解决文件过大的问题。

  2. 核心就是包组件定义变成一个函数,依赖import() 语法,可以实现文件的分割加载。


components:{   AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([]) }
复制代码


原理


export function ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void {     // async component     let asyncFactory     if (isUndef(Ctor.cid)) {         asyncFactory = Ctor         Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默认调用此函数时返回 undefiend         // 第二次渲染时Ctor不为undefined         if (Ctor === undefined) {             return createAsyncPlaceholder( // 渲染占位符 空虚拟节点                 asyncFactory,                 data,                 context,                 children,                 tag             )         }     } }function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void {     if (isDef(factory.resolved)) {         // 3.在次渲染时可以拿到获取的最新组件         return factory.resolved     }    const resolve = once((res: Object | Class<Component>) => {         factory.resolved = ensureCtor(res, baseCtor)         if (!sync) {             forceRender(true) //2. 强制更新视图重新渲染         } else {             owners.length = 0         }     })    const reject = once(reason => {         if (isDef(factory.errorComp)) {             factory.error = true forceRender(true)         }     })    const res = factory(resolve, reject)// 1.将resolve方法和reject方法传入,用户调用 resolve方法后     sync = false     return factory.resolved }
复制代码

Vue 中 computed 和 watch 有什么区别?

计算属性 computed:(1)支持缓存,只有依赖数据发生变化时,才会重新进行计算函数;(2)计算属性内不支持异步操作;(3)计算属性的函数中都有一个 get(默认具有,获取计算属性)和 set(手动添加,设置计算属性)方法;(4)计算属性是自动监听依赖值的变化,从而动态返回内容。


侦听属性 watch:(1)不支持缓存,只要数据发生变化,就会执行侦听函数;(2)侦听属性内支持异步操作;(3)侦听属性的值可以是一个对象,接收 handler 回调,deep,immediate 三个属性;(3)监听是一个过程,在监听的值变化时,可以触发一个回调,并做一些其他事情

请说出 vue cli 项目中 src 目录每个文件夹和文件的用法

  • assets文件夹是放静态资源;

  • components是放组件;

  • router是定义路由相关的配置;

  • view视图;

  • app.vue是一个应用主组件;

  • main.js是入口文件

谈谈 Vue 和 React 组件化的思想

  • 1.我们在各个页面开发的时候,会产生很多重复的功能,比如 element 中的 xxxx。像这种纯粹非页面的 UI,便成为我们常用的 UI 组件,最初的前端组件也就仅仅指的是 UI 组件

  • 2.随着业务逻辑变得越来多是,我们就想要我们的组件可以处理很多事,这就是我们常说的组件化,这个组件就不是 UI 组件了,而是包具体业务的业务组件

  • 3.这种开发思想就是分而治之。最大程度的降低开发难度和维护成本的效果。并且可以多人协作,每个人写不同的组件,最后像撘积木一样的把它构成一个页面

Vue 3.0 中的 Vue Composition API?

在 Vue2 中,代码是 Options API 风格的,也就是通过填充 (option) data、methods、computed 等属性来完成一个 Vue 组件。这种风格使得 Vue 相对于 React 极为容易上手,同时也造成了几个问题:


  1. 由于 Options API 不够灵活的开发方式,使得 Vue 开发缺乏优雅的方法来在组件间共用代码。

  2. Vue 组件过于依赖this上下文,Vue 背后的一些小技巧使得 Vue 组件的开发看起来与 JavaScript 的开发原则相悖,比如在methods 中的this竟然指向组件实例来不指向methods所在的对象。这也使得 TypeScript 在 Vue2 中很不好用。


于是在 Vue3 中,舍弃了 Options API,转而投向 Composition API。Composition API 本质上是将 Options API 背后的机制暴露给用户直接使用,这样用户就拥有了更多的灵活性,也使得 Vue3 更适合于 TypeScript 结合。


如下,是一个使用了 Vue Composition API 的 Vue3 组件:


<template>  <button @click="increment">    Count: {{ count }}  </button></template>
<script>// Composition API 将组件属性暴露为函数,因此第一步是导入所需的函数import { ref, computed, onMounted } from 'vue'
export default { setup() {// 使用 ref 函数声明了称为 count 的响应属性,对应于Vue2中的data函数 const count = ref(0) // Vue2中需要在methods option中声明的函数,现在直接声明 function increment() { count.value++ } // 对应于Vue2中的mounted声明周期 onMounted(() => console.log('component mounted!')) return { count, increment } }}</script>
复制代码


显而易见,Vue Composition API 使得 Vue3 的开发风格更接近于原生 JavaScript,带给开发者更多地灵活性

请说明 Vue 中 key 的作用和原理,谈谈你对它的理解


  • key是为Vue中的VNode标记的唯一id,在patch过程中通过key可以判断两个虚拟节点是否是相同节点,通过这个key,我们的diff操作可以更准确、更快速

  • diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行比对,然后检出差异

  • 尽量不要采用索引作为key

  • 如果不加key,那么vue会选择复用节点(Vue 的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug

  • 更准确 :因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。

  • 更快速key的唯一性可以被Map数据结构充分利用,相比于遍历查找的时间复杂度O(n)Map的时间复杂度仅仅为O(1),比遍历方式更快。


源码如下:


function createKeyToOldIdx (children, beginIdx, endIdx) {  let i, key  const map = {}  for (i = beginIdx; i <= endIdx; ++i) {    key = children[i].key    if (isDef(key)) map[key] = i  }  return map}
复制代码


回答范例


分析


这是一道特别常见的问题,主要考查大家对虚拟DOMpatch细节的掌握程度,能够反映面试者理解层次


思路分析:


  • 给出结论,key的作用是用于优化patch性能

  • key的必要性

  • 实际使用方式

  • 总结:可从源码层面描述一下vue如何判断两个节点是否相同


回答范例:


  1. key的作用主要是为了更高效的更新虚拟DOM

  2. vuepatch过程中 判断两个节点是否是相同节点是key是一个必要条件 ,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能

  3. 实际使用中在渲染一组列表时key必须设置,而且必须是唯一标识,应该避免使用数组索引作为key,这可能导致一些隐蔽的bugvue中在使用相同标签元素过渡切换时,也会使用key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果

  4. 从源码中可以知道,vue判断两个节点是否相同时主要判断两者的key标签类型(如div)等,因此如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的


如果不使用 keyVue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key 是为 Vuevnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速



diff 程可以概括为:oldChnewCh各有两个头尾的变量 StartIdxEndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldChnewCh至少有一个已经遍历完了,就会结束比较,这四种比较方式就是旧尾新头旧头新尾


相关代码如下


// 判断两个vnode的标签和key是否相同 如果相同 就可以认为是同一节点就地复用function isSameVnode(oldVnode, newVnode) {  return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;}
// 根据key来创建老的儿子的index映射表 类似 {'a':0,'b':1} 代表key为'a'的节点在第一个位置 key为'b'的节点在第二个位置function makeIndexByKey(children) { let map = {}; children.forEach((item, index) => { map[item.key] = index; }); return map;}// 生成的映射表let map = makeIndexByKey(oldCh);
复制代码


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022.09.01 加入

还未添加个人简介

评论

发布
暂无评论
19道高频vue面试题,顺便写一下自己的答案_Vue_bb_xiaxia1998_InfoQ写作社区