一、响应式数据原理
什么是双向数据绑定:Vue将视图层与数据层做了一个双向的数据同步。甚至是组件之间也可以通过v-model方式进行数据绑定。最大的作用是方便。更效率的实现业务。
vue
实现数据双向绑定主要是:采用数据劫持结合发布-订阅者模式的方式,通过 Object.defineProperty()
来劫持data中对象的各个属性的 setter
,getter
,在数据变动时发布消息给订阅者,触发相应回调。每个组件实例都有相应的 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()
来劫持,可以监听整个数据的变化,无需进行层级递归。
整理如下:
- 观察者Observer(监听器):⾸先通过观察者对
data
中的属性使⽤object.defineproperty
劫持数据的getter和setter,通知订阅者,触发他的update⽅法,对视图进⾏更新 - 解析器Compile:⽤来扫描和解析每个节点的相关指令,并替换模板数据,初始化视图,初始化相应的订阅器
- 订阅者Watcher:订阅者接到通知后,调⽤对应的更新函数update⽅法更新对应的视图
- 订阅器Dep:订阅者可能有多个,因此需要订阅器Dep来专门接收这些订阅者,并统⼀管理
但在vue3中抛弃了object.defineproperty
⽅法,因为
Object.defineproperty
⽆法监测对象属性的添加和删除、数组索引和长度的变更,因此vue重写了数组的push/pop/shift/unshift/splice/sort/reverse⽅法Object.defineProperty
只能劫持对象的属性,因此我们需要对每个对象的每个属性进⾏遍历,这样很消耗性能
Vue3中实现数据双向绑定的原理是数据代理,使⽤proxy实现。Proxy 可以理解成,在⽬标对象之前架设⼀层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了⼀种机制,可以对外界的访问进⾏过滤和改写。
二、Vue响应式原理整体叙述(用自己的话)
- 通过数据劫持和发布订阅模式来实现。
- 初始化时会调用initData,并通过Observer方法对数据进行观测(这里会判断数据是否已被观测,观测时分对象和数组两种观测方法),如果是对象,对象会进行遍历和递归,然后利用Object.defindProperty()方法重新定义了data对象中所有的属性的get和set方法(这个过程如下图)。
- 当访问数据时触发对应的get,这时会通过
dep.depend()
进行依赖收集,收集当前的watcher(观察者,一个组件创建一个watcher,watcher实例与组件实例一一对应) - 修改数据时触发对应的set。如果新的值与旧值不一样,这时会触发
dep.notify()
回调,通知数据更新。 - 由于一个watcher中可能包含有很多个观测的数据,当某个数据更新时怎样找到变化的地方而实现局部响应式更新,这时就需要虚拟DOM和diff算法的支持。
- 虚拟DOM是将真实DOM用对象的形式表示,与真实DOM是映射关系。数据变化会修改虚拟DOM对应的节点。然后再通过diff算法,比较新旧虚拟DOM的差异,从而通过打补丁的方式修改真实DOM。(在源码中,patchVnode是diff发生的地方,新旧节点对比方式为:深度优先(递归先找孩子,有孩子先比孩子,没有再比较同层),同层比较。)
三、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函数。
- 模板解析阶段:将一堆模板字符串用正则等方式解析成抽象语法树AST;
- 优化阶段:遍历AST,找出其中的静态节点,并打上标记;
- 代码生成阶段:将AST转换成渲染函数;
四、nextTick实现原理
nextTick
主要是利用js事件循环机制定义了一个异步方法。在这个异步方法中包含宏任务和微任务(主线程先执行,异步线程(工作线程)在主线程执行结束后再将其推入主线程中,异步任务中的微任务会优先于宏任务先执行)。多次调用nextTick
会将这些任务存入任务队列,在主线程任务执行完毕后会读取任务队列中的任务进行清空。
用户手动调用的nextTick(cb)
也会将cb回调存入任务数组推入任务队列,先进先出原则进行执行。
Vue中的nextTick实现所用的方法:
Vue的nextTick将任务数组推入任务队列所使用的方法是微任务:Promise
、MutationObserver
;若这些方法不支持,会使用宏任务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占位符。
作用域插槽,就是使用子作用域数据(子组件属性数据)的插槽。
普通插槽原理:
- 父组件先解析,把插槽元素或字符串当做子元素处理,生成含有children的v-dom节点。
- 子组件解析,slot 作为一个占位符,会被解析成一个
_xxx('default')
函数。这个_xxx
函数,传入'default '
参数并执行。如果给了名字,就传入插槽的名字。 - 这个函数的作用,是把第一步解析得到的插槽节点拿到,然后返回,那么子组件的节点就完整了,插槽也成功认了爹。
作用域插槽原理:
- 父子组件在生成v-dom节点时,父组件解析成一个含有props参数的函数,直接传入到children中未执行。
- 子组件slot占位符同样解析成一个函数,参数除了插槽名称,还有props数据对象。
- slot函数在执行时,通过插槽名称找到对应父组件的函数,并执行,将props传入,这样最终返回的就是含有作用域数据的节点
大概原理就是这样,当然在实际源码中,要复杂很多。
六、缓存组件keep-alive原理
用法:
- 用于Vue性能优化。缓存组件。
- 频繁切换,不需要重复渲染。
- keep-alive有include和exclude属性,这两个属性决定了哪些组件可以进入缓存。
- keep-alive还有一个max属性,通过它可以设置最大缓存数,当缓存的实例超过max的时候,vue会删除最久没有使用的缓存,属于LRU缓存策略。
- 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缓存原理
- 一开始每个
computed
新建自己的watcher
时,会设置 watcher.dirty = true,以便于computed
被使用时,会计算得到值 - 当依赖的数据变化了,通知
computed
时,会赋值 watcher.dirty = true,此时重新读取computed
时,会执行get
函数重新计算。 computed
计算完成之后,会设置 watcher.dirty = false,以便于其他地方再次读取时,使用缓存,免于计算。
九、生命周期钩子原理
无非就是回调函数。在不同的节点插入不同的钩子。
十、前端路由URL跳转原理(hash和history路由)
history路由:history.pushState
和history.replaceState
方法(Html5新增方法,用于在不刷新页面的情况下切换url路径)
hash路由:hash路由切换本身不会刷新页面。监听hash切换使用onhashchange
事件
history.pushState(stateData,nameString,pathUrl) //把路由页面放入历史记录
history.replaceState(stateData,nameString,pathUrl) //不放入历史,直接切换
//stateData-传参,nameString-路由名,pathUrl-路由地址
十一、v-if、v-show指令原理
v-if
在内部使用with
语法,使用三元运算符判断v-if的值的真假。若为真,则渲染节点。若为假,则不渲染。v-show
在内部使用创建指令方式,判断值为假,则改变节点样式display:none
,若为真,则使用原有样式。
十二、Vue中事件绑定原理
在template中绑定事件有两种方式。一种是原生dom绑定,如使用<button @click="fn"></button>
,另一种是vue组件(添加到组件上的)自定义事件绑定<el-form @click="fn"><el-form>
。两种的区别:
- 两者编译后的结果不同:原生结果
{nativeOnOn:{click}}
,等价于普通元素on;自定义事件结果{on:{click}}
,会单独处理。 - 原生绑定采用
addEventListener
方法添加事件。而组件的自定义事件采用vm.$on()
方法,使用vue的发布订阅模式绑定事件,需要使然$emit来触发事件。 - 在组件上添加原生事件的方法是使用native修饰符:
<el-form @click.native="fn"><el-form>
。 - 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,转载须征得作者授权。