生活值得你继续努力,加油!
Vue2基础知识
模板编译、数据绑定、依赖收集、派发更新
如果面试官有问到上述4个点,我一般会询问面试官,我有一个场景可以把上面4点知识点串起来解释下看可以不?
场景:页面有一个div,然后div绑定了data里面的变量a,值为1.两秒之后将a的值改为2.
通过这个场景从页面展示1到变成2将模板编译、数据绑定、依赖收集、派发更新这几个点串一下
首先Vue会遍历data里面的属性,内部通过defineReactive方法,使用Object.defineProperty将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现(原型继承)。 多层对象是通过递归来实现劫持。然后在劫持的时候,会让每个属性拥有自己的dep,存放他所依赖的watcher,当属性变化的时候会通知它对应的watcher去更新(dep.notify)。
然后开始模板编译过程,如果options有render方法就直接取render执行。
如果没有render且options中有template和el,那么template得优先级更高。如果不存在template,则取el得outHTML为template;
1、将template转成AST语法树
2、静态优化,优化不会改变得内容,做diff算法得时候可以跳过被标记成静态节点的元素
3、将AST语法树转化成render函数(_c、_s、_v这些),
4、执行render函数(_render)生成虚拟节点
5、然后将虚拟节点转化成真实节点
这里需要注意一点就是执行render函数的时候是通过watcher执行,也就是我们说的渲染watcher。
然后执行render的时候会对a进行取值操作,触发了a的getter,a的dep属性就收集了当前watcher(渲染watcher)
然后就结束拉,页面展示为1。
两秒之后修改a的值,走到了a的setter,拿到a的dep收集的全部watcher执行,也就是执行render方法,页面重新渲染(DIFF),这个时候页面就变为2拉。
批量更新和nextTick原理
Vue里面如果对一个属性进行重复赋值,到最后会发现只更新了一次,这个取决于Vue内部实现了批量更新
this.name = "1";
this.name = "2";
this.name = "3";
拿这个举个例子 name属性的dep收集了页面更新的watcher,如果没有批量更新操作,那就会执行三次render函数。所以Vue内部维护了一个队列,对相同的watcher进行过滤(通过watcher的id),上述代码实际上队列只有一个watcher,然后调用nexttick对这个队列进行循环执行。
而nexttick的原理是
1、首先会将把nexttick的回调放进一个数组callbacks
2、然后根据当前的环境按promise、MutationObserver、setImmediate、settimeout的顺序降级的处理flushCallbacks方法(遍历callbacks执行),赋值给timerFunc
3、然后执行timerFunc方法
那为什么nexttick可以获取到最新DOM?
按上面所说数据在更新的时候会调用nexttick(flushSchedulerQueue), 其中flushSchedulerQueue就是把属性所有相关watcher执行的方法,按照上面nexttick的源码解析会将刷新视图的方法当作一个异步任务来处理,因为调用顺序的原因,flushSchedulerQueue先执行,在执行nexttick的回调,所以可以拿到最新的DOM
computed和watch的区别 & 原理
watcher有三种 渲染watcher 用户watcher 和computed watcher
watch的原理是遍历watch的属性的到表达式,然后为每一个属性生成一个watcher(用户watcher),然后将表达式转化为一个函数执行,这个时候函数内部获取了vm[表达式]的值,触发了表达式的get,开始收集当前生成的watcher。然后数据发生变化的时候,回调函数就会执行。
计算属性原理稍微复杂一点
假设页面代码如下
<div>{{name}}</div>
<div>{{newName}}</div>
data() { return { name: 'chenying' } },
computed: { newName() { return this.name + 1 } }
1、 Vue的双向数据绑定依赖于Dep.target的巧妙设计,其中维护了一个全局的数组targetStack,然后通过popTarget和pushTarget的方法来控制Dep.target
2、每个watcher有一个deps属性,存放所以调用了这个watcher的dep,每个dep有一个subs数组,存放所有依赖的watcher,他们是相互关联的
2、首先源码会遍历computed然后给每一项添加一个computed watcher,其中computed watcher和普通watcher的区别是传入了参数lazy:true,并且不会立即执行传入的表达式
3、然后通过Object.defineProperty方法为每一项计算属性添加getter 和setter
4、然后全部renderwatcher执行,页面获取到newName的值,触发getter方法
5、其中getter方法里面会调用watcher.evaluate() - 将dirty改为false,然后将当前Dep.tagert改为computed watcher,调用get方法,get方法里面会对name取值所以调用name的getter方法,也就是给name的subs添加newName的watcher
5、然后Dep.target中的computed watcher出栈,此时Dep.target为页面watcher
6、再然后手动调用计算属性watcher.depend()方法,遍历这个watcher的deps,找到所有有关系的dep,然后往dep.subs里面添加当前的Dep.target,也就是页面watcher
7、这个时候name的subs里面是这样的 [compouted watcher, render watcher],所以name更新会触发这2个watcher
而我们常说的计算属性可以缓存,这个缓存指的是如果依赖的值没有变化,获取计算属性的值的时候不会在调用这个计算属性watcher的get方法,而是直接从watcher.value里面取
VUE2 DIFF算法原理
-
会先比较标签是否一致,如果标签不一致用新的标签替换掉老的标签
-
如果标签一致,有可能都是文本节点,那就比较文本的内容即可
-
当标签相同时,复用老的标签元素,并且进行属性的比对。(如果老的有新的没有,直接删除。如果老的有,新的也有,以新的为准。如果老的没有,新的有,直接用新的)
-
然后判断有没有孩子节点
4-1. 如果老的有孩子,新的没有孩子,就删除老的孩子元素
4-2. 如果老的没孩子,新的有孩子,则将新的孩子添加到老的上面
4-3. 如果老的有孩子,新的也有孩子,就进行比较(双指针)
4-3-1. 首先优化4个场景
-
头头比较: 如果新老头节点一致,则复用老节点 - abcd->abcde 实际上abcd都是复用的,新增e
-
尾尾比较: 如果新老尾节点一致,则复用老节点 - abcd->eabcd 实际上abcd都是复用的,新增e
-
新头旧尾比较: 如果新头和旧尾一致,则复用旧尾,移动到头部位置 - abcd -> dabc
-
旧头新尾比较: 如果旧头和新尾一致,则复用老旧头,移动到尾部位置 - abcd -> bcda
4-3-2. 如果优化策略没有命中,就开始暴力对比,用老节点造一个映射表(key:索引的结构),去搜索看是否存在,如果存在则复用,不存在则新增节点,最后删除多余的老节点
abcd->dcba的流程
首先命中优化策略 旧头新尾 ,复用a,将a移动到最尾部。然后命中 旧头新尾 ,复用b,移动到a前面。然后命中优化策略 旧头新尾 ,复用c,移动到b前面。最后命中优化策略 头头 ,直接复用d
在 v-for 时写了 key ,将第二个元素和第三个元素交换了顺序,实际的 diff 算法怎样的
触发头头,复用第一个节点。后续触发尾尾,复用第四个节点至最后一个节点的全部节点。然后触发旧头新尾,复用老的第二个节点,移动位置。最后触发头头,复用老的第三个节点
组件渲染流程
Vue.extend实际上是创建一个子类Sub,将子类的原型指向Vue.prototype(Sub.prototype = Vue.prototype),并且将自己的component属性和全局的options.conponents属性进行合并,这也是为啥优先取自己定义的同名组件
Vue.component实际上就是内部调用Vue.extend生成一个构造器,并存Vue.options.components中
组件的渲染流程如下:
调用mount方法,解析模板,生成render函数,执行render函数,这个时候通过标签tag判断他是一个组件,会特殊创建组件的虚拟节点,虚拟节点里面有组件构造器和一些hooks(init),然后通过虚拟节点挂载真实节点,挂载时候调用init方法,也就是通过构造器生成组件实例,并且调用组件实例的mount方法进行挂载。
组件更新流程
组件更新会触发组件的prepatch方法,会复用这个组件,并且比较组件的插槽-属性-事件
因为父组件传递给子组件的props是响应式的,在渲染子组件模板时候如果使用了则会收集当前子组件的渲染watcher,然后父组件数据变化自动触发子组件更新
异步组件原理
先渲染一个占位符组件,如果组件异步加载回来,则调组件的forceUpdate强制刷新组件,类似于图片懒加载
v-model的原理
v-model对应普通表单元素和组件是不一样的。
对于不同的表单元素v-model会有不同的结果,如input和checked。对于input来说会被编译成(input + value + 指令),并且value和input实现的双向数据绑定阻止了中文的触发,取之的是指令内部手动的触发双向绑定。
对于组件来说就很简单了,就是默认编译为:value和@input,但是也可以通过model: {props: xx, event:xx}来修改默认绑定
keep-alive原理
keepalive原理是默认缓存加载过的组件对应的实例,内部采用LRU算法。大致逻辑是内部维护一个cache对象,为每个加载的组件生成一个唯一key来做cache的键,将vnode中的data.keepAlive设置为true(vnode中有data.keepAlive 并且有componentInstance说明这个虚拟节点是缓存过的),下次再加载相同的组件,则从cache缓存中取$el,判断标识就不走mount这些逻辑了,并且触发actived和deactived钩子.
而include和exclude是根据组件的name或者tag来判断的
Vue.use原理
mixin原理和合并策略
其实都是合并到Vue.options属性里面去,然后Vue.$options是option和Vue.options进行合并的。
其中生命周期用策略模式合并成数组,也就是每个相同的生命周期都会执行。
对于components的合并是子的比全局的优先,如果子没有则取全局的
对于props、methods、inject、computed,则是创建第三方对象ret,像将父的上述属性赋值给ret,再将子的上述属性赋值给ret,所以子的后替换父的
对于data,data内部进行递归合并,在和组件的数据发生冲突时以组件数据优先
生命周期和原理
beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。
created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。
beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
原理
Vue的生命周期钩子就是回调函数(callhook)而已,当创建组件实例的过程中会调用对应的钩子方法
Vue在内部会把用户的选项进行合并处理, mergeOptions针对不同的字段进行合并。其中就包含生命周期hook合并,内部是把合并的同名钩子合并成一个数组,最后在初始化的时候把数组里面每个钩子依次执行
v-if和v-show的区别
v-if控制的是元素是否渲染,会被编译成三元表达式。
而v-show控制的是样式,会被编译成指令,内部首先先把元素原始的display保存下来,然后隐藏的话赋值为display为none,显示的时候赋值为原来的值
为什么v-for 和 v-if不能连用
v-for的优先级比较高,会被编译成_l,而v-if会被编译成三元表达式。因为v-for的优先级比较高,如果联用的话相当于每条数据都进行一次三元表达式运算,浪费性能
组件的data为啥要返回一个函数
每个组件内部实际上都是通过Vue.extend来创建一个子类,而extend方法会把组件的options放倒Sub类上,如果说用对象的话,那相当于全部组件都公用一个Sub类上的data,引用类型就会互相影响
function Vue() {}
function Sub() {
// 会将data存起来 this.data = this.constructor.options.data;
}
Vue.extend = function(options) { Sub.options = options; return Sub; }
let Child = Vue.extend({ data: { name: 'chen' } }); // 两个组件就是两个实例, 希望数据互不干扰
let child1 = new Child();
let child2 = new Child();
console.log(child1.data.name);
child1.data.name = 'ying';
console.log(child2.data.name);
Vue.$set原理
针对对象采用的是defineReactive,将对象新增的属性变成响应式的。因为有可能对象本身有dep属性,所以需要触发对象本身存储的watcher来实现重新渲染的逻辑。
针对数组 直接调用splice方法即可实现数据变化更新视图
Vue-router原理和核心实现API
Vue-route里面我们最常用的就是hash模式和history模式,还有一个abstract这个我认识就比较少了
hash模式的原理是利用location.hash 来实现的,location.hash 可以获取到url后面 #/xxx的值,我们可以通过location.hash.slice(1)获取#后面的路径,然后赋值给window.location.hash就可以跳转啦,但是我们
还需要监听hashchange的方式来监听hash的变化,来实时改变window.location.hash的值
history模式的原理是通过HTML5提供的history api,我们可以使用window.location.pathname获取当前路由的路径,然后通过history.pushState({}, null, path)的方式可以进行跳转,但是history api是不支持浏览器的前进和后退的,所以我们需要监听popstate,来实现浏览器的前进后退
其中Vue.use(router)的时候大致做了这些,将传进来的routes规则扁平化成路由映射表,然后通过Vue.mixin在beforeCreate将router的实例共享给全部组件,并且代理成route,然后注册两个组件link和view,最后开启路由监听啥的。而我们经常用的 添加动态路由的方法addRoute和addRoutes实际上就是往上面说的路由映射表添加数据
Vue中路由懒加载原理
和webpack懒加载是一样的,import代码切割 + JSONP引入
Vue为什么需要虚拟DOM
虚拟DOM就是为了diff算法做准备的,因为没有虚拟DOM的话,实际上进行的是两个字符串模板的比对,就很麻烦。
再者直接操作DOM是比较浪费性能的,因为DOM上的属性很多,而操作虚拟DOM相当于对象间的比对,相比来说所消耗内存的性能就少很多了
而且虚拟DOM具有跨平台的能力
Vue中key的作用和原理,谈谈你对它的理解
Vue在patch过程中通过key可以判断两个虚拟节点是否是相同节点。 (可以复用老节点)
无key会导致更新的时候出问题
尽量不要采用索引作为key,会出现问题,如数组最前面插入一项,会出现意想不到的问题
Q:对MVVM的理解
MVVM分别是model、viewModel、view 目的是为了实现分层,借助了后端MVC的思想,但是发现后端把全部视图更新,数据处理逻辑都丢在constructor层,逻辑就臃肿。
所以MVVM是隐藏constructor这层,直接将数据映射到视图上,并且自动监控视图的变化,视图变化之后更新数据
VUE3基础知识
VUE3代理那块为啥性能比VUE2高
首先Vue3采用proxy,Vue2采用defineProperty进行代理。而proxy天生支持代理数组等,而defineProperty需要重写数组的方法实现代码,还不可以支持索引和length变化的劫持。
再者Vue2里面是一上来就递归data的全部属性通过defineProperty进行代理。而Vue3只是通过reactive进行手动代理,就是说哪些数据需要响应式的能力由业务方决定,节省了性能。并且Vue3-proxy是懒递归如:
const proxy = reactive({
address: {
num: 1
}
})
proxy只是代理到address这一层,后续如果代码有对num的取值如proxy.address.num,这个时候会再代理一次
Vue3依赖收集和派发更新原理
假设现在有这样一个代码,然后我们逐渐分析下代码是怎么依赖收集和派发更新的。
import { reactive, effect } from 'vue'
const obj = { name: 'cy' }
const proxy = reactive(obj)
effect(() => {
console.log(proxy.name)
})
settimeout(() => {
proxy.name = "666"
}, 1000)
我们会发现一开始输出cy,然后1s之后输出666,那具体代码怎么走的呢?
首先reactive会把传入的对象通过proxy进行代码,赋予了getter和setter的能力。
然后effect(fn)会将fn包装成一个effect,然后执行effect,这个时候会触发proxy.name的getter,然后proxy.name的get回调里面会调用track方法,在全局里面有一个targetMap,通过全局的targetMap存储proxy.name对应的effect.如下图:
这个时候依赖收集就完成了,其实vue3依赖收集收集的是effect(fn)内部把fn封装成的fn。
然后1s之后触发proxy.name的get回调,会调用trigger方法,从全局的targetMap里面找到proxy.name对应得effect遍历执行,也就是console.log(proxy.name)会再次执行,就是派发更新得逻辑。
effect里面嵌套调用effect,内部保证属性收集正确得effect?
effect(() => {
proxy.name = 111
effect(() => {
proxy.name = 222
})
proxy.name = 333
})
其实effect内部是实现一个栈结构,嵌套调用会把effect进栈,执行完出栈,然后收集的是当前栈顶得effect
ref原理
我们知道reactive是对传入的对象进行代理,赋予响应式的能力。而ref(x)是让x具有响应式的能力,无论x是普通类型还是引用类型。
let isShow = ref(true)
effect(() => {
console.log(isShow.value)
})
isShow.value = false
上述会输出true和false,也就是让一个布尔类型的值具有响应式的能力。
ref内部原理是生成一个类RefImpl
RefImpl内部会先判断传递进来的是原始类型还是对象类型,如果是对象类型则调用reactive进行代理。
如果是原始类型则手动调用track和trriger实现响应式,具体步骤如下:
将val赋值给this._value
RefImpl类内部有对value的get和set
class RefImpl {
get value() {
// 调用track进行依赖收集
track()
return this._value
}
set value(newVal) {
// 调用trriger进行派发更新
trriger()
this._value = newVal
}
}
get value的时候调用track收集依赖
set value的时候调用trriger派发更新
Vue3 toRef、torefs的原理
内部其实就是一个类,有get value方法,实际上调用get value方法是做了层代理,取得是state.age,然后触发age的getter从而触发track收集依赖啥的
Vue3computed原理
const proxy = reactive({
age: 25
})
let newAge = computed(() => {
return proxy.age * 2
})
effect(() => {
console.log(newAge.value)
})
proxy.age = 30
首先computed执行会创建一个computed effect,和其他effect不一样的是它传入参数有lazy和scheduler,并且不会立即执行
const runner = effect(getter, {lazy:true, scheduler: () => {dirty: true, trigger()}})
然后会返回一个计算属性类,类上有get value 和 set value
{
__v_isRef: true,
get value() {
if (dirty) {
value = runner(); // 取值时运行effect
dirty = false;
}
track(computed, "get", 'value');
return value;
},
set value(newValue) {
setter(newValue)
}
}
然后后续如果调用返回类的get value,则执行effect的getter,触发了proxy.age的getter(track),这个时候age收集了这个计算属性effect,然后计算属性类又收集了外层的effect。
也就是age收集computed effect, 而计算属性类又收集了外层的effect
然后修改age的值的时候,调用trriger方法,computed effect会执行,但是因为传入了scheduler,所以执行的是scheduler函数而不是getter,然后将dirty置为true,表示值发生改变了(下次在调用计算属性类get value的时候会重新执行computed effect的getter,重新获取最新值),然后触发计算属性类自身收集的effect,就是外层的effect会执行
至于vue3如何实现计算属性的缓存,上述有提到一丢丢,这里总结一下,内部通过dirty变量来控制,一开始dirty为true。第一次获取计算属性的值的时候,因为dirty为true,所以调用computed effect的getter获取最新值,然后将dirty置为false。后续如果dirty一直为false不管调用多少次get value,都一直返回上一次的值。当age发生改变的时候触发trigger,也就是执行到computed的scheduler,这个时候会将dirty置为true,表示依赖的值发生改变,计算属性需要重新计算,这个时候再调用计算属性get value的时候,就重新执行effect getter获取最新值返回