Maison >interface Web >Voir.js >Une analyse approfondie du système de rendu dans vue3
Cet article vous donnera une analyse approfondie du système de rendu dans vue3. J'espère qu'il vous sera utile !
En matière de marathon, tout le monde sait que le marathon est l'épreuve d'athlétisme la plus longue au monde (42,195 kilomètres au total). C'est le sport le plus exigeant physiquement, et c'est aussi l'épreuve la plus exigeante. teste sa volonté. Si vous pouvez persister à courir tout le marathon, alors dans quoi d'autre ne pouvez-vous pas persister ?
De même, lorsque nous étudions le code source de certaines excellentes bibliothèques de classes, l'ensemble du processus est ennuyeux, tout comme participer à un marathon au niveau du code source. Alors aujourd'hui, je vais vous emmener organiser un marathon sur la version d'analyse de code source du système de rendu Vue.js 3.0 Pendant tout le processus, l'auteur vous fournira également une station d'approvisionnement (organigramme) pour votre. commodité. .
Réflexion
Avant de commencer l'article d'aujourd'hui, vous pouvez réfléchir à :
Comment le fichier vue
est converti en un nœud DOM
et rendu vers le navigateur ? 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
文件的方式进行源码调试分析。
vue3
源码下载
# 源码地址(推荐ssh方式下载) https://github.com/vuejs/vue-next # 或者下载笔者做笔记用的版本 https://github.com/AsyncGuo/vue-next/tree/vue3_notes
生成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
启动开发环境
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>
创建应用
从上面的测试代码,我们会发现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
首先通过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移除渲染相关的代码,大大减少包的体积。
createRenderer
通过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
的具体实现:
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
。
这里有个值得注意的点:
processus de mise à jour🎜 ? 🎜🎜app
对象上的use
、mixin
、component
和directive
方法都返回了app
Lorsque les données sont mises à jour, à quoi ressemble l'ensemble duvuejs
comporte deux étapes : 🎜temps de compilation🎜 et 🎜runtime🎜. 🎜🎜🎜🎜Pendant la compilation🎜🎜🎜🎜Les fichiers.vue
que nous écrivons habituellement pendant le développement ne peuvent pas être exécutés directement dans le navigateur, donc pendant la phase dewebpack
🎜compilation🎜 , vous devez compiler le fichier.vue
viavue-loader
pour générer le codejs
correspondant, et levue
composant correspondant au modèle code>template sera converti en unefonction de rendu
par le 🎜compilateur🎜. 🎜🎜🎜🎜Runtime🎜🎜🎜🎜Ensuite, lorsque le code compilé est réellement exécuté dans le navigateur, lafonction de rendu
sera exécutée etVNode
sera renvoyé, c'est-à-dire Le soi-disantVirtual DOM
transforme enfin leVNode
en un véritableNœud DOM
. 🎜🎜🎜🎜Comprendre Après avoir terminé l'idée du rendu du composantvue
, commençons par le code source de 🎜Vue.js 3.0🎜 (ci-après dénommévue3
) et examinons -compréhension approfondie devue
Quel est le 🎜processus de rendu complet🎜 du composant code> ? 🎜🎜🎜🎜Préparation🎜🎜🎜🎜Cet article analyse principalement le système de rendu de🎜🎜🎜Téléchargement du code sourcevue3
Afin de faciliter le débogage, nous introduisons directement le fichiervue.js
. . Analyse du débogage du code source. 🎜vue3
🎜// 一直use一直爽 createApp(App).use(Router).use(Vuex).component('component',{}).mount("#app")🎜🎜🎜Générer le fichiervue.global.js
🎜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; });🎜🎜🎜Démarrer l'environnement de développement🎜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; }; }🎜🎜🎜Tester le code🎜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() }🎜🎜🎜Créer l'application🎜🎜🎜🎜 à partir de Avec le code de test ci-dessus, nous constaterons que les méthodes de montage devue3
etvue2
sont différentes.vue3
utilise la fonction d'entrée decreateApp.
Créez l'application. Examinons ensuite l'implémentation spécifique decreateApp
: 🎜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; }🎜🎜ensureRenderer🎜🎜🎜🎜Premier passageensureRenderer code> Pour créer un moteur de rendu côté web, regardons l'implémentation spécifique : 🎜<pre class="brush:js;toolbar:false;">// 实际调用的render方法即为baseCreateRenderer方法中缓存的render方法 function baseCreateRenderer() { const render = (vnode, container) => { if (vnode == null) { if (container._vnode) { // 卸载组件 unmount() } } else { // 正常挂载 patch(container._vnode || null, vnode, container) } } }</pre><blockquote>🎜Comme vous pouvez le voir ici, en retardant la création du moteur de rendu, alors qu'on s'appuie uniquement sur des packages responsives, on peut déplacez-le via 🎜tree-shaking🎜 À l'exception du code lié au rendu, la taille du package est considérablement réduite. 🎜</blockquote>🎜🎜<span style="font-size: 16px;">createRenderer🎜🎜🎜🎜Comme vous pouvez le voir sur <code>ensureRenderer
, la véritable entrée est cecreateRenderer code >Méthode : 🎜<pre class="brush:js;toolbar:false;">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, ...); } } }</pre>🎜 Ensuite, ignorons l'implémentation de ces fonctions introverties (nous l'analyserons en détail lorsqu'elles seront utilisées dans le processus de rendu ultérieur), et examinons l'implémentation spécifique de <code>createAppAPI
: 🎜 🎜🎜 createAppAPI🎜🎜🎜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); } };🎜On peut voir que la fonctioncreateApp
renvoyée parcreateAppAPI
est la vraie 🎜 créer une application🎜 Entrée. DanscreateApp
, le contexte de l'applicationvue
sera créé, tandis queapp
sera initialisé et le contexte de l'application sera lié auapp
activée, et revient enfin àapp
. 🎜🎜Il y a quelque chose à noter ici :use
,mixin
,component
sur l'objetapp
Les deux les méthodesdirective
etdirective
renvoient l'instance d'applicationapp
, et les développeurs peuvent 🎜chaîner les appels🎜. 🎜// 一直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教程》】
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!