Vue源码分析
前言
Vue,前端领域一款渐进式,mvvm框架,可根据自己项目的复杂程度逐步引入不同功能,除了vue.js核心库,还有路由、状态管理等。
常见的mvvm框架还有 React, Angular等。
本次分享会介绍vue的MVVM设计模式、初始化流程、数据响应式原理、computed、watch、mixins的实现原理。
再介绍数据响应式的阶段,因为vue的数据响应是由defineProperty、观察者模式等核心代码支撑,所以会介绍到definePrperty 和 观察者模式的实现方式。
一、MVVM设计模式
MVVM全称: Model-View-ViewModel
作用: 将视图(View)和数据模型(Model)通过ViewModel进行双向绑定,实现视图与数据的自动同步。
View: 视图层,负责显示数据
Model: 数据层,负责存储数据
ViewModel: ViewModel层,连接View 层和 Model,驱动视图和数据的变化
视图层变化,数据层随之变化数据层变化,视图层随之变化
包含以下三个步骤:
- 视图监听:视图通过事件监听等方式监测用户操作和数据变化。
- 数据模型更新:ViewModel会监听数据模型的变化,并在数据发生改变时更新视图。
- 视图更新:ViewModel将更新后的数据同步到视图上,保持视图和数据的一致性。
graph RL
subgraph 视图
A[View]
end
subgraph vue核心层
B[ViewModel]
end
subgraph 数据
C[Model数据]
end
A -->|触发事件| B
B -->|更新数据| C
C -->|数据变化| B
B -->|更新视图| A
二、vue源码文件结构
├── scripts # 构建相关的配置文件
├── dist # 构建后的文件输出目录
├── examples # 示例代码
├── compiler-sfc # 单文件组件相关
├── packages # 独立构建的模块
├── src # Vue.js的源码目录
│ ├── compiler # 编译相关的代码
│ ├── core # 核心代码
│ │ ├── components # 内置组件
│ │ ├── global-api # 全局API
│ │ ├── instance # Vue实例相关
│ │ ├── observer # 响应式系统
│ │ ├── util # 工具函数
│ │ └── vdom # 虚拟DOM相关
│ ├── vue3 # vue3相关代码
│ ├── platforms # 平台相关的代码
│ └── shared # 共享代码
├── test # 测试相关的代码
└── types # TypeScript类型声明文件
三、vue初始化执行流程
创建Vue实例,会经历一系列的初始化过程,确保Vue能正常运行。以下是new Vue初始化流程的主要步骤:
- 创建Vue实例:通过new Vue()来创建一个Vue实例。
- 初始化数据:Vue会将用户传入的data对象进行响应式化处理,将其转换为getter和setter,并建立依赖关系。
- 模板编译:将用户定义的模板编译为渲染函数。
- 生成渲染函数:根据编译后的模板生成渲染函数,用于渲染组件的虚拟DOM。
- 创建组件实例:根据渲染函数创建组件实例,并执行组件的生命周期钩子函数。
- 将组件挂载到DOM:将组件的虚拟DOM挂载到真实的DOM上。
graph LR
A[创建Vue实例] -->|初始化数据| B[数据响应式化]
B -->|模板编译| C[生成渲染函数]
C -->|组件实例化| D[创建组件实例]
D -->|组件挂载| E[将组件挂载到DOM]
具体执行流程
graph LR
A[new Vue] --> B[init]
B --> D[beforeCreate]
B --> C[mount]
C --> E[beforeMount]
C --> F[create virtual dom]
F --> G[compile template]
E --> H[mounted]
D --> I[init lifecycle]
D --> J[init events]
D --> K[init render]
I --> L[init data]
L --> M[init props]
L --> N[init data]
L --> O[init computed]
L --> P[init methods]
L --> Q[init watch]
P --> R[bind methods]
Q --> S[init watch]
G --> T[create render function]
T --> U[generate virtual dom]
T --> V[generate render code]
U --> W[traverse virtual dom]
W --> X[generate code for vnode]
X --> Y[patch]
Y --> Z[update real dom]
Z --> AA[postpatch]
AA --> AB[invoke updated hook]
Z --> AC[remove real dom]
AC --> AD[invoke destroy hook]
四、响应式原理
响应式原理是Vue实现数据绑定的核心,通过数据劫持和依赖收集实现视图和数据的自动同步。数据发生变化,Vue自动更新视图。响应式原理的主要步骤如下:
- 数据劫持:Vue会将data对象转换为响应式对象,为每个属性添加getter和setter方法,用于监听属性的读取和修改操作。
- 依赖收集:Vue会在编译模板时,收集模板中使用的所有数据属性,并建立数据与视图之间的依赖关系。
- 数据变化:当数据发生变化时,触发setter方法,通知依赖更新。
- 视图更新:依赖收集建立的依赖关系会触发视图的更新,保持视图与数据的同步。
graph LR
A[数据] -->|数据劫持| B[依赖收集]
B -->|数据变化| C[派发更新]
C -->|生成虚拟DOM| D[更新视图]
Vue响应式原理具体流程图
graph LR
A[初始化Vue实例] --> B[执行_init方法]
B --> C[调用initState方法]
C --> D[初始化data]
D --> E[调用observe方法]
E --> F[检测data中的属性]
F --> G[创建Observer对象]
G --> H[创建Dep对象]
F --> I[为每个属性创建getter和setter]
I --> J[在getter中收集依赖]
I --> K[在setter中触发依赖更新]
D --> L[调用proxy方法]
L --> M[将data中的属性代理到Vue实例]
C --> N[调用initComputed方法]
N --> O[初始化computed]
O --> P[创建Watcher对象]
P --> Q[在Watcher对象中计算computed的值]
Q --> R[在getter中收集依赖]
O --> S[为每个computed创建getter]
S --> T[在getter中触发依赖更新]
C --> U[调用initWatch方法]
U --> V[初始化watch]
V --> W[创建Watcher对象]
W --> X[在Watcher对象中计算watch的值]
X --> Y[在getter中收集依赖]
W --> Z[为每个watch创建getter]
Z --> AA[在getter中触发依赖更新]
D --> BB[调用compile方法]
BB --> CC[编译模板]
CC --> DD[解析模板指令]
DD --> EE[创建Watcher对象]
EE --> FF[在Watcher对象中处理指令]
FF --> GG[在getter中收集依赖]
EE --> HH[为每个指令创建getter]
HH --> II[在getter中触发依赖更新]
利用defineProperty进行数据劫持
function defineReactive(obj, key, value) {
let internalValue = value;
Object.defineProperty(obj, key, {
get() {
console.log(`获取属性 ${key}: ${internalValue}`);
return internalValue;
},
set(newValue) {
console.log(`设置属性 ${key}: ${newValue}`);
internalValue = newValue;
}
});
}
// 通过observe进行data数据劫持
function observe(obj) {
if (typeof obj !== 'object' || obj === null) {
return;
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
const data = {
name: 'Alice',
age: 25
};
observe(data);
// 通过获取属性触发 get 方法
console.log(data.name); // 输出: 获取属性 name: Alice
// 通过设置属性触发 set 方法
data.age = 30; // 输出: 设置属性 age: 30
观察者和发布订阅的区别
(一)观察者模式原理
一对多的关系,一个被观察者对象可以有多个观察者,- 被观察者状态发生变化,所有观察者收到通知并进行相应更新。
- 观察者需要直接订阅被观察者,并与被观察者产生依赖关系。
//被观察者
class Dep {
constructor() {
this.subscribers = [];
}
addSubscriber(subscriber) {
this.subscribers.push(subscriber);
}
notify(message) {
this.subscribers.forEach(subscriber => {
subscriber.update(message);
});
}
}
//观察者
class Watcher {
constructor(updateCallback) {
this.updateCallback = updateCallback;
}
update(message) {
this.updateCallback(message)
}
}
const dep = new Dep();
const watcher1 = new Watcher(() => {
console.log('Watcher 1');
});
const watcher2 = new Watcher(() => {
console.log('Watcher 2');
});
// 添加观察者
dep.addObserver(watcher1);
dep.addObserver(watcher2);
五、data、computed、watch实现原理
(一)data响应式实现原理
graph LR
A[页面 A]
A1[页面A watcher]
B[data B]
A --> |依赖| B
A --> |建立watcher| A1
A1 --> |观察B数据| B
B --> |改变时通知页面A的watcher执行update更新页面A| A
- 页面A 依赖 dataB
- 页面A 建立 watcher
- 页面A的watcher观察 dataB
- dataB发生改变通知页面A的watcher执行update去更新页面A
(二)computed实现原理
一种计算属性,具有缓存的特性,也具有数据响应式 响应式实现大概流程
graph LR
A[页面 A]
A1[页面A watcher]
B[computed B]
B1[computedB watcher]
C[data C]
A --> |依赖| B
A --> |建立watcher| A1
B --> |依赖| C
B --> |建立watcher| B1
B1 --> |观察C数据| C
C --> |改变时通知| B1
B1 --> |接到通知重新计算| B
A1 --> |观察C数据| C
C --> |改变时通知| A1
A1 --> |重新渲染| A
-
页面A 依赖 computed B
-
页面A 建立watcher 间接观察computed B,实际观察了data C
-
computed B 依赖了 data C
-
computed B建立watcher观察了data C
-
dataC改变,通知computed B的watcher执行了update方法使dirty为true,再通知页面A的watcher重新计算computed并更新页面
(三)watch实现原理
graph LR
A[watch A]
A1[watch watcher]
B[data B]
A --> |依赖| B
A --> |建立watcher| A1
A1 --> |观察data B| B
B --> |改变时通知并执行函数| A1
- watch A 依赖 dataB
- watch A 建立watcher 观察 data B
- data B 变化后通知 watchA 的watcher执行update方法并执行了watch的函数。
六、mixins实现原理
-
mixin是一种代码混入的方式,按照一定的规则将mixin里的代码合并到组件的代码中。
-
合并过程会涵盖 data、methods、computed、watch、生命周期钩子等选项
-
mixin 和组件有相同的属性或方法时,组件的选项会覆盖 mixin 的。
-
如果有多个 mixin 中有相同的属性或方法,后面的 mixin 会覆盖前面的,最终组件的选项会覆盖所有 mixin。这种合并顺序是从左到右的。
graph LR
A[创建 Vue 组件] --> B[调用 initInternalComponent 初始化选项]
B --> C[处理 mixins 选项]
C --> D[遍历 mixins 数组]
D --> E[合并每个 mixin 中的选项到组件中]
E --> F[使用 mergeOptions 函数进行合并]
F --> G[处理不同类型的选项合并策略]
G --> H[处理重名选项 组件覆盖 mixin]
H --> I[合并后的选项用于初始化和渲染组件]
function mergeOptions(parent, child) {
const options = {};
// 合并父选项和子选项
for (const key in parent) {
mergeField(key);
}
for (const key in child) {
if (!parent.hasOwnProperty(key)) {
mergeField(key);
}
}
function mergeField(key) {
// 根据不同的选项类型执行不同的合并策略
if (strats[key]) {
options[key] = strats[key](parent[key], child[key]);
} else {
options[key] = child[key] || parent[key];
}
}
return options;
}
const strats = {
data: function(parentVal, childVal) {
return mergeData(parentVal, childVal);
},
// 其他选项的合并策略
};
function mergeData(parentVal, childVal) {
if (!childVal) {
return parentVal;
}
if (!parentVal) {
return childVal;
}
return function mergedDataFn() {
return mergeData(
typeof parentVal === 'function' ? parentVal.call(this) : parentVal,
typeof childVal === 'function' ? childVal.call(this) : childVal
);
};
}