Vue源码分析

167 阅读4分钟

Vue源码分析

1. 模板编译

  • Runtime + Compiler : 包含模板编译器
  • Runtime Only : 不包含模板编译器

模板编译流程

  • 第一步: 解析模板字符串生成抽象语法树对象(ast)
  • 第二步: 对AST进行优化(vue3会添加静态标记)
  • 第三步: 根据ast生成用于生成VDOM的render函数代码

2. 响应式原理

2.1 new Vue()

new Vue() 做了什么

  • 合并传入的options到$options上

2.2 数据劫持

  • vue的响应式原理: 主要弄清楚2个问题
    • vue如何知道data对象中的数据变化了? ==> 数据劫持
    • 某个数据变化后, 如何实现对应DOM的更新? ==> 收集依赖 + 派发更新 订阅-发布模式
  • 数据代理:
    • 理解: 通过vm来代理对vm中data对象中属性的读/写操作
    • 作用: 简化读/写data中数据的编码
    • 实现:
      • 通过defineProperty给vm添加与data对象中属性对应的属性, 指定getter和setter
      • 在getter中读取返回data中对应的属性值
      • 在setter中将最新设置的值保存到data中对应的属性上
  • 数据劫持
    • 理解: 能监视到data中任意层级的属性值的更新
    • 实现:
      • 通过defineProperty来修改data中所有层级的属性, 给属性添加getter和setter
        • getter: 用于收集依赖
        • setter: 用于监视属性值的变化

2.3 三种watcher

  1. render watcher | 渲染watcher

    • 专门用于组件DOM的渲染, 包括初始渲染和更新渲染
    • 在初始化的最后, 执行mount后会进行初始渲染操作
  2. computed watcher | 计算watcher

    • 每个计算属性, 会通过defineProperty给组件对象定义对应的属性
    • 每个计算属性, 创建对应的watcher对象
    • 初始时: lazy为true, dirty为true, 初始时不会调用getter进行计算,
    • 当第一次读取属性值时才会计算, 并将结果保存到watcher的value, 将dirty改为false
    • 当后面多次读取计算属性, 由于dirty是false, 直接读取watcher的value, 而不会重新调用getter来计算 ==> 计算属性有缓存
    • 由于计算属性getter中读取了依赖属性, 会将当前watcher收到到依赖属性的dep中, 当依赖属性变化时,
  3. user watcher | 用户watcher

    • 每个组件中配置的watch和调用的$watch, 都会创建一个user为true的watcher
    • 如果immediate为true, 会在初始化时, 立即调用一次watch的回调
    • 如果deep为true, 会对属性对象进行深度依赖收集当前watcher, 当任意层级属性发生变化时, 当前watcher中的回调都会调用
    • 计算watcher的回调是同步执行的, 用户watcher的回调和渲染watcher对应的更新DOM的回调都是异步执行的
    • 但用户watcher的回调先执行, 所以其回调中只能看到旧的DOM

2.4 收集依赖与派发更新

  • 初始化data时,创建observer,为data的所有层级的属性都通过defineProperty添加getter和setter,为data的所有数据添加数据劫持,变成响应式数据;为每个属性创建一个对应的dep,用于收集依赖这个属性的watcher的
  • 接着会创建不同类型的watcher,当读取data数据时,会执行对应的getter方法,收集依赖
  • 当更新了data中的某个属性,会遍历内部所有的watch去更新
    • 如果是computed watcher 那么在渲染(执行render函数)时调用watcher执行
    • 如果是user watcher / render watcher , 那么会准备异步执行(调用nextTick())

2.5 nextTick

Vue 在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。

  • nextTick可以传两个参数: 回调函数 和 环境对象
  • nextTick做了什么: 把传入的函数放入callback数组,并且执行timerFunc函数
  • timerFunc函数:去判断当前环境是否支持原生Promise,原生MutationObserver,尝试把回调函数放入微任务队列去执行,如果不支持,则检查是否支持setImmediate,不支持就用setTimeout. 对环境进行一个降级处理,去执行flushCallbacks函数
  • flushCallbacks函数: 就是for循环执行callback队列

nextTick原理.png

响应式原理总结:

原理:通过数据劫持 defineProperty + 发布订阅者模式,当 vue 实例初始化后 observer 会针对实例中的 data 中的每一个属性进行劫持并通过 defineProperty() 设置值后在 get() 中向发布者添加该属性的订阅者,这里在编译模板时就会初始化每一属性的 watcher,在数据发生更新后调用 set 时会通知发布者 notify 通知对应的订阅者做出数据更新,同时将新的数据根性到视图上显示。
缺陷:只能够监听初始化实例中的 data 数据,动态添加值不能响应,要使用对应的 Vue.set()。

对象类型做数据劫持

使用 Object.defineProperty 方法添加对象,重写了原有的 get 和 set 方法,这就是数据劫持。

3. 虚拟DOM和 DOM Diff

3.1 虚拟DOM

  • 比较虚拟DOM与真实DOM
    • 区别: 一个较轻, 一个较重
    • 关系:
      • 初始化阶段会根据虚拟DOM生成真实DOM
      • 更新阶段会进行新旧虚拟DOM的 diff比较, 来进行真实DOM的更新
  • 虚拟DOM的作用
    • 生成真实DOM
    • 基于新旧虚拟DOM进行 DOM Diff 实现最小化DOM更新

3.2 Diff 算法

  • 目标:
    • 确定哪些真实DOM可以复用, 哪些真实DOM需要新建
    • 为了提高效率: 做最少的查找比较, 能复用的尽量复用
    • 不要出现效果的问题: 不要复用错误的真实DOM
  • 基本流程
    • 先确定比较的范围是同层的vnode
    • 看key
    • 看tag
    • 复用真实DOM, 如果数据有变化, 更新真实DOM
    • 根据虚拟DOM, 创建新的真实DOM

3.3 key的深入理解

  • key是虚拟DOM的唯一标识,只有找到相同的key的虚拟DOM,才能复用真实DOM,不然只能重新创建
  • 要保证key的唯一性和稳定性,真实DOM会尽量复用
  • index作为key的问题,key是不稳定的,假如遍历的是数组 数组的每个项对应的下标会改变 不稳定, 所以一般用后台返回的id

4. keep-alive

作用

keep-alive是Vue的内置组件,可以包裹动态组件/路由组件,在组件切换时,不会销毁组件,而且缓存暂时不显示的组件 防止重复渲染DOM,减少加载时间和性能消耗,提高用户体验

原理:

render中:

  • 获取了默认插槽组件的vnode
  • 如果vnode已经有缓存,那么会取出缓存的组件实例,保存到当前的vnode,就不会再创建组件实例

生命周期的变化

用到keep-alive的组件会执行activated和deactivated钩子函数(激活与未激活)

  • activated在初始mounted之后和每次再次回来时调用
  • deactivated在每次离开时调用