Vue源码分析

231 阅读6分钟

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,驱动视图和数据的变化

  • 视图层变化,数据层随之变化
  • 数据层变化,视图层随之变化

包含以下三个步骤:

  1. 视图监听:视图通过事件监听等方式监测用户操作和数据变化。
  2. 数据模型更新:ViewModel会监听数据模型的变化,并在数据发生改变时更新视图。
  3. 视图更新: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初始化流程的主要步骤:

  1. 创建Vue实例:通过new Vue()来创建一个Vue实例。
  2. 初始化数据:Vue会将用户传入的data对象进行响应式化处理,将其转换为getter和setter,并建立依赖关系。
  3. 模板编译:将用户定义的模板编译为渲染函数。
  4. 生成渲染函数:根据编译后的模板生成渲染函数,用于渲染组件的虚拟DOM。
  5. 创建组件实例:根据渲染函数创建组件实例,并执行组件的生命周期钩子函数。
  6. 将组件挂载到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]

vueinit.drawio.png

四、响应式原理

响应式原理是Vue实现数据绑定的核心,通过数据劫持依赖收集实现视图数据的自动同步。数据发生变化,Vue自动更新视图。响应式原理的主要步骤如下:

  1. 数据劫持:Vue会将data对象转换为响应式对象,为每个属性添加getter和setter方法,用于监听属性的读取和修改操作。
  2. 依赖收集:Vue会在编译模板时,收集模板中使用的所有数据属性,并建立数据与视图之间的依赖关系。
  3. 数据变化:当数据发生变化时,触发setter方法,通知依赖更新。
  4. 视图更新:依赖收集建立的依赖关系会触发视图的更新,保持视图与数据的同步。
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

观察者和发布订阅的区别

(一)观察者模式原理

  1. 一对多的关系,一个被观察者对象可以有多个观察者
  2. 被观察者状态发生变化,所有观察者收到通知并进行相应更新
  3. 观察者需要直接订阅被观察者,并与被观察者产生依赖关系。
//被观察者
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
    
  1. 页面A 依赖 dataB
  2. 页面A 建立 watcher
  3. 页面A的watcher观察 dataB
  4. 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
    
  1. 页面A 依赖 computed B

  2. 页面A 建立watcher 间接观察computed B,实际观察了data C

  3. computed B 依赖了 data C

  4. computed B建立watcher观察了data C

  5. 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
    
  1. watch A 依赖 dataB
  2. watch A 建立watcher 观察 data B
  3. data B 变化后通知 watchA 的watcher执行update方法并执行了watch的函数

六、mixins实现原理

  1. mixin是一种代码混入的方式,按照一定的规则将mixin里的代码合并到组件的代码中。

  2. 合并过程会涵盖 data、methods、computed、watch、生命周期钩子等选项

  3. mixin 和组件有相同的属性或方法时,组件的选项会覆盖 mixin 的。

  4. 如果有多个 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
    );
  };
}

参考文章