目前社区有很多 Vue3 的源码解析文章,但是质量层次不齐,不够系统和全面,总是一个知识点一个知识点的解读,这样我在拜读中,会出现断层,无法将整个vue3的知识体系融合,于是只能自己操刀来了
并且自己建了一个github 我会将我在源码中的一些注释,以及我对源码的一些理解,尽数放到此github中,既是为了自己能温故而知新,也能方便后来的小伙伴能更快的了解vue的整个体系
github地址奉上vue 源码解析 v3.2.26
项目中包含思维导图(后续会慢慢更新),源码注释,简版手写原理,以及帮助理解的文章,希望大家手动star
言归正传,我们这是要做源码导读,这次源码导读,将会分为一下几个方面:
我们一个个来分析
Monorepo的意思是在版本控制系统的单个代码库里包含了许多项目的代码。这些项目虽然有可能是相关的,但通常在逻辑上是独立的,并由不同的团队维护。 想要理解他到底是个是,我们用一张图彻底理解
如上图所示,大致意思就是一个仓库管理者很多的包,他们可以统一打包统一发布,也可以分开打包分开发布,并且由不同的人维护,这也是目前很多开源库所采用的方案
Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,也是目前库的打包首选bundler
Typescirpt自不用多说,社区火爆
每个项目不一样,感兴趣的可以参考
pnpm - 速度快、节省磁盘空间的软件包管理器
Jest 是一个令人愉快的 JavaScript 测试框架,专注于 简洁明快
ESLint可组装的JavaScript和JSX检查工具
这里我们主要介绍几个主要的核心库,本身看源码的就无需事无巨细,我们只需要了解主要的原理,以及能吸收一些优秀的代码设计和思想
learn 构建工程,所有的目录散在packages中,并且packages中的包也能单独使用比较重要的包
Vue3 另一个核心思想是组件化。所谓组件化,就是把页面拆分成多个组件 (component),每个组件依赖的 CSS、JavaScript、模板、图片等资源放在一起开发和维护。组件是资源独立的,组件在系统内部可复用,组件和组件之间可以嵌套。
我们在用 Vue3开发实际项目的时候,就是像搭积木一样,编写一堆组件拼装生成页面。在 Vue.js 的官网中,也是花了大篇幅来介绍什么是组件,如何编写组件以及组件拥有的属性和特性。
当然组件化这个概念也不是vue3独有的,从很早以前就有这个就流传开来
在 JQuery
年代,模板引擎的概念,干的年头长的都应该听过。
举个例子
import { template } from 'lodash'
const compiler = template('<h1><%= title %></h1>')
const html = compiler({ title: 'My Component' })
document.getElementById('app').innerHTML = html
上述代码中就是lodash的一个模板引擎他的本质就是:模板+数据=HTML 相信很多老前端都知道当年前后端不分离的时代,套模板是大多数web站点的宿命。那个年代的模板srr方案其实本质就是模板引擎
而在现在的vue、react年代模板引擎的变了
模板+数据=Vdom 他引入了Virtual DOM的概念
在vue 3 中,我们的模板就会给抽象成render函数,这个render函数就是我们的模板,举个例子:
<div id="demo">
<div @click="handle">
点击切换
</div>
<div @click="handle1">
点击切换1
</div>
<div v-if="falg">{{num}}</div>
<div v-else>{{num1}}</div>
</div>
他最后编译的结果就会是这个样子
const _Vue = Vue
const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue
const _hoisted_1 = ["onClick"]
const _hoisted_2 = ["onClick"]
const _hoisted_3 = { key: 0 }
const _hoisted_4 = { key: 1 }
return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode, Fragment: _Fragment } = _Vue
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("div", { onClick: handle }, " 点击切换 ", 8 /* PROPS */, _hoisted_1),
_createElementVNode("div", { onClick: handle1 }, " 点击切换1 ", 8 /* PROPS */, _hoisted_2),
falg
? (_openBlock(), _createElementBlock("div", _hoisted_3, _toDisplayString(num), 1 /* TEXT */))
: (_openBlock(), _createElementBlock("div", _hoisted_4, _toDisplayString(num1), 1 /* TEXT */))
], 64 /* STABLE_FRAGMENT */))
}
}
而render 函数执行的结果就应该是一个vdom
Virtual DOM 他就是个js 对象,比如
{
tag: "div",
props: {},
children: [
"Hello World",
{
tag: "ul",
props: {},
children: [{
tag: "li",
props: {
id: 1,
class: "li-1"
},
children: ["第", 1]
}]
}
]
}
他对应的就是表达的dom
<div>
Hello World
<ul>
<li id="1" class="li-1">
第1
</li>
</ul>
</div>
至于为何组件要从直接产出 html
变成产出 Virtual DOM
呢?其原因是 Virtual DOM
带来了 分层设计,它对渲染过程的抽象,使得框架可以渲染到 web
(浏览器) 以外的平台,以及能够实现 SSR
等 ,并不是Virtual DOM 的性能好
具体的请参考网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么?
在我们日常写的组件如下:
<template>
<div>
这是一个组件{{num}}
</div>
</template>
<script>
export default {
name:home
setup(){
const num=ref(1)
return {num}
}
};
</script>
编译后的结果如下:
const home={
setup(){
const num=ref(1)
function handleClick() {
num.value++
}
return {num,handleClick}
},
render(){
with (_ctx) {
const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", { onClick: handleClick }, " 这是一个组件" + _toDisplayString(num), 9 /* TEXT, PROPS */, _hoisted_1))
}
}
}
其实你发现他就是个配置对象,里面包含了数据,操作数据的方法,以及编译后的模板模板函数
而在我们使用的时候 给抽象成标签引用
<template>
<div>
<home></home>
</div>
</template>
最后编译后的结果
const elementVNode = {
tag: 'div',
data: null,
children: {
tag: home,
data: null
}
}
如此以来,我们的组件就能参与到vdom中来,我们的整个页面就能渲染成一个包含组件的vdom树,这样就能通过搭积木的方式将很多个组件拼装为一个一个页面,就像下图一样:
所谓渲染器,简单的说就是将 搭积木形成Virtual DOM
渲染成特定平台下真实 DOM
的工具(就是一个函数,通常叫 render
),渲染器的工作流程分为两个阶段:mount
和 patch
,如果旧的 VNode
存在,则会使用新的 VNode
与旧的 VNode
进行对比,试图以最小的资源开销完成 DOM
的更新,这个过程就叫 patch
,或“打补丁”。如果旧的 VNode
不存在,则直接将新的 VNode
挂载成全新的 DOM
,这个过程叫做 mount
。
在此之前我们先来看 Virtual DOM的种类(引用大佬的图)
对应的在vue中也通过二进制位的方式来表示vnode类型
export const enum ShapeFlags {
ELEMENT = 1, // 普通节点
FUNCTIONAL_COMPONENT = 1 << 1,//2 // 函数组件
STATEFUL_COMPONENT = 1 << 2,//4 // 普通组件
TEXT_CHILDREN = 1 << 3,//8 // 文本子节点
ARRAY_CHILDREN = 1 << 4,//16 // 数组子节点
SLOTS_CHILDREN = 1 << 5,//32
TELEPORT = 1 << 6,//64 // 传送门
SUSPENSE = 1 << 7,//128 // 可以在组件中异步
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,//256
COMPONENT_KEPT_ALIVE = 1 << 9,//512// keepALIVE
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT // 6 表示函数组件和普通组件
}
而有了这些类型区分,我们就能通过不同的类型来执行不同的挂载逻辑以及patch 逻辑
switch (type) {
// 文本节点
case Text:
processText(n1, n2, container, anchor)
break
// 注释节点
case Comment:
processCommentNode(n1, n2, container, anchor)
break
// 静态节点, 这个应该是在ssr的时候用到的
// 因为只有在ssr的时候才会常见static类型的vnode
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
// Fragment 片段
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
// 去除了特殊情况节点的渲染,就是正常的vnode 渲染
// 如果是个节点类型
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
// 如果是个组件类型
// 第一次执行挂载时候也被当做组件类型初始化的
// vue3改版之后直接用配置去常见对象去创建组件vnode
// 这个配置需要用一个函数去拿,也是动态加载的
// 传入名字在运行时去去通过resolvecompinent 来拿
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
// 如果是个传送门
} else if (shapeFlag & ShapeFlags.TELEPORT) {
; (type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
// Suspense 实验性内容
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
; (type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
}
}
那么到此,整个渲染器的主要方法patch的核心逻辑就解释到这了,我们意在了解整个源码的体系,以及脉络,不纠结具体实现,具体实现我们还需要在源码中找寻。
组件挂载 就是组件类型的vnode 节点的初始化 包含响应式初始化,依赖收集,生命周期,编译等,所以我们有必要来了解一下整个组件挂载的全过程,来研究一下组件挂载的流程,让我们更清晰的明白整个vue中的数据怎样和render函数绑定的。
我们抽离了渲染器的的核心逻辑意在解释整个组件的初始化的流程
const nodeOps = {
insert: (child, parent) => {
parent.insertBefore(child, null)
},
createElement: (tag) => {
return document.createElement(tag)
},
setElementText: (el, text) => {
el.textContent = text
},
}
// 这里我们只考虑 事件patch 的情况,其他暂不处理
function patchEvent(el, key, val) {
el.addEventListener(key.slice(2).toLowerCase(), function () {
val()
})
}
// 处理props
function patchProp(el, key, val) {
patchEvent(el, key, val)
}
// 判断是不是ref
function isRef(r) {
return Boolean(r && r.__v_isRef === true)
}
// 返回正确的值
function unref(ref) {
return isRef(ref) ? ref.value : ref
}
const toDisplayString = (val) => {
return val == null
? ''
: String(val)
}
const computed = Vue.computed
Vue.computed = function (fn) {
const val = computed(fn)
// 模拟拿到effect
recordEffectScope(val.effect)
return val
}
const ref = Vue.ref
Vue.ref = function (val) {
const newRef = ref(val)
// 模拟拿到effect
recordEffectScope(val.effect)
return newRef
}
// 建立关联的函数,当前函数在computed、watch、watchEffect等函数中使用
function recordEffectScope(effect, scope) {
scope = scope || activeEffectScope
if (scope && scope.active) {
scope.effects.push(effect)
}
}
// vue的内部使用
// 在页面内部使用EffectScope是为了在组件销毁的时候卸载依赖
//当前实例
let currentInstance = null
// 设置当前实例
const setCurrentInstance = (instance) => {
currentInstance = instance
instance.scope.on()
}
function parentNode(node) {
return node.parentNode
}
// 容错处理
function callWithErrorHandling(
fn,
instancel,
args
) {
let res
try {
res = args ? fn(...args) : fn()
} catch (err) {
console.error(err)
}
return res
}
// 建立响应式
function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get: (target, key, receiver) => {
return unref(Reflect.get(target, key, receiver))
},
set: (target, key, value, receiver) => {
return Reflect.set(target, key, value, receiver)
}
})
}
// 赋值render函数,咱们这里暂不处理编译相关,只赋值render 函数即可
function finishComponentSetup(instance) {
Component = instance.type
// 中间可能有编译过程,暂时省略不处理
instance.render = Component.render
}
function handleSetupResult(instance, setupResult) {
instance.setupState = proxyRefs(setupResult)
// 下方还有编译的内容
finishComponentSetup(instance)
}
// 创建组件实例
function createComponentInstance(vnode, parent) {
// 拿到类型
const type = vnode.type
const instance = {
vnode,
type,
parent,
// 此时已经创建了一个EffectScope 为了批量处理依赖
scope: new EffectScope(true /* detached */),
render: null,
subTree: null,
effect: null,
update: null,
}
return instance
}
// 组件类型的处理,里面包含mount和update
function processComponent(n1, n2, container) {
if (n1 == null) {
mountComponent(n2, container)
} else {
updateComponent()
}
}
const processText = (n1, n2, container, anchor) => {
if (n1 == null) {
nodeOps.insert(
(n2.el = createText(n2.children)),
container
)
}
}
// 节点初始化
function mountElement(n2, container, parentComponent) {
const { type, props, children, shapeFlag } = n2
let el = nodeOps.createElement(type)
// 判断出来带文本子节点的内容
if (n2.shapeFlag === 9) {
nodeOps.setElementText(el, children)
}
// 如果有props 的情况
if (props) {
for (key in props) {
// 注意这里只处理事件的情况,暂不处理样式等情况
patchProp(el, key, props[key])
}
}
nodeOps.insert(el, container)
}
// 节点类型的处理
function processElement(n1, n2, container, parentComponent) {
if (n1 == null) {
// 如果第一次没有,那么就是走mountelement的类型
mountElement(
n2,
container,
parentComponent
)
} else {
// 接下来就是组件更新,走的是patch的内容,内部包含diff,也就是最核心的内容
}
}
// 组件销毁方法
function unmountComponent() {
}
//创建vnode
function createVNode(type, props, children, shapeFlag = 1) {
if (typeof children === "string") {
shapeFlag = 9
}
const vnode = {
type,
props,
children,
shapeFlag,
component: null,
}
return vnode
}
// patch 包含各种组件,节点,注释等内容的挂载,这里为了分析依赖追踪,我们只分析组件的挂载
function patch(n1, n2, container, parentComponent = null) {
// 如果相等就返回表示没变
if (n1 === n2) {
return
}
const { type, shapeFlag } = n2
switch (type) {
// 前面逻辑省略,我们只处理组件类型和普通节点类型
default:
if (shapeFlag & 1) {
processElement(n1, n2, container, parentComponent)
} else {
processComponent(n1, n2, container)
}
}
}
function setupStatefulComponent(instance) {
const Component = instance.type
const { setup } = Component
if (setup) {
setCurrentInstance(instance)
// 拿到setup结果
const setupResult = callWithErrorHandling(
setup,
instance,
[instance.props]
)
// 这里我们只判断返回对象的情况,不考虑别的情况
handleSetupResult(instance, setupResult)
}
}
// 执行setup函数
function setupComponent(instance) {
// 上方的一些不重要的逻辑暂不处理,执行核心逻辑
const setupResult = setupStatefulComponent(instance)
return setupResult
}
// 拿到vnode
function renderComponentRoot(instance) {
// 源码中使用一个Proxy来代理所有的响应式内容的访问和修改 并且存入 instance.proxy中,统一处理
// 我们这里只是为了梳理主流程 只需要setupState即可 porps 暂不考虑
const {
type: Component,
render,
setupState,
} = instance
debugger
result = render.call(
setupState,
setupState,
)
return result
}
// 依赖收集 生成effect
function setupRenderEffect(instance, n2, container) {
const componentUpdateFn = () => {
const nextTree = renderComponentRoot(instance)
const prevTree = instance.subTree
instance.subTree = nextTree
// 这是组件内部的patch走diff
patch(
prevTree,
nextTree,
container,
instance
)
}
// 这里直接调用内部的ReactiveEffect即可不用自己重写
const effect = (instance.effect = new Vue.ReactiveEffect(
componentUpdateFn,
null,
instance.scope // track it in component's effect scope 在组件的影响范围内跟踪它 依赖追踪使用
))
const update = (instance.update = effect.run.bind(effect))
update()
}
// 组件初始化方法
function mountComponent(initialVNode, container, parentComponent = null) {
const instance =
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
))
// 执行setup
setupComponent(instance)
// 依赖收集
setupRenderEffect(instance, initialVNode, container)
}
// 组件更新
function updateComponent() {
// 更新逻辑咱暂时不看
}
// render 组件的初始化主要就是执行path
function createApp(rootComponent, rootProps = null) {
// 这里我们直接传值,源码中是根据type 去判断的
const vnode = createVNode(rootComponent, rootProps, null, 4)
const mount = (container) => {
if (typeof container === 'string') {
container = document.querySelector(container)
}
patch(null, vnode, container)
}
return { mount }
}
// 初始化
createApp({
setup() {
const num = Vue.ref(1)
console.log(num)
const double = Vue.computed(() => {
console.log(1111)
debugger
return num.value * 2
})
function handleClick() {
num.value++
}
return {
num,
double,
handleClick
}
},
render(_ctx) {
with (_ctx) {
return createVNode("div", { onClick: handleClick }, toDisplayString(double))
}
}
}).mount('#app')
上述代码大家可以自行粘贴运行,也可在github上查看
简单解释一下,核心逻辑我们首先createApp之后,其实就是传入当前组件配置,然后就开始走patch方法来执行组件的初始化,然后走setupComponent 执行setup的初始化 注意这个时候只是初始化响应式相关,实例this并没有通过call传入,所以函数中拿不到this 。
接下来执行setupRenderEffect 开始依赖收集,在当前方法中执行了render方法开始了触发在setup 中的响应式的get从而触发依赖收集,具体的依赖收集过程,我们后续讲响应式这块会详细分析
ReactiveEffect
是响应式系统的核心,而响应式系统又是 vue3
中的核心,所以我们必须要理解ReactiveEffect
到底是干什么的
首先我们要理解的是ReactiveEffect
作为 reactive
的核心,主要负责收集依赖,更新依赖,在vue2中他叫Watcher,我们来看源码:
// 记录当前活跃的对象
let activeEffect
// 标记是否追踪
let shouldTrack = false
class ReactiveEffect{
active = true // 是否为激活状态
deps = [] // 所有依赖这个 effect 的响应式对象
onStop = null // function
constructor(fn, scheduler) {
this.fn = fn // 回调函数,如: computed(/* fn */() => { return testRef.value ++ })
// function类型,不为空那么在 TriggerRefValue 函数中会执行 effect.scheduler,否则会执行 effect.run
this.scheduler = scheduler
}
run() {
// 如果这个 effect 不需要被响应式对象收集
if(!this.active) {
return this.fn()
}
// 源码这里用了两个工具函数:pauseTracking 和 enableTracking 来改变 shouldTrack的状态
shouldTrack = true
activeEffect = this
// 在设置完 activeEffect 后执行,在方法中能够对当前活跃的 activeEffect 进行依赖收集
const result = this.fn()
shouldTrack = false
// 执行完副作用函数后要清空当前活跃的 effect
activeEffect = undefined
return result
}
// 暂停追踪
stop() {
if (this.active) {
// 找到所有依赖这个 effect 的响应式对象
// 从这些响应式对象里面把 effect 给删除掉
cleanupEffect(this)
// 执行onStop回调函数
if (this.onStop) {
this.onStop();
}
this.active = false;
}
}
}
this.fn()
在组件级别就是一个渲染的render函数,也是通过重新执行当前方法,来达到触发更新的目的
而在整个响应式系统中,他通过一个dep 建立了响应式数据和 ReactiveEffect 的关系,dep 中保存了ReactiveEffect ,而一个响应式数据又会有个dep 小管家管理了与之相关的ReactiveEffect 这样的时候当响应式数据变化的时候,会触发dep小管家,去批量处理他里面的ReactiveEffect 去做更新,注意在组件化中,一个组件只有一个渲染ReactiveEffect。
提起执行render 不得不想到diff
我们上文说道,之所以采用Virtual DOM
的目的不是为了性能,而是为了跨平台,所以,当页面大量的内容更新的时候性能就没法保证,就需要有一种算法来减小DOM操作的性能开销
市面上的diff 算法基本原理的核心是Diff同层对比,不做跨层级对比,这样能大大减少js 的计算,而在同层对比的核心算法上出现了不同的流派
React
系列的diff 算法 ---从前到后找到需要移动的节点vue3目前使用的是 inferno 其中的核心算法就是最长递归子序列
在这里我们不在赘述,整个对比过程,很多大佬也都分析了,咱们只是导论,不做深入探究,意在和大家一起掌握真个vue 源码的体系
响应性--这个术语在程序设计中经常被提及,但这是什么意思呢?响应性是一种允许我们以声明式的方式去适应变化的编程范例。
而在vue3中的响应性原理就离不开proxy
文档中是这样描述的
get
处理函数中 track
函数记录了该 property 和当前副作用。set
处理函数。trigger
函数查找哪些副作用依赖于该 property 并执行它们。该被代理的对象对于用户来说是不可见的,但是在内部,它们使 Vue 能够在 property 的值被访问或修改的情况下进行依赖跟踪和变更通知。
那他通知的是什么呢?这里就用到了我们前面ReactiveEffect 我们通知当前响应式变量中的小管家deps 依次执行其中的ReactiveEffect.run
方法,来达到触发更新的目的 ,原理图如下(感谢大佬提供的图)
我们也准备了可运行的代码,各位大佬们也可粘贴下来,实际体验
// 保存临时依赖函数用于包装
const effectStack = [];
// 依赖关系的map对象只能接受对象
let targetMap = new WeakMap();
// 判断是不是对象
const isObject = (val) => val !== null && typeof val === 'object';
// ref的函数
function ref(val) {
// 此处源码中为了保持一致,在对象情况下也做了用value 访问的情况value->proxy对象
// 我们在对象情况下就不在使用value 访问
return isObject(val) ? reactive(val) : new refObj(val);
}
//创建响应式对象
class refObj {
constructor(val) {
this._value = val;
}
get value() {
// 在第一次执行之后触发get来收集依赖
track(this, 'value');
return this._value;
}
set value(newVal) {
console.log(newVal);
this._value = newVal;
trigger(this, 'value');
}
};
// 对象的响应式处理 在这里我们为了理解原理原理暂时不考虑对象里嵌套对象的情况
// 其实对象的响应式处理也就是重复执行reactive
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// Reflect用于执行对象默认操作,更规范、函数式
// Proxy和Object的方法Reflect都有对应
const res = Reflect.get(target, key, receiver);
track(target, key);
return res;
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver);
trigger(target, key);
return res;
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key);
trigger(target, key);
return res;
}
});
}
// 到此处,当前的ref 对象就已经实现了对数据改变的监听
const newRef = ref(0);
// 但是还是没有响应式的能力,那么他是怎样实现响应式的呢----依赖收集,触发更新=
// 用来做依赖收集
// 在源码中为为了方法的通用性,他还传入了很多参数用于兼容不同情况
// 我们意在理解原理,只需要包装fn 即可
function effect(fn) {
// 包装当前依赖函数
const effect = function reactiveEffect() {
// 模拟源码中也加入错误处理,为了避免你瞎写出现错误的情况,这就是框架的高明之处
if (!effectStack.includes(effect)) {
try {
// 给当前函数放入临时栈中,为在下面执行中,触发get,在依赖收集中能找到当前变量的依赖项来建立关系
effectStack.push(fn);
// 执行当前函数,开始依赖收集了
return fn();
} finally {
// 执行成功了出栈
effectStack.pop();
}
};
};
effect();
}
// 在收集的依赖中建立关系
function track(target, key) {
// 取出最后一个数据内容
const effect = effectStack[effectStack.length - 1];
// 如果当前变量有依赖
if (effect) {
//判断当前的map中是否有target
let depsMap = targetMap.get(target);
// 如果没有
if (!depsMap) {
// new map存储当前weakmap
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 获取key对应的响应函数集
let deps = depsMap.get(key);
if (!deps) {
// 建立当前key 和依赖的关系,因为一个key 会有多个依赖
// 为了防止重复依赖,使用set
deps = new Set();
depsMap.set(key, deps);
}
// 存入当前依赖
if (!deps.has(effect)) {
deps.add(effect);
}
}
}
// 用于触发更新
function trigger(target, key) {
// 获取所有依赖内容
const depsMap = targetMap.get(target);
// 如果有依赖的话全部拉出来执行
if (depsMap) {
// 获取响应函数集合
const deps = depsMap.get(key);
if (deps) {
// 执行所有响应函数
const run = (effect) => {
// 源码中有异步调度任务,我们在这里省略
effect();
};
deps.forEach(run);
}
}
}
effect(() => {
console.log(11111);
// 在自己实现的effect中,由于为了演示原理,没有做兼容,不能来触发set,否则会死循环
// vue源码中触发对effect中的做了兼容处理只会执行一次
newRef.value;
});
newRef.value++;
上述代码中,没有渲染的effect 因为,如果使用渲染effect 篇幅太大,大家容易迷糊,我们使用一个相当于是vue3的Composition API中的用户effect, 和渲染effect 异曲同工
,不同的是渲染effect 是一个ReactiveEffect 而用户effect 是我们传入的一个函数,他们最终都会被放进deps小管家
中去
vue3之所以会有很大的性能提升,编译器起到了很大的作用,由于模板的可遍历性,所以在编译阶段可以做很多优化,在此之前我们先大致简述一下整个编译器的基本流程
如果了解过编译器的工作流程的同学应该知道,一个完整的编译器的工作流程会是这样:
parse
解析原始代码字符串,生成抽象语法树 AST。transform
转化抽象语法树,让它变成更贴近目标「DSL」的结构。codegen
根据转化后的抽象语法树生成目标「DSL」(一种为特定领域设计的,具有受限表达性的编程语言)的可执行代码。那放到vue中来也是一样的
parse
函数就是发生在把template
转换成ast
的这过程,具体是通过一些正则表达式的匹配template
中的字符串,parse
解析之后得到的是一个粗糙的ast
对象,所谓ast它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。在我们vue 编译中,就是个js 对象
比如如下模板:
<template>
<p>hello World!</p>
</template>
变成ast
{
"type": 0,
"children": [
{
"type": 1,
"ns": 0,
"tag": "template",
"tagType": 0,
"props": [],
"isSelfClosing": false,
"children": [
{
"type": 1,
"ns": 0,
"tag": "p",
"tagType": 0,
"props": [],
"isSelfClosing": false,
"children": [
{
"type": 2,
"content": "hello World!",
"loc": {
"start": {
"column": 6,
"line": 2,
"offset": 16
},
"end": {
"column": 18,
"line": 2,
"offset": 28
},
"source": "hello World!"
}
}
],
"loc": {
"start": {
"column": 3,
"line": 2,
"offset": 13
},
"end": {
"column": 22,
"line": 2,
"offset": 32
},
"source": "<p>hello World!</p>"
}
}
],
"loc": {
"start": {
"column": 1,
"line": 1,
"offset": 0
},
"end": {
"column": 12,
"line": 3,
"offset": 44
},
"source": "<template>\n <p>hello World!</p>\n</template>"
}
}
],
"helpers": [],
"components": [],
"directives": [],
"hoists": [],
"imports": [],
"cached": 0,
"temps": 0,
"loc": {
"start": {
"column": 1,
"line": 1,
"offset": 0
},
"end": {
"column": 1,
"line": 4,
"offset": 45
},
"source": "<template>\n <p>hello World!</p>\n</template>\n"
}
}
如果有兴趣可以去 AST explorer 可以在线看到不同的 parser 解析 js 代码后得到的 AST。
transform阶段,Vue 将对 AST 执行一些转换操作,进行深加工ue的一些的hoistStatic 静态提升 、cacheHandlers 缓存函数 PatchFlags补丁标识 都是在这个阶段处理的
生成render函数
我们之前说过,Transform 阶段 做了很多优化,我们就来具体梳理一下,先来一段编译后的代码
const _Vue = Vue
const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue
const _hoisted_1 = ["onClick"]
const _hoisted_2 = ["onClick"]
const _hoisted_3 = /*#__PURE__*/_createElementVNode("div", null, "静态节点", -1 /* HOISTED */)
return function render(_ctx, _cache) {
with (_ctx) {
const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createCommentVNode(" <div>\n 注释\n</div> "),
_createElementVNode("div", { onClick: handle }, " 点击切换 ", 8 /* PROPS */, _hoisted_1),
_createElementVNode("div", { onClick: handle1 }, " 点击切换1 ", 8 /* PROPS */, _hoisted_2),
_hoisted_3,
_createElementVNode("div", null, _toDisplayString(num1) + "动态节点", 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}
}
所谓静态节点提升为了在diff 的时候防止patch 从而在编译的时候标记处理,使得在将静态内容排除在render之外,防止render 执行的时候,被再次创建
// 通过闭包将执行结果放在render之内,防止重复执行render再次执行当前静态节点的创建
const _hoisted_3 = /*#__PURE__*/_createElementVNode("div", null, "静态节点", -1 /* HOISTED */)
_createElementVNode("div", { onClick: handle }, " 点击切换 ", 8 /* PROPS */, _hoisted_1),
上述代码中有个8 这就是 patchFlag
,他的所有类型展示如下:
export const enum PatchFlags {
// 动态文本节点
TEXT = 1,
// 2 动态class
CLASS = 1 << 1,
// 4 动态style
STYLE = 1 << 2,
// 动态props
PROPS = 1 << 3,
/**
*指示带有带有动态关键点的道具的元素。当钥匙更换时,一个完整的
*总是需要diff来移除旧密钥。这面旗是相互的
*独家与阶级,风格和道具。
*/
// 具有动态key属性,当key改变时,需要进行完整的diff
FULL_PROPS = 1 << 4,
// 32 带有监听事件的节点
HYDRATE_EVENTS = 1 << 5,
// 64 一个不会改变子节点顺序的fragment
STABLE_FRAGMENT = 1 << 6,
// 128 带有key的fragment
KEYED_FRAGMENT = 1 << 7,
// 256 没有key的fragment
UNKEYED_FRAGMENT = 1 << 8,
// 512 一个子节点只会进行非props比较
NEED_PATCH = 1 << 9,
// 1024 动态插槽
DYNAMIC_SLOTS = 1 << 10,
// 下面是特殊的,即在diff阶段会被跳过的
// 2048 表示仅因为用户在模板的根级别放置注释而创建的片段,这是一个仅用于开发的标志,因为注释在生产中被剥离
DEV_ROOT_FRAGMENT = 1 << 11,
// 静态节点,它的内容永远不会改变,不需要进行diff
HOISTED = -1,
// 用来表示一个节点的diff应该结束
BAIL = -2
}
有个patchFlag
之后 就能根据patchFlag的类型,执行特殊的diff 逻辑,能防止全量的diff 造成的性能浪费
默认情况下onClick会被视为动态绑定,所以每次都会去追踪它的变化 但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可
// 模板
<div> <button @click = 'onClick'>点我</button> </div>
// 编译后
import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("button", {
onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.onClick && _ctx.onClick(...args)))
}, "点我")
]))
}
// Check the console for the AST
上述代码中,没有了patchFlag的类型 , 也就是patchFlag是一个默认值为0,这样的话就不走diff
if (patchFlag > 0) {
//patchFlag的存在意味着该元素的渲染代码
//由编译器生成,可以采取快速路径。
//在该路径中,旧节点和新节点保证具有相同的形状
//(即,在源模板中完全相同的位置)
if (patchFlag & PatchFlags.FULL_PROPS) {
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
} else {
// class
if (patchFlag & PatchFlags.CLASS) {
if (oldProps.class !== newProps.class) {
hostPatchProp(el, 'class', null, newProps.class, isSVG)
}
}
// style
// this flag is matched when the element has dynamic style bindings
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
}
if (patchFlag & PatchFlags.PROPS) {
// if the flag is present then dynamicProps must be non-null
const propsToUpdate = n2.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
const key = propsToUpdate[i]
const prev = oldProps[key]
const next = newProps[key]
// #1471 force patch value
if (next !== prev || key === 'value') {
hostPatchProp(
el,
key,
prev,
next,
isSVG,
n1.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
}
}
// text
// This flag is matched when the element has only dynamic text children.
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children as string)
}
}
} else if (!optimized && dynamicChildren == null) {
// unoptimized, full diff
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
}
上述代码中就是根据patchFlag类型来走不同的diff 逻辑,而一旦没有了patchFlag diff也就不走了,直接复用老得vnode ,而老的vnode 的事件中有了缓存,我们直接取用即可,省去了重新创建包装函数的开销,很多大佬可能不明白我说的啥意思,贴上vue 代码,大家就明白了:
export function patchEvent(
el: Element & { _vei?: Record<string, Invoker | undefined> },
rawName: string,
prevValue: EventValue | null,
nextValue: EventValue | null,
instance: ComponentInternalInstance | null = null
) {
// vei = vue event invokers
const invokers = el._vei || (el._vei = {})
const existingInvoker = invokers[rawName]
if (nextValue && existingInvoker) {
// patch
existingInvoker.value = nextValue
} else {
const [name, options] = parseName(rawName)
if (nextValue) {
// 先对事件函数来一层包装,在将包装函数绑定到dom 上去
// 如果开启缓存,就会将当前包装函数缓存,省去了diff 的 开销,直接复用
const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
addEventListener(el, name, invoker, options)
} else if (existingInvoker) {
// remove
removeEventListener(el, name, existingInvoker, options)
invokers[rawName] = undefined
}
}
}
创建一个openBlock()
,意思是先打开一个块,再createBlock
创建一个块,把那些动态的部分保存在一个叫dynamicChildren的属性里,将来这个模块更新的时候,只做dynamicChildren里更新,其他不再处理,这样patch只会处理动态的子节点,从而提高性能,我们也梳理了一下代码:
/**
* 主要思想就是巧妙的利用栈的结构将动态节点放入dynamicChildren中
**/
let blockStack = []
let currentBlock = null
//初始化将快放入栈中利用栈的结构放入block 的子动态元素
function _openBlock(disableTracking = false) {
blockStack.push((currentBlock = disableTracking ? null : []))
}
function closeBlock() {
blockStack.pop()
currentBlock = blockStack[blockStack.length - 1] || null
}
function setupBlock(vnode) {
vnode.dynamicChildren = currentBlock
closeBlock()
return vnode
}
function createVnode(type, porps, children, isBlockNode = false,
) {
const vnode = {
type,
porps,
children,
isBlockNode,
dynamicChildren: null
}
if (!isBlockNode && currentBlock) {
currentBlock.push(vnode)
}
return vnode
}
function _createElementBlock(type, porps, children) {
// 传入true直接表示当前vnode是一个当前block 的开始
return setupBlock(createVnode(type, porps, children, true))
}
// 我们假设source是一个number
function _renderList(source, renderItem,) {
ret = new Array(source)
for (let i = 0; i < source; i++) {
ret[i] = renderItem(i + 1)
}
return ret
}
// 生成vnode
function render(ctx) {
return (_openBlock(), _createElementBlock('Fragment', null, [
createVnode("div", null, " 11111 ", true/* CLASS */),
createVnode("div", null, ctx.currentBranch, /* TEXT */),
(_openBlock(true), _createElementBlock('Fragment', null, _renderList(5, (i) => {
return (_openBlock(), _createElementBlock("div", null, i, /* TEXT */))
})))
]))
}
console.log(render({ currentBranch: "老骥伏枥" }))
大家可以复制下来,体会一下,怎么生成的动态dynamicChildren
1、2、5 我们不在过多解释,大家都明白,我们重点看一下3和4
举两个例子
// 插件机制技巧
// 一个全局变量
let compile
function registerRuntimeCompiler(_compile) {
//将当前模板编译器赋值方便当前模板内别的函数能调用到
//之所以要有这个注册方法,是为了让runtime 中使用
compile = _compile
}
// 如此一来在代码导出时,只需要在在非runtime版本中使用注册方法注册即可
//finishComponentSetup 内部包含编译逻辑
function finishComponentSetup(instance) {
Component = instance.type
// 只需要判断有没有compile并且有没有render 即可
if (compile && !Component.render) {
// 执行编译逻辑
}
}
//vue3中的柯理化技巧
function makeMap ( str, expectsLowerCase ) {
var map = Object.create(null);
var list = str.split(',');
for (var i = 0; i < list.length; i++) {
map[list[i]] = true;
}
return expectsLowerCase
? function (val) { return map[val.toLowerCase()]; }
: function (val) { return map[val]; }
}
var isHTMLTag = makeMap(
'html,body,base,head,link,meta,style,title,' +
'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +
'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' +
'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +
's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +
'embed,object,param,source,canvas,script,noscript,del,ins,' +
'caption,col,colgroup,table,thead,tbody,td,th,tr,' +
'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +
'output,progress,select,textarea,' +
'details,dialog,menu,menuitem,summary,' +
'content,element,shadow,template,blockquote,iframe,tfoot'
);
var isHTMLTag = isHTMLTag('div');
// 函数的拓展性技巧
function createAppAPI(rootComponent, rootProps = null) {
const mount = (container) => {
}
return { mount }
}
function createRenderer() {
return {
createApp: createAppAPI()
}
}
function createApp(...args) {
const app = createRenderer().createApp(...args)
const { mount } = app
app.mount = () => {
console.log('执行自己的逻辑')
mount()
}
}
// 实现一个弹窗的封装技巧
// 通过createVNode render 生成真实dom
const { createVNode, render, ref } = Vue
const message = {
setup() {
const num = ref(1)
return {
num
}
},
template: `<div>
<div>{{num}}</div>
<div>这是一个弹窗</div>
</div>`
}
// 生成实例
const vm = createVNode(message)
const container = document.createElement('div')
//通过patch 变成dom
render(vm, container)
document.body.appendChild(container.firstElementChild)
整个vue 源码的体系导读,本人才疏学浅,所有的理解都到这了,如有错误之处,请大佬们批评指正。
到这,在来个升华,给大家一点诚实的灌输
就是我最近发现社区里,很多人都特别的浮躁,认为看懂源码就有多了不得,不懂源码的都是菜鸡,在这里我想说一点我看源码的一点小小的感受: