Vue部分源码分析
Vue部分源码分析
月晕VUE3 框架的三大模块
- compiler 编译系统
- runtime 渲染系统
- reactive 响应系统
Reactive 的实现
在 Vue2 中实现响应式是使用Object.defineProperty
来实现数据劫持如下
1 | let number = 18 |
在 Vue3 中使用 Proxy 来实现数据劫持
1 | let number = 18 |
Proxy 的优势
- Proxy 可以直接监听对象而非属性
- Proxy 用于创建一个对象的代理,从而实现基本操作的拦截和自定义劫持整个对象,并返回一个新对象。
- Object.defineProperty 无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实施响应。
- Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue2.X 里,是通过递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象才是更好的选择。
Reflect
- 不是一个函数对象,因此它是不可构造的。直接使用静态方式即可。
- 反射
- 作用:可以通过编程的方式操作对象
- 用法和 Object 类似,但是 Object 具有局限性
- 比如增加删除属性需要写 try catch,而 Reflect 不需要 直接 if else 判断即可
- 比如在 object 的 key 只能是 String,而 Reflect 可以 value-value
- Reflect 提供的是一整套反射能力 API,它们的调用方式,参数和返回值都是统一风格的,我们可以使用 Reflect 写出更优雅的反射代码。
Reactive 的实现
- reactive.js
1 | // 引入一些工具函数 |
- effect.js
1 | /** |
- utils.js
1 | export function isObject(val) { |
Ref 实现
ref.js 如下
1 | import { isObject, hasChanged } from './utils' |
computed 实现
computed.js 如下
1 | import { isFunction } from '../utils' |
runtime 渲染系统
首先介绍一下什么是虚拟 DOM
虚拟 DOM:
- 用 JS 对象来描述 DOM 节点
- 种类有:Element、Text、Fragment、
- Element:
对应普通元素,如 div、p、span 等,使用 doucment.createElement 创建,type 指定标签名,props 指定元素属性,children 指定子元素,可以为数组或者字符串,为字符串时代表只有一个文本子节点
1 | // 类型定义 |
- Text:
对应文本节点,使用 document.createTextNode 创建,text 指定文本内容
1 | { |
- Fragment:
对应 Fragment,不会渲染的节点,相当于 templete 或 react 的 Fragment,type 为 symbol,props 为 null,children 为数组表示子节点,最后渲染时会将子节点的所有子节点挂载到 Fragment 父节点上
1 | { |
- Components:
Component 是组件,组件有自己特殊的一套渲染方法,但组件的最终产物,也是上面三种 VNode 的集合。组件的 type,就是组件定义的对象,props 即是外部传入组件的 props 数据,children 是组件的 slot
1 | // 类型定义 |
ShaperFlags
- 一组标记,用于快速识别 VNode 的类型和他的子节点类型
- 使用位运算
1 | // 例子 |
1 | const ShapeFlags = { |
采用二进制位运算<<
和|
,使用时用&
运算判断,如下:
1 | if (flag & ShapeFlags.ELEMENT) { |
生成可以用let flag = ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN
VNode 的初步形成
1 | { |
vnode.js
1 | import { isArray, isNumber, isString } from '../utils' |
render.js
1 | import { ShapeFlags } from './vnode' |
实现广义的 diff 算法 patch
render.js
1 | import { ShapeFlags } from './vnode' |
vnode.js
1 | //在return中新添加属性 |
vue3 中 diff 算法
首先是重新完成 render.js
1 | import { ShapeFlags } from './vnode' |
组件的实现方法
从开发者的视角:组件分为状态组件和函数组件
vue3 中的状态组件和函数组件类似,下面只讨论状态组件的实现
React 的组件示例(class 组件)
1 | class Counter extends React.Component { |
Vue3 的组件示例(optional) (渲染函数)
1 | createApp({ |
Vue3 的组件示例(composition) (渲染函数)
1 | createApp({ |
可以看出 从实现的角度上来说,组件都有以下几个共同点:
- 都有
instance
(实例) 以承载内部的状态,方法等 - 都有一个
render
函数 - 都通过
render
函数产出VNode
- 都有一套更新的策略,以重新执行
render
函数 - 在此基础上附加各种能力,如生命周期,通信机制,slot,provide,inject 等
component.js
1 | import { reactive } from '../reactive/reactive' |
scheduler.js (调度机制) nextTick 原理
1 | const queue = [] |
编译步骤
模板代码 –> parse
–> AST –> transform
–> AST+codegenNode –> codegen
–> 渲染函数代码
parse: 原始的模板代码就是一段字符串,通过解析parse
转换为原始AST
抽象语法树
transform: 对AST
进行转换,转换为codegenNode
, codegenNode
是AST
到生成渲染函数代码的中间步骤,它由解析原始AST
的语义而得来
1 | <div v-if="ok" /> |
没有什么区别,都是一个元素,带有一个不同的属性而已。然而v-if
是带有特殊语义的,不能像一般的纯元素节点一样采用同样的代码生成方式。transform
的作用就在于此,一方面解析原始AST
的语义,另一方面要为生成代码做准备. transfrom 是整个 vue compiler 模块中最复杂的部分
codegen: 即是 code generate
遍历 codegenNode 递归地生成最终的渲染函数的代码
parse 实现
认识 AST
1 | <div id="foo" v-if="ok">hello {{name}}</div> |
AST Node 的类型
1 | const NodeTypes = { |
- 根节点
1 | { |
- 纯文本节点
1 | type: NodeTypes.TEXT, |
- 表达式节点
1 | { |
- 插值节点
1 | { |
- 元素节点
1 | { |
- 属性节点
1 | { |
- 指令节点
1 | { |
如
1 | <div v-bind:class="myClass"></div> |
最终展示结果<div id="foo" v-if="ok">hello {{name}}</div>
1 | { |
ast.js
1 | export const NodeTypes = { |
parse.js
1 | import { NodeTypes, ElementTypes, createRoot } from './ast' |