乐趣区

关于前端:2023前端vue面试题及答案

Vue3.0 为什么要用 proxy?

在 Vue2 中,0bject.defineProperty 会扭转原始数据,而 Proxy 是创建对象的虚构示意,并提供 set、get 和 deleteProperty 等处理器,这些处理器可在拜访或批改原始对象上的属性时进行拦挡,有以下特点∶

  • 不需用应用 Vue.$setVue.$delete 触发响应式。
  • 全方位的数组变化检测,打消了 Vue2 有效的边界状况。
  • 反对 Map,Set,WeakMap 和 WeakSet。

Proxy 实现的响应式原理与 Vue2 的实现原理雷同,实现形式大同小异∶

  • get 收集依赖
  • Set、delete 等触发依赖
  • 对于汇合类型,就是对汇合对象的办法做一层包装:原办法执行后执行依赖相干的收集或触发逻辑。

说说你对 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 插槽向组件外部指定地位传递内容,实现这个复用组件在不同场景的利用

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

应用 vue 渲染大量数据时应该怎么优化?说下你的思路!

剖析

企业级我的项目中渲染大量数据的状况比拟常见,因而这是一道十分好的综合实际题目。

答复

  1. 在大型企业级我的项目中常常须要渲染大量数据,此时很容易呈现卡顿的状况。比方大数据量的表格、树
  2. 解决时要依据状况做不同解决:
  3. 能够采取分页的形式获取,防止渲染大量数据
  • vue-virtual-scroller (opens new window)等虚构滚动计划,只渲染视口范畴内的数据
  • 如果不须要更新,能够应用 v -once 形式只渲染一次
  • 通过 v -memo (opens new window)能够缓存后果,联合 v-for 应用,防止数据变动时不必要的 VNode 创立
  • 能够采纳懒加载形式,在用户须要的时候再加载数据,比方 tree 组件子树的懒加载
  • 还是要看具体需要,首先从设计上防止大数据获取和渲染;切实须要这样做能够采纳虚表的形式优化渲染;最初优化更新,如果不须要更新能够 v-once 解决,须要更新能够 v-memo 进一步优化大数据更新性能。其余能够采纳的是交互方式优化,无线滚动、懒加载等计划

scoped 款式穿透

scoped尽管防止了组件间款式净化,然而很多时候咱们须要批改组件中的某个款式,然而又不想去除 scoped 属性

  1. 应用/deep/
<!-- Parent -->
<template>
<div class="wrap">
    <Child />
</div>
</template>

<style lang="scss" scoped>
.wrap /deep/ .box{background: red;}
</style>

<!-- Child -->
<template>
    <div class="box"></div>
</template>
  1. 应用两个 style 标签
<!-- Parent -->
<template>
<div class="wrap">
    <Child />
</div>
</template>

<style lang="scss" scoped>
/* 其余款式 */
</style>
<style lang="scss">
.wrap .box{background: red;}
</style>

<!-- Child -->
<template>
  <div class="box"></div>
</template>

Vue 中 v -html 会导致哪些问题

  • 可能会导致 xss 攻打
  • v-html 会替换掉标签外部的子元素
let template = require('vue-template-compiler'); 
let r = template.compile(`<div v-html="'<span>hello</span>'"></div>`) 

// with(this){return _c('div',{domProps: {"innerHTML":_s('<span>hello</span>')}})} 
console.log(r.render);

// _c 定义在 core/instance/render.js 
// _s 定义在 core/instance/render-helpers/index,js
if (key === 'textContent' || key === 'innerHTML') {if (vnode.children) vnode.children.length = 0 
    if (cur === oldProps[key]) continue // #6601 work around Chrome version <= 55 bug where single textNode // replaced by innerHTML/textContent retains its parentNode property 
    if (elm.childNodes.length === 1) {elm.removeChild(elm.childNodes[0]) 
    } 
}

如果让你从零开始写一个 vuex,说说你的思路

思路剖析

这个题目很有难度,首先思考 vuex 解决的问题:存储用户全局状态并提供治理状态 API。

  • vuex需要剖析
  • 如何实现这些需要

答复范例

  1. 官网说 vuex 是一个状态管理模式和库,并确保这些状态以可预期的形式变更。可见要实现一个vuex
  2. 要实现一个 Store 存储全局状态
  3. 要提供批改状态所需 API:commit(type, payload), dispatch(type, payload)
  4. 实现 Store 时,能够定义 Store 类,构造函数接管选项 options,设置属性state 对外裸露状态,提供 commitdispatch批改属性 state。这里须要设置state 为响应式对象,同时将 Store 定义为一个 Vue 插件
  5. commit(type, payload)办法中能够获取用户传入 mutations 并执行它,这样能够按用户提供的办法批改状态。dispatch(type, payload)相似,但须要留神它可能是异步的,须要返回一个 Promise 给用户以解决异步后果

实际

Store的实现:

class Store {constructor(options) {this.state = reactive(options.state)
        this.options = options
    }
    commit(type, payload) {this.options.mutations[type].call(this, this.state, payload)
    }
}

vuex 简易版

/**
 * 1 实现插件,挂载 $store
 * 2 实现 store
 */

let Vue;

class Store {constructor(options) {
    // state 响应式解决
    // 内部拜访:this.$store.state.***
    // 第一种写法
    // this.state = new Vue({
    //   data: options.state
    // })

    // 第二种写法:避免外界间接接触外部 vue 实例,避免内部强行变更
    this._vm = new Vue({
      data: {$$state: options.state}
    })

    this._mutations = options.mutations
    this._actions = options.actions
    this.getters = {}
    options.getters && this.handleGetters(options.getters)

    this.commit = this.commit.bind(this)
    this.dispatch = this.dispatch.bind(this)
  }

  get state () {return this._vm._data.$$state}

  set state (val) {return new Error('Please use replaceState to reset state')
  }

  handleGetters (getters) {Object.keys(getters).map(key => {
      Object.defineProperty(this.getters, key, {get: () => getters[key](this.state)
      })
    })
  }

  commit (type, payload) {let entry = this._mutations[type]
    if (!entry) {return new Error(`${type} is not defined`)
    }

    entry(this.state, payload)
  }

  dispatch (type, payload) {let entry = this._actions[type]
    if (!entry) {return new Error(`${type} is not defined`)
    }

    entry(this, payload)
  }
}

const install = (_Vue) => {
  Vue = _Vue

  Vue.mixin({beforeCreate () {if (this.$options.store) {Vue.prototype.$store = this.$options.store}
    },
  })
}


export default {Store, install}

验证形式

import Vue from 'vue'
import Vuex from './vuex'
// this.$store
Vue.use(Vuex)

export default new Vuex.Store({
  state: {counter: 0},
  mutations: {
    // state 从哪里来的
    add (state) {state.counter++}
  },
  getters: {doubleCounter (state) {return state.counter * 2}
  },
  actions: {add ({ commit}) {setTimeout(() => {commit('add')
      }, 1000)
    }
  },
  modules: {}})

参考 前端进阶面试题具体解答

Vue 与 Angular 以及 React 的区别?

Vue 与 AngularJS 的区别

  • Angular采纳 TypeScript 开发, 而 Vue 能够应用 javascript 也能够应用TypeScript
  • AngularJS依赖对数据做脏查看,所以 Watcher 越多越慢;Vue.js应用基于依赖追踪的察看并且应用异步队列更新,所有的数据都是独立触发的。
  • AngularJS社区欠缺, Vue的学习老本较小

Vue 与 React 的区别

相同点:

  1. Virtual DOM。其中最大的一个相似之处就是都应用了 Virtual DOM。(当然Vue 是在 Vue2.x 才援用的)也就是能让咱们通过操作数据的形式来扭转实在的 DOM 状态。因为其实 Virtual DOM 的实质就是一个 JS 对象,它保留了对实在 DOM 的所有形容,是实在 DOM 的一个映射,所以当咱们在进行频繁更新元素的时候,扭转这个 JS 对象的开销远比间接扭转实在 DOM 要小得多。
  2. 组件化的开发思维。第二点来说就是它们都提倡这种组件化的开发思维,也就是倡议将利用分拆成一个个性能明确的模块,再将这些模块整合在一起以满足咱们的业务需要。
  3. PropsVueReact 中都有 props 的概念,容许父组件向子组件传递数据。
  4. 构建工具、Chrome 插件、配套框架。还有就是它们的构建工具以及 Chrome 插件、配套框架都很欠缺。比方构建工具,React中能够应用 CRAVue 中能够应用对应的脚手架 vue-cli。对于配套框架Vue 中有 vuex、vue-routerReact 中有react-router、redux

不同点

  1. 模版的编写。最大的不同就是模版的编写,Vue激励你去写近似惯例 HTML 的模板,React举荐你应用 JSX 去书写。
  2. 状态治理与对象属性。在 React 中,利用的状态是比拟要害的概念,也就是 state 对象,它容许你应用 setState 去更新状态。然而在 Vue 中,state对象并不是必须的,数据是由 data 属性在 Vue 对象中进行治理。
  3. 虚构 DOM 的解决形式不同。Vue中的虚构 DOM 管制了颗粒度,组件层面走 watcher 告诉,而组件外部走 vdomdiff,这样,既不会有太多 watcher,也不会让vdom 的规模过大。而 React 走了相似于 CPU 调度的逻辑,把 vdom 这棵树,宏观上变成了链表,而后利用浏览器的闲暇工夫来做diff

Vue 我的项目中你是如何解决跨域的呢

一、跨域是什么

跨域实质是浏览器基于 同源策略 的一种平安伎俩

同源策略(Sameoriginpolicy),是一种约定,它是浏览器最外围也最根本的平安性能

所谓同源(即指在同一个域)具备以下三个相同点

  • 协定雷同(protocol)
  • 主机雷同(host)
  • 端口雷同(port)

反之非同源申请,也就是协定、端口、主机其中一项不雷同的时候,这时候就会产生跨域

肯定要留神跨域是浏览器的限度,你用抓包工具抓取接口数据,是能够看到接口曾经把数据返回回来了,只是浏览器的限度,你获取不到数据。用 postman 申请接口可能申请到数据。这些再次印证了跨域是浏览器的限度。

Class 与 Style 如何动静绑定

Class 能够通过对象语法和数组语法进行动静绑定

对象语法:

<div v-bind:class="{active: isActive,'text-danger': hasError}"></div>

data: {
  isActive: true,
  hasError: false
}

数组语法:

<div v-bind:class="[isActive ? activeClass :'', errorClass]"></div>

data: {
  activeClass: 'active',
  errorClass: 'text-danger'
}

Style 也能够通过对象语法和数组语法进行动静绑定

对象语法:

<div v-bind:style="{color: activeColor, fontSize: fontSize +'px'}"></div>

data: {
  activeColor: 'red',
  fontSize: 30
}

数组语法:

<div v-bind:style="[styleColor, styleSize]"></div>

data: {
  styleColor: {color: 'red'},
  styleSize:{fontSize:'23px'}
}

理解 history 有哪些办法吗?说下它们的区别

history 这个对象在 html5 的时候新退出两个 api history.pushState()history.repalceState() 这两个API 能够在不进行刷新的状况下,操作浏览器的历史纪录。惟一不同的是,前者是新增一个历史记录,后者是间接替换以后的历史记录。

从参数上来说:

window.history.pushState(state,title,url)
//state:须要保留的数据,这个数据在触发 popstate 事件时,能够在 event.state 里获取
//title:题目,根本没用,个别传 null
//url:设定新的历史纪录的 url。新的 url 与以后 url 的 origin 必须是一样的,否则会抛出谬误。url 能够时绝对路径,也能够是相对路径。// 如 以后 url 是 https://www.baidu.com/a/, 执行 history.pushState(null, null, './qq/'),则变成 https://www.baidu.com/a/qq/,// 执行 history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/

window.history.replaceState(state,title,url)
// 与 pushState 基本相同,但她是批改以后历史纪录,而 pushState 是创立新的历史纪录

另外还有:

  • window.history.back() 后退
  • window.history.forward()后退
  • window.history.go(1) 后退或者后退几步

从触发事件的监听上来说:

  • pushState()replaceState() 不能被 popstate 事件所监听
  • 而前面三者能够,且用户点击浏览器后退后退键时也能够

在 Vue 中应用插件的步骤

  • 采纳 ES6import ... from ...语法或 CommonJSrequire()办法引入插件
  • 应用全局办法 Vue.use(plugin) 应用插件, 能够传入一个选项对象Vue.use(MyPlugin, { someOption: true})

$route$router 的区别

  • $route是“路由信息对象”,包含 pathparamshashqueryfullPathmatchedname 等路由信息参数。
  • $router 是“路由实例”对象包含了路由的跳转办法,钩子函数等

为什么要应用异步组件

  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 
}

函数式组件劣势和原理

函数组件的特点

  1. 函数式组件须要在申明组件是指定 functional:true
  2. 不须要实例化,所以没有 this,this 通过 render 函数的第二个参数 context 来代替
  3. 没有生命周期钩子函数,不能应用计算属性,watch
  4. 不能通过 $emit 对外裸露事件,调用事件只能通过context.listeners.click 的形式调用内部传入的事件
  5. 因为函数式组件是没有实例化的,所以在内部通过 ref 去援用组件时,理论援用的是HTMLElement
  6. 函数式组件的 props 能够不必显示申明,所以没有在 props 外面申明的属性都会被主动隐式解析为 prop, 而一般组件所有未声明的属性都解析到$attrs 外面,并主动挂载到组件根元素下面 (能够通过inheritAttrs 属性禁止)

长处

  1. 因为函数式组件不须要实例化,无状态,没有生命周期,所以渲染性能要好于一般组件
  2. 函数式组件构造比较简单,代码构造更清晰

应用场景:

  • 一个简略的展现组件,作为容器组件应用 比方 router-view 就是一个函数式组件
  • “高阶组件”——用于接管一个组件作为参数,返回一个被包装过的组件

例子

Vue.component('functional',{ // 构造函数产生虚构节点的
    functional:true, // 函数式组件 // data={attrs:{}}
    render(h){return h('div','test')
    }
})
const vm = new Vue({el: '#app'})

源码相干

// functional component
if (isTrue(Ctor.options.functional)) { // 带有 functional 的属性的就是函数式组件
  return createFunctionalComponent(Ctor, propsData, data, context, children)
}

// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on // 处理事件
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn // 解决原生事件

// install component management hooks onto the placeholder node
installComponentHooks(data) // 装置组件相干钩子(函数式组件没有调用此办法,从而性能高于一般组件)

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 44
export 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 201
export 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 为什么没有相似于 React 中 shouldComponentUpdate 的生命周期

  • 考点: Vue的变动侦测原理
  • 前置常识: 依赖收集、虚构DOM、响应式零碎

根本原因是 VueReact的变动侦测形式有所不同

  • 当 React 晓得发生变化后,会应用 Virtual Dom Diff 进行差别检测,然而很多组件实际上是必定不会发生变化的,这个时候须要 shouldComponentUpdate 进行手动操作来缩小diff,从而进步程序整体的性能
  • Vue在一开始就晓得那个组件产生了变动,不须要手动管制 diff,而组件外部采纳的diff 形式实际上是能够引入相似于 shouldComponentUpdate 相干生命周期的,然而通常正当大小的组件不会有适量的 diff,手动优化的价值无限,因而目前 Vue 并没有思考引入 shouldComponentUpdate 这种手动优化的生命周期

vue-router 中如何爱护路由

剖析

路由爱护在利用开发过程中十分重要,简直每个利用都要做各种路由权限治理,因而相当考查使用者基本功。

体验

全局守卫:

const router = createRouter({...})
​
router.beforeEach((to, from) => {
  // ...
  // 返回 false 以勾销导航
  return false
})

路由独享守卫:

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from) => {
      // reject the navigation
      return false
    },
  },
]

组件内的守卫:

const UserDetails = {
  template: `...`,
  beforeRouteEnter(to, from) {// 在渲染该组件的对应路由被验证前调用},
  beforeRouteUpdate(to, from) {// 在以后路由扭转,然而该组件被复用时调用},
  beforeRouteLeave(to, from) {// 在导航来到渲染该组件的对应路由时调用},
}

答复

  • vue-router中爱护路由的办法叫做路由守卫,次要用来通过跳转或勾销的形式守卫导航。
  • 路由守卫有三个级别:全局 路由独享 组件级。影响范畴由大到小,例如全局的router.beforeEach(),能够注册一个全局前置守卫,每次路由导航都会通过这个守卫,因而在其外部能够退出管制逻辑决定用户是否能够导航到指标路由;在路由注册的时候能够退出单路由独享的守卫,例如beforeEnter,守卫只在进入路由时触发,因而只会影响这个路由,管制更准确;咱们还能够为路由组件增加守卫配置,例如beforeRouteEnter,会在渲染该组件的对应路由被验证前调用,管制的范畴更准确了。
  • 用户的任何导航行为都会走 navigate 办法,外部有个 guards 队列按程序执行用户注册的守卫钩子函数,如果没有通过验证逻辑则会勾销原有的导航。

原理

runGuardQueue(guards)链式的执行用户在各级别注册的守卫钩子函数,通过则持续下一个级别的守卫,不通过进入 catch 流程勾销本来导航

// 源码
runGuardQueue(guards)
  .then(() => {
    // check global guards beforeEach
    guards = []
    for (const guard of beforeGuards.list()) {guards.push(guardToPromiseFn(guard, to, from))
    }
    guards.push(canceledNavigationCheck)

    return runGuardQueue(guards)
  })
  .then(() => {
    // check in components beforeRouteUpdate
    guards = extractComponentsGuards(
      updatingRecords,
      'beforeRouteUpdate',
      to,
      from
    )

    for (const record of updatingRecords) {
      record.updateGuards.forEach(guard => {guards.push(guardToPromiseFn(guard, to, from))
      })
    }
    guards.push(canceledNavigationCheck)

    // run the queue of per route beforeEnter guards
    return runGuardQueue(guards)
  })
  .then(() => {
    // check the route beforeEnter
    guards = []
    for (const record of to.matched) {
      // do not trigger beforeEnter on reused views
      if (record.beforeEnter && !from.matched.includes(record)) {if (isArray(record.beforeEnter)) {for (const beforeEnter of record.beforeEnter)
            guards.push(guardToPromiseFn(beforeEnter, to, from))
        } else {guards.push(guardToPromiseFn(record.beforeEnter, to, from))
        }
      }
    }
    guards.push(canceledNavigationCheck)

    // run the queue of per route beforeEnter guards
    return runGuardQueue(guards)
  })
  .then(() => {// NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>

    // clear existing enterCallbacks, these are added by extractComponentsGuards
    to.matched.forEach(record => (record.enterCallbacks = {}))

    // check in-component beforeRouteEnter
    guards = extractComponentsGuards(
      enteringRecords,
      'beforeRouteEnter',
      to,
      from
    )
    guards.push(canceledNavigationCheck)

    // run the queue of per route beforeEnter guards
    return runGuardQueue(guards)
  })
  .then(() => {
    // check global guards beforeResolve
    guards = []
    for (const guard of beforeResolveGuards.list()) {guards.push(guardToPromiseFn(guard, to, from))
    }
    guards.push(canceledNavigationCheck)

    return runGuardQueue(guards)
  })
  // catch any navigation canceled
  .catch(err =>
    isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
      ? err
      : Promise.reject(err)
  )

源码地位(opens new window)

Vue-router 路由钩子在生命周期的体现

一、Vue-Router 导航守卫

有的时候,须要通过路由来进行一些操作,比方最常见的登录权限验证,当用户满足条件时,才让其进入导航,否则就勾销跳转,并跳到登录页面让其登录。
为此有很多种办法能够植入路由的导航过程:全局的,单个路由独享的,或者组件级的

  1. 全局路由钩子

vue-router 全局有三个路由钩子;

  • router.beforeEach 全局前置守卫 进入路由之前
  • router.beforeResolve 全局解析守卫(2.5.0+)在 beforeRouteEnter 调用之后调用
  • router.afterEach 全局后置钩子 进入路由之后

具体应用∶

  • beforeEach(判断是否登录了,没登录就跳转到登录页)
router.beforeEach((to, from, next) => {let ifInfo = Vue.prototype.$common.getSession('userData');  // 判断是否登录的存储信息
    if (!ifInfo) { 
        // sessionStorage 里没有贮存 user 信息    
        if (to.path == '/') {// 如果是登录页面门路,就间接 next()      
            next();} else { 
            // 不然就跳转到登录      
            Message.warning("请从新登录!");     
            window.location.href = Vue.prototype.$loginUrl;    
        }  
    } else {return next();  
    }
})
  • afterEach(跳转之后滚动条回到顶部)
router.afterEach((to, from) => {  
    // 跳转之后滚动条回到顶部  
    window.scrollTo(0,0);
});
  1. 单个路由独享钩子

beforeEnter 如果不想全局配置守卫的话,能够为某些路由独自配置守卫,有三个参数∶ to、from、next

export default [    
    {        
        path: '/',        
        name: 'login',        
        component: login,        
        beforeEnter: (to, from, next) => {console.log('行将进入登录页面')          
            next()}    
    }
]
  1. 组件内钩子

beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave

这三个钩子都有三个参数∶to、from、next

  • beforeRouteEnter∶ 进入组件前触发
  • beforeRouteUpdate∶ 以后地址扭转并且改选件被复用时触发,举例来说,带有动静参数的门路 foo/∶id,在 /foo/1 和 /foo/2 之间跳转的时候,因为会渲染同样的 foa 组件,这个钩子在这种状况下就会被调用
  • beforeRouteLeave∶ 来到组件被调用

留神点,beforeRouteEnter 组件内还拜访不到 this,因为该守卫执行前组件实例还没有被创立,须要传一个回调给 next 来拜访,例如:

beforeRouteEnter(to, from, next) {      
    next(target => {if (from.path == '/classProcess') {target.isFromProcess = true}      
    })    
}

二、Vue 路由钩子在生命周期函数的体现

  1. 残缺的路由导航解析流程(不包含其余生命周期)
  2. 触发进入其余路由。
  • 调用要来到路由的组件守卫 beforeRouteLeave
  • 调用局前置守卫∶ beforeEach
  • 在重用的组件里调用 beforeRouteUpdate
  • 调用路由独享守卫 beforeEnter。
  • 解析异步路由组件。
  • 在将要进入的路由组件中调用 beforeRouteEnter
  • 调用全局解析守卫 beforeResolve
  • 导航被确认。
  • 调用全局后置钩子的 afterEach 钩子。
  • 触发 DOM 更新(mounted)。
  • 执行 beforeRouteEnter 守卫中传给 next 的回调函数
  • 触发钩子的残缺程序

路由导航、keep-alive、和组件生命周期钩子联合起来的,触发程序,假如是从 a 组件来到,第一次进入 b 组件∶

  • beforeRouteLeave:路由组件的组件来到路由前钩子,可勾销路由来到。
  • beforeEach:路由全局前置守卫,可用于登录验证、全局路由 loading 等。
  • beforeEnter:路由独享守卫
  • beforeRouteEnter:路由组件的组件进入路由前钩子。
  • beforeResolve:路由全局解析守卫
  • afterEach:路由全局后置钩子
  • beforeCreate:组件生命周期,不能拜访 tAis。
  • created; 组件生命周期,能够拜访 tAis,不能拜访 dom。
  • beforeMount:组件生命周期
  • deactivated:来到缓存组件 a,或者触发 a 的 beforeDestroy 和 destroyed 组件销毁钩子。
  • mounted:拜访 / 操作 dom。
  • activated:进入缓存组件,进入 a 的嵌套子组件(如果有的话)。
  • 执行 beforeRouteEnter 回调函数 next。
  • 导航行为被触发到导航实现的整个过程
  • 导航行为被触发,此时导航未被确认。
  • 在失活的组件里调用来到守卫 beforeRouteLeave。
  • 调用全局的 beforeEach 守卫。
  • 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  • 在路由配置里调用 beforeEnteY。
  • 解析异步路由组件(如果有)。
  • 在被激活的组件里调用 beforeRouteEnter。
  • 调用全局的 beforeResolve 守卫(2.5+),标示解析阶段实现。
  • 导航被确认。
  • 调用全局的 afterEach 钩子。
  • 非重用组件,开始组件实例的生命周期:beforeCreate&created、beforeMount&mounted
  • 触发 DOM 更新。
  • 用创立好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。
  • 导航实现

Vue-router 导航守卫有哪些

  • 全局前置 / 钩子:beforeEach、beforeResolve、afterEach
  • 路由独享的守卫:beforeEnter
  • 组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave

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 节点再分状况操作
退出移动版