1.DIff算法看不懂就一起来砍我(带)
2.Luy 1.0 :一个React-like轮子的源码诞生
3.Vue源码-Virtual DOM
4.最全vue面试必问题(附题)
DIff算法看不懂就一起来砍我(带)
面试官:“你对虚拟DOM(Virtual DOM)和Diff算法了解吗?请描述一下。”
我:“额,源码那个,源码嗯...”突然智商不在线,源码没组织好语言,源码没答好或者压根就答不出来。源码本地源码页面找不到
所以这次我总结一下相关的源码知识点,让你可以有一个清晰的源码认知,也会让你在今后遇到这种情况可以坦然自若,源码应付自如,源码游刃有余:
相关知识点:
虚拟DOM(Virtual DOM):
Diff算法:
虚拟DOM(Virtual DOM):
什么是源码虚拟DOM:
一句话总结虚拟DOM就是一个用来描述真实DOM的JavaScript对象,这样说可能不够形象,源码那我们来举个:分别用代码来描述真实DOM以及虚拟DOM。源码
真实DOM:
对应的源码虚拟DOM:
控制台打印出来的Vnode:
h函数生成的虚拟DOM这个JS对象(Vnode)的源码:
补充:
上面的h函数大家可能有点熟悉的感觉但是一时间也没想起来,没关系我来帮大伙回忆;开发中常见的源码现实场景,render函数渲染:
为什么要使用虚拟DOM:
灵魂发问:使用了虚拟DOM就一定会比直接渲染真实DOM快吗?答案当然是否定的,且听我说:
举例:当一个节点变更时DOMA->DOMB
上述情况:
示例1是创建一个DOMB然后替换掉DOMA;
示例2去创建虚拟DOM+Diff算法比对发现DOMB跟DOMA不是相同的节点,最后还是创建一个DOMB然后替换掉DOMA;
可以明显看出1是更快的,同样的结果,2还要去创建虚拟DOM+Diff算法对比
所以说使用虚拟DOM比直接操作真实DOM就一定要快这个说法是错误的,不严谨的
举例:当DOM树里面的某个子节点的内容变更时:
当一些复杂的节点,比如说一个父节点里面有多个子节点,当只是一个子节点的内容发生了改变,那么我们没有必要像示例1重新去渲染这个DOM树,这个时候虚拟DOM+Diff算法就能够得到很好的体现,我们通过示例2使用虚拟DOM+Diff算法找出改变了的子节点更新它的内容就可以了
总结:复杂视图情况下提升渲染性能,因为虚拟DOM+Diff算法可以精准找到DOM树变更的地方,减少DOM的操作(重排重绘)
虚拟dom库Diff算法:
在看完上述的文章之后相信大家已经对Diff算法有一个初步的概念,没错,Diff算法其实就是找出两者之间的差异;
diff算法首先要明确一个概念就是Diff的对象是虚拟DOM(virtual dom),更新真实DOM是Diff算法的结果。
下面我将会手撕snabbdom源码核心部分为大家打开Diff的心,给点耐心,别关网页,我知道你们都是这样:
snabbdom的核心
init函数
init函数时设置模块,然后创建patch()函数,我们先通过场景案例来有一个直观的体现:
当init使用了导入的模块就能够在h函数中用这些模块提供的api去创建虚拟DOM(Vnode)对象;在上文中就使用了样式模块以及事件模块让创建的这个虚拟DOM具备样式属性以及事件属性,最终通过patch函数对比两个虚拟dom(会先把app转换成虚拟dom),更新视图;
我们再简单看看init的恒达源码搭建源码部分:
这些地方也会用createElement来命名,它们是一样的东西,都是创建虚拟DOM的,在上述文章中相信大伙已经对h函数有一个初步的了解并且已经联想了使用场景,就不作场景案例介绍了,直接上源码部分:
总结:h函数先生成一个vnode函数,然后vnode函数再生成一个Vnode对象(虚拟DOM对象)
补充:
在h函数源码部分涉及一个函数重载的概念,简单说明一下:
重载这个概念和参数相关,和返回值无关
patch函数(核心):
要是看完前面的铺垫,看到这里你可能走神了,醒醒啊,这是核心啊,上高地了兄弟;
源码:
看得可能有点蒙蔽,下面再上一副思维导图:
题外话:Diff算法简介:
传统Diff算法
snabbdom的Diff算法优化
下面我们就会介绍updateChildren函数怎么去对比子节点的异同,也是Diff算法里面的一个核心以及难点;
updateChildren(核中核:判断子节点的差异):
为了更加直观的了解,我们再来看看同级别节点比较的五种情况的实现细节:
新开始节点和旧开始节点(情况1)新结束节点和旧结束节点(情况2)旧开始节点/新结束节点(情况3)旧结束节点/新开始节点(情况4)新开始节点/旧节点数组中寻找节点(情况5)
下面我们再介绍一下结束循环的收尾工作(oldStartIdx>oldEndIdx || newStartIdx>newEndIdx):
最后附上源码:
key的作用:
以下我们看看这些作用的实例:
Diff操作可以更加准确;(避免渲染错误)
实例:a、b、c三个DOM元素中的b、c间插入一个z元素
没有设置key
没有设置key
当设置了key:
Diff操作可以更加准确;(避免渲染错误)
实例:a、b、c三个DOM元素,修改了a元素的某个属性再去在a元素前新增一个z元素
没有设置key:
因为没有设置key,默认都是undefined,所以节点都是相同的,更新了text的内容但还是沿用了之前的DOM,所以实际上a->z(a原本打勾的状态保留了,只改变了text),b->a,c->b,d->c,遍历完毕发现还要增加一个DOM,在最后新增一个text为d的DOM元素
设置了key:
当设置了key,a、b、c、d都有对应的key,a->a,b->b,c->c,众人帮网页源码d->d,内容相同无需更新,遍历结束,新增一个text为z的DOM元素
不推荐使用索引作为key:
设置索引为key:
这明显效率不高,我们只希望找出不同的节点更新,而使用索引作为key会增加运算时间,我们可以把key设置为与节点text为一致就可以解决这个问题:
最后:
如有描述错误或者不明的地方请在下方评论联系我,我会立刻更新,如有收获,请为我点个赞,这是对我的莫大的支持,谢谢各位。
Luy 1.0 :一个React-like轮子的诞生
在过去的月余,我从零开始构建了一个类React的框架——Luy,以此深入学习React内部算法与数据结构。今日,Luy的首个版本终于发布,通过实测一个小型项目,证明其可行性。您可访问动态简历luy版本进行预览,或访问仓库主页获取源码,期待您的支持。
实则并非从零开始,Luy利用官方解析器处理jsx。面对这一项目,我感到既困惑又畏惧,但通过研读源码及搜索文章,逐渐理解了React的复杂之处。
构建Luy时,挑战尤其集中在更新机制上,这是虚拟DOM实现的关键。了解inverno.js的快速性能,可发现其算法优势。Luy采用的是snabbdom源码学习算法,其执行效率令人满意。与官方React相比,速度优势明显。
对于希望深入学习React源码的源码找特征码朋友,建议先动手构建一个框架,再阅读源码。理解框架中每行代码背后解决问题的逻辑,是掌握React内核的关键。Google搜索辅助理解,阅读源码过程虽艰难,但收获巨大。
Luy的未来充满可能。最初目标为学习React原理,但随着项目进展,我决定持续维护并跟进React官方更新。Luy支持如createPortal等特性,适合作为学习React套路的工具。面对公司新项目,我决定尝试使用Luy,以实践所学。
Luy框架总计约行代码,简洁明了,可作为学习React框架的入门选择。访问Luy框架地址获取源码。代码实践是学习编程的基石,正如@vczh所言,热爱编程的年轻人应铭记学习的重要性。
Vue源码-Virtual DOM
虚拟 DOM 是 Vue.js 中用于提升渲染效率的关键概念,它通过使用 JavaScript 对象来模拟 DOM 树,从而避免了每次状态变化时对真实 DOM 的频繁操作,显著减少了性能开销。
Vue 中的虚拟 DOM 是基于 Snabbdom 的实现,并集成了一些 Vue 特有的功能,比如指令和组件机制。这种设计使得 Vue 能够高效地响应数据变化,优化渲染流程。
Vue 从 2.x 版本开始,引入了虚拟 DOM 来提升性能。在 Vue 1.x 中,每一项属性变化都触发了一个 watcher,导致了过高的开销。Vue 2.x 则采取了一种更高效的广西玩法完整源码方式:每个组件关联一个 watcher,当组件状态发生变化时,Vue 仅对组件进行更新,并通过虚拟 DOM 进行对比和渲染,以确保效率。
在实际应用中,虚拟 DOM 的作用主要体现在渲染函数和 JSX 的使用上。通过这些功能,开发者可以轻松地将组件的状态和属性映射到虚拟 DOM 树上,而 Vue 则会负责将虚拟 DOM 转换成真实的 DOM,进行视图渲染。
Vue 中的 `h` 函数是生成虚拟 DOM 对象的关键。它是通过 `vm._render()` 函数生成相应的虚拟 DOM,然后通过 `vm._update()` 进行转换,从而完成视图更新过程。`h` 函数本质上就是 `vm.$createElement`,这个函数是 Vue 在初始化阶段注入到实例中的核心工具。
在 Vue 的创建阶段,`$createElement` 的定义在 Vue 的初始化构造函数中,它负责解析渲染函数并生成虚拟 DOM 对象。`$createElement` 实际上调用了 `createElement` 方法,并通过 `normalizationType` 参数控制了 DOM 结构的规范化。生成的虚拟 DOM 对象,如 `_createElement`,是后续处理过程的基础。
虚拟 DOM 的处理过程涉及一系列步骤,包括比较新旧虚拟节点、判断是否存在先前处理过的节点、调用 `__patch__` 函数进行实际的 DOM 更新,以及通过 `patch` 函数执行具体的 DOM 操作。在这一过程中,`patch` 函数通过创建 DOM 节点、比较和更新虚拟节点来优化渲染效率。
使用 `key` 的好处在于显著提升了渲染效率。在处理子节点时,设置 `key` 可以帮助 Vue 更快地识别哪些节点发生了变化,从而减少不必要的 DOM 操作。当 `key` 相同的节点在更新过程中保持一致时,Vue 只需要进行简单的比较,而不需要进行全盘的 DOM 更新,从而大幅减少了性能开销。
总结而言,虚拟 DOM 是 Vue.js 实现高效数据绑定和组件更新的核心机制。它通过将数据变化映射到虚拟树上,再将虚拟树转换为真实 DOM,有效降低了渲染成本,提升了应用性能。
最全vue面试必问题(附题)
1. 请解释MVVM。
MVVM是Model-View-ViewModel的缩写,它将MVC中的Controller演变为ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。
2. 请说明Vue的生命周期。
beforeCreate是new Vue()之后触发的第一个钩子,在当前阶段data、methods、computed以及watch上的数据和方法都不能被访问。created在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发updated函数。可以做一些初始数据的获取,在当前阶段无法与Dom进行交互,如果非要想,可以通过vm.$nextTick来访问Dom。beforeMount发生在挂载之前,在这之前template模板已导入渲染函数编译。而当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。mounted在挂载完成后发生,在当前阶段,真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。beforeUpdate发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。updated发生在更新完成之后,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。beforeDestroy发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。destroyed发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。
3. 你的接口请求一般放在哪个生命周期中?
接口请求一般放在mounted中,但需要注意的是服务端渲染时不支持mounted,需要放到created中。
4. 再说一下Computed和Watch。
Computed本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理。Watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch手动注销哦。
5. 请说明v-if和v-show的区别。
当条件不成立时,v-if不会渲染DOM元素,v-show操作的是样式(display),切换当前DOM的显示和隐藏。
6. Vue模版编译原理知道吗,能简单说一下吗?
简单说,Vue的编译过程就是将template转化为render函数的过程。会经历以下阶段:生成AST树、优化codegen。首先解析模版,生成AST语法树(一种用JavaScript对象的形式来描述整个模板)。使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用。编译的最后一步是将优化后的AST树转换为可执行的代码。
7. Vue2.x和Vue3.x渲染器的diff算法分别说一下。
同级比较,再比较子节点。先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)。比较都有子节点的情况(核心diff)。递归比较子节点。正常Diff两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动DOM,所以Vue将Diff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。Vue3.x借鉴了 ivi算法和 inferno算法 在创建VNode时就确定其类型,以及在 mount/patch 的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升。(实际的实现可以结合Vue3.x源码看。) 该算法中还运用了动态规划的思想求解最长递归子序列。
8. 再说一下虚拟Dom以及key属性的作用。
由于在浏览器中操作DOM是很昂贵的。频繁的操作DOM,会产生一定的性能问题。这就是虚拟Dom的产生原因。Vue2的Virtual DOM借鉴了开源库snabbdom的实现。Virtual DOM本质就是用一个原生的JS对象去描述一个DOM节点。是对真实DOM的一层抽象。(也就是源码中的VNode类,它定义在src/core/vdom/vnode.js中。) VirtualDOM映射到真实DOM要经历VNode的create、diff、patch等阶段。key的作用是尽可能的复用 DOM 元素。新旧 children 中的节点只有顺序是不同的时候,最佳的操作应该是通过移动元素的位置来达到更新的目的。需要在新旧 children 的节点中保存映射关系,以便能够在旧 children 的节点中找到可复用的节点。key也就是children中节点的唯一标识。
9. Vue2.x组件通信有哪些方式?
父子组件通信:父->子props,子->父 $on、$emit 获取父子组件实例 $parent、$children Ref 获取实例的方式调用组件的属性或者方法 Provide、inject 官方不推荐使用,但是写组件库时很常用 兄弟组件通信 Event Bus 实现跨组件通信 Vue.prototype.$bus = new Vue Vuex 跨级组件通信 Vuex $attrs、$listeners Provide、inject
. v-model是如何实现双向绑定的?
v-model是用来在表单控件或者组件上创建双向绑定的。
他的本质是v-bind和v-on的语法糖。
在一个组件上使用v-model,默认会为组件绑定名为value的prop和名为input的事件。
. 怎样理解Vue的单向数据流?所有的prop都使得其父子prop之间形成了一个单向下行绑定:父级prop的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父级组件发生更新时,子组件中所有的prop都将刷新为最新的值。这意味着你不应该在一个子组件内部改变prop。如果你这样做了,Vue会在浏览器的控制台中发出警告。子组件想修改时,只能通过$emit派发一个自定义事件,父组件接收到后,由父组件修改。
有两种常见的试图改变一个prop的情形:
这个prop用来传递一个初始值;这个子组件接下来希望将其作为一个本地的prop数据来使用。在这种情况下,最好定义一个本地的data属性并将这个prop用作其初始值:
这个prop以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个prop的值来定义一个计算属性。
. hash路由和history路由实现原理说一下。
location.hash的值实际就是URL中#后面的东西。history实际采用了HTML5中提供的API来实现,主要有history.pushState()和history.replaceState()。