前言:大厂面试被问到Vue的时候,你是不是也一样总是在“深挖环节”栽跟头?明明会用Vue开发,却答不出底层逻辑?
别让这种痛苦继续!阅读这个系列文章,帮你打通从“会用”到“懂原理”的任督二脉!
1、Vue的响应式原理(vue如何监听数据的变化更新视图)
1️⃣Vue通过数据劫持(如vue2用的Object.defineProerty,Vue3用的proxy代理)来拦截对象属性的读写;
2️⃣当组件初始化渲染、计算属性执行或watch回调触发时,若访问到响应式属性,会触发getter函数进行依赖收集,当修改属性值时,触发setter函数,通知依赖该属性的watcher执行更新;
3️⃣依赖收集包括dep(依赖管理器)和watcher(依赖订阅者),每个响应式属性对应一个dep实例,用于储存所有依赖它的watcher,当getter被触发时,会通过当前活跃的watch注册到dep,而当属性变化时,setter被调用,触发对应的notify(),dep会遍历所有订阅的watcher,调用其update()方法,watcher将自身加入异步更新队列([使用nextTick机制,优先使用微任务]避免重复渲染),最终执行回调,实现响应式渲染。
2、数据劫持具体是怎么劫持,它对基础数据类型和引用数据类型有哪些不一样的操作吗(对第1个问题的扩展)
1️⃣数据劫持的核心是通过特定API拦截数据的读写行为,从而在数据被访问或修改时执行额外逻辑,Vue2基于Object.defineProerty对对象的每个属性进行劫持,通过重定向get和set方法来实现拦截,Vue3则是基于proxy对整个对象进行代理,通过proxy的get和set陷阱拦截所有属性的读写;
2️⃣对于基础数据类型,Vue2无法直接劫持,因为Object.defineProperty只能劫持对象的属性,但如果组件中使用基础数据类型,需要将其包装在对象中,通过劫持对象的属性间接拦截基础数据读写,Vue3也无法直接代理基础数据类型,需包装在对象中,通过代理对象的属性实现拦截;
3️⃣而对于引用数据类型:
(1)对象:Vue2需递归遍历所有属性,对每个属性单独劫持,但新增/删除属性需手动触发(this.$set)劫持,Vue3的proxy天然支持拦截所有属性(包括新增、删除),无需递归劫持,性能更优;
(2)数组:Vue2中重写数组7个变异方法(如 push 、 pop、splice ),通过这些方法修改实现拦截,直接修改索引(如 arr[0] = 1 )和长度无法被拦截,Vue3中proxy的get和set陷阱可直接拦截数组的索引访问,长度修改,无需单独重写方法。
✡〖简洁版:数据劫持核心就是拦截“读写行为”,Vue2通过属性的defineProperty,Vue3通过对象的proxy,基础数据类型在Vue2Vue3都需要包装在对象中才能被劫持,引用数据类型中,Vue2需要对对象递归劫持(初始化时),对数组重写方法,Vue3则无需,proxy(访问时动态劫持)可统一拦截所有操作〗
3、数据更新完虚拟节点怎么更新对DOM对应修改
1️⃣数据变化触发组件重新渲染,生成新的vnode(虚拟dom树);
2️⃣通过diff算法对比新旧vnode找出差异(如节点类型,属性,文本内容、子节点变化),得到“差异补丁”(记录哪里节点需要增删改);
3️⃣根据差异补丁执行对应的dom操作,重新渲染到真实dom树中。
4、diff算法的原理是什么?(第3/5个问题扩展)
在vue中,虚拟dom的diff比较发生在组件更新阶段。数据变化后,会生成新的虚拟树,diff算法通过同层比较〖仅对比同一层级的节点,若父节点类型不同,则直接销毁旧节点所在的整个子树并创建新子树〗和key识别节点〖通过节点的key属性判断是否为同一节点,key相同的节点会被视为可复用节点,key不同则直接销毁旧节点,并创建新节点〗,找出新旧虚拟dom的差异,只更新变化部分(类型、属性、内容等),未变部分直接复用,以此高效更新真实dom。
5、虚拟dom中的key有什么作用?为什么不建议用索引作为key?(第4个问题扩展)
1️⃣key是虚拟dom节点的唯一标识,可以帮助diff算法快速识别同一节点如第4问题key识别节点解释;
2️⃣当列表头部插入新元素时,原有元素的索引会整体往后移,导致新旧虚拟dom中,索引key相同的节点其实是不同内容的节点,此时diff算法会错误的认为这些节点是同一节点而尝试复用,导致dom内容与数据不匹配,甚至触发不必要的节点更新,降低性能,所以不建议用索引作为key。
ps:以上面试问题收集于网络,答案都是自己对Vue的理解,若有不完整或者表述有问题的,欢迎指正。持续更新中!