Vue 发展历程
(1)源码优化:
对于 Vue.js 框架本身开发的优化。
目的:让代码更易于开发和维护
源码的优化主要体现在使用 monorepo和TypeScript 管理和开发源码。
这样做的目标是提升自身代码可维护性
①更好的代码管理方式: monorepo
相对于 Vue.js 2.x的源码组织方式,monorepo 把这些模块拆分到不同的 package 中,每个 package 有各自的 API、类型定义和测试。
这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性。
例:package(比如 reactivity 响应式库)是可以独立于 Vue.js 使用的。
这样用户如果只想使用 Vue.js 3.0的响应式能力,可单独依赖这个响应式库。而不用去依赖整个 Vue.js,减小了引用包的体积大小,而 Vue.js2.x是做不到这一点的
②有类型的 JavaScript: TypeScript
——>更有利于代码的维护
- 因为它可以在编码期间帮你做类型检查 避免一些因类型问题导致的错误
- 有利于它去定义接口的类型,利于IDE(集成开发环境 常见的IDE有Visual Studio、Eclipse、IntelliJ IDEA等)对变量类型的推导
因此就选用了 TypeScript 进行整个项目的重构,TypeScript 提供了更好的类型检查,能支持复杂类型的推导。源码采用TS编写,也省去了维护地点TS文件的麻烦。
(2)性能优化:
对于 Vue.js 2.x 已经足够优秀的前端框架 它的性能优化可以从哪些方面进行突破呢?
①源码体积优化:
②数据劫持优化:
Vue.js 区别于 React.js 的一大特色是Vue的数据是响应式的。
DOM 是数据的一种映射, 数据发生变化后可以自动更新DOM,用户只需要专注于数据的修改,没有其余的性质负担。但是这样子的功能实现必须需要劫持数据的访问与更新。
Vue.js 怎么知道更新哪一片 DOM 呢?
因为在渲染 DOM 的时候访问了数据,我们可以对它进行访问劫持。
这样就在内部建立了依赖关系,也就知道数据对应的 DOM 是什么了 。内部需要依赖watcher的数据结构做依赖管理
Vue1 和 Vue2 通过 object.defineProperty 进行数据劫持:
劫持了一整个对象,因此对对象属性的增加和删除都能够检测到。
这样的好处是真正访问到的内部对象才会变成响应式,而不是无脑递归这样无疑也在很大程度上提升了性能。
(3)编译优化:Block tree
响应式过程发生在new Vue到 init 阶段。
代码只有一个动态节点,很多的 diff 和遍历其实是不需要的,导致Windows 性能与模板大小正相关,跟动态节点的数量无关。当一些组件整个模板只有少量动态节点时,这些遍历都是性能的浪费。
理想状态只需要 diff 这个绑定 message 动态节点的p标签即可
(4)语法API优化: Composition API
①优化逻辑组织 
按照逻辑关注点做颜色编码, 当使用Option API 编写组件时,逻辑关注点都是非常分散的。
开发项目变得复杂时,免不了需要抽象出一些复用逻辑。
鼠标位置监听的例子:
如果有大量的Mixin就会有命名冲突和数据来源不清晰。
②引入 RFC:
使每个版本改动可控
过渡期:
组件渲染
在 Vue.js 中,组件是一个非常重要的概念,整个应用的页面都是通过组件渲染来实现的
编写组件开始,到最终真实的 DOM 又是怎样的一个转变过程呢?
组件:
①应用程序初始化:
一个组件可以通过 “模板加对象描述” 的方式创建,组件创建好以后是如何被调用并初始化的呢?
因为整个组件树是由根组件开始渲染的。
为了找到根组件的渲染入口,需要从应用程序的初始化过程开始分析
- 在整个 app 对象创建过程中,Vue.js 利用 闭包和函数柯里化 的技巧,很好地实现了参数保留
- 比如,在执行 app.mount 的时候,不需要传入 渲染器render(因为在执行 createAppAPI的时候渲染器 render 参数已经被保留下来了 )
先思考一下,为什么要重写 app.mount 这个方法而不把相关逻辑放在 app 对象的 mount 方法内部来实现呢?
②核心渲染流程:
创建 vnode 和渲染 vnode
1️⃣普通元素节点:
2️⃣组件节点
即不会真的在页面上渲染一个 CustomComponent 标签,而是渲染组件内部定义的 HTML 标签。
3️⃣纯文本Vnode
4️⃣注释Vnode
内部还针对Vnode tag做了更详细的分类,并且把VNode类型做编码,以便在后面的配置阶段,可以根据不同的类型执行相应的处理逻辑:
那么 vnode 有什么优势呢?
为什么一定要设计 vnode 这样的数据结构呢?
app.mount 内部通过执行 render 函数去渲染创建的 VNode
重点关注对组件的处理、对普通 DOM 元素 两种类型的处理节点的渲染逻辑
主要操作:创建组件实例, 设置组件实例,设置并运行带副作用的渲染函数
初始渲染主要做两件事情: 渲染组件生成subTree(VNode对象)、把 subTree 挂载到 container 中
- hello节点渲染生成的Vnode就是hello对应的init Vnode(组件Vnode)
- hello组件内部的整个DOM节点对应的Vnode就是执行 renderComponentRoot 渲染生成对应的subTree,可以称之为子树Vnode
每个组件都有对应的render函数
renderComponentRoot 就是执行 render 函数,创建整个组件内部的Vnode,把这个 Vnode 再经过内部一层标准化就能得到该函数的返回结果即子树Vnode。渲染生成子树Vnode后就是大勇patch函数把子树Vnode挂载到content中。
对普通DOM元素的处理流程:
如果是其他平台比如 Weex,hostCreateElement 方法就不再是操作 DOM而是平台相关的 API了,这些平台相关的方法是在创建渲染器阶段作为参数传入的
创建完 DOM 节点后,接下来要做的是判断如果有 props 的话给这个 DOM 节点添加相关的 class、style、event等属性,并做相关的处理这些逻辑都是在 hostPatchProp 函数内部做的
DOM和VNode都是一棵树,并且结构和DOM一一映射。
处理完所有子节点后, 通过insert的方法把创建DOM元素节点挂载到content下
因为 insert 的执行是在处理子节点后,所以挂载的顺序是先子节点,后父节点
最终挂载到最外层的容器上
在 mountChildren 的时候递归执行的是 patch 函数,而不是 mountElement 函数
这是因为子节点可能有其他类型的vnode,比如组件 vnode
- 梳理了组件渲染的过程,本质上就是把各种类型的 vnode 渲染成真实 DOM
- 组件是由模板、组件描述对象和数据构成的,数据的变化会影响组件的变化
- 组件的渲染过程中创建了一个带副作用的渲染函数
- 当数据变化的时候就会执行这个渲染函数来触发组件的更新