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,jsif (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.$storeVue.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 componentif (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 listenersconst 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 nodeinstallComponentHooks(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 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为什么没有相似于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 节点再分状况操作