皮皮网
皮皮网

【食探源码】【超级澡堂源码分析】【小程序 游戏源码】vue fragment 源码

来源:cci背离指标源码 发表时间:2024-11-26 14:39:46

1.vue早期源码学习系列之四:如何实现动态数据绑定
2.浅谈vue3的编译优化
3.Vue 3 全方位详解
4.问起Vue的原理时不要再只说defineProperty了
5.vue3的学习_介绍他
6.5分钟让你读懂Vue双向数据绑定的原理解析及代码实现

vue fragment 源码

vue早期源码学习系列之四:如何实现动态数据绑定

       本系列更多文章,请查阅我的博客:GitHub - youngwind/blog: 梁少峰的个人博客。或扫描下方二维码关注公众号“前端控”,欢迎交流讨论!

       在前一篇中,我们学习了如何监听数据变化以及使用观察者模式和事件传递响应变化事件。食探源码今天,我们将探讨如何基于watch库实现动态数据绑定。

       我们可以将问题具体化为一个例子:如何实现当user.name或user.age发生变化时,HTML上的DOM元素也能相应地改变?

       最初,我采取了以下方法:在数据顶层注册一个事件,当任意属性改变时,重新遍历DOM模板,将{ { user.name}}等转换为实际值,在内存中拼接成fragment,最后将新fragment替换掉原有的DOM结构。这种方法虽然简单,但存在不少问题。

       基于上述问题,我们需要改进这种做法。为此,我们引入了Directive(指令)的概念,其构造函数和原型方法如下所示。关键实现思路是:在遍历DOM模板的过程中,当遍历到文本节点:"{ { name}}"时,先将其中的表达式"name"匹配出来,然后新建一个空的textNode,插入到这个文本节点的前面,最后移除这个文本节点。这样,就实现了用一个程序生成的textNode代替原来的textNode,从而实现每个textNode都跟它的超级澡堂源码分析表达式一一对应起来。

       然而,这种方法仍然存在问题。为了解决这些问题,我们引入了Binding和Watcher这两个“类”。Binding用于解决键值索引,Watcher用于解决$watch。Binding、Watcher和Directive三者之间的关系如下:(此处插入)

       从图中可以看出,有一个_rootBind对象,其属性是按照DOM模板中用到的数据层层深入排列下去的。在每个属性上都有一个_subs数组,这个数组表示订阅的意思,里面存放的是一系列Watcher。Watcher既可以装载Directive,也可以装载$watch。这就是vue实现动态数据绑定的三大核心概念。

       学习Vue.js,我checkout的是vue的这个版本。相比于之前的学习,代码量大幅增加,从七八百行增加到差不多五千行。特别是Binding、Watcher和Directive这几个核心概念,一开始让人难以理解。经过多日的思考和不断调试,我才逐渐明白。

       此外,通过Binding、Watcher、Directive构建的动态数据绑定体系还存在一个重大缺陷,我们将在下一篇文章中专门阐述。小程序 游戏源码

浅谈vue3的编译优化

       编译优化:编译器将模版编译为渲染函数的过程中,尽可能地提取关键信息,并以此指导生成最优代码的过程。

       优化的方向:尽可能地区分动态内容和静态内容,并针对不同的内容采用不同的优化策略

1.动态节点收集与补丁标志1.1传统diff算法的问题

       比对新旧两棵虚拟DOM树的时候,总是要按照虚拟DOM的层级结构“一层一层”地遍历

<divid="foo"><pclass="bar">{ { text}}</p></div>

       上面这段代码中,当响应式数据text值发生变化的时候,最高效的更新方式是直接设置p标签的文本内容

       传统Diff算法做不到如此高效,当text值发生变化的时候,会产生一颗新的虚拟DOM树,对比新旧虚拟DOM过程如下:

       对比div节点,以及该节点的属性和子节点

       对比p节点,以及该节点的属性和子节点

       对比p节点的文本子节点,如果文本子节点的内容变了,则更新,否则什么都不做

       可以发现,有很多无意义的对比操作。

       总结:

       传统diff算法的问题:无法利用编译时提取到的任何关键信息,导致渲染器在运行时不会去做相关的优化。

       vue3的编译器会将编译得到的关键信息“附着”在它生成的虚拟DOM上,传递给渲染器,执行“快捷路径”。

1.2Block与PatchFlags

       传统Diff算法无法避免新旧虚拟DOM树间无用的比较操作,是因为运行时得不到足够的关键信息,从而无法区分动态内容和静态内容。换句话说,只要运行时能够区分动态内容和静态内容,就可以实现极简的优化策略

       举个例子:

<div><div>foo</div><p>{ { bar}}</p></div>

       只有{ { bar}}是动态的内容。理想情况下,当数据bar的值变化时,只需要更新p标签的我的源码基地文本节点即可。为了实现这个目标,需要提供信息给运行时

//传统虚拟DOM描述constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar},]}//编译优化后constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar,patchFlag:1},//这是动态节点]}

       可以发现,虚拟节点多了一个额外的属性,即patchFlag(补丁标志),存在该属性,就认为是动态节点

       patchFlag(补丁标志)可以理解为一系列的数字标记,含义如下

constPatchFlags={ TEXT:1,//代表节点有动态的textContentCLASS:2,//代表元素有动态的class绑定STYLE:3//其他。。。}

       可以在虚拟节点的创建阶段,把它的动态子节点提取出来,并存储到该虚拟节点的dynamicChildren数组中

constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar,patchFlag:1},//这是动态节点],//将children中的动态节点提取到dynamicChildren数组中dynamicChildren:[{ tag:'p',children:ctx.bar,patchFlag:PatchFlags.TEXT}]}

       Block定义:带有dynamicChildren属性的虚拟节点称为“块”,即(Block)

       一个Block本质上也是一个虚拟DOM,比普通的虚拟节点多处一个用来存储动态节点的dynamicChildren属性。(能够收集所有的动态子代节点)

       渲染器的更新操作会以Block为维度。当渲染器在更新一个Block时,会忽略虚拟节点的children数组,直接找到dynamicChildren数组,并只更新该数组中的动态节点。跳过了静态内容,只更新动态内容。同时,由于存在对应的补丁标志,也能够做到靶向更新。

       Block节点有哪些:模版根节点、带有v-for、v-if/v-else-if/v-else等指令的节点

1.3收集动态节点

       编译器生成的渲染函数代码中,不会直接包含用来描述虚拟节点的数据结构,而是包含着用来创建虚拟DOM节点的辅助函数,如下

render(){ returncreateVNode('div',{ id:'foo'},[createVNode('p',null,'text')])}functioncreateVNode(tag,props,children){ constkey=props&&props.keyprops&&deleteprops.key//省略部分代码return{ tag,props,children,key}}

       createVNode的返回值是一个虚拟DOM节点

       举个例子:

<divid="foo"><pclass="bar">{ { bar}}</p></div>

       上面模版生成带有补丁标志的渲染函数如下:

render(){ returncreateVNode('div',{ id:'foo'},[createVNode('p',{ class:'bar'},text,PatchFlags.TEXT)])}

       怎么将根节点变成一个Block,如何将动态子代节点收集到该Block的dynamicChildren数组中?

       可以发现,在渲染函数内,js手势缩放源码对createVNode函数的调用是层层嵌套结构,执行顺序是内层先执行,外层再执行,当外层createVNode函数执行时,内层的createVNode函数已经执行完毕了。因此,为了让外层Block节点能够收集到内层动态节点,需要一个栈结构的数据来临时存储内层的动态节点。代码实现如下:

//动态节点constdynamicChildrenStack=[]//当前动态节点集合letcurrentDynamicChildren=null//openBlock用来创建一个新的动态节点集合,并将该集合压入栈中functionopenBlock(){ dynamicChildrenStack.push((currentDynamicChildren=[]))}//closeBlock用来通过openBlock创建的动态节点集合从栈中弹出functioncloseBlock(){ currentDynamicChildren=dynamicChildrenStack.pop()}

       然后调整createVNode函数

<div><div>foo</div><p>{ { bar}}</p></div>0

       接着调整

<div><div>foo</div><p>{ { bar}}</p></div>.4.渲染器的运行时支持

       传统的节点更新方式如下:

<div><div>foo</div><p>{ { bar}}</p></div>2

       优化后的更新方式,直接对比动态节点

<div><div>foo</div><p>{ { bar}}</p></div>3

       存在对应的补丁标志,可以针对性地完成靶向更新

<div><div>foo</div><p>{ { bar}}</p></div>.Block树

       除了模版的根节点是Block外,带有结构化指令的节点,如:v-if、v-for,也都应该是Block

2.1带有v-if指令的节点<div><div>foo</div><p>{ { bar}}</p></div>5

       假设只有最外层的div标签会作为Block,那么变量foo的值为true还是false,block收集到的动态节点都是一样的,如下:

<div><div>foo</div><p>{ { bar}}</p></div>6

       这意味着,在Diff阶段不会更新。显然,foo不同值下,一个是section,一个是div,是不同标签,是需要更新的。

       再举个例子:

<div><div>foo</div><p>{ { bar}}</p></div>7

       一样会导致更新失败

       问题在于:dynamicChildren收集的动态节点是忽略虚拟DOM树层级的,结构化指令会导致更新前后模版的结构发生变化,即模版结构不稳定

       解决方法:让带有v-if/v-else-if/v-else等结构化指令的节点也作为Block即可,如下所示

<div><div>foo</div><p>{ { bar}}</p></div>8<div><div>foo</div><p>{ { bar}}</p></div>9

       在Diff过程中,渲染器根据key值区分,使用新的Block替换旧的Block

2.2带有v-for指令的节点

       带有v-for指令的节点也会让虚拟DOM树变得不稳定

       例子:

//传统虚拟DOM描述constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar},]}0

       list的值由[1,2]变成[1]

       更新前后对应的Block树如下:

//传统虚拟DOM描述constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar},]}1

       更新前后,动态节点数量不一致,无法进行diff操作(diff操作的前提是:操作的节点必须是同层级节点,dynamicChildren不一定是同层级的)

       解决方法:让v-for指令的标签也作为Block角色,保证虚拟DOM树具有稳定的结构,无论v-for在运行时怎样变化。如下:

//传统虚拟DOM描述constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar},]}2

       由于v-for指令渲染的是一个片段,所以类型用Fragment

2.3Fragment的稳定性//传统虚拟DOM描述constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar},]}3

       发现Fragment本身收集的动态节点存在结构是不稳定的情况

       结构不稳定:指更新前后一个block的dynamicChildren数组中收集的动态节点的数量或顺序不一致

       这种情况无法直接进行靶向更新

       解决方法:回退到传统虚拟DOM的Diff手段,即直接使用Fragment的children而非dynamicChildren来进行Diff操作

       Fragment的子节点仍然可以是由Block组成的数组

//传统虚拟DOM描述constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar},]}4

       当Fragment的子节点更新时,就可以恢复优化模式

       有稳定的Fragment吗?如下:

//传统虚拟DOM描述constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar},]}5

       稳定的Fragment,可以使用优化模式

       vue3模版中的多个根节点,也是稳定的Fragment

//传统虚拟DOM描述constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar},]}.静态提升

       减少更新时创建虚拟DOM带来的性能开销和内存占用

       如:

//传统虚拟DOM描述constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar},]}7

       没有静态提升时,渲染函数是:

//传统虚拟DOM描述constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar},]}8

       响应式数据title变化后,整个渲染函数会重新执行

       把纯静态的节点提升到渲染函数之外

//传统虚拟DOM描述constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar},]}9

       响应式数据title变化后,不会重新创建静态的虚拟节点

       注:静态提升是以树为单位的

       包含动态绑定的节点本身不会被提升,但是该节点上的静态属性是可以被提升的

//编译优化后constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar,patchFlag:1},//这是动态节点]}0

       可以减少创建虚拟DOM产生的开销以及内存占用

4.预字符串化

       基于静态提升,进一步采用预字符串化优化。

//编译优化后constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar,patchFlag:1},//这是动态节点]}1

       采用静态提升优化策略后

//编译优化后constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar,patchFlag:1},//这是动态节点]}2

       采用预字符串化将这些静态节点序列化为字符串,并生成一个Static类型的VNode

//编译优化后constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar,patchFlag:1},//这是动态节点]}3

       优势:

       大块的静态内容可以通过innerHTML设置,在性能上有一定优势

       减少创建虚拟节点产生的性能开销

       减少内存占用

5.缓存内联事件处理函数//编译优化后constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar,patchFlag:1},//这是动态节点]}4//编译优化后constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar,patchFlag:1},//这是动态节点]}5

       每次重新渲染时,都会为Com组件创建一个全新的props对象。同时,props对象中onChange属性的值也会是全新的函数。造成额外的性能开销

//编译优化后constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar,patchFlag:1},//这是动态节点]}.v-once

       v-once可以对虚拟DOM进行缓存

//编译优化后constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar,patchFlag:1},//这是动态节点]}7

       由于节点被缓存,意味着更新前后的虚拟节点不会发生变化,因此也就不需要这些被缓存的虚拟节点参与Diff操作了。编译后的结果如下:

//编译优化后constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar,patchFlag:1},//这是动态节点]}8

       v-once包裹的动态节点不会被父级Block收集,因此不会参与Diff操作

       v-once指令通常用于不会发生改变的动态绑定中,例如绑定一个常量

//编译优化后constvnode={ tag:'div',children:[{ tag:'div',children:'foo'},{ tag:'p',children:ctx.bar,patchFlag:1},//这是动态节点]}9

       v-once带来的性能提升

       避免组件更新时重新创建虚拟DOM带来的性能开销。因为虚拟DOM被缓存了,所以更新时无需重新创建

       避免无用的Diff开销。因为被v-once标记的虚拟DOM树不会被父级Block节点收集

7.总结

       1.vue3提出了Block的概念,利用Block树及补丁标志

       2.静态提升:可以减少更新时创建虚拟DOM产生的性能开销和内存占用

       3.预字符串化:在静态提升的基础上,对静态节点进行字符串化。这样做能够减少创建虚拟节点产生的性能开销以及内存占用

       4.缓存内联事件处理函数:避免造成不必要的组件更新

       5.v-once指令:缓存全部或部分虚拟节点,能够避免组件更新时重新创建虚拟DOM带来的性能开销,也可以避免无用的Diff操作

原文:/post/

Vue 3 全方位详解

       这是我整理的学习资料,非常系统和完善,欢迎一起学习

       Vue简介

       Vue是一个用于构建用户界面和单页面应用的渐进式框架。相比其他框架,Vue具有以下特点:

       Vue 3于年9月发布,是一次重要升级。本文将全面介绍其新特性和用法。

       基础Vite项目创建

       使用Vite可以快速创建Vue 3项目:

       Vite具有以下特点:

       相比Webpack,大大提升了开发体验。

       应用入口

       应用入口通过createApp创建:

       等同于Vue 2中的:

       组件编写

       组件不再使用选项式API,而是 introduced Composition API:

       setup取代了beforeCreate和created钩子,不需要this上下文。

       响应系统响应式原理

       Vue通过响应系统实现数据驱动视图动态更新:

       数据变化驱动视图自动更新。

       响应式APIref

       ref用于创建基础类型的响应式数据:

       ref内部通过Object.defineProperty的get和set来实现响应式。

       reactive

       reactive用于创建对象(或数组)类型的响应式数据:

       reactive通过Proxy对象实现响应式,可以直接操作属性而无需.value。

       实现区别

       reactive可以检测更多数据操作,实现深层响应式。

       Composition API

       Composition API是Vue 3中的重要加强之一。

       setup函数

       setup是Composition API的入口:

       setup类似于Vue 2的beforeCreate钩子,不需要this上下文。

       setup返回的值可以在模板中使用。

       响应式编程

       在setup中通过ref和reactive创建响应式状态:

       修改数据时,视图会进行响应更新。

       计算属性

       通过computed创建计算属性:

       计算属性会根据依赖响应式进行缓存和更新。

       监听器

       watch用来监听数据变化:

       生命周期钩子

       Composition API提供新的生命周期钩子:

       自定义Hooks

       可以将共享逻辑抽取到自定义Hooks中:

       然后在组件中使用自定义Hook:

       高级功能浅层响应式

       shallowReactive和shallowRef只处理对象最外层属性的响应式,深层次属性不会转换为响应式。

       只读数据

       readonly可以创建一个深层只读响应式数据,无法修改。

       toRefs和toRef

       toRefs和toRef用来创建对响应式对象属性的ref引用。

       自定义Ref

       customRef允许自定义Ref的获取和设置行为:

       用于实现自定义的响应式行为。

       新组件Fragment

       Fragment允许组件没有根节点:

       避免添加无意义的根节点。

       Teleport

       Teleport可以将组件挂载到DOM中的任意位置:

       常用于构建弹窗、提示等组件。

       Suspense

       Suspense组件可以控制组件等待加载状态:

       等待内部异步组件加载完成后再渲染。

       Vue生态状态管理

       provide/inject提供依赖注入:

       Vuex和Pinia提供更完整的状态管理解决方案。

       路由管理

       Vue Router提供路由系统:

       支持动态路由、嵌套路由等。

       UI框架

       常用的Vue UI框架:

       测试方案

       常用的Vue测试工具:

       等等。

       其他改变总结

       Vue 3进行了诸多改进,使得Vue的核心更简洁高效,拓展性更强。

       Vue 3为未来Web应用开发提供了坚实基础。其文档完善,生态繁荣,可以考虑逐步迁移项目到Vue 3。

问起Vue的原理时不要再只说defineProperty了

       深入浅出,剖析Vue原理

       面试提及Vue原理时,常言通过`Object.defineProperty`修改属性的`get`和`set`方法,以此实现数据变动。然而,Vue的MVVM架构核心作用远不止于此,它依托于`Observer`、`Dep`、`Watcher`和`Compile`四个关键类,以及辅助类`CpompileUtil`协同工作。本文旨在简明解读从初始化DOM和数据到数据渲染的整个流程,探究Vue背后的操作机制。

       首先,构建Vue实例需定义DOM元素与数据源`data`。在`Observer`类中,通过`observe`方法遍历数据源,若属性非对象,则返回;否则,调用`defineReactive`方法拦截`get`和`set`方法,并创建依赖收集器`Dep`。

       `Dep`类维护一个`Watcher`数组。在`defineReactive`方法中,当属性`get`方法被修改时,根据`Dep.target`的存在与否决定将`Watcher`添加到数组中。当属性通过`set`方法改变时,调用该属性的`Dep.notify`方法通知所有订阅该属性的`Watcher`更新状态。

       `Compile`(在Vue中,此步骤由`virtualDOM`处理)负责编译绑定的`el`节点。首先,将`el`的`childNodes`创建为一个`fragment`,以避免频繁访问真实DOM带来的性能损耗。接着,根据`fragment`中的每个`Node`类型进行判断。

       若`Node`为`Element`类型,获取其`attributes`,查找与指令`v-xx`相关的`CompileUtil`方法,并将其添加到`Dep`中,递归调用`compile`方法直至文本节点`text`为止。若`Node`为文本类型,则调用`compileText`方法,获取`textContent`表达式,调用相应`CompileUtil`方法操作该表达式。

       `CompileUtil`类包含`updateFn`方法,使用数组`reduce`函数拆解`expr`并获取属性,实现表达式的解析。初始化时,只获取数据值进行DOM渲染,此时`Dep.target`为`null`,无需依赖收集;数据变动时,通过`Dep`的`notify`方法通知相关`Watcher`更新DOM。

       综上所述,Vue的MVVM架构通过这些核心组件协同工作,实现数据与视图的高效同步,提供简洁且高效的响应式数据绑定机制。

vue3的学习_介绍他

       vue3带来了什么?

       æ€§èƒ½çš„提升

       æ‰“包大小减少%

       åˆæ¸²æŸ“å¿«%,更新渲染%

       å†…存减少%

       æºç çš„提升

       ä½¿ç”¨proxy代替defineProperty实现响应式

       é‡å†™è™šæ‹ŸDOM的实现和Tree-Shaking

       æ‹¥æŠ±TypeScript

       vue3可以更好的支持typescript

       æ–°çš„特性

       æ–°çš„生命周期钩子

       data选项应始终被声明为一个函数

       ç§»é™¤keyCode支持作为v-on的修饰符

       Fragment

       Teleport

       Suspense

       setup配置

       ref和reactive

       watch与watchEffect

       provide与inject

       1.compositionAPI(组合API)

       2.新的内置组件

       3.其他改变

创建vue3工程

       æ–¹æ³•ä¸€ï¼šä½¿ç”¨vue-cli创建

#查看vue-cli版本确保都在4.5.0以上vue?--version#安装或升级你的@vue-clinpm?install?-g?@vue/cli#创建vue?create?vue_test#启动cd?vue_testnpm?run?server

       æˆåŠŸ

       æ–¹æ³•äºŒï¼šä½¿ç”¨vite创建

#创建项目npm?init?vite-app?<project-name>#进入工程目录cd?<project-name>#安装依赖npm?install#运行npm?run?devMain.js的不同//?引入的不再是vue构造函数了,引入的是一个名为createApp的工厂函数import?{ ?createApp?}?from?'vue'import?App?from?'./App.vue'//?创建应用实例对象——app(类似于之前Vue2中的vm,但app比vm更轻)//?createApp(App).mount('#app')const?app?=?createApp(App);console.log('@@@',app);app.mount('#app');关闭语法检查vue3组件模板结构可以没有根标签Vue3-dev-tools插件安装1.拉开序幕的setup

       ç†è§£ï¼šVue3.0中一个新的配置项,值为一个函数。

       setup是所有CompositionAPI(组合API)“表演的舞台”。

       ç»„件中所用到的:数据、方法等等,均要配置在setup中。

       setup函数的两种返回值:

       è‹¥è¿”回一个对象,则对象中的属性、方法,在模板中均可以直接使用。(重点关注!)

       è‹¥è¿”回一个渲染函数:则可以自定义渲染内容。(了解)

       æ³¨æ„ç‚¹ï¼š

       Vue2.x配置(data、methos、computed...)中可以访问到setup中的属性、方法。

       ä½†åœ¨setup中不能访问到Vue2.x配置(data、methos、computed...)。

       å¦‚果有重名,setup优先。

       å°½é‡ä¸è¦ä¸ŽVue2.x配置混用

       setup不能是一个async函数,因为返回值不再是return的对象,而是promise,模板看不到return对象中的属性。(后期也可以返回一个Promise实例,但需要Suspense和异步组件的配合)

2.响应式数据

       RefImpl-reference引用implement实现

       ref加工生成的是一个引用实现的实例对象=》引用对象

       æµç¨‹1:

       æµç¨‹2:

       æµç¨‹3:

       å¼•ç”¨å¯¹è±¡åœ¨å¼•ç”¨æ•°æ•°å€¼å’Œå¯¹è±¡æ—¶çš„结构分析

2.1ref函数

       ä½œç”¨:定义一个响应式的数据

       è¯­æ³•:?constxxx=ref(initValue)

       åˆ›å»ºä¸€ä¸ªåŒ…含响应式数据的引用对象(reference对象,简称ref对象)。

       JS中操作数据:?xxx.value

       æ¨¡æ¿ä¸­è¯»å–数据:不需要.value,直接:<div>{ { xxx}}</div>

       å¤‡æ³¨ï¼š

       æŽ¥æ”¶çš„数据可以是:基本类型、也可以是对象类型。

       åŸºæœ¬ç±»åž‹çš„数据:响应式依然是靠Object.defineProperty()的get与set完成的。

       å¯¹è±¡ç±»åž‹çš„数据:内部?“求助”?了Vue3.0中的一个新函数——?reactive函数。

2.2reactive函数

       ä½œç”¨:定义一个对象类型的响应式数据(基本类型不要用它,要用ref函数)

       è¯­æ³•ï¼šconst代理对象=reactive(源对象)接收一个对象(或数组),返回一个代理对象(Proxy的实例对象,简称proxy对象)

       reactive定义的响应式数据是“深层次的”。

       å†…部基于ES6的Proxy实现,通过代理对象操作源对象内部数据进行操作。

4.Vue3.0中的响应式原理vue2.x的响应式

       å®žçŽ°åŽŸç†ï¼š

Object.defineProperty(data,?'count',?{ get?()?{ },?set?()?{ }})

       å¯¹è±¡ç±»åž‹ï¼šé€šè¿‡Object.defineProperty()对属性的读取、修改进行拦截(数据劫持)。

       æ•°ç»„类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。

       å­˜åœ¨é—®é¢˜ï¼š

       æ–°å¢žå±žæ€§ã€åˆ é™¤å±žæ€§,界面不会更新。

       ç›´æŽ¥é€šè¿‡ä¸‹æ ‡ä¿®æ”¹æ•°ç»„,界面不会自动更新。

       è§£å†³æ–¹æ³•(添加属性)

       this.$set(obj,key,value);

       importVuefrom'vue'/Vue.set(obj,key,value)

       è§£å†³æ–¹æ³•ï¼ˆåˆ é™¤å±žæ€§ï¼‰

       this.$delete(obj,key)

       importVuefrom'vue'/Vue.delete(obj,key,value)

       è§£å†³æ–¹æ³•ï¼ˆä¿®æ”¹æ•°ç»„)

       this.$set(Arr,index,value)

       this.Arr.splice(index,num)

Vue3.0的响应式

       å®žçŽ°åŽŸç†:

new?Proxy(data,?{ //?拦截读取属性值get?(target,?prop)?{ return?Reflect.get(target,?prop)},//?拦截设置属性值或添加新属性set?(target,?prop,?value)?{ return?Reflect.set(target,?prop,?value)},//?拦截删除属性deleteProperty?(target,?prop)?{ return?Reflect.deleteProperty(target,?prop)}})proxy.name?=?'tom'

       æ¨¡æ‹Ÿvue3中实现响应式使用reflect操作源对象内部的数据

//?模拟vue3中实现响应式const?p?=?new?Proxy(person,{ get(target,propName){ ?//?有人读取p的某个属性时调用?return?Reflect.get(target,propName)},set(target,propName,value){ ?//?有人改变p的属性时调用?return?Reflect.set(target,propName,value);},deleteProperty(target,propName){ ?//有人删除p的某个属性时调用?return?Reflect.deleteProperty(target,propName)}})

       Proxy:/post/

5分钟让你读懂Vue双向数据绑定的原理解析及代码实现

       5分钟快速理解Vue双向数据绑定原理和代码示例

       当你刚开始一天的工作,前端同事就分享了一篇关于Vue双向数据绑定的解析文章。虽然标题看似复杂,但别担心,让我们一起简化理解。

       首先,双向绑定的关键在于Vue的`Object.defineProperty()`方法。它通过`get`和`set`函数,实现数据的读取和修改同步到视图。例如,当文本框内容改变时,`set`方法会被调用,更新数据的同时更新显示在`span`中的内容。

       实现基本的双向绑定,我们可以通过监听`keyup`事件,当输入变化时,设置方法会更新数据并反映到DOM中。下面是一个简单示例:

       通过监听输入框变化,双向同步数据和视图:输入框内容改变→数据更新→视图更新。

       在Vue中,真正的双向绑定更为复杂,涉及到`DocumentFragment`的使用,以提高性能。我们分解任务:绑定数据到输入框和文本节点,同时处理数据变化和视图更新,利用`Dep`和`Watcher`实现通知机制。

       最后,双向数据绑定的机制包括:数据监听(`observe`)、视图更新(`Dep.notify()`),以及使用`Watcher`确保视图始终与数据保持同步。

       总的来说,理解Vue的双向数据绑定,关键在于掌握访问器属性和事件驱动的更新机制。通过实际操作和实践,你将更深入地掌握这一核心概念。

vue3中的编译器原理和优化策略

       å­¦ä¹ ç›®æ ‡

       ç¼–译器原理

       vue3编译过程剖析

       vue3编译优化策略

       åœ¨åˆå§‹åŒ–之前可能有编译的过程,最终的产物是个渲染函数,我们知道渲染函数返回的值是一个虚拟DOM(vnode),那么这个虚拟DOM在我们后续的更新过程中到底有什么作用呢?我们今天就来探讨一下。

编译器原理1.概念

       å¹¿ä¹‰ä¸Šçš„编译原理:编译器是将源代码转化成机器码的软件;所以编译的过程则是将源代码转化成机器码的过程,也就是cpu可执行的二进制代码。例如使用高级语言java编写的程序需要编译成我们看不懂但计算机能看懂的的字节码。

       å¦‚果了解过编译器的工作流程的同学应该知道,一个完整的编译器的工作流程会是这样:

       é¦–先,parse解析原始代码字符串,生成抽象语法树AST。

       å…¶æ¬¡ï¼Œtransform转化抽象语法树,让它变成更贴近目标「DSL」的结构。

       æœ€åŽï¼Œcodegen根据转化后的抽象语法树生成目标「DSL」的可执行代码。

2.vue中的编译

       åœ¨vue里也有编译的过程,我们经常写的那个HTML模版,在真正工作的时候,并不是那个HTML模版,它实际上是一个渲染函数,在这个过程中就发生了转换,也就是编译,也就是那个字符串的模版最终会变成一个JS函数,叫render函数。所以在这个过程中我们就需要引入编译器的概念。在计算机中当一种东西从一种形态到另一种形态进行转换的时候,就需要编译。编译器:用来将模板字符串编译成为JavaScript渲染函数的代码

       é‚£ä¹ˆvue中的编译发生在什么时候呢?

       è¿™ä¸ªæ—¶å€™æˆ‘们就需要进一步了解vue包的不同版本的不同功能了。vue有携带编译器和不携带编译的包(对不同构建版本的解释)。

3.运行时编译

       åœ¨ä½¿ç”¨æºå¸¦ç¼–译器(compiler)的vue包的时候,vue编译的时刻是发生在挂载($mount)的时候。

4.运行时不编译

       å¦‚果使用未携带编译器的vue包的时候,vue在运行时是不会进行编译的。那么它的编译又发生在什么时候呢?使用未携带编译器的vue包的时候,需要进行预编译,也就是基于构建工具使用,就是我们平时使用的vue-cli进行构建的项目,就是使用webpack调用vue-loader进行预编译,将所有vue文件,就是SFC,将里面的template模版部分转换成render函数。这样做的好处就是vue的包体积变小了,执行的时候速度更快了,因为不需要进行编译了。

vue编译器原理

       ç®€å•æ¥è¯´å°±æ˜¯ï¼šå…ˆå°†template模版转换成ast抽象语法树,ast再转换成渲染函数render。

       é‚£ä¹ˆä»€ä¹ˆæ˜¯æ˜¯ast抽象语法树呢?

1.ast抽象语法树

       åœ¨template模版和render函数之间有一个中间产物叫做ast抽象语法树。它就是个js对象,它能够描述当前模版的结构信息,跟vnode很类似。注意,ast只是程序运行过程中编译产生的,它跟我们最终程序的运行是没有任何关系的。也就是当这个渲染函数生成之后,ast的生命周期就结束了,不再需要了,而那个虚拟DOM则伴随整个程序的生命周期。这个就是ast和虚拟DOM的本质区别。

2.为什么需要ast呢

       åœ¨ast转换成render函数的过程中,需要进行特别的操作。第一次,将template转成的ast是个非常粗糙的js对象,是一次非常粗糙的转换,类似正则表达式的匹配,然后我们的template模版中还有很多表达式,指令,事件需要重新解析,经过这些具体的深加工的解析(transform)之后会得到一个终极ast,然后这个对这个终极ast进行generate,生成render函数

template=>ast=>transform=>ast=>render3.mini版vue编译器

       ä¸‹é¢æˆ‘们来看一个mini版的vue编译器,具体代码已省略,具体代码我已经放在Github上了:mini-vue-compiler

functiontokenizer(input){ ...}functionparse(template){ consttokens=tokenizer(template)...}functiontransform(ast){ ...}functiontraverse(ast,context){ ...}functiongenerate(ast){ ...}functioncompile(template){ //1.解析constast=parse(template)console.log(JSON.stringify(ast,null,2))//2.转换transform(ast)//3.生成constcode=generate(ast)console.log(code)//returnfunctionrender(ctx){ //returnh("h3",{ },//ctx.title//)}returnnewFunction(code)()}lettmpl=`<h3>{ { title}}</h3>`compile(tmpl)

       å¤§æ¦‚有以上操作,其中parse函数就是发生在把template转换成ast的这过程,具体是通过一些正则表达式的匹配template中的字符串。比如将

xxx

       è½¬æˆast对象,那么就是通过正则表达式匹配如果是

那么就设置一个开始标记,再往后面匹配到xxx内容,然后就设置一个子元素,最后匹配到

       é‚£ä¹ˆå°±è®¾ç½®ä¸€ä¸ªç»“束标记,以此类推。parse解析之后得到的是一个粗糙的ast对象。经过parse解析得到一个粗糙的ast对象之后,就用transform进行深加工,最后要经过generate生成代码。

Vue3编译过程剖析

       æŒ‚载的时候先把template编译成render函数,在创建实例之后,直接调用组件实例的render函数创建这个组件的真实DOM,然后继续向下做递归。

1.vue2.x和vue3.x的编译对比

       Vue2.x中的Compile过程会是这样:

       parse词法分析,编译模板生成原始粗糙的AST。

       optimize优化原始AST,标记ASTElement为静态根节点或静态节点。

       generate根据优化后的AST,生成可执行代码,例如_c、_l之类的。

       åœ¨Vue3中,整体的Compile过程仍然是三个阶段,但是不同于Vue2.x的是,第二个阶段换成了正常编译器都会存在的阶段transform。

       parse词法分析,编译模板生成原始粗糙的AST。

       transform遍历AST,对每一个ASTelement进行转化,例如文本元素、指令元素、动态元素等等的转化

       generate根据优化后的AST,生成可执行代码函数。

2.源码编译入口

       æˆ‘们先从一个入口来开始我们的源码阅读,packages/vue/index.ts。

//web平台特有编译函数functioncompileToFunction(template:string|HTMLElement,options?:CompilerOptions):RenderFunction{ //省略...if(template[0]==='#'){ //获取模版内容constel=document.querySelector(template)//省略...template=el?el.innerHTML:''}//编译const{ code}=compile(template,extend({ //省略...},options))constrender=(__GLOBAL__?newFunction(code)():newFunction('Vue',code)(runtimeDom))asRenderFunction//省略...return(compileCache[key]=render)}//注册编译函数registerRuntimeCompiler(compileToFunction)export{ compileToFunctionascompile}

       è¿™ä¸ªå…¥å£æ–‡ä»¶çš„代码比较简单,只有一个compileToFunction函数,但函数体内的内容却又比较关键,主要是经历以下步骤:

       ä¾èµ–注入编译函数至runtimeregisterRuntimeCompiler(compileToFunction)

       runtime调用编译函数compileToFunction

       è°ƒç”¨compile函数

       è¿”回包含code的编译结果

       å°†code作为参数传入Function的构造函数将生成的函数赋值给render变量

       å°†render函数作为编译结果返回

3.template获取

       app.mount()获取了templatepackages/runtime-dom/src/index.ts

4.编译template

       compile将传?template编译为render函数,packages/runtime-core/src/component.ts

       å®žé™…执?的是baseCompile,packages/compiler-core/src/compile.ts

       ç¬¬?步解析-parse:解析字符串template为抽象语法树ast

       ç¬¬?步转换-transform:解析属性、样式、指令等

       ç¬¬ä¸‰æ­¥?成-generate:将ast转换为渲染函数

Vue3编译器优化策略

       è¿™æ˜¯ä¸€ä¸ªéžå¸¸å…¸åž‹çš„用内存换时间的操作

1.静态节点提升<div><div>{ { msg}}</div><p>coboy</p><p>coboy</p><p>coboy</p></div>

       ä»¥ä¸Šè¿™ä¸ªæ®µtemplate如果没有开启静态节点提升它编译后是这样的:

import{ toDisplayStringas_toDisplayString,createVNodeas_createVNode,openBlockas_openBlock,createBlockas_createBlock}from"vue"exportfunctionrender(_ctx,_cache,$props,$setup,$data,$options){ return(_openBlock(),_createBlock("div",null,[_createVNode("div",null,_toDisplayString(_ctx.msg),1/*TEXT*/),_createVNode("p",null,"coboy"),_createVNode("p",null,"coboy"),_createVNode("p",null,"coboy")]))}

       å¦‚果开启了静态节点提升之后它编译后则是这样的:

import{ toDisplayStringas_toDisplayString,createVNodeas_createVNode,openBlockas_openBlock,createBlockas_createBlock}from"vue"const_hoisted_1=/*#__PURE__*/_createVNode("p",null,"coboy",-1/*HOISTED*/)const_hoisted_2=/*#__PURE__*/_createVNode("p",null,"coboy",-1/*HOISTED*/)const_hoisted_3=/*#__PURE__*/_createVNode("p",null,"coboy",-1/*HOISTED*/)exportfunctionrender(_ctx,_cache,$props,$setup,$data,$options){ return(_openBlock(),_createBlock("div",null,[_createVNode("div",null,_toDisplayString(_ctx.msg),1/*TEXT*/),_hoisted_1,_hoisted_2,_hoisted_3]))}

       æˆ‘们可以看到template里存在大量的不会变的p标签,所以当这个组件重新渲染的时候,这些静态的不会变的标签就不应该再次创建了。所以vue3就把这些静态的不会变的标签的VNode放在了render函数作用域的外面,在下次render函数再次执行的时候,那些静态标签的VNode已经在内存里了,不需要重新创建了。相当于占用当前机器的内存,避免重复创建VNode,用内存来换时间。大家仔细斟酌一番静态提升的字眼,静态二字我们可以不看,但是提升二字,直抒本意地表达出它(静态节点)被提高了。

2.补丁标记和动态属性记录<div><div:title="title">coboy</div></div>

       æ„æ€å°±æ˜¯åœ¨ç¼–译的过程中,像人眼一样对模版进行扫描看哪些东西是动态的,然后提前把这些动态的东西提前保存起来,作个标记和记录,等下次更新的时候,只更新这些保存起来的动态的记录。比如上面模版的title是动态的,提前做个标记和记录,更新的时候就只更新title部分的内容。

import{ createVNodeas_createVNode,openBlockas_openBlock,createBlockas_createBlock}from"vue"exportfunctionrender(_ctx,_cache,$props,$setup,$data,$options){ return(_openBlock(),_createBlock("div",null,[_createVNode("div",{ title:_ctx.title},"coboy",8/*PROPS*/,["title"])]))}<div><div:title="title">{ { text}}</div></div>import{ toDisplayStringas_toDisplayString,createVNodeas_createVNode,openBlockas_openBlock,createBlockas_createBlock}from"vue"exportfunctionrender(_ctx,_cache,$props,$setup,$data,$options){ return(_openBlock(),_createBlock("div",null,[_createVNode("div",{ title:_ctx.title},_toDisplayString(_ctx.text),9/*TEXT,PROPS*/,["title"])]))}

       æˆ‘们可以观察到在_createVNode函数的第四个参数是个9,后面是一个注释:/TEXT,PROPS/,这个是表示在当前的节点里面有两个东西是动态的,一个是内部的文本,一个是属性,然后具体是哪个属性,在第五个参数的数组里面则记录了下来["title"],有个title的属性是动态的。

       åœ¨å°†æ¥è¿›è¡Œpatch更新的时候,就可以根据当前记录的信息,进行更新,缩减更新过程和操作,可以非常精确地只进行title和文本的更新。

       å¦‚æžœdiv标签里是静态文本的话,_createVNode函数的第四个参数则变成了8,后面的注释变成了:/PROPS/,后面的第五个参数数据不变。

       _createVNode函数的第四个参数的数字其实是一个二进制数字转成十进制的数字。

       8的二进制是,9的二进制是,很容易可以看出二进制的每一位的数字都代表着特殊的含义。这些数字就是patchFlag,那么什么是patchFlag呢?

什么是patchFlag

       patchFlag是complier时的transform阶段解析ASTElement打上的补丁标记。它会为runtime时的patchVNode提供依据,从而实现靶向更新VNode和静态提升的效果。

       patchFlag被定义为一个数字枚举类型,它的每一个枚举值对应的标识意义是:

       TEXT=1动态文本的元素

       CLASS=2动态绑定class的元素

       STYLE=4动态绑定style的元素

       PROPS=8动态props的元素,且不含有class、style绑定

       FULL_PROPS=动态props和带有key值绑定的元素

       HYDRATE_EVENTS=事件监听的元素

       STABLE_FRAGMENT=子元素的订阅不会改变的Fragment元素

       KEYED_FRAGMENT=自己或子元素带有key值绑定的Fragment元素

       UNKEYED_FRAGMENT=没有key值绑定的Fragment元素

       NEED_PATCH=带有ref、指令的元素

       DYNAMIC_SLOTS=动态slot的组件元素

       HOISTED=-1静态的元素

       BAIL=-2不是render函数生成的一些元素,例如renderSlot

       æ•´ä½“上patchFlag的分为两大类:

       å½“patchFlag的值大于0时,代表所对应的元素在patchVNode时或render时是可以被优化生成或更新的

       å½“patchFlag的值小于0时,代表所对应的元素在patchVNode时,是需要被fulldiff,即进行递归遍历VNodetree的比较更新过程。

       ä»¥ä¸Šå°±æ˜¯vue3的一个非常高效的优化策略叫补丁标记和动态属性记录。

3.缓存事件处理程序functiontokenizer(input){ ...}functionparse(template){ consttokens=tokenizer(template)...}functiontransform(ast){ ...}functiontraverse(ast,context){ ...}functiongenerate(ast){ ...}functioncompile(template){ //1.解析constast=parse(template)console.log(JSON.stringify(ast,null,2))//2.转换transform(ast)//3.生成constcode=generate(ast)console.log(code)//returnfunctionrender(ctx){ //returnh("h3",{ },//ctx.title//)}returnnewFunction(code)()}lettmpl=`<h3>{ { title}}</h3>`compile(tmpl)0

       å°†æ¥æ¡†æž¶ä¼šåƒreact那样把@click="onClick"变成@click="()=>onClick()",最后可能是这样的一个箭头函数。那就意味着每次onClick的函数都是一个全新的函数,那就会造成这个回调函数明明没有变,都会被认为变了,那就必须进行一系列的更新,那么如果能把这个回调函数缓存起来,更新的时候,就不要再创建了。

       æœªè¿›è¡Œç¼“存事件处理程序之前的编译

functiontokenizer(input){ ...}functionparse(template){ consttokens=tokenizer(template)...}functiontransform(ast){ ...}functiontraverse(ast,context){ ...}functiongenerate(ast){ ...}functioncompile(template){ //1.解析constast=parse(template)console.log(JSON.stringify(ast,null,2))//2.转换transform(ast)//3.生成constcode=generate(ast)console.log(code)//returnfunctionrender(ctx){ //returnh("h3",{ },//ctx.title//)}returnnewFunction(code)()}lettmpl=`<h3>{ { title}}</h3>`compile(tmpl)1

       è¿›è¡Œç¼“存事件处理程序之后的编译

functiontokenizer(input){ ...}functionparse(template){ consttokens=tokenizer(template)...}functiontransform(ast){ ...}functiontraverse(ast,context){ ...}functiongenerate(ast){ ...}functioncompile(template){ //1.解析constast=parse(template)console.log(JSON.stringify(ast,null,2))//2.转换transform(ast)//3.生成constcode=generate(ast)console.log(code)//returnfunctionrender(ctx){ //returnh("h3",{ },//ctx.title//)}returnnewFunction(code)()}lettmpl=`<h3>{ { title}}</h3>`compile(tmpl).块block

       è¿™æ˜¯ä»€ä¹ˆæ„æ€å‘¢ï¼Ÿæ ¹æ®å°¤é›¨æºªæœ¬äººçš„解析,他说,根据他的统计那个动态的部分最多只有三分之一,基本上都是静态部分,所以在编译的过程中,能不能发现那个比较小的动态部分,把它放到比较靠上

相关栏目:探索

.重点关注