1、Vue三要素
1)响应式
vue如何监听到 data 每个属性变化?
2)模板引擎
vue的模板如何被解析,指令如何处理?
1】本质是字符串,是以字符串存在的,只不过像html
2】有逻辑,比如判断,循环这些,如v-if,v-for等,怎么会有逻辑呢,之前写html就没逻辑
3】与html格式很像,但有很大区别。首先html在语法上是不认识v-if,v-for这些的。第二个是html是静态的,没有逻辑,vue是动态的,有逻辑的。它们只是格式很像
3)渲染
vue 的模板如何被渲染成 html?以及渲染过程
浏览器只认识html,不能解析vue格式的模板引擎,因此,vue要将其转换为浏览器认识的html+css+js。
1】把模板编译为 render函数
(render 函数即渲染函数,它的参数也是个函数,即 js 的 createElement,返回值是虚拟dom)
2】实例进行挂载, 根据根节点render函数的调用,递归的生成虚拟dom
3】虚拟dom对比数据更新的差异,渲染到真实dom
4】监听组件内部data,若发生变化,重新调用render函数,生成虚拟dom, 返回到步骤3
2、Vue模版编译原理知道吗,能简单说一下吗?
简单说,Vue的编译过程就是将template转化为render函数的过程。会经历以下阶段:
a. 生成AST树
b. 优化
c. codegen
首先解析模版,生成AST语法树(一种用JavaScript对象的形式来描述整个模板)。使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用。
编译的最后一步是将优化后的AST树转换为可执行的代码。
3、vue 的双向绑定(MVVM)的原理是什么 ?(响应式原理)
1)首先什么是响应式?
响应式就是,当监听到数据发生变化后,无需进行DOM操作,会自动重新渲染页面。
2)想完成这个过程,我们需要:
1】侦测数据的变化(数据劫持)
2】收集视图依赖了哪些数据(依赖收集)
3】数据变化时,自动“通知”需要更新的视图部分,并进行更新(发布订阅模式)
a、vue2.x
通过一个observer函数(观察者),去遍历data(),拿到data里面的每一项变量之后,通过以下的方式去代理监听:
vue中的数据响应式其实是个发布订阅模式:
通过Object.defineProperty() 通过遍历对象(data()),来劫持对象的setter和getter。
在数据劫持中,get函数是触发监听,把data()里面的每一个属性都绑定上了一个Watcher。
set函数是发布者,因为只要数据已更新,就会触发set函数,set函数就会告诉Dep让其执行notify,相当于this.$emit(‘notify’)。
Dep是调度中心,负责收集依赖和收到发布者的命令执行notify方法,调度中心通知Watcher执行更新方法。Watcher去调用自己的update方法,经历Diff算法,最终执行render()去更新DOM。
get函数是订阅者,用于订阅Watcher。
Vue2.x的响应式使用了Object.defineProperty(),但是存在一些不足:
无法检测对象属性的新增删除
无法检测数组的变化:
a.通过索引改变数组的操作
b.数组长度发生了变化
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性(需要通过遍历这个对象), 并返回这个对象(原对象)。数组的索引也是属性,所以我们是可以监听到数组元素的变化的。
Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Object.defineProperty本身是可以监控到数组下标的变化的,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。比如,假设数组只有4个有意义的值,但是长度确实1000,我们不可能为1000个元素每个都做遍历检测操作。
b、vue3.x
vue3.x采用了proxy
Proxy直接可以劫持整个对象,并返回一个新对象。proxy在目标对象的外层搭建了一层拦截,外界对目标对象的某些操作,比如属性的操作(get 和 set),必须通过这层拦截。
A、相比Object.defineProperty()的优点:
1】Proxy可以直接监听整个对象的变化,无需通过遍历来劫持属性。
2】Proxy可以监听到属性的新增/删除,数组长度和下标的变化。
3】Proxy还支持监听Map,Set,WeakMap和WeakSet这些数据结构。
B、缺点:
只能监听对象的一层,深度监听需要进行递归操作。
解决方案:
Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。
reactive的用法是将数据变成响应式数据,当数据发生变化时UI也会自动更新。主要用于复杂数据类型,比如对象和数组。
****reactive响应式化:
给对象里每一个属性都注册getter和setter函数
可以利用 Object.defineProperty 或 Proxy 来实现
4、vue2.x中如何监测数组变化
使用了函数劫持的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组api 时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。
****监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?
我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger。
5、js实现简单的双向绑定
6、介绍虚拟DOM
1】什么是vdom
虚拟DOM本质就是真实DOM的结构映射过来的一个JS对象
用 JS 模拟 DOM 结构
DOM 变化的对比,放在 JS 层来做
这样做可以提高浏览器的重绘性能(最消耗浏览器性能的是dom的渲染,将多次渲染合并为一次)
2】vdom如何模拟真实的dom
真实dom:
虚拟dom:tag标签名、attr属性(id\class\name)、children子节点、文本节点、绑定的事件
3】vdom核心API
a、h函数
实质上是createElement的一种封装,用于创建vdom
语法:
h(‘标签名’, {属性}, [子元素])
h(‘标签名’, {属性}, [文本])
b、patch函数(补丁)
patch(container, vnode) //初次渲染
patch(vnode, newVnode) //dom节点更新渲染
4】实现一个vdom的流程
a、compile,如何把真实 DOM 编译成 vnode。
(vnode,真实dom节点的抽象)
b、diff,我们要如何知道 oldVnode 和 newVnode 之间有什么变化。
只更新需要改动的内容,其他不更新的内容不更新,这样做到尽可能少的操作DOM。
c、patch, 如果把这些变化用打补丁的方式更新到真实 dom 上去
7、写 Vue 项目时为什么要在列表组件中写 key,其作用是什么?
key的作用:为了高效的查找和更新虚拟DOM
a、列表节点唯一标识,利用快速节点比对
对两个节点进行比较的时候,会优先判断 key 是否一致
v-for=“i in dataList” 会有提示我们需要加上 key ,因为循环后的 dom 节点的结构没特殊处理的话是相同的。
b、利于节点高效查找
同一层vnode节点是以数组的方式存储,那么如果节点非常多,通过遍历查找就稍微有点慢,因此,内部将 vnode 列表转换成对象,直接通过 key 查找到数组下标,利于加快查找时间。
******为什么不推荐使用index作为key值?
如果不涉及数组元素的新增/删除,可以使用index作为key;但是如果涉及数组元素的新增/删除,数组元素的index会发生变化,dom会重新渲染。但是使用唯一不重复的string作为key,dom则默认用“就地复用”策略,只更新变化的那一部分。
8、Diff 算法
作用:对比dom节点的差异,找出需要更新的节点
diff的实现过程: patch(container, vnode) 和 - patch(vnode, newVnode)
diff的实现核心: createElement 和 updateChildren
注意:diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较
1】调用patch函数对比新旧节点
function sameVnode (a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 标签名
a.isComment === b.isComment && // 是否为注释节点
// 是否都定义了data,data包含一些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是的时候,type必须相同
)
}
两个节点相同的依据: key值、标签名、是否为注释节点、事件、标签属性是否有关联等等
较为表层的比较,不涉及子节点的比较
function patch (oldVnode, newVnode) {
if (sameVnode(oldVnode, vnode)) { //判断两节点是否值得比较
patchVnode(oldVnode, vnode) //如果两个节点值得比较,则进入深层次对比
} else {
//不值得比较则用 newVnode 替换 oldVnode
createEle(vnode) // 根据Vnode生成新元素
oldVnode = null
}
return vnode
}
2】两节点相同,则进入深层次比较(patchVnode)
patchVnode函数:
a、找到对应的真实dom,称为 el
b、判断 newVnode 和 oldVnode 是否相等,如果是,那么直接 return;
如果他们不相等并且都有文本节点(文本节点出现差异),那么将 el 的文本节点设置为 newVnode 的文本节点。
c、如果 oldVnode 有子节点而 newVnode 没有,则删除 el 的子节点
d、如果 oldVnode 没有子节点而 newVnode 有子节点,则将 newVnode 的子节点真实化之后添加到 el
e、如果两者相等并且都有子节点,则执行 updateChildren 函数比较子节点,这一步很重要
3】updateChildren(深层次比较old与new节点的异同)
oldVnode:startIndex、endIndex
newVnode:startIndex、endIndex
startIndex向左移动、endIndex向右移动
形成了4种对比的情况:
a、老开始与新开始对比
b、老结束与新结束对比
c、老开始与新结束对比
d、老结束与新开始对比
一旦 startIndex > endIndex 表明 oldVnode 和 newVnode 至少有一个已经遍历完了,就会结束比较。
如果以上4种情况都没命中,则会开始对比key:
拿到newVode startIndex的key,在oldVnode里面去找有没有某个节点有对应这个key
a、oldVnode没有找到对应的key,则直接insert
b、oldVnode找到了对应的key,判断两个node是否相等
不相等,newNode直接insert;相等,调用patchVnode函数进行更新
4】diff算法的粒度
Vue的Diff算法分为两个粒度:
a、组件级别(component Diff)
b、元素级别(Element Diff)
组件级别的Diff算法比较简单,节点不相同就进行创建和替换,节点相同的话就会对其子节点进行更新;对子节点进行更新也就是元素级别的Diff,通过插入、移动和删除等方式对旧列表改造成和新列表一致。
9、Vue2.x和Vue3.x渲染器的diff算法分别说一下
双端对比算法
简单来说,diff算法有以下过程:同级比较,再比较子节点
先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
比较都有子节点的情况(核心diff)
递归比较子节点
正常Diff两个树的时间复杂度是O(n3),但实际情况下我们很少会进行跨层级的移动DOM,所以Vue将Diff进行了优化,从O(n3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。
Vue3.x借鉴了ivi算法和 inferno算法,在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升。(实际的实现可以结合Vue3.x源码看。)
该算法中还运用了动态规划的思想求解最长递归子序列。
10、Vue.js的全局运行机制
流程分析:
先进行初始化及挂载:init以及mount
再进行模板编译:compile,编译成渲染函数render function
再进行响应式依赖收集:render function => getter、setter => Watcher 进行update => patch 的过程以及使用队列来异步更新的策略。
依赖收集的同时生产Virtual DOM:render function 被转化成 VNode 节点
通过diff算法后进行patch更新视图
11、Vue UI框架源码阅读(组件库设计原理 )
1】slot插槽
slot插槽占位符,可以实现父子组件传参
父组件templet模版可将子组件slot内容替换。当slot未命名时将父组件全部替换,当定义name时,可以实现父组件对子组件的指定位置显示内容或传参。
2】多个样式
事先在子组件里面定义好几个样式,props传入样式名,在对号入座
自定义验证,当没有遵循传入规则时需要对其进行一个预先检查,validator可以通过自定义函数对传入的参数进行校验
type: {
type: String,
default: ‘default’,//[‘default’,success’, ‘warning’, ‘error’, ‘info’]
validator(value) {
let types = [‘default’,‘success’, ‘warning’, ‘error’, ‘info’]
return types.includes(value) || !value
}
}
12、computed原理、watch原理、computed和watch的区别
1)computed原理(get/set)
1】将computed对象中的每一个key创建一个watcher,watcher的getter就是你写的函数,最开始watcher的属性lazy为true。当依赖变化的时候,这个watcher 会将自己的lazy属性设置为true。(dirty也是true)
2】defineComputed:将computed的key通过设置defineProperty的getter setter设置到viewModel上
3】缓存:dirty为true,才会重新计算
初始化实例的时候watcher的dirty属性等于传入的配置lazy。 所以在第一次访问计算属性时,会做一次求值,执行set更改value值,然后将dirty置为false。后续我们访问的时候,检测到dirty为false,就会直接返回这个值,并不会再次计算。
应用场景:用于双大括号里面的一些数值计算,购物车金额计算
2)watch原理
1】在created函数调用之前,调用了initWatcher方法,为每一个watcher属性实例化了一个Watcher
new Watcher (监听的属性key, 回调函数和options(包括handler、deep和immediate的值))
2】Watcher对象里面有get 和 set 方法
3】Get 时,如果deep为真,则会递归监听所有的属性;如果immediate为true后,则监听的这个对象会立即输出
应用场景:监听路由变化、监听一些value的变化
3)区别
1】computed有缓存,watch没有
有缓存的话,如果下次执行没有发生任何变化,那么不会执行计算属性中的函数,直接返回结果。这样快且节约性能。
2】computed只能同步,watch同时支持同步和异步
3】watch会监听整个对象的所有属性,比较消耗性能(可以用字符串形式来优化)。而computed适用于计算比较消耗性能的计算场景。
13、Vuex
Vuex是一种状态管理工具。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
1)基本用法
1】state
state是存储的单一状态,是存储的基本数据
2】getters
getters是store的计算属性,对state进行类似computed计算,是派生出来的数据。就像computed计算属性一样,getter返回的值会根据它的依赖被缓存起来,且只有当它的依赖值发生改变才会被重新计算
3】mutations(mutations同步函数)
mutations提交更改数据,使用store.commit方法更改state存储的状态
Mutation为啥只能是同步的?
答:Mutation如果是异步,则可能会在内部其他异步函数执行前就先执行,这样会造成state状态改变的不可控不可追踪。
4】actions
actions也是用于更改state的,包含任何异步操作,本质上还是通过commit去提交mutation,而不是直接变更状态。
5】module
module是store分割的模块,每个模块拥有自己的state、getters、mutations、actions
2)实现原理
将数据存放到统一的store,在入口文件main.js里面,利用插件机制Vue.use(vuex),将store对象挂载到全局,同时利用vue的mixin混入机制,在每一个实例组件的beforeCreate钩子前混入vuex的init方法,vuex的init方法实现了store对象注入vue组件实例。这样每个vue组件实例都可以利用this.$store来调用。
1】vuex如何监听各个组件之间数据更新?
a、响应式:Vuex的state状态是响应式,本质上是借助vue组件的data的响应式,将state存入vue实例组件的data中(见上)
b、computed:Vuex的getters则是借助vue的计算属性computed实现数据实时监听,vuex的state也有缓存(见上)
3)vuex的缺陷:刷新后状态会丢失
解决方案:将其存储在webstorage里面
4)vuex的替代方案
就像 Vuex官方文档所说的,如果应用不够大,为避免代码繁琐冗余,最好不要使用它。2.6 新增加的 Observable API ,通过使用这个 api 我们可以应对一些简单的跨组件数据状态共享的情况。这个可以说是个精简版的vuex。
// store/store.js
import Vue from ‘vue’
export const store = Vue.observable({ count: 0 })
export const mutations = {
setCount (count) {
store.count = count
}
}