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: 用于监视属性值的变化
- 通过defineProperty来修改data中所有层级的属性, 给属性添加getter和setter
2.3 三种watcher
-
render watcher | 渲染watcher
- 专门用于组件DOM的渲染, 包括初始渲染和更新渲染
- 在初始化的最后, 执行mount后会进行初始渲染操作
-
computed watcher | 计算watcher
- 每个计算属性, 会通过defineProperty给组件对象定义对应的属性
- 每个计算属性, 创建对应的watcher对象
- 初始时: lazy为true, dirty为true, 初始时不会调用getter进行计算,
- 当第一次读取属性值时才会计算, 并将结果保存到watcher的value, 将dirty改为false
- 当后面多次读取计算属性, 由于dirty是false, 直接读取watcher的value, 而不会重新调用getter来计算 ==> 计算属性有缓存
- 由于计算属性getter中读取了依赖属性, 会将当前watcher收到到依赖属性的dep中, 当依赖属性变化时,
-
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队列
响应式原理总结:
原理:通过数据劫持 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在每次离开时调用