Home > Article > Web Front-end > An in-depth analysis of the rendering system in vue3
This article will give you an in-depth analysis of the rendering system in vue3. I hope it will be helpful to you!
When it comes to marathon, everyone knows that marathon is the longest track and field event in the world (the total distance is 42.195 kilometers). It is the most physically demanding and the most tiring of all sports. A project to practice one's will. If you can persist in running the entire marathon, then what else can't you persist in?
Similarly, when we study the source code of some excellent class libraries, the whole process is boring, just like participating in a source code-level marathon. So today I will take you to run a marathon about the source code analysis version of the Vue.js 3.0 rendering system. During the whole process, the author will also provide you with a supply station (Flowchart) for everyone’s convenience.
Thinking
Before starting today’s article, you can think about it:
# How are ##vue files converted into
DOM nodes and rendered to the browser?
update process look like?
vuejs has two stages:
compile time and runtime.
When compiling
The.vue file we write during normal development cannot be run directly in the browser , so in the
webpack compilation phase, the .vue
file needs to be compiled through
vue-loader to generate the corresponding
jsCode, the
template template corresponding to the
vue component will be
compiler converted into a render function.
Runtime
Next, when the compiled code actually runs in the browser, therender function# will be executed. ##And return VNode
, which is the so-called virtual DOM
, and finally render VNode
into a real DOM node
.
After understanding the idea of
vue component rendering, let us start from Vue.js 3.0
(hereinafter referred to as ## Starting from the source code of #vue3), let’s have an in-depth understanding of the whole rendering process of the
vue component.
This article mainly analyzes the rendering system of
vue3. In order to facilitate debugging, we directly pass Introduce thevue.js
file for source code debugging and analysis.
# 源码地址(推荐ssh方式下载) https://github.com/vuejs/vue-next # 或者下载笔者做笔记用的版本 https://github.com/AsyncGuo/vue-next/tree/vue3_notes
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
npm run serve
<!-- 调试代码目录:/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>
From the above test code, we will find that vue3
and # The mounting method of ##vue2 is different. vue3
creates applications through the entry function createApp
. Next, let’s take a look at the specific implementation of createApp
: <pre class="brush:js;toolbar:false;">// 入口文件: /vue-next/packages/runtime-dom/src/index.ts
const createApp = ((...args) => {
console.log(&#39;createApp入参:&#39;, ...args);
// 创建应用
const app = ensureRenderer().createApp(...args);
const { mount } = app;
// 重写mount
app.mount = (containerOrSelector) => {
// ...
};
return app;
});</pre>
ensureRenderer
First pass ensureRendererCreate the renderer on the web side, let's take a look at the specific implementation:
// 更新属性的方法 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))); }
It can be seen here that by delaying the creation of the renderer, when we only rely on the responsive package, we can pass
tree-shaking
createRenderer
It can be seen from ensureRenderer that the real entrance is this
createRendererMethod: <pre class="brush:js;toolbar:false;">// /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)
}
}</pre>
Next, let’s skip the implementation of these introverted functions (we will analyze it in detail when used in the subsequent rendering process) and look at the specific implementation of createAppAPI
:
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;
};
}
It can be seen that the createApp function returned by
is Reallycreate the entrance to the application
. In createApp
, the vue application context will be created, app
will be initialized at the same time, and the application context will be bound to the app
instance, and finally return app
. There is something worth noting here:
use
mixin,
component
on theapp
object anddirective
methods both returnapp
application instances, developers canchain call
.
// 一直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
,可能更多人会和高性能联想到一起,误以为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
阶段都做了什么:
规范化class & style(例如:class=[]、class={}、style=[]等格式)
标记vnode
的类型shapeFlag
,即根组件对应的vnode
类型(type即为根组件rootComponent
,此时根组件为对象格式,所以shapeFlag
即为4)
标准化子节点(初始化时,children为空)
收集动态子代节点或子代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到容器上,以便后续更新阶段通过新旧vnode
进行patch
⚠️:接下来,整个渲染过程将会在
baseCreateRenderer
这个核心函数的内敛函数中执行~
patch
接下来,我们来看下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元素的处理。
processComponent
初始化渲染时,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
接下来我们继续看挂载组件的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
函数主要做了三件事:
创建组件实例
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
内容:
设置组件实例
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); }
设置组件实例的render
方法,分析finishComponentSetup
函数,render
函数有三种设置方式:
若setup
返回值为函数类型,则instance.render = setupResult
若组件存在render
方法,则instance.render = component.render
若组件存在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
的data
、proxy
、render
、setupState
已经绑定上了初始值。
设置并运行带有副作用的渲染函数
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
的内容:
此时,可能有些同学开始疑惑了,为什么会有两颗vnode
树呢?这两颗vnode
树又有什么区别呢?
initialVNode
initialVNode
就是组件的vnode
,即描述整个组件对象的,组件vnode
会定义一些和组件相关的属性:data
、props
、生命周期
等。通过渲染组件vnode
,生成子树vnode
。
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
方法内容:
(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**
的开销
最后再按照顺序获取:setupState => data => props => ctx
PublicInstanceProxyHandlers
的set
和has
的处理逻辑,同样以这个顺序处理
若存在时,先设置缓存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 }
访问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
的内容,验证一下我们的分析:
通过截图,我们可以看到此时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整个渲染过程的轮廓,有了整体的印象,再去分析了解更加细节的点的时候,也会更有思路和方向。
最后,附一张完整的渲染流程图,与君共享。
【相关推荐:《vue.js教程》】
The above is the detailed content of An in-depth analysis of the rendering system in vue3. For more information, please follow other related articles on the PHP Chinese website!