Vue知识总结

257 阅读27分钟

Vue基础

Vue是一个用于创建用户界面的开源、渐进式javascript框架

Vue的基本原理

image.png 当创建一个Vue实例时,Vue会遍历data中的属性,vue2.x中用Object.defineProperty 将它们转化为getter/setter, 并在内部追踪相关依赖,在属性被访问和修改的时候通知变化。 每个组件的实例都有相应的watcher实例, 在组件渲染的过程中把属性记录为依赖,之后当依赖项发生改变时,触发setter被调用,会通知watcher重新计算,从而使它关联的组件重新渲染更新。

即vue中实现响应式=> 当数据发生变化后,会重新对页面渲染,这个过程中,涉及到的事情有

  1. 数据劫持/数据代理,检测数据的变化
  2. 依赖收集,收集视图依赖了哪些数据
  3. 发布订阅模式,当数据变化时候,自动通知需要更新的视图进行更新

Object.defineProperty() 来进行数据劫持有什么缺点

对一些属性进行操作时,无法进行拦截,如通过索引直接修改一个数组数据,或者给对象添加,删除操作,都不能触发组件的重新渲染,需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题

在vue3中,使用新Proxy对象进行代理,实现数据劫持。proxy监听是针对一个对象的,即一个对象的所有操作都会被监听到 => 劫持整个对象,并返回一个新对象

Proxy 不兼容IE,也没有 polyfilldefineProperty 能支持到IE9

Object.defineProperty()方法会直接在一个对象上定义新的属性,或者修改一个对象的现有属性,并返回这个对象,Object.defineProperty(obj, prop, descriptor)

 let aValue = 30;
 Object.defineProperty(obj, 'key', {
   get(){
     return aValue
   },
   set(newValue){
    aValue = newValue
   },
  enumerable : true,
  configurable : true
   writable: true,
   value: 'static'
 })

Vue中重写数组的方法实现响应式,没有用Object.defineProperty

  1. Object.defineProperty: 本身是可以监听数组属性的变化,但是因为性能消耗严重,没有采用
  2. 如果检测的对象存在深层次的嵌套对象关系,就需要深层次的监听,会造成很大的性能问题
  3. vue实现响应式,把无法监听数组的情况,通过重写属性的部分方法来实现响应式,只对push, pop, shift, unshift, splice, sort, reverse这7种方法重写。

生命周期

image.png

  • beforeCreate:数据观测和初始化事件还没有开始,此时的data的响应式追踪,event/watcher都还没有被设置,即不能访问到data, computed, watch, methods上的方法和数据
  • created: 实例初始化完成,实例上配置的options包含data,computed, watch, methods都初始化完成,,但是此时渲染得到节点还没有挂载到DOM, 不能访问到$el属性 => 尚未开始渲染模版
  • beforeMount:相关的render函数首次被调用,实例已经完成模版编译的配置,即把data里面的数据和模版生成html, 此时还没有挂载到html到页面
  • mounted: 在el被新创建的vm.$el替换,并挂载到实例上之后调用。实例已经完成了:将编译好的html内容替换成el属性指向DOM对象,完成模版中html渲染到html页面。此时可以进行ajax请求交互
  • beforeUpdate: 响应式数据更新时调用,此时像是数据更新了,但是对应的真实DOM还没有被渲染
  • updated: 此时DOM已经根据响应式数据的变化更新了,调用时,组件DOM已经更新了,在这个时期可以执行DOM的操作。大多数情况下,应该避免此期间更改状态,因为可能导致更新无限循环。该钩子函数在服务端渲染不会被调用
  • beforeDestory:实例销毁之前调用,这一步,实例仍然可以用,this仍然获取实例
  • destory: 实例销毁后,调用后,Vue实例指示的所有东西都会解绑,所有的事件都会被移除,所有的子实例也会被销毁。**该钩子函数在服务端渲染不会被调用 **

keep-alive独有的生命周期,分别为activated, deactivated,。 用kepp-alive包裹的组件在切换的时候,不会进行销毁,而是缓存到内存中,并且执行deactivated,命中缓存渲染后会执行activated钩子函数

Vue中子组件和父组件的执行顺序

加载渲染过程

  1. 父组件beforeCreate

  2. 父组件Created

  3. 父组件beforeMount

  4. 子组件beforeCreate

  5. 子组件Created

  6. 子组件beforeMount

  7. 子组件mounted

  8. 父组件mounted 更新过程阶段

  9. 父组件beforeUpdate

  10. 子组件beforeUpdate

  11. 子组件updated

  12. 父组件updated 销毁阶段

  13. 父组件beforeDestory

  14. 子组件beforeDestory

  15. 父组件destoryed 4.子组件destoryed

一般在哪个生命周期请求异步数据

可以在钩子函数created, beforeMount, mounted进行。因为在这三个钩子函数,data已经创建了,可以将服务端返回的数据进行赋值 但是推荐在created钩子函数进行异步请求,因为有以下几个好处

  1. 能更快获取服务端数据,减少页面加载时间,用户体验更好
  2. ssr不支持beforeMount 、mounted 钩子函数,放在 created 中有助于一致性。

keep-alive和生命周期

  1. keep-alive是vue内置组件,可以实现在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
  2. 组件包裹了 keep-alive,keep-alive独有的两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁
  3. 当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated钩子函数

组件通信的方式有哪些

  1. props/emit:父组件通过props向子组件传递数据,子组件通过emit: 父组件通过props向子组件传递数据,子组件通过emit和父组件通信 父组件向子组件传值
    • props父组件到子组件传值(单向数据流) ,子组件的数据会随着父组件不断更新
    • props定义的数据,可以是各种数据类型,也可传递一个函数 子组件向父组件传值
  • $emit绑定一个自定义事件,当这个事件被执行的时候,就会将参数传递给父组件,父组件通过v-on监听并接收参数,类似react中通过回调的形式子组件到父组件的数据传递
//这里简略写法 父组件===start====
 <com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article>
 //父组件===end====
 
 
 
 ///=====子组件
 //子组件
<template>
 <div>
   <div v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</div>
 </div>
</template>

<script>
export default {
 props: ['articles'],
 methods: {
   emitIndex(index) {
     this.$emit('onEmitIndex', index) // 触发父组件的方法,并传递参数index
   }
 }
}
</script>
 
  1. eventBus事件总线(emit/emit/on (1)创建事件中心管理组件之间的通信,本质是通过创建一个空的Vue实例来作为消息传递的对象,
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()

(2)发送事件 (3)接收事件 3. 利用 provide/ inject 在层数很深的情况下,可以使用这种方法来进行传值,不用一层一层的传递;该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件; 在父组件中通过 provide提供变量,在子组件中通过 inject 来将变量注入到组件中。不论子组件有多深,只要调用了 inject 那么就可以注入 provide中的数据

依赖注入所提供的属性是非响应式的 (1)provide: 该钩子用来发送数据或方法; 提供一个值,可以被后代组件注入;provide() 接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值 (2)provide可以写在最顶层文件中,这样相当于全局配置,其子自己都可以通过inject注入拿到配置

image.png

image.png 4. ref/$refs ref属性用在子组件上,它的引用就指向子组件的实例,可以通过实例来访问组件的数据和方法

image.png image.png 5. 第三方状态管理库,如vuex 6. parent,parent, attr

双向数据绑定的原理

nextTick原理

解释nextTick,先得从Vue中DOM的更新时机讲起

DOM更新时机

使用响应式更改状态时候,DOM会自动更新,但是,DOM的更新并不是同步的,Vue将缓冲它们直到更新周期的下一个时机,确保无论你更改了多少次状态变更每个组件都只更新一次。=> 如果要等待一个状态改变后的DOM更新完成,可以使用nextTick

如果同一个watcher被多次触发,只会被推入到队列中一次,在缓冲时期去除重复数据避免不必要的计算和DOM的操作,在下一个的事件循环tick中,vue刷新队列执行实际已经去重的工作

MVVM, MVC, MVP

  • MVVM:
    • model:模型层,负责处理业务逻辑和服务器端交互
    • view:视图层,UI展示页面
    • viewModel:视图模型层,将model和view层进行连接
      • 职责:
        • 数据变化后更新视图
        • 视图变化后更新数据
      • 组成部分
        • 监听器Observer: 对所有数据的属性进行监听
        • 解析器Compiler:对每个元素节点的指令进行扫描和解析,根据指令模版替换数据,绑定相应的更新函数

Computed, methods区别

Computed, watch区别

  1. Computed 是工厂函数,创建一个新的对象返回值
  2. watch发布订阅模式

vue-router原理

  1. 使用path-to-regexp作为路径匹配引擎,用来匹配path, params

vue中v-model是如何实现的,语法糖实际是什么

vue如何监听对象或者数组的某个属性的变化

Vue模版编译原理

vue中的template不是正确的HTML语法,不能被浏览器解析并渲染,需要将template转化成一个javascript函数,这样浏览器可以执行这个函数并且渲染出对应的HTML元素,让视图跑起来,这个转化的过程就是模版编译。

模版编译分为3个阶段:解析parse, 优化optimize, 生成generator,最后生成可执行函数render

  • 解析阶段:使用大量的正则表达式对template字符串进行解析,将标签,指令,属性等转化成抽象语法树AST
  • 优化阶段optimize:深度遍历AST,查看每个子树的节点元素是否为静态节点或者静态节点根,找到其中的一些静态节点进行标记,方便在页面重新渲染的时候进行diff比较,直接跳过这一些静态节点,优化runtime的性能
  • 生成阶段:将最终的AST转换成render函数字符串

vue组件挂载的时候会发生以下

  1. 编译: vue模版被编译成渲染函数(用来返回虚拟DOM树的函数),这个步骤可以通过构建步骤提前完成,也可以通过使用运行时编译器即时完成。
  2. 挂载:运行时渲染器调用渲染函数,遍历返回的虚拟DOM树,并基于它创建实际的DOM节点,这一步会作为响应式副作用执行,因此它会追踪其中作用到的所有响应式依赖
  3. 更新:当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟DOM,运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去

Vue3 Composition API生命周期有何区别

  • setup代替了beforeCreate, created
  • 使用Hooks函数的形式,如mounted改成onMounted()

image.png

template和jsx的区别

SSR

keep-alive如何实现的,具体缓存的是什么

一般情况下,一个组件实例在被替换掉后会被销毁,导致页面中所有已变化的状态丢失,当组件再一次被显示时,只会创建一个带有初始状态的实例

Vue.use

image.png

image.png

Vue.use()用来使用插件,通过Vue.use()注册的插件在Vue中全局可用, 插件的功能没有限制

  1. 语法:vue.use(plugin, arguments);参数 plugin: Function | Object,
  2. 用法:如果是vue安装的组件类型必须为Function或者是Object; 是一个对象,必须提供install方法,如果是一个函数, 会被直接当作install函数执行
  3. 通过全局注册的插件,需要在调用new Vue()之前调用
  4. 插件功能注册大致使用场景:
  • 添加全局的方法或属性,
  • 添加全局资源,如指令,过滤器,等
  • 通过全局混入添加一些组件选项:Vue-router
  • 给实例子添加方法,添加到vue.prototype上,
    • 如axios, Vue.prototype.$http = axios,之后调用可以直接this.$https

Vue.use的实现原理

  1. 先判断插件plugin是否是对象或者函数
  2. 判断该插件是否已经注册过,如果注册过,就跳出方法,直接return this => 返回vue
  3. 获取vue.use参数
  4. 判断插件是否有install方法,如果有,就执行install方法,没有就把plugin当作install执行
Vue.use()和Vue.prototype的区别
  1. Vue.use()注册的插件,install后,插件的变量能够全局使用,
  2. Vue.prototype注册的变量,需要用this方法调用,并且注册的全局变量要以$开头

开发一个插件

首先,如果是对象,需要有一个install方法,第一个参数是Vue, 其他参数自定义

const MyPlugin = {
  install(Vue, params){
  // code here for your business
  }
}
import Vue from "vue" 
Vue.use(MyPlugin)

延伸webpack插件的编写

plugin格式要求

  1. plugin是一个类,通过创建一个实例
  2. 需要暴露一个apply方法,apply方法里面根据业务调用不同的钩子函数进行逻辑处理
const { ExternalModule } = require("webpack");
const HtmlWebpackPlugin = require('html-webpack-plugin')

/** 1. 实现目标:
 * - 自动实现extrnals
 * - 自动向产出的html文件插入脚本,
 * 
 * 2. 实现过程
 * - 通过AST语法树检测当前的项目脚本中引入了哪些模块,是不是引入了jquery
 * - 如果发现引入了,就要 自动插入CDN脚本
 * - AsyncSeriesBailHook如果监听到有该事件就直接返回,如果没有的话,会继续下面的执行
 */
class AutoExternalPlugin {
  constructor(options){
    this.options = options;
    this.extrnalModules = Object.keys(this.options);  // 如['jquery','lodash']
    this.importedModules = new Set(); // 存放所有导入的外部依赖模块

  }
  apply(compiler){
    // 每种模块都会有一个对应的模块工厂来创建这个模块,普通模块对应的工作就是普通模块工厂
    compiler.hooks.normalModuleFactory.tap('AutoExternalPlugin',(normalModuleFactory) => {
      // 拿到普通工厂之后,注册一个hooks钩子,每个工厂都有一个解析器
      // webpack4加入来模块类型之后,Parser获取需要指定类型moduleType,一般用javascript/auto
      normalModuleFactory.hooks.parser
      .for('javascript/auto')
       // 每拿到一个模块,都会让编译器去解析,如parser babel  reperima  acorn(webpack用的是acorn库)可以把源代码抽象成语法树
        // 遍历到不同类型的节点,会触发不同的钩子,执行对应的事件函数
      .tap('AutoExternalPlugin', parser => { // 解析器是一个hookMap,对应每个类型的模块,会有对应的hookMap
         parser.hooks.import.tap('AutoExternalPlugin', (state, source) => { // 拦截import导入的插件
          console.log(source,'source---')
           if(this.extrnalModules.includes(source)){
              this.importedModules.add(source)
           }
         })

         parser.hooks.call.for('require').tap('AutoExternalPlugin', (expression) => {
          //  拦截require导入
           let value = expression.arguments[0].value
          if(this.extrnalModules.includes(value)){
            this.importedModules.add(value)
          }

         })

      })

      // 改造创建模块的过程
      normalModuleFactory.hooks.factorize.tapAsync('AutoExternalPlugin', (resolveData, callback) => {
        let request = resolveData.request;
        if(this.extrnalModules.includes(request)){
          // 如果这个模块是外部模块,进行拦截
          let expose = this.options[request].expose
          // 创建一个外部模块,并且返回,如jquery = window.JQuery
          // webpack 中的extrnal依赖是通过ExternalModulePlugin实现的,
          // ExternalModulePlugin会通过tap, NormalModuleFactory在在每次创建Module是否是ExternalModule
          callback(null, new ExternalModule(expose, 'window', request))
        }else{
          callback() // 如果正常模块,会直接调用callback向后执行
        }
      })
    })
    
    compiler.hooks.compilation.tap('AutoExternalPlugin', (compilation) => {
      HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync('AutoExternalPlugin', (htmlData, callback) => {
        let {assetTags} = htmlData
        // 找到实际引入来哪些模块,如jquery, lodash
        let importedExternalModules = Object.keys(this.options).filter(item => this.importedModules.has(item))
        console.log(this.options, this.importedModules)
        importedExternalModules.forEach(key => { // ['jquery']
          assetTags.scripts.unshift({
            tagName: 'script',
            voidTag: false,
            url: this.options[key].url,
            attributes: {
              src: this.options[key].url,
              defer: false
            }
          })
        })
        console.log(assetTags,'assetTags===',this.options)
        callback(null, htmlData)
      })

    })
  }
}
module.exports = AutoExternalPlugin

image.png

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

v-if、v-show、v-html 的原理

  • v-if:会调用addIfCondition方法,生成vnode的时候会忽略对应的节点,render的时候就不会渲染
  • v-show: 会生成vnode, render的时候会渲染成真实的节点,只是在render过程中会在节点的属性修改show属性值,即display
  • v-html: 会先移除节点下所有的节点,调用html方法,通过addProp添加innerHtml属性

v-if和v-show的区别

  1. v-if是动态的向DOM树内添加或者删除DOM元素,v-show通过css属性display控制显示隐藏
  2. 编译过程:v-if切换有一个局部编译/卸载的过程,切换的过程中会合适的销毁和重建内部的事件监听和子组件;v-show只是单纯的css切换
  3. 编译条件: v-if是惰性的, 如果条件为假,就什么都不做,只有在条件为真的情况下才开始局部编译;v-show是在任何条件都被编译(无论首次条件是否为真),然后被缓存,DOM元素被保留
  4. 性能消耗:v-if有更高的切换消耗,v-show有更高的初始化渲染消耗
  5. 使用场景:v-if适合不太改变; v-show适合频繁的切换

Vue data中某个属性的值发生改变后,视图会立即同步执行重新渲染吗

mixins和mixin区别

vue-router实现原理

  1. 路由:根据不同的url地址展示不同的内容或者页面
  2. vue单页面应用,只有一个完整的页面,在加载页面时,不会加载整个页面,而是只更新某个指定容器中的内容
  3. 基于路由和组件,路由用于设定访问路径,建立路径和组件映射关系
  4. vue-router两种模式: hash模式,history模式,MenoryHistor,默认hash模式
hash模式
  • hash(#)是URL的一个锚点,代表网页中的一个位置,改变#后面的部分内容,页面不会重新加载,不会包含在http请求中,对后端没有影响;
    • 每一次改变#后的内容,会在浏览器的访问历史记录中增加一个记录
    • 利用onhashchange事件监听hash变化
    • 浏览器兼容性好,低版本的IE也能支持
    • 页面刷新不会出现404
history模式

html5中新增pushState(), replaceState(),popState(),

  • 没有#,属性页面会向服务器发送请求
  • 需要后台配置支持,如果后台没有配置某个页面的路由,URL 匹配不到任何静态资源,会出现404
  • history.pushState(), history.replaceState()不会触发popState事件,只有触发浏览器动作时才会触发该事件,如浏览器回退,调用history.back()
  • 当我们手动修改url的hash,或者window.location.hash = 'xx',history.go(-1), history.back(),history.forward()才会触发onpopstate, onhashchange事件
  • window.history.replaceState, windwo.history.pushState只会改变历史记录条目,不能触发onpopstate,
  • back,current,forward, replace, scroll

install.js

  • 注册RouterLink, RouterView,
  • 设置全局属性router,router, route,
  • 根据地址栏进行首次路由跳转
  • 向app注入一些路由信息,如路由实例,响应式的当前路由信息对象
  • 拦截app.unmount方法,在卸载之前重置一些属性,删除一些监听函数

对传入的base进行标准化,区分浏览器环境和非浏览器环境(取/), createWebHistory, 如果history.state是空的,构建一条新的历史记录,利用history.replaceState/pushState修改历史记录

MenoryHistor(V4之前叫abstract history)

切换的时候路由不会发生变化,类似tab切换效果,相当于一个组件

扩展:React-router也有相同的3种模式

组件化

  • 降低整个系统的耦合度,通过组件的组装快速搭建页面需求,
  • 方便调试:组件单一原则,可以快速的排除或定位问题
  • 提高维护性:组件的单一职责,可复用,利于维护

Vue-Router 的懒加载如何实现

vuex

vue3.0

vue3.0更新

  1. 监测机制改变
  • 使用Proxy的observer实现跟变化,消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制
  1. 只能监测属性,不能监测对象
  • 监测属性的添加和删除
  • 监测数组索引和长度的变更
  • 支持Map, Set, WeakMap, WeakSet
  1. 模版
  • 作用域插槽,2.x的机制导致作用域插槽变了,父组件也会重新渲染,,但是vue3.0把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升性能
  • render函数,vue3.0支持jsx更方便
  1. 对象式的组件声明方式
  • vue2.x中的组件通过声明式传入一系列的option, 和Typescript的结合需要通过一些装饰器才能实现,比较麻烦
  • vue3.0改成用类形式的写法,使得和 TypeScript 的结合变得很容易

5.其他方面的更改

  • 支持 自定义渲染器,使得weex可以通过自定义渲染器的方式来扩展,不是直接fork源码来改的方式
  • 支持Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理
  • 基于tree shaking 优化,提供了更多的内置功能

defineProperty, proxy区别

Vue在实例化时遍历data中的所有属性,使用Object.defineProperty把这些属性全部转为getter/setter,当数据发生变化的时候,setter会被调用 另外Object.defineProperty会改变原始数据,proxy是创建对象的虚拟表示,并提供set 、get 和 deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截(get收集依赖,- Set、delete 等触发依赖;对于集合类型,就是对集合对象的方法做一层包装:原方法执行后执行依赖相关的收集或触发逻辑)

存在以下问题

  • 添加或删除对象属性的时候,Vue检测不到,因为添加或者删除的对象没有在初始化进行响应式处理,只能通过$set来调用Object.defineProperty()处理
  • 无法监测到数组下标和长度的变化 proxy的优点:
  • proxy直接代理的是整个对象而不是属性,这样只需要做一层代理就可以监听到同级结构下的所有属性额度变化,包括新增属性和删除属性
  • Proxy可以监听数组的变化

Composition API与React Hook很像,区别是什么

从React Hook实现角度看, React Hook是根据useState调用顺序来确定下一次重新渲染的state是来源于哪个useState,所以出现以下限制

  • 不能在循环,条件,嵌套函数中调用Hook
  • 必须确保总是在你的React函数的顶层调用Hook
  • useEffect, useMemo等必须手动确定依赖关系 Composition API是基于vue响应式系统实现的,
  • 声明在setup函数内,一次组件实例化只调用一次setup; React Hook每次重新渲染都要调用 React Hook,使得React的GC比Vue更有压力,性能相对于Vue也比较慢
  • Composition API不需要考虑调用顺序,可以在循环,条件,嵌套函数中使用
  • 响应式系统自动实现依赖收集,组件的部分性能优化由Vue内部自己完成;React Hook需要手动传入依赖,必须保证依赖的顺序,让useEffect, useMemo等函数正确的捕获依赖变量,否则会因为依赖不正确导致组件性能下降
  • Composition API的设计思想是借鉴React Hook

虚拟DOM

对虚拟DOM 的理解

  • vnode本质是一个javascript对象,提供对象的方式来表示DOM结果
  • 将页面的状态抽象为js对象的形式,配合不同的渲染工具,实现跨平台;
  • 通过事务处理机制,将多次DOM修改结果一次性的更新到页面,减少页面的渲染次数,减少DOM的重绘,重排次数,提高性能
  • 虚拟DOM设计的最初目的是为了更好的跨平台 => 对于没有DOM的node, 实现SSR,借助虚拟DOM,在代码渲染页面指向,将代码转换成一个对象,以对象的形式描述真实的DOM,最终渲染到页面,在每次数据发生变化的前,虚拟DOM都会缓存一份,变换时,现在的虚拟DOM和缓存的虚拟DOM进行比较,通过diff算法比较,渲染时修改改变的变化,原先没有发生变化的通过原先的数据进行渲染

虚拟DOM的解析过程

  1. 将要插入到文档中的DOM树结构进行分析,使用js对象将他表示处理,如一个元素对象包含TagName, props, children等属性,将这个js对象树保存下来,最后再将DOM片段插入到文档中
  2. 当页面的状态发生变化的时候,需要对页面的DOM结构进行调整的时候,首先根据变更状态,重新构造一棵对象树,然后将这颗新的对象树和旧的对象树进行比较,记录下两棵树的差异
  3. 最后将记录的有差异的地方应用到真正的DOM树中去,这样视图就更新了

使用虚拟DOM的原因

  1. 保证性能下限, 在不进行手动优化的情况下,提供过得去的性能

  2. 页面渲染的过程:解析HTML -〉 生成DOM -> 生成CSSOM -》 布局 -》 绘制 -> 渲染都页面;在真实DOM中:涉及到生成HTML字符,+重构所有DOM元素 虚拟DOM: 生成vnode+DOM diff + 必要dom更新 所以,虚拟DOM的更新DOM的准备工作耗费更多的时间;但是对于频繁的操作DOM的更新来说,虚拟DOM的相比于更多的DOM操作它的消费是极其便宜的

  3. 跨平台

虚拟DOM的真的比真实的DOM性能好吗

首次渲染大量的DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢 虚拟DOM的能保证性能下限,在真实DOM操作的时候,进行针对性的优化时,还是比较快的

Diff算法的原理

新老虚拟节点的对比

  • 对比节点本身,判断是否为同一个节点,如果不是相同节点,就删除该节点重新创建新节点进行替换
  • 如果是相同节点,进行patchValue, 判断
  • 如果都有子节点,就进行updateChildren, 判断如何对这些老的节点进行操作
  • 匹配到时,找到相同的节点,递归比较子节点
  • 在diff中,只对同层的子节点进行比较,放弃跨层级节点的比较,比较的过程中,双指针模式,从两边向中间收拢

Vue2,Vue3,React三者diff算法有何区别

diff算法

  • 不管是vue, 还是React, 都是数据更新之后重新生成新的vdom(虚拟DOM),两个进行对比虚拟DOM,得出一个需要更新的DOM,然后去更新,这个对比的过程就是diff算法
  • diff算法很早就有,应用广泛,如github的Pull Request中的代码diff
  • 如果要赶个diff两棵树,时间复杂度O (n^3),不可用
  • react 和 Vue中的Tree diff优化(优化后的时间复杂度O(n^2))
    • 只比较同曾几何时的, 不跨级比较
    • tag不同就删除重建,不再去比较内部的细节
    • 子节点通过key区分,key的重要性(写循环的时候要有key, 不写会有警告 => 因为key是深入到内部的diff 算法优化的)

React diff-仅右移

image.png

Vue2- 双端比较

image.png 即头头,尾尾,头尾,尾头,然后双指针向中间移动

Vue3最长递增子序列

比如一个数组[3,5,7,1,2,8],这个数组的最长递增子序列是多少?=> [3,5,7,8]

image.png

总结

最大程度的去减少操作DOM树,找出那些真正的不同,把它们该怎么做就怎么做。 所以不管是Vue2,Vue3,React,不管它们的算法怎么优化,他们的核心目的只有一个,就是尽量少的去减少DOM树的 操作,找出那些不得不操作的部分,让自己的算法优化到最极致。

vue封装axios

  • 如何封装:
    • 与后端协商好数据约定:状态码,请求头,请求超时时间等
    • 设置接口请求前缀:根据开发,测试,生产环境不同,通过前缀区分(process.env.NODE_ENV取值,设置axios.defaults.baseURL)
    • 请求头:实现具体的业务: 对普遍使用的请求头作为基础,特殊的情况通过参数传入,覆盖基础配置
    • 状态码: 根据返回的不同status执行不同的业务
    • 请求方法:对get, post方法根据业务需求进行二次封装,方便使用
    • 请求拦截器: 根据请求头设定,决定哪些请求可以访问=> token拦截等
    • 响应拦截器: 根据后端返回的状态码执行不同的业务 => 如根据状态码判断登录状态、授权

axios源码分析

  1. 将Axios.prototype上的方法扩展到instance对象上,并指定上下文为context,这样执行Axios原型链上的方法时,this会指向context; 将context对象上的自身属性和方法都扩展到instance上,使得instance 有 defaults、interceptors 属性
  2. 各种请求方式的调用实现都在 request 内部实现的,Axios.prototype.request
Axios.prototype.request = function request(config) {
  // Allow for axios('example/url'[, config]) a la fetch API
  // 判断 config 参数是否是 字符串,如果是则认为第一个参数是 URL,第二个参数是真正的config
  if (typeof config === 'string') {
    config = arguments[1] || {};
    // 把 url 放置到 config 对象中,便于之后的 mergeConfig
    config.url = arguments[0];
  } else {
    // 如果 config 参数是否是 字符串,则整体都当做config
    config = config || {};
  }
  // 合并默认配置和传入的配置
  config = mergeConfig(this.defaults, config);
  // 设置请求方法
  config.method = config.method ? config.method.toLowerCase() : 'get';
  
 
// Hook up interceptors middleware 创建拦截器链. dispatchRequest 是重中之重,后续重点
  var chain = [dispatchRequest, undefined];

  // push各个拦截器方法 
  //注意:interceptor.fulfilled 或 interceptor.rejected 是可能为undefined
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    // 请求拦截器逆序 注意此处的 forEach 是自定义的拦截器的forEach方法
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    // 响应拦截器顺序 注意此处的 forEach 是自定义的拦截器的forEach方法
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  // 初始化一个promise对象,状态为resolved,接收到的参数为已经处理合并过的config对象
  var promise = Promise.resolve(config);

  // 循环拦截器的链
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift()); // 每一次向外弹出拦截器
  }
   // 返回 promise
   return promise;
};
  
  
  

// 在 Axios 原型上挂载 'delete', 'get', 'head', 'options' 且不传参的请求方法,实现内部也是 request
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});

// 在 Axios 原型上挂载 'post', 'put', 'patch' 且传参的请求方法,实现内部同样也是 request
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

拦截器:

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(), // 请求拦截
    response: new InterceptorManager() // 响应拦截
  };
}

InterceptorManager构造函数,统一管理request, response:

// 拦截器的初始化, 一组钩子函数
function InterceptorManager() {
  this.handlers = [];
}

// 调用拦截器实例的use时,即往钩子函数中push方法
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

// 拦截器是可以取消的,根据use的时候返回的ID,把某一个拦截器方法置为null
// 不能用 splice 或者 slice 的原因是 删除之后 id 就会变化,导致之后的顺序或者是操作不可控
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

// 这就是在 Axios的request方法中 中循环拦截器的方法 forEach 循环执行钩子函数
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
}

dispatchRequest的方法:处理请求URL,请求头合并处理,adapter发送请求,根据请求结果分别处理

var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
var isAbsoluteURL = require('./../helpers/isAbsoluteURL');
var combineURLs = require('./../helpers/combineURLs');

// 判断请求是否已被取消,如果已经被取消,抛出已取消
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  // 如果包含baseUrl, 并且不是config.url绝对路径,组合baseUrl以及config.url
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    // 组合baseURL与url形成完整的请求路径
    config.url = combineURLs(config.baseURL, config.url);
  }

  config.headers = config.headers || {};

  // 使用/lib/defaults.js中的transformRequest方法,对config.headers和config.data进行格式化
  // 比如将headers中的Accept,Content-Type统一处理成大写
  // 比如如果请求正文是一个Object会格式化为JSON字符串,并添加application/json;charset=utf-8的Content-Type
  // 等一系列操作
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  // 合并不同配置的headers,config.headers的配置优先级更高
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );

  // 删除headers中的method属性
  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

  // 如果config配置了adapter,使用config中配置adapter的替代默认的请求方法
  var adapter = config.adapter || defaults.adapter;

  // 使用adapter方法发起请求(adapter根据浏览器环境或者Node环境会有不同)
  return adapter(config).then(
    // 请求正确返回的回调
    function onAdapterResolution(response) {
      // 判断是否以及取消了请求,如果取消了请求抛出以取消
      throwIfCancellationRequested(config);

      // 使用/lib/defaults.js中的transformResponse方法,对服务器返回的数据进行格式化
      // 例如,使用JSON.parse对响应正文进行解析
      response.data = transformData(
        response.data,
        response.headers,
        config.transformResponse
      );

      return response;
    },
    // 请求失败的回调
    function onAdapterRejection(reason) {
      if (!isCancel(reason)) {
        throwIfCancellationRequested(config);

        if (reason && reason.response) {
          reason.response.data = transformData(
            reason.response.data,
            reason.response.headers,
            config.transformResponse
          );
        }
      }
      return Promise.reject(reason);
    }
  );
};

vue中权限管理

权限大致可以分为资源权限,数据权限 系统路由的页面可见,面上的菜单数据的显示与否

  1. 资源权限:只能看到自己有权限浏览的内容和有权限操作的控件
  2. 路由权限:
  • 路由可访问:注册好所有路由,用户登录后,访问路由的时候,在路由拦截器里面router.beforeEach()做判断,这种方式会有缺陷如下:
    • 如果路由很多,但是用户不是所有的路由都有权限,就会对性能有影响、
    • 全局路由守卫,每次路由跳转都要做权限判断
    • 菜单信息写死在前端, 如果要改现实文字或权限信息,就需要重新编译
    • 菜单和路由耦合在一起
  • 路由不可访问:用户访问某个没有权限的路由,直接404访问不存在 => 首先注册不需要权限的路由,然后通过vue-router的api router.addRoutes()动态添加生成有权限的路由

前端权限控制划分

  • 接口权限: 接口层面做处理,没有对应权限返回401,前端根据返回状态码做页面重定向到登录页面,登录之后,将token存起来,接口请求request方法里面封装请求头带上token,或自定义拦截器里面拦截
  • 菜单权限:
    • 菜单和路由都由后端返回:后端返回的路由数据处理之后,前端通过addRoutes动态挂载 => 前后端配合要求高,属于全局路由守卫里,每次路由跳转也要做判断
    • 菜单与路由分离,菜单由后端返回,前端定义路由表:后端返回的菜单权限数据已经过滤了,前后端约定好菜单name;每次路由跳转的时候都要判断权限
  • 按钮权限:根据业务逻辑控制按钮显示
    • 自定义指令进行按钮权限的判断,配合路由表meta属性控制按钮权限字段的值处理

  • 路由权限:上面

vue和React区别

vite

底层是基于esbuild预构建依赖的,esbuild是go写的,比js写的打包器预构建快10-100倍

  1. 启动差异 webpack先打包,后启动服务;请求服务器时直接将打包的结果给他
    • 打包需要分析模块依赖,再编译
    • 热更新效率低:应用大的项目,效率也会降低 vite先启动服务,请求哪个模块再对该模块进行实时编译 =》 以原生ESM方式服务源码,让浏览器接管打包程序的工作,vite只需要再浏览器请求源码的时候进行转换并按需提供源码
  • vite启动的时候不需要打包,=》不需要分析模
  • 块的依赖,不需要编译=〉动态编译的方式,
  • 对项目复杂,模块多的极大的缩减了编译时间;在HMR热更新方面,改动一个模块之后,只需让浏览器重新请求该模块,
  • 不需要像webpack那样把模块相关的依赖都全部编译一次
  • vite利用HTTP头来加速整个页面的重新加载,源码模块的请求会根据304进行协商缓存,依赖模块请求会通过Cache-control:max-age, immutable进行强缓存,=》缓存之后就不再进行请求
  1. 缺点
  • vite生态比如webpack:vite刚出来不久,需要时间来构建完善他的社区,webpack社区比较完善,loader, plugin非常丰富
  • vite生产环境构建用rollup:原因是因为esbuild对css和代码的分割不是很友好 => 优势体现再开发阶段
  • 项目的开发浏览器需要支持esmodule,不能识别commonjs语法,对一些项目中的依赖库如antd-mobile,里面es目录下,有import, require混用的情况,vite开发环境支持,构建不支持,只能用webpack,
  • vite目前还没有大规模的使用,很多问题或者诉求没有真正暴露出来
rollup

javascript模块打包器,可以将小块代码编译成大块复杂的代码,CommonJS 和 AMD, 使用ES6模块,静态分析代码中的import, 排除任何没有实际使用的代码

  • tree shaking
  • 通过解析javascript大依赖树将代码输出指定版本的javascript
  • 构建的代码尽量爆出原有的代码,不会像webpack那样注入了大量的webpack内部结构
  • rollup没有devServer工具