提到马拉松,大家都知道马拉松是世界上最长的田径项目(全程42.195公里),是所有体育运动中体力消耗最大,同时也是最磨练一个人的意志的项目。如果说你可以坚持跑完整个马拉松,那还有什么是你不可以坚持下来的呢。
同样,在我们学习研究一些优秀类库的源码时,整个过程也是枯燥乏味,亦如参加一场源码级的马拉松。那今天笔者就带着大家一起跑一场关于Vue.js 3.0渲染系统的源码解析版的马拉松,在整个过程中,笔者也会给大家提供补给站(流程图),方便大家阅读。
在开始今天的文章之前,大家可以想一下:
vue
文件是如何转换成DOM
节点,并渲染到浏览器上的?️️️️️️️️️️️️️️️️
vuejs
有两个阶段:编译时和运行时。
我们平常开发时写的.vue
文件是无法直接运行在浏览器中的,所以在webpack
编译阶段,需要通过vue-loader
将.vue
文件编译生成对应的js
代码,vue
组件对应的template
模板会被编译器转化为render函数
。
接下来,当编译后的代码真正运行在浏览器时,便会执行render函数
并返回VNode
,也就是所谓的虚拟DOM
,最后将VNode
渲染成真实的DOM节点
。
了解完vue
组件渲染的思路后,接下来让我们从Vue.js 3.0(后续简称vue3
)的源码出发,深入了解vue
组件的整个渲染流程是怎么样的?
本文主要是分析
vue3
的渲染系统,为了方便调试,我们直接通过引入vue.js
文件的方式进行源码调试分析。
1 . vue3
源码下载
# 源码地址(推荐ssh方式下载)
https://github.com/vuejs/vue-next
# 或者下载笔者做笔记用的版本
https://github.com/AsyncGuo/vue-next/tree/vue3_notes
2 . 生成vue.global.js
文件
npm run dev
# bundles .../vue-next/packages/vue/src/index.ts → packages/vue/dist/vue.global.js...
# created packages/vue/dist/vue.global.js in 2.8s
3 . 启动开发环境
npm run serve
4 . 测试代码
<!-- 调试代码目录:/packages/vue/examples/test.html -->
<script src="./../dist/vue.global.js"></script>
<div id="app">
<div>static node</div>
<div>{{title}}</div>
<button @click="add">click</button>
<Item :msg="title"/>
</div>
<script>
const Item = {
props: ['msg'],
template: `<div>{{ msg }}</div>`
}
const app = Vue.createApp({
components: {
Item
},
setup() {
return {
title: Vue.ref(0)
}
},
methods: {
add() {
this.title += 1
}
},
})
app.mount('#app')
</script>
从上面的测试代码,我们会发现vue3
和vue2
的挂载方式是不同的,vue3
是通过createApp
这个入口函数进行应用的创建。接下来我们来看下createApp
的具体实现:
// 入口文件: /vue-next/packages/runtime-dom/src/index.ts
const createApp = ((...args) => {
console.log('createApp入参:', ...args);
// 创建应用
const app = ensureRenderer().createApp(...args);
const { mount } = app;
// 重写mount
app.mount = (containerOrSelector) => {
// ...
};
return app;
});
首先通过ensureRenderer
创建web端的渲染器,我们来看下具体实现:
// 更新属性的方法
const patchProp = () => {
// ...
}
// 操作DOM的方法
const nodeOps = {
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
remove: child => {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
...
}
// web端的渲染器所需的参数设置
const rendererOptions = extend({ patchProp }, nodeOps);
let renderer;
// 延迟创建renderer
function ensureRenderer() {
return (renderer || (renderer = createRenderer(rendererOptions)));
}
在这里可以看出,通过延迟创建渲染器,当我们只依赖响应式包的情况下,可以通过tree-shaking移除渲染相关的代码,大大减少包的体积。
通过ensureRenderer
可以看出,真正的入口是这个createRenderer
方法:
// /vue-next/packages/runtime-core/src/renderer.ts
export function createRenderer(options) {
return baseCreateRenderer(options)
}
function baseCreateRenderer(options, createHydrationFns) {
// 通用的DOM操作方法
const {
insert: hostInsert,
remove: hostRemove,
...
} = options
// =======================
// 渲染的核心流程
// 通过闭包缓存内敛函数
// =======================
const patch = () => {} // 核心diff过程
const processElement = () => {} // 处理element
const mountElement = () => {} // 挂载element
const mountChildren = () => {} // 挂载子节点
const processFragment = () => {} // 处理fragment节点
const processComponent = () => {} // 处理组件
const mountComponent = () => {} // 挂载组件
const setupRenderEffect = () => {} // 运行带副作用的render函数
const render = () => {} // 渲染挂载流程
// ...
// =======================
// 2000+行的内敛函数
// =======================
return {
render,
hydrate, // 服务端渲染相关
createApp: createAppAPI(render, hydrate)
}
}
接下来我们先跳过这些内敛函数的实现(后面的渲染流程用到时,我们再具体分析),来看下createAppAPI
的具体实现:
function createAppAPI(render, hydrate) {
// 真正创建app的入口
return function createApp(rootComponent, rootProps = null) {
// 创建vue应用上下文
const context = createAppContext();
// 已安装的vue插件
const installedPlugins = new Set();
let isMounted = false;
const app = (context.app = {
_uid: uid++,
_component: rootComponent, // 根组件
use(plugin, ...options) {
// ...
return app
},
mixin(mixin) {},
component(name, component) {},
directive(name, directive) {},
mount(rootContainer) {},
unmount() {},
provide(key, value) {}
});
return app;
};
}
可以看出,createAppAPI
返回的createApp
函数才是真正创建应用的入口。在createApp
里会创建vue
应用的上下文,同时初始化app
,并绑定应用上下文到app
实例上,最后返回app
。
这里有个值得注意的点:
app
对象上的use
、mixin
、component
和directive
方法都返回了app
应用实例,开发者可以链式调用。
// 一直use一直爽
createApp(App).use(Router).use(Vuex).component('component',{}).mount("#app")
到此app应用实例已经创建好了~,打印查看下创建的app
应用:
总结一下创建app
应用实例的过程:
web端
对应的渲染器(延迟创建,tree-shaking)baseCreateRenderer
方法(通过闭包缓存内敛函数,后续挂载阶段的主流程)createAppAPI
方法(1. 创建应用上下文;2. 创建app并返回)创建应用
接下来,当我们执行app.mount
时,便会开始挂载组件。而我们调用的app.mount
则是重写后的mount
方法:
const createApp = ((...args) => {
// ...
const { mount } = app; // 缓存原始的mount方法
// 重写mount
app.mount = (containerOrSelector) => {
// 获取容器
const container = normalizeContainer(containerOrSelector);
if (!container) return;
const component = app._component;
// 判断如果传入的根组件不是函数&根组件没有render函数&没有template,就把容器的内容设置为根组件的template
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML;
}
// 清空容器内容
container.innerHTML = '';
// 执行缓存的mount方法
const proxy = mount(container, false, container);
return proxy;
};
return app;
});
执行完web端
重写的mount
方法后,才是真正挂载组件的开始,即调用createAppAPI
返回的app应用
上的mount
方法:
function createAppAPI(render, hydrate) {
// 真正创建app的入口
return function createApp(rootComponent, rootProps = null) {
// ...
const app = (context.app = {
// 挂载根组件
mount(rootContainer, isHydrate, isSVG) {
if (!isMounted) {
// 创建根组件对应的vnode
const vnode = createVNode(rootComponent, rootProps);
// 根级vnode存在应用上下文
vnode.appContext = context;
// 将虚拟vnode节点渲染成真实节点,并挂载
render(vnode, rootContainer, isSVG);
isMounted = true;
// 记录应用的根组件容器
app._container = rootContainer;
rootContainer.__vue_app__ = app;
app._instance = vnode.component;
return vnode.component.proxy;
}
}
});
return app;
};
}
总结一下,mount
方法主要做了什么呢?
vnode
根组件vnode
绑定应用上下文context
vnode
成真实节点,并挂载细心的同学可能已经发现了,这里的
mount
方法是一个标准的跨平台渲染流程,抽象vnode
,然后通过rootContainer
实现特定平台的渲染,例如在浏览器环境下,它就是一个DOM
对象,在其他平台就是其他特定的值。这也就是为什么我们在调用runtime-dom
包的creataApp
方法时,重写mount
方法,完善不同平台的渲染逻辑。
提到
vnode
,可能更多人会和高性能联想到一起,误以为vnode
的性能就一定比手动操作DOM
的高,其实不然。vnode
的底层同样是要操作DOM
,相反如果vnode
的patch
过程过长,同样会导致页面的卡顿。而vnode
的提出则是对原生DOM
的抽象,在跨平台设计的处理上会起到一定的抽象化。例如:服务端渲染、小程序端渲染、weex平台...
接下来,我们来看下创建vnode
的过程:
function _createVNode(
type,
props,
children,
patchFlag,
...
): VNode {
// 规范化class & style
// 例如:class=[]、class={}、style=[]等格式,需规范化
if (props) {
// ...
}
// 获取vnode类型
const shapeFlag = isString(type)
? 1 /* ELEMENT */
: isSuspense(type)
? 128 /* SUSPENSE */
: isTeleport(type)
? 64 /* TELEPORT */
: isObject(type)
? 4 /* STATEFUL_COMPONENT */
: isFunction(type)
? 2 /* FUNCTIONAL_COMPONENT */
: 0;
return createBaseVNode()
}
function createBaseVNode(
type,
props = null,
children = null,
...
) {
// vnode的默认结构
const vnode = {
__v_isVNode: true, // 是否为vnode
__v_skip: true, // 跳过响应式数据化
type, // 创建vnode的第一个参数
props, // DOM参数
children,
component: null, // 组件实例(instance),通过createComponentInstance创建
shapeFlag, // 类型标记,在patch阶段,通过匹配shapeFlag进行相应的渲染过程
...
};
// 标准化子节点
if (needFullChildrenNormalization) {
normalizeChildren(vnode, children);
}
// 收集动态子代节点或子代block到父级block tree
if (isBlockTreeEnabled > 0 &&
!isBlockNode &&
currentBlock &&
(vnode.patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) &&
vnode.patchFlag !== 32 /* HYDRATE_EVENTS */) {
currentBlock.push(vnode);
}
return vnode;
}
通过上面的代码,我们可以总结一下,创建vnode
阶段都做了什么:
vnode
的类型shapeFlag
,即根组件对应的vnode
类型(type即为根组件rootComponent
,此时根组件为对象格式,所以shapeFlag
即为4)block
到父级block tree
(这里便是vue3
引入的新概念:block tree
,篇幅有限,本文就不展开陈述了)这里,我们可以打印查看一下此时根组件对应的vnode
结构:
vnode结构
通过createVNode
获取到根组件对应的vnode
,然后执行render
方法,而这里的render
函数便是baseCreateRenderer
通过闭包缓存的render
函数:
// 实际调用的render方法即为baseCreateRenderer方法中缓存的render方法
function baseCreateRenderer() {
const render = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
// 卸载组件
unmount()
}
} else {
// 正常挂载
patch(container._vnode || null, vnode, container)
}
}
}
vnode
为null
&存在老的vnode
,则进行卸载组件vnode
进行patch
⚠️:接下来,整个渲染过程将会在
baseCreateRenderer
这个核心函数的内敛函数中执行~
接下来,我们来看下render
过程中的patch
函数的实现:
const patch = (
n1, // 旧的vnode
n2, // 新的vnode
container, // 挂载的容器
...
) => {
// ...
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
// 处理文本
processText(n1, n2, container, anchor)
break
case Comment:
// 注释节点
processCommentNode(n1, n2, container, anchor)
break
case Static:
// 静态节点
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
}
break
case Fragment:
// fragment节点
processFragment(n1, n2, container, ...)
break
default:
if (shapeFlag & 1 /* ELEMENT */) { // 处理DOM元素
processElement(n1, n2, container, ...);
}
else if (shapeFlag & 6 /* COMPONENT */) { // 处理组件
processComponent(n1, n2, container, ...);
}
else if (shapeFlag & 64 /* TELEPORT */) {
type.process(n1, n2, container, ...);
}
else if (shapeFlag & 128 /* SUSPENSE */) {
type.process(n1, n2, container, ...);
}
}
}
分析patch
函数,我们会发现patch
函数会通过判断type
和shapeFlag
的不同来走不同的处理逻辑,今天我们主要分析组件类型和普通DOM元素的处理。
初始化渲染时,type
为object
并且shapeFlag
对应的值为4
(位运算4 & 6),即对应processComponent
组件的处理方法:
const processComponent = (n1, n2, container, ...) => {
if (n1 == null) {
if (n2.shapeFlag & 512 /* COMPONENT_KEPT_ALIVE */) {
// 激活组件(已缓存的组件)
parentComponent.ctx.activate(n2, container, ...);
}
else {
// 挂载组件
mountComponent(n2, container, ...);
}
}
else {
// 更新组件
updateComponent(n1, n2, optimized);
}
};
如果n1
为null
,则执行挂载组件;否则更新组件。
接下来我们继续看挂载组件的mountComponent
函数的实现:
const mountComponent = (initialVNode, container, ...) => {
// 1. 创建组件实例
const instance = (
// 这个时候就把组件实例挂载到了组件vnode的component属性上了
initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense)
);
// 2. 设置组件实例
setupComponent(instance);
// 3. 设置并运行带有副作用的渲染函数
setupRenderEffect(instance, initialVNode, container,...);
};
省略掉无关主流程的代码后,可以看到,mountComponent
函数主要做了三件事:
1 . 创建组件实例
function createComponentInstance(vnode, parent, suspense) {
const type = vnode.type;
// 绑定应用上下文
const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
// 组件实例的默认值
const instance = {
uid: uid$1++, //组件唯一id
vnode, // 当前组件的vnode
type, // vnode节点类型
parent, // 父组件的实例instance
appContext, // 应用上下文
root: null, // 根实例
next: null, // 当前组件mounted时,为null,将设置为instance.vnode,下次update时,将执行updateComponentPreRender
subTree: null, // 组件的渲染vnode,由组件的render函数生成,创建后同步
update: null, // 组件内容挂载或更新到视图的执行回调,创建后同步
scope: new EffectScope(true /* detached */),
render: null, // 组件的render函数,在setupStatefulComponent阶段赋值
proxy: null, // 是一个proxy代理ctx字段,内部使用this时,指向它
// local resovled assets
// resolved props and emits options
// emit
// props default value
// inheritAttrs
// state
// suspense related
// lifecycle hooks
};
{
instance.ctx = createDevRenderContext(instance);
}
instance.root = parent ? parent.root : instance;
instance.emit = emit.bind(null, instance);
return instance;
}
createComponentInstance
函数主要是初始化组件实例并返回,打印查看下根组件对应的instance
内容:
instance内容
2 . 设置组件实例
function setupComponent(instance, isSSR = false) {
const { props, children } = instance.vnode;
// 判断是否为状态组件
const isStateful = isStatefulComponent(instance);
// 初始化组件属性、slots
initProps(instance, props, isStateful, isSSR);
initSlots(instance, children);
// 当状态组件时,挂载setup信息
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined;
return setupResult;
}
setupComponent
的逻辑也很简单,首先初始化组件props
和slots
挂载到组件实例instance
上,然后根据组件类型vnode.shapeFlag===4
,判断是否挂载setup
信息(也就是vue3的composition api)。
function setupStatefulComponent(instance, isSSR) {
const Component = instance.type;
// 创建渲染上下文的属性访问缓存
instance.accessCache = Object.create(null);
// 创建渲染上下文代理
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));
const { setup } = Component;
// 判断组件是否存在setup
if (setup) {
// 判断setup是否有参数,有的话,创建setup上下文并挂载组件实例
// 例如:setup(props) => {}
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null);
// 执行setup函数
const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [shallowReadonly(instance.props) , setupContext]);
handleSetupResult(instance, setupResult, isSSR);
}
else {
finishComponentSetup(instance, isSSR);
}
}
判断组件是否设置了setup
函数:
setup
函数,则执行setup
函数,并判断其返回值的类型。若返回值类型为函数时,则设置组件实例render
的值为setupResult
,否则作为组件实例setupState
的值function handleSetupResult(instance, setupResult, isSSR) {
// 判断setup返回值类型
if (isFunction(setupResult)) {
// 返回值为函数时,则当作组件实例的render方法
instance.render = setupResult;
}
else if (isObject(setupResult)) {
// 返回值为对象时,则当作组件实例的setupState
instance.setupState = proxyRefs(setupResult);
}
else if (setupResult !== undefined) {
warn$1(`setup() should return an object. Received: ${setupResult === null ? 'null' : typeof setupResult}`);
}
finishComponentSetup(instance, isSSR);
}
2 . 设置组件实例的render
方法,分析finishComponentSetup
函数,render
函数有三种设置方式:
3 . 若setup
返回值为函数类型,则instance.render = setupResult
4 . 若组件存在render
方法,则instance.render = component.render
5 . 若组件存在template
模板,则instance.render = compile(template)
组件实例的
render
优化级:instance.render = setup() || component.render || compile(template)
function finishComponentSetup(instance, ...) {
const Component = instance.type;
// 绑定render方法到组件实例上
if (!instance.render) {
if (compile && !Component.render) {
const template = Component.template;
if (template) {
// 通过编译器编译template,生成render函数
Component.render = compile(template, ...);
}
}
instance.render = (Component.render || NOOP);
}
// support for 2.x options
...
}
设置完组件后,我们可以再查看下instance
的内容有发生什么变化:
设置组件后的instance
这个时候组件实例instance
的data
、proxy
、render
、setupState
已经绑定上了初始值。
3 . 设置并运行带有副作用的渲染函数
const setupRenderEffect = (instance, initialVNode, container, ...) => {
// 创建响应式的副作用函数
const componentUpdateFn = () => {
// 首次渲染
if (!instance.isMounted) {
// 渲染组件生成子树vnode
const subTree = (instance.subTree = renderComponentRoot(instance));
patch(null, subTree, container, ...);
initialVNode.el = subTree.el;
instance.isMounted = true;
}
else {
// 更新
}
};
// 创建渲染effcet
const effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(instance.update),
instance.scope // track it in component's effect scope
);
const update = (instance.update = effect.run.bind(effect));
update.id = instance.uid;
update();
};
接下来继续执行setupRenderEffect
函数,首先会创建渲染effect
(响应式系统还包括其他副作用:computed effect、watch effect),并绑定副作用执行函数到组件实例的update
属性上(更新流程会再次触发update
函数),并立即执行update
函数,触发首次更新。
function renderComponentRoot(instance) {
const { proxy, withProxy, render, ... } = instance;
let result;
try {
const proxyToUse = withProxy || proxy;
// 执行实例的render方法,返回vnode,然后再标准化vnode
// 执行render方法时,会调用proxyToUse,即会触发PublicInstanceProxyHandlers的get
result = normalizeVNode(render.call(proxyToUse, proxyToUse, ...));
}
return result;
}
此时,renderComponentRoot
函数会执行实例的render
方法,即setupComponent
阶段绑定在实例render
方法上的函数,同时标准化render
返回的vnode
并返回,作为子树vnode
。
同样我们可以打印查看一下子树vnode
的内容:
sub tree
此时,可能有些同学开始疑惑了,为什么会有两颗vnode
树呢?这两颗vnode
树又有什么区别呢?
1 . initialVNode
initialVNode
就是组件的vnode
,即描述整个组件对象的,组件vnode
会定义一些和组件相关的属性:data
、props
、生命周期
等。通过渲染组件vnode
,生成子树vnode
。
2 . sub tree
子树vnode
是通过组件vnode
的render
方法生成的,其实也就是对组件模板template
的描述,即真正要渲染到浏览器的DOM vnode
。
生成subTree
后,接下来就继续通过patch
方法,把subTree
节点挂载到container
上。接下来,我们继续往下分析,大家可以看下上面subTree
的截图:subTree
的type
值为Fragment
,回忆下patch
方法的实现:
const patch = (
n1, // 旧的vnode
n2, // 新的vnode
container, // 挂载的容器
...
) => {
const { type, ref, shapeFlag } = n2
switch (type) {
case Fragment:
// fragment节点
processFragment(n1, n2, container, ...)
break
default:
// ...
}
}
Fragment
也就是vue3
提到的新特性之一,在vue2
中,是不支持多根节点组件,而vue3
则是正式支持的。细想一下,其实还是单个根节点组件,只是vue3
的底层用Fragment
包裹了一层。我们再看下processFragment
的实现:
const processFragment = (n1, n2, container, ...) => {
// 创建碎片开始、结束的文本节点
const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''));
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''));
if (n1 == null) {
hostInsert(fragmentStartAnchor, container, anchor);
hostInsert(fragmentEndAnchor, container, anchor);
// 挂载子节点数组
mountChildren(n2.children, container, ...);
} else {
// 更新
}
};
接下来继续挂载子节点数组:
const mountChildren = (children, container, ...) => {
for (let i = start; i < children.length; i++) {
const child = (children[i] = optimized
? cloneIfMounted(children[i])
: normalizeVNode(children[i]));
patch(null, child, container, ...);
}
};
遍历子节点,patch
每个子节点,根据child
节点的type
递归处理。接下来,我们主要看下type
为ELEMENT
类型的DOM
元素,即processElement
:
const processElement = (n1, n2, container, ...) => {
if (n1 == null) {
// 挂载DOM元素
mountElement(n2, container,...)
} else {
// 更新
}
}
const mountElement = (vnode, container, ...) => {
let el;
let vnodeHook;
const { type, props, shapeFlag, ... } = vnode;
{
// 创建DOM节点,并绑定到当前vnode的el上
el = vnode.el = hostCreateElement(vnode.type, ...);
}
// 插入父级节点
hostInsert(el, container, anchor);
};
创建DOM
节点,并挂载到vnode.el
上,然后把DOM
节点挂载到container
中,继续递归其他vnode
的处理,最后挂载整个vnode
到浏览器视图中,至此完成vue3
的首次渲染整个流程。mountElement
方法中提到到hostCreateElement
、hostInsert
也就是在最开始创建渲染器时传入的参数对应的处理方法,也就完成整个跨平台的初次渲染流程。
初次渲染流程
分析完vue3
首次渲染的整个流程后,那么在数据更新后,vue3
又是怎么更新渲染呢?接下来分析更新流程阶段就要涉及到vue3
的响应式系统的知识了(由于篇幅有限,我们不会展开更多响应式的知识,大家可以持续关注我们的公众号,期待后续篇章的更加详细的分析)。
回忆下在首次渲染时的设置组件实例setupComponent
阶段会创建渲染上下文代理,而在生成subTree
阶段,会通过renderComponentRoot
函数,执行组件vnode
的render
方法,同时会触发渲染上下文代理的PublicInstanceProxyHandlers
的get
,从而实现依赖收集。
function setupStatefulComponent(instance, isSSR) {
...
// 创建渲染上下文代理
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));
}
function renderComponentRoot(instance) {
const proxyToUse = withProxy || proxy;
// 执行render方法时,会调用proxyToUse,即会触发PublicInstanceProxyHandlers的get
result = normalizeVNode(
render.call(proxyToUse, proxyToUse, ...)
);
return result;
}
我们可以查看下此时组件vnode
的render
方法的内容:
render
或者打印查看render
方法内容:
(function anonymous() {
const _Vue = Vue
const { createVNode: _createVNode, createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, "static node", -1 /* HOISTED */)
const _hoisted_2 = ["onClick"]
return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, createVNode: _createVNode, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
const _component_item = _resolveComponent("item")
return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_1,
_createElementVNode("div", null, _toDisplayString(title), 1 /* TEXT */),
_createElementVNode("button", { onClick: add }, "click", 8 /* PROPS */, _hoisted_2),
_createVNode(_component_item, { msg: title }, null, 8 /* PROPS */, ["msg"])
], 64 /* STABLE_FRAGMENT */))
}
}
})
仔细观察render
的第一个参数_ctx
,即传入的渲染上下文代理proxy
,当访问title
字段时,就会触发PublicInstanceProxyHandlers
的get
方法,那PublicInstanceProxyHandlers
的逻辑又是怎么呢?
// 代理渲染上下文的handler实现
const PublicInstanceProxyHandlers = {
get({ _: instance }, key) {
const { ctx, setupState, data, props, accessCache, type, appContext } = instance;
let normalizedProps;
// key值不以$开头的属性
if (key[0] !== '$') {
// 优先从缓存中判断当前属性需要从哪里获取
// 性能优化:缓存属性应该根据哪种类型获取,避免每次都触发hasOwn的开销
const n = accessCache[key];
if (n !== undefined) {
switch (n) {
case 0 /* SETUP */:
return setupState[key];
case 1 /* DATA */:
return data[key];
case 3 /* CONTEXT */:
return ctx[key];
case 2 /* PROPS */:
return props[key];
// default: just fallthrough
}
}
// 获取属性值的顺序:setupState => data => props => ctx => 取值失败
else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
accessCache[key] = 0 /* SETUP */;
return setupState[key];
}
else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
accessCache[key] = 1 /* DATA */;
return data[key];
}
else if (
(normalizedProps = instance.propsOptions[0]) &&
hasOwn(normalizedProps, key)) {
accessCache[key] = 2 /* PROPS */;
return props[key];
}
else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
accessCache[key] = 3 /* CONTEXT */;
return ctx[key];
}
else if (shouldCacheAccess) {
accessCache[key] = 4 /* OTHER */;
}
}
},
set() {},
has() {}
};
接下来我们以key
为title
的例子简单介绍下get
的逻辑:
key
值是否已$
开头,明显title
走否的逻辑accessCache
缓存中是否存在性能优化:缓存属性应该根据哪种类型获取,避免每次都触发hasOwn
的开销
3. 最后再按照顺序获取:setupState => data => props => ctx``PublicInstanceProxyHandlers
的set
和has
的处理逻辑,同样以这个顺序处理
4. 若存在时,先设置缓存accessCache
,再从setupState
中获取title
对应的值
重点来了,当访问setupState.title
时,触发proxy
的get
的流程会有两个阶段:
setupState
对应的proxy
的get
,然后获取title
的值,判断其是否为Ref
?ref.value
,即触发ref
类型的依赖收集流程// 设置组件实例时会设置setupState的代理prxoy
// 设置流程:setupComponent=>setupStatefulComponent=>handleSetupResult
instance.setupState = proxyRefs(setupResult)
export function proxyRefs(objectWithRefs) {
return isReactive(objectWithRefs)
? objectWithRefs
: new Proxy(objectWithRefs, {
get: (target, key, receiver) => {
return unref(Reflect.get(target, key, receiver))
},
set: (target, key, value, receiver) => {}
})
}
export function unref(ref) {
return isRef(ref) ? ref.value : ref
}
2 . 访问ref.value
时,触发ref
的依赖收集。那我们先来分析Vue.ref()
的实现逻辑又是什么呢?
// 调用Vue.ref(0),从而触发createRef的流程
// 省略其他无关代码
function ref(value) {
return createRef(value, false)
}
function createRef(rawValue) {
return new RefImpl(rawValue, false)
}
// ref的实现
class RefImpl {
constructor(value) {
this._rawValue = toRaw(value)
this._value = toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
}
function trackRefValue(ref) {
if (isTracking()) {
if (!ref.dep) {
ref.dep = new Set()
}
// 添加副作用,进行依赖收集
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
分析ref
的实现,会发现当访问ref.value
时,会触发RefImpl
实例的value
方法,从而触发trackRefValue
,进行依赖收集dep.add(activeEffect)
。那这时的activeEffect
又是谁呢?
回忆下setupRenderEffect
阶段的实现:
const setupRenderEffect = (instance, initialVNode, container, ...) => {
// 创建响应式的副作用函数
const componentUpdateFn = () => {};
// 创建渲染effcet
const effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(instance.update),
instance.scope
);
const update = (instance.update = effect.run.bind(effect));
update();
};
// 创建effect类的实现
class ReactiveEffect {
run() {
try {
effectStack.push((activeEffect = this))
// ...
return this.fn()
} finally {}
}
}
当执行update
函数时(即渲染effect
实例的run
方法),从而设置全局activeEffect
为当前渲染effect
,也就是说此时dep.add(activeEffect)
收集的activeEffect
就是这个渲染effect
,从而实现了依赖收集。
我们可以打印一下setupState
的内容,验证一下我们的分析:
setupState
通过截图,我们可以看到此时title
收集的副作用就是渲染effect
,细心的同学就发现了截图中的fn
方法就是componentUpdateFn
函数,执行fn()
继续挂载children
。
依赖收集流程图
分析完依赖收集阶段,我们再看下,vue3
又是如何进行派发更新呢?
当我们点击按钮执行this.title += 1
时,同样会触发PublicInstanceProxyHandlers
的set
方法,而set
的触发顺序同样和get
一致:setupState
=>data
=>其他不允许修改的判断(例如:props
、$开头的保留字段
)
// 代理渲染上下文的handler实现
const PublicInstanceProxyHandlers = {
set({ _: instance }, key, value) {
const { data, setupState, ctx } = instance;
// 1. 更新setupState的属性值
if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
setupState[key] = value;
}
// 2. 更新data的属性值
else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
data[key] = value;
}
// ...
return true;
}
};
设置setupState[key]
从而继续触发setupState
的set
方法:
const shallowUnwrapHandlers: ProxyHandler<any> = {
set: (target, key, value, receiver) => {
const oldValue = target[key]
// oldValue为ref类型&value不是ref时执行
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
} else {
// 否则,直接返回
return Reflect.set(target, key, value, receiver)
}
}
}
当设置oldValue.value
的值时继续触发ref
的set
方法,判断ref
是否存在dep
,执行副作用effect.run()
,从而派发更新,完成更新流程。
class RefImpl{
set value(newVal) {
newVal = this._shallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
}
// 判断ref是否存在依赖,从而派发更新
function triggerRefValue(ref) {
ref = toRaw(ref)
if (ref.dep) {
triggerEffects(ref.dep)
}
}
// 派发更新
function triggerEffects(dep) {
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) {
// 执行副作用
effect.run()
}
}
}
派发更新流程图
综上,我们分析完了vue3
的整个渲染过程和更新流程,当然我们只是从主要的渲染流程分析,完整的渲染过程的复杂度不止于此,比如基于block tree
的优化实现,patch
阶段的diff
优化以及在更新流程中的响应式阶段的优化又是怎样的等细节。感兴趣的同学可以自己分析学习或者关注我们公众号的后续分析。
本文的初衷便是给大家提供分析vue3整个渲染过程的轮廓,有了整体的印象,再去分析了解更加细节的点的时候,也会更有思路和方向。
最后,附一张完整的渲染流程图,与君共享。
整个渲染流程图
[1] Vue.js 3.0源码地址: https://github.com/vuejs/vue-next
[2]细说 Vue.js 3.2 关于响应式部分的优化: https://zhuanlan.zhihu.com/p/401416696
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8