vue2
源码内容
文件命名规范
- runtime 仅包涵运行时,不包涵译器
- common cjs规范(webpack1)
- esm ES规范(webpack2+)
- umd: 命名上没有特征(兼容cjs,amd异步,一般用在游览器)
源码文件(src下)
platforms/web/entry-runtime-with-compiler:入口文件,主要覆盖$mount执行模板解析和编译
platforms/web/runtime/index.js: 定义$mount(将首次渲染结果替换el),patch方法(负责根据vnode生成真实node,页面初始化和diff式更新)
core/index.js: 定义全局api,use方法(参数可能是对象或函数,对象则找install方法执行)
core/instance/index.js: 定义vue构造函数: 初始化相关, 定义实例方法。
通过
initMixin
为vue实例添加_init初始化方法initState
进行数据初始化,响应式先初始化
inject
再响应式再provide
的原因:- 注入的数据需要在响应式时进行判断
inject
注入的数据有可能再被provide
初始化过程:
1.new Vue()
2.this._init(option)初始化各属性
3.$mount调用mountComponent()
4.mountComponent声明updateComponent方法,创建Watcher
5._render获取虚拟dom
6._update(调用_patch进行diff)转换为真实dom
core/instance/init.js: 定义
initMixin
及其义_init
方法(负责初始化和选项合并)core/instance/state.js:
initData
主要 获取data,设置代理,启动响应式observercore/observer/index.js:
每个响应式数据都有一个
ob
属性(Observer实例)为什么要在
Observer
实例也要创建一个dep
:为了对象或数组新增删除属性等内部变动时也能通知更新
data数据中这些实例的个数:observer(对象个数),dep(key个数),watcher(组件个数)
core/observer/array.js:
对数组变动七个方法覆盖并新增通知更新功能notify(来自数组对象上放置的ob的dep)
其他
package.json=>dev对应的target指明对应配置文件,在entry找到入口文件位置
源码判断顺序优先级:render>template>el。template实际上还是经过ast抽象语法树解析成render,生成vnode再挂载到页面
template选项设置后不$mount则模板数据不会解析
组件创建是自上而下,挂载是自下而上
vue2中dep和watcher是多对多,dep管理一个key的一组watcher,一个wacher管理一个组件,组件内可以也有多个用到的key对应其dep
响应式原理
梗概
Object.defineProperty
监听对象属性改变。- 发布者订阅者设计模式,当数据改变时通知界面对应更新。
Observer
类执行数据响应化(要分辨对象和数组),每监听一个数据创建对应的Dep实例(添加订阅者)Dep
依赖管理类管理某个key(响应式数据的key)对应的多个Watcher
,批量更新
依赖管理思路:defineReactive
响应化时为每个key创建Dep
实例- 初始化视图时每读取一个key新增一个
Watcher
- 由于触发响应数据getter,将每个
Watcher
添加到对应的Dep
中,相同的key添加到同一个Dep
- 响应式数据更新触发setter,通过对应
Dep
通知其下的所有Watcher更新Watcher
类负责执行自身更新视图。update
实例方法Compiler
类执行编译(模板,指令),初始化视图,收集依赖(更新视图,Watcher
创建)
正则.匹配任意字符(除特殊字符),*表示0或多个。regExp.$n拿到匹配(test)到的内容的第n个()中内容defineProperty
对应get触发时创建并添加订阅者,对应set时调用notify
更新相关视图Dep
静态属性target
保存当前的watcher
,再手动触发getter添加到Dep
依赖后就置空
vue1中是细粒度的数据变化侦测,会造成大量开销。vue2中每个组件只有一个
watcher
(中粒度),通过diff+vnode行局部更新为什么每个key都创建
dep
为了能服务于watch,computed等不需要执行render渲染视图,只需要执行回调的key,需要这种低粒度的
watcher
创建dep流程:·
new Watcher() => render() => get() => dep
异步更新队列
vue高效的秘诀是一套批量,异步的更新策略
宏任务
独立的工作单元,游览器完成一个宏任务会重新渲染游览器再进行下一个宏任务(创建主文档对象,解析html,执行主线js(包括定时器,ajax),各种事件)
微任务
当前宏任务执行后立即执行,有微任务游览器会清空微任务再重新渲染(promise回调,dom发生变化)
vue将所有watcher的update放到微任务队列,队列执行完毕后再一次性刷新页面。vue中的timerFunc
就是根据环境兼容选择执行任务的方法,优先使用promise等微任务,都不支持时用定时器。最后执行一次run方法完成游览器的渲染
注意:定时器是与主线js同级的宏任务,因此定时器和promise都存在时,执行顺序为: 同步js=>promise的then回调=>定时器
虚拟dom和diff
虚拟dom优点
- 轻量快速,通过vnode新旧对比进行最小的dom操作量
- 跨平台,虚拟dom更新操作内部在不同平台(如web,app)进行不同操作达到效果
- 兼容性,可以加入兼容性代码
patch实现
源码位置:core/vdom/patch.js
- 进行树级比较,进行增删改
- 没有new vnode 删(没有内容)
- 没有old vnode 增(初始化)
- 都存在就进行新旧vnode对比(diff) 再更新
属性key就是能更方便比较不同vnode
页面更新流程
watcher.run() => componentUpdate() => render() => update=> patch()
patchVnode实现
从此处开始diff
对比新旧vnode,包括三种更新操作: 属性,文本,子节点
对比新旧vnode的子节点children有如下情况:
- 均有子节点:
进行diff操作,调用updateChildren
- 只有新节点有子节点:
清空老节点文本内容,新增子节点 - 只有老节点有子节点:
移除所有子节点 - 都无子节点:
只是文本内容替换
updateChildren实现
主要以较高效的方式对新旧vnode的children得出最小操作的重排补丁。传统方式是双循环,vue对web场景做了特殊优化:
- 新旧vnode头尾两侧有变量标记,遍历过程中都向中心靠拢,当oldStartIdx>oldEndIdx或newStartIdx>newStartIdx结束循环。
- 遍历规则:oldStartVnode和newEndVnode,newStartVnode和oldEndVnode两两交叉比较,有四种情况(reorder重排)
- oldStartVnode和newStartVnode,或者oldEndVnode和newEndVnode满足sameVnode(key,tag,属性等依次比较),直接patchVnode完成当前循环
- oldStartVnode和newEndVnode满足sameVnode,说明oldStartVnode跑到oldEndVnode后面了,所以patchVnode时还需要将真实dom移动到oldEndVnode后面
- 同理上一条对应,newStartVnode和oldEndVnode满足sameVnode,patchVnode时还需要将真实dom移动到oldStartVnode前面
以上都不符合,则只有双循环(在oldVnode中找与newStartVnode满足sameVnode的vnodeToMove进行patchVnode,并将其真实dom放到oldStartVnode前。若找不到一致key或都不满足sameVnode,会用createElem创建新dom)
如果有一方剩下节点,新的多新增,旧的多则删除
简述diff
- diff是什么: 对比vnode,得出最小dom操作内容的算法。
- 优点: 性能,跨平台,兼容
- 在哪:patch方法中,存在新旧vnode时
- 执行:
遵循同级比较,深度优先- 先假设首尾相同进行重排(reorder)比较,4种情况都不符合则开始递归双循环:
- 从新数组中取出一个,在节点老数组中找到
sameVnode
的并根据两者相对位置移动并对两者打补丁 - 由父节点开始对比子节点,从第一个子节点比较到最后一层的第一个子节点(向下递归),再从第一层第二个开始。
vue还做了优化,使首尾的四个指针向中间靠拢,减少循环次数
vue在将template解析为ast对象时,标记了静态节点(起码两层嵌套是静态才算),在diff时patch可以直接跳过,每次渲染也不用为其创建新节点
vue3
虚拟dom和diff
相比vue2中全量的双端比较做法,vue3进行了优化:
- 创建vnode tree时,根据dom中内容是否回变化,添加静态标记
PatchFlags
。在新旧vnode对比时,只会对比带有静态标记的节点
v-for加key
key可以使diff算法更加准确、高效
在新旧vnode比对时,根据是否有key(通过静态标记PatchFlags
判断,vue3特有) 分别执行不同方法:
源码位置: packages => runtime-core => src =>renderer.ts
patchUnkeyedChildren
- 不使用key时- 直接从头部按顺序比对新旧vnode,进行
patch
。然后根据旧vnode多少进行移除(unmountChildren
)和新增(mountChildren
) - 相同则尽可能复用,不同则新增
- 改动项只后的内容也会受变动
- 直接从头部按顺序比对新旧vnode,进行
patchKeyedChildren
- 使用key 基于key变化排列元素,移除没有的元素- 先从头部遍历,
isSameVnodeType
则进行patch
- 如果遇到不同则从尾部开始遍历,
isSameVnodeType
则patch
,不同就停止循环 - 还剩余新vnode则执行挂载(
patch(null, ...)
) - 还剩余旧vnode则执行删除(
unmount
) - 最后剩余中间有无序的vnode,会进行交叉比对、移动;相同则
patch
尽可能复用,然后删除多余节点,挂载新节点
- 先从头部遍历,
isSameVnodeType方法通过key和type对比vnode是否相同
patch方法第一个参数为null执行挂载新元素,不为空则执行更新
- 本文作者: MR-QXJ
- 本文链接: https://mr-qxj.github.io/2021/12/29/框架/vue源码/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!