Menu

Vue重点知识总结—原理篇

一、响应式数据原理

什么是双向数据绑定:Vue将视图层与数据层做了一个双向的数据同步。甚至是组件之间也可以通过v-model方式进行数据绑定。最大的作用是方便。更效率的实现业务。

vue 实现数据双向绑定主要是:采用数据劫持结合发布-订阅者模式的方式,通过 Object.defineProperty() 来劫持data中对象的各个属性的 settergetter,在数据变动时发布消息给订阅者,触发相应回调。每个组件实例都有相应的 watcher (观察者)程序实例,当页面取相应的属性时,会进行依赖收集(收集组件的watcher),之后当依赖项属性的 setter 被调用时,会通知 watcher 重新计算,从而致使组件得以更新。

Object.defineProperty() 的问题主要有三个:不能监听数组的变化(因为数组可能项目过多,遍历数组太吃性能。需要重写数组的一些方法来实现数组的劫持)、必须遍历对象的每个属性、必须深层遍历嵌套的对象。

Vue在监听数组变化时用的不是Object.defineProperty(),而是把数组的原型方法进行了重写。当用户使用数组的一些方法操作数组时,走的就是VUE自己的方法,然后通知依赖更新。如果数组中包含引用类型如对象,会对其再次进行监控。Vue改写了数组的7个方法(push,pop,shift,unshift,splice,sort,reverse),因为只有这7个方法才能改变数组。另外通过引索改变数组是无法监听的,只能使用$set(arr,index,value)来更新。

在新版Vue3中使用Proxy代理取代Object.defineProperty()来劫持,可以监听整个数据的变化,无需进行层级递归。

整理如下:

  1. 观察者Observer(监听器):⾸先通过观察者对data中的属性使⽤object.defineproperty劫持数据的getter和setter,通知订阅者,触发他的update⽅法,对视图进⾏更新
  2. 解析器Compile:⽤来扫描和解析每个节点的相关指令,并替换模板数据,初始化视图,初始化相应的订阅器
  3. 订阅者Watcher:订阅者接到通知后,调⽤对应的更新函数update⽅法更新对应的视图
  4. 订阅器Dep:订阅者可能有多个,因此需要订阅器Dep来专门接收这些订阅者,并统⼀管理

但在vue3中抛弃了object.defineproperty⽅法,因为

  1. Object.defineproperty⽆法监测对象属性的添加和删除、数组索引和长度的变更,因此vue重写了数组的push/pop/shift/unshift/splice/sort/reverse⽅法
  2. Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进⾏遍历,这样很消耗性能

Vue3中实现数据双向绑定的原理是数据代理,使⽤proxy实现。Proxy 可以理解成,在⽬标对象之前架设⼀层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了⼀种机制,可以对外界的访问进⾏过滤和改写。

二、Vue响应式原理整体叙述(用自己的话)

  1. 通过数据劫持和发布订阅模式来实现。
  2. 初始化时会调用initData,并通过Observer方法对数据进行观测(这里会判断数据是否已被观测,观测时分对象和数组两种观测方法),如果是对象,对象会进行遍历和递归,然后利用Object.defindProperty()方法重新定义了data对象中所有的属性的get和set方法(这个过程如下图)。
  3. 当访问数据时触发对应的get,这时会通过dep.depend()进行依赖收集,收集当前的watcher(观察者,一个组件创建一个watcher,watcher实例与组件实例一一对应)
  4. 修改数据时触发对应的set。如果新的值与旧值不一样,这时会触发dep.notify()回调,通知数据更新。
  5. 由于一个watcher中可能包含有很多个观测的数据,当某个数据更新时怎样找到变化的地方而实现局部响应式更新,这时就需要虚拟DOM和diff算法的支持。
  6. 虚拟DOM是将真实DOM用对象的形式表示,与真实DOM是映射关系。数据变化会修改虚拟DOM对应的节点。然后再通过diff算法,比较新旧虚拟DOM的差异,从而通过打补丁的方式修改真实DOM。(在源码中,patchVnode是diff发生的地方,新旧节点对比方式为:深度优先(递归先找孩子,有孩子先比孩子,没有再比较同层),同层比较。)

Vue重点知识总结—原理篇

三、Vue模板编译原理(template->v-dom->dom)

template –>parse(template) –> ast树(抽象语法树,vue中使用jquery之父的一个作品) –>gencode(ast,options) –>优化树 –> 生成 render 函数 –> 执行 render 函数生成 VNode(虚拟DOM)–> 通过虚拟DOM创建真实DOM

1)什么是模板编译?

把写在<template></template>标签中的类似于原生HTML的内容称之为模板。

在<template></template>标签中除了写一些原生HTML的标签,我们还会写一些变量插值,或者写一些Vue指令,这些东西都是在原生HTML语法中不存在的,不被接受的。render函数会将模板内容生成对应的VNode,VNode经过patch过程从而得到将要渲染的视图中的VNode,最后根据VNode创建真实的DOM节点并插入到视图中, 最终完成视图的渲染更新——模板编译过程。

2)整体渲染过程

在这里插入图片描述

3)模板编译内部流程

借助抽象语法树解析<template></template>标签中写的模板。

4)抽象语法树AST

抽象语法树(AbstractSyntaxTree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。

将一堆字符串模板解析成抽象语法树AST后,我们就可以对其进行各种操作处理了,处理完后用处理后的AST来生成render函数。

  1. 模板解析阶段:将一堆模板字符串用正则等方式解析成抽象语法树AST;
  2. 优化阶段:遍历AST,找出其中的静态节点,并打上标记;
  3. 代码生成阶段:将AST转换成渲染函数;

四、nextTick实现原理

nextTick主要是利用js事件循环机制定义了一个异步方法。在这个异步方法中包含宏任务微任务(主线程先执行,异步线程(工作线程)在主线程执行结束后再将其推入主线程中,异步任务中的微任务会优先于宏任务先执行)。多次调用nextTick会将这些任务存入任务队列,在主线程任务执行完毕后会读取任务队列中的任务进行清空。

用户手动调用的nextTick(cb)也会将cb回调存入任务数组推入任务队列,先进先出原则进行执行。

Vue中的nextTick实现所用的方法:

Vue的nextTick将任务数组推入任务队列所使用的方法是微任务:PromiseMutationObserver;若这些方法不支持,会使用宏任务setImmediate(IE,优先于setTimeout)、setTimeout方法。

原理:

  • 先定义了一个 callbacks 存放所有的 nextTick 里的回调函数
  • 然后判断当前环境是否支持 Promise,如果支持,就用 Promise 来触发回调函数
  • 如果不支持 Promise 就判断是否支持 MutationObserver,通过观察文本节点发生变化,去触发执行所有异步回调函数
  • 如果不支持 MutationObserver 就判断是否支持 setImmediate,如果支持,就通过setImmediate 来触发回调函数
  • 如果以上都不支持就只能用 setTimeout 来完成异步执行

延迟调用优先级如下:

Promise > MutationObserver > setImmediate > setTimeout

$nextTick可以在vue的created生命周期中使用,在回调中可以用$refs获取dom元素。

nextTick作用:

在修改数据之后使⽤nextTick⽤于下次Dom更新循环结束之后执⾏延迟回调。在修改数据之后使⽤nextTick,则可以在回调中获取更新后的DOM。

五、插槽slot原理

插槽,可以分为两种,一种是普通插槽,一种是作用域插槽。普通插槽,又分为默认插槽和具名插槽。具名插槽可以将不同元素插入到不同slot占位符。

作用域插槽,就是使用子作用域数据(子组件属性数据)的插槽。

普通插槽原理:

  1. 父组件先解析,把插槽元素或字符串当做子元素处理,生成含有children的v-dom节点。
  2. 子组件解析,slot 作为一个占位符,会被解析成一个_xxx('default')函数。这个_xxx函数,传入 'default ' 参数并执行。如果给了名字,就传入插槽的名字。
  3. 这个函数的作用,是把第一步解析得到的插槽节点拿到,然后返回,那么子组件的节点就完整了,插槽也成功认了爹。

作用域插槽原理:

  1. 父子组件在生成v-dom节点时,父组件解析成一个含有props参数的函数,直接传入到children中未执行。
  2. 子组件slot占位符同样解析成一个函数,参数除了插槽名称,还有props数据对象。
  3. slot函数在执行时,通过插槽名称找到对应父组件的函数,并执行,将props传入,这样最终返回的就是含有作用域数据的节点

大概原理就是这样,当然在实际源码中,要复杂很多。

六、缓存组件keep-alive原理

用法:

  1. 用于Vue性能优化。缓存组件。
  2. 频繁切换,不需要重复渲染。
  3. keep-alive有include和exclude属性,这两个属性决定了哪些组件可以进入缓存。
  4. keep-alive还有一个max属性,通过它可以设置最大缓存数,当缓存的实例超过max的时候,vue会删除最久没有使用的缓存,属于LRU缓存策略。
  5. keep-alive其内部所有嵌套的组件都具有两个生命周期钩子函数,分别是activated和deactivated,它们分别在组件激活和失活的时候触发。

可以利用keep-alive提供的include和exclude指定缓存哪些组件不缓存哪些组件,然后配合vuex等状态管理工具实现动态控制。

原理:

  • 维护了一个key数组和一个缓存对象,这个key数组记录目前缓存的组件的key值,
  • 如果这个组件没有指定key值,会自动生成一个唯一的key值
  • 缓存对象会以key值为键,vnode为值,用于缓存组件对应的虚拟DOM
  • 在keep-alive的渲染函数中,其基本逻辑是判断当前渲染的vnode是否有对应的缓存,如果有则从缓存中读取到对应的组件实例,没有就把它缓存。

七、路由组件router-view原理

router-view通过判断当前组件的嵌套层次,然后通过这个层次从route.matches数组中获取当前需要渲染的组件,最后调用全局的$createElement来创建对应的VNode完成渲染的。

八、计算属性computed原理

你给 computed 设置的 get 和 set 函数,会与 Object.defineProperty 关联起来。

所以 Vue 能监听捕捉到,读取 computed 和 赋值 computed 的操作。

在读取 computed 时,会执行设置的 get 函数,但是并没有这么简单,因为还有一层缓存的操作。如果数据没有被污染,不为脏数据(标识dirty),那将直接从缓存中取值,而不会去执行 get 函数。

赋值 computed 时,会执行所设置的 set 函数。这个就比较简单,会直接把 set 赋值给 Object.defineProperty – set。

computed缓存原理

  1. 一开始每个 computed 新建自己的 watcher时,会设置 watcher.dirty = true,以便于 computed 被使用时,会计算得到值
  2. 当依赖的数据变化了,通知 computed 时,会赋值 watcher.dirty = true,此时重新读取 computed 时,会执行 get 函数重新计算。
  3. computed 计算完成之后,会设置 watcher.dirty = false,以便于其他地方再次读取时,使用缓存,免于计算。

九、生命周期钩子原理

无非就是回调函数。在不同的节点插入不同的钩子。

十、前端路由URL跳转原理(hash和history路由

history路由:history.pushStatehistory.replaceState方法(Html5新增方法,用于在不刷新页面的情况下切换url路径)

hash路由:hash路由切换本身不会刷新页面。监听hash切换使用onhashchange事件

history.pushState(stateData,nameString,pathUrl)  //把路由页面放入历史记录
history.replaceState(stateData,nameString,pathUrl)  //不放入历史,直接切换
//stateData-传参,nameString-路由名,pathUrl-路由地址

十一、v-if、v-show指令原理

  1. v-if在内部使用with语法,使用三元运算符判断v-if的值的真假。若为真,则渲染节点。若为假,则不渲染。
  2. v-show在内部使用创建指令方式,判断值为假,则改变节点样式display:none,若为真,则使用原有样式。

十二、Vue中事件绑定原理

在template中绑定事件有两种方式。一种是原生dom绑定,如使用<button @click="fn"></button>,另一种是vue组件(添加到组件上的)自定义事件绑定<el-form @click="fn"><el-form>。两种的区别:

  1. 两者编译后的结果不同:原生结果{nativeOnOn:{click}},等价于普通元素on;自定义事件结果{on:{click}},会单独处理。
  2. 原生绑定采用addEventListener方法添加事件。而组件的自定义事件采用vm.$on()方法,使用vue的发布订阅模式绑定事件,需要使然$emit来触发事件。
  3. 在组件上添加原生事件的方法是使用native修饰符:<el-form @click.native="fn"><el-form>
  4. Vue中没有事件代理的机制。如果要在v-for渲染的多个节点上添加事件,会多次调用addEventListener方法,性能不高。可以在外层添加事件,采用代理模式区分每个子节点的事件。

十三、v-model的实现原理

v-model就是value+input事件的语法糖。

原理:会将组件的v-model转换成value+input

除input标签外,select标签等无法使用input事件的标签,其事件会使用change或自定义input

自定义组件实现v-model

Vue.component("el-checkbox",{
    template:`<input type="checkbox" :checked="check" @change="$emit("change",$event.target.checked)"/>`,
    model:{
        prop:"check",  // 默认为value,这里更改为check
        event:"change" // 默认为input,这里改为change
    },
    props:{
        check:boolean
    }
})

普通标签v-model原理:除value+input之外,还会添加指令。会根据元素的type在编译时绑定不同的属性和事件。比如checkbox会绑定checked属性和change事件。

当添加.lazy修饰符之后(v-model.lazy=""),改变input框中的内容并不会使得其他绑定的数据发生变化,当输入框失去焦点后触发change事件才会改变。

十四、vue.$set原理

强制更新。将未被劫持的数据重新劫持,然后再跑一遍Vue的响应式流程。在Vue3中已废弃。

十五、路由懒加载如何实现,原理是什么

路由懒加载需要导入异步组件,最常用的是通过import()来实现它。

function load(component) {
    return () => import(`views/${component}`)
}
// 箭头函数
const asyncPage = () => import('./views/home.vue')
// import()函数需要作为返回值,其返回Promise

编译打包后,会把每个路由组件的代码分割成一个js文件,初始化时不会加载这些js文件,只当激活路由时(访问该路由时)才会去按需加载对应的路由组件js文件。可以加快项⽬的加载速度。

在Vue3中,在组件中使用异步组件时需要使用defineAsyncComponent()函数才能实现懒加载,但在vue-router中同样可以使用import()方法

这时由于在 Vue 3 中,函数组件被定义为纯函数,异步组件定义需要通过将其包装在一个新的 defineAsyncComponent helper 中来显式定义。

import { defineAsyncComponent } from 'vue'
const home = defineAsyncComponent(() => import('@/views/home.vue'))
export default {
  name: 'async-components',
  components:{
    home
  }
};

十六. Vue3中proxy的原理

Proxy 可以理解成,在⽬标对象之前架设⼀层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了⼀种机制,可以对外界的访问进⾏过滤和改写。

主要通过Proxy对对象进⾏绑定监听处理,通过new Map对对象的属性操作进⾏处理,将要执⾏的函数匹配到存到对应的prop上⾯,通过每次的访问触发get⽅法,进⾏订阅操作,通过修改触发set⽅法,此时通过回调函数通知订阅者,触发他的update⽅法,对视图进⾏更新,达到修改数据和视图的 。

本文固定连接:https://code.zuifengyun.com/2020/07/3315.html,转载须征得作者授权。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

¥ 打赏支持