Après avoir lu les articles précédents, je pense que tout le monde a Vue3
une compréhension relativement approfondie du principe responsive. Mais il ne suffit pas de saisir les principes de réactivité, je pense Vue3
qu'il y a 3 piliers.
- L'un est le système réactif mentionné ci-dessus;
- Le second est le processus de conversion du code que nous avons écrit
template
en un nœudjsx
virtuel Ce processus est implémenté par la fonction fournie par .compiler-dom
compiler-core
compiler
render
render
- La troisième est la fonction qui convertit le Node virtuel en Node réel
render
. Dans les articles suivants, j'appellerai cetterender
fonction le rendu render ;
Noter:
- Puisque le code source analysé dans cet article est
runtime-dom
leruntime-core
contenu de , sauf indication contraire, lesrender
fonctions mentionnées dans cet article font toutes référence à des fonctions de rendurender
.- Le nœud virtuel mentionné ci-dessus est souvent appelé le DOM virtuel Les deux ont la même signification, et il en va de même pour le nœud réel et le DOM réel .
En fait, la description ci-dessus de ces trois piliers a très bien résumé les Vue3
fonctions essentielles de l'ensemble du cadre. Cet article commencera par un bref cas et présentera l' createApp
implémentation de la fonction. Au cours de cette analyse, il parlera de la relation de coopération de code entre et la runtime-dom
fonction , ainsi que de la logique d'implémentation spécifique de la fonction et de la logique d'implémentation se terminera par l'appel de la fonction. Les détails d'implémentation de la fonction seront analysés étape par étape dans les articles suivants.runtime-core
createApp
render
render
Cas - initialiser une application Vue3
Dans le développement réel, nous utilisons généralement les éléments suivants pour initialiser une Vue
application :
// 代码片段1
import { createApp } from 'vue'
// import the root component App from a single-file component.
import App from './App.vue'
const vueApp = createApp(App)
vueApp.mount('#app')
复制代码
Juste quelques lignes de code, il y a en fait beaucoup de travail à faire, car la première chose à faire est de convertir App.vue
le contenu en un Node virtuel . Une fois la compilation terminée, le paramètre passé à la fonction dans le fragment de code 1 est un objet composant. C'est un objet. L'objet a une méthode . La fonction de cette fonction est de convertir l'objet composant en un Node virtuel , puis de convertir le Node virtuel en un Node réel et de le monter sous l' élément pointé . Concernant le processus de compilation, il sera expliqué en détail dans l'analyse et les articles à venir, qui ne seront pas mentionnés dans cet article.createApp
App
vueApp
mount
App
#app
DOM
compiler-dom
compiler-core
Écrire du code qui ne nécessite ni compilation ni transformation
Pour comprendre le fonctionnement normal du programme, l'utilisation de nœud virtuel est indispensable . Nous transformons le programme sous la forme suivante :
<!--代码片段2-->
<html>
<body>
<div id="app1"></div>
</body>
<script src="./runtime-dom.global.js"></script>
<script>
const { createApp, h } = VueRuntimeDOM
const RootComponent = {
render(){
return h('div','杨艺韬喜欢研究源码')
}
}
createApp(RootComponent).mount("#app1")
</script>
</html>
复制代码
最明显的变化就是我们在直接定义组件对象,而不需要通过编译把App.vue
文件的内容转化成组件对象,同时在组件对象中手写了一个编译render
函数,也不需要继续编译把template
转化成编译render
函数来。注意这里这里涉及两个编译过程,一个是.vue
文件转化成组件对象的编译过程,另一个编译过程是将组件对象中所涉及的template
转化成编译render
函数,这两者都暂时不提,后续的文章中都会详细介绍。
事实上,代码片段2中RootComponent
对象的编译render
函数会在某个时机执行,具体在哪里执行,我们在本文分析createApp
内部实现的时候进行解释。
初识编译编译render函数
但是我们知道Vue3
一个重要的特点是可以自由控制哪些数据具备响应式的能力,这就离不开我们的setup
方法。我们把代码片段2进一步转化成如下形式:
<!--代码片段3-->
<html>
<body>
<div id="app" haha="5"></div>
</body>
<script src="./runtime-dom.global.js"></script>
<script>
const { createApp, h, reactive } = VueRuntimeDOM
const RootComponent = {
setup(props, context){
let relativeData = reactive({
name:'杨艺韬',
age: 60
})
let agePlus = ()=>{
relativeData.age++
}
return {relativeData, agePlus}
},
render(proxy){
return h('div', {
onClick: proxy.agePlus,
innerHTML:`${proxy.relativeData.name}已经${proxy.relativeData.age}岁了,点击这里继续增加年龄`
} )
}
}
createApp(RootComponent).mount("#app")
</script>
</html>
复制代码
我们从代码片段3中可以发现,setup
方法的返回值,可以在编译render
函数中通过prxoy
参数获取到。大家可能会觉得这种写法有些冗余,确实是这样。因为这里的编译render
函数本身就是Vue2
的产物。在Vue3
中我们可以直接这样写,代码变化如下:
<!--代码片段4-->
<html>
<body>
<div id="app" haha="5"></div>
</body>
<script src="./runtime-dom.global.js"></script>
<script>
const { createApp, h, reactive } = VueRuntimeDOM
const RootComponent = {
setup(props, context){
let relativeData = reactive({
name:'杨艺韬',
age: 60
})
let agePlus = ()=>{
relativeData.age++
}
return ()=>h('div', {
onClick: agePlus,
innerHTML:`${relativeData.name}已经${relativeData.age}岁了,点击这里继续增加年龄`
} )
}
}
createApp(RootComponent).mount("#app")
</script>
</html>
复制代码
在实际开发中,一般来说setup
的返回值,要么是一个对象,要么是一个返回jsx
的函数,这里的jsx
代码会在编译阶段转化成类似代码片段4的形式,这种情况下这些代码所在文件格式是tsx
。而如果是返回对象,通常是在.vue
文件中编写了template
代码。这两种形式都可以采用,但需要知道的是template
会有编译时的静态分析,提升性能,而jsx
则更加灵活。
小结
上面我们简要介绍了在Vue3
中一些简单的组件编码形式,理解了传递给函数createApp
的组件对象在实际工作中是如何发挥基础作用的。下面我们就进入createApp
函数的实现。在分析createApp
的时候,有时候会再次回顾上文提到的一些运行效果,让这些运行效果和具体源码对照起来,更容易加深对Vue3
的理解。
createApp的代码实现
createApp的外包装
我们先将视线移到core/packages/runtime-dom
目录下的index.ts
文件中去,会发现对外暴露了很多API
,但是没关系,我们先看我们今天的主角createApp
,其他暂时忽略,将来再单独介绍其他暴露的API
的具体含义:
// 代码片段5
// 此处省略若干代码...
import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp'
// 此处省略若干代码...
const rendererOptions = extend({ patchProp }, nodeOps)
// 此处省略若干代码...
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
// 此处省略若干代码...
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
// 此处省略若干代码...
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 此处省略若干代码...
const proxy = mount(container, false, container instanceof SVGElement)
// 此处省略若干代码...
return proxy
}
return app
}) as CreateAppFunction<Element>
// 此处省略若干代码...
复制代码
我们将代码做了一系列的精简后,发现三个重点:
- 真正的
Vue
应用对象,是执行ensureRenderer().createApp(...args)
创建的,而ensureRenderer
函数内部调用了createRenderer
函数。这个createRenderer
函数位于runtime-core
中; - 在调用函数
createRender
函数的时候,传入了参数rendererOptions
,这些参数是一些操作DOM
节点和DOM
节点属性的具体方法。 - 创建了
Vue
应用对象app
后,重写了其mount
方法,重写的mount
方法内部,做了些跟浏览器强相关的操作,比如清空DOM
节点。接着又调用了重写前的mount
方法进行挂载操作。
总之,runtime-dom
真正提供的能力是操作浏览器平台DOM
节点。而跟平台无关的动作全部在runtime-core
完成,这是有些朋友可能会疑惑,怎么就跟平台无关了,我们不是传递了操作具体DOM
节点的方法rendererOptions
给了runtime-core
暴露的方法了吗。正是因为操作真实浏览器DOM
的方法是通过参数传递过去的,所以这里也可以是其他平台操作节点的具体方法。也就是说,runtime-core
只知道需要对某些节点进行增添、修改、删除,但这些节点是浏览器DOM
还是其他平台的节点都不会关系,参数传来的是什么,runtime-core
就调用什么,这就是所谓的和平台无关,其实在实际编码中完全可以借鉴这种分层的编码思想。
createRenderer
接下来我们就进入core/packages/runtime-core/src/render.ts
中的createRenderer
函数:
// 代码片段6
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
复制代码
我们接着进入函数baseCreateRenderer
,该函数2000多行代码,我对其进行了大量精简:
// 代码片段7
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// 此处省略2000行左右的代码...
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
复制代码
代码片段7中省略了绝大部分代码,我只留下了返回值。实际上,函数baseCreateRenderer
可以说是整个runtime-core
的核心,因为所有的关于虚拟Node
转化成真实Node
的逻辑都包括在了该函数中,常常提起的diff
算法也包括在其中。本文暂时不会分析baseCreateRenderer
函数内部的逻辑,贴合主题,只关注这里的createApp
对应的值createAppAPI(render, hydrate)
,实际上createAppAPI(render, hydrate)
返回的是一个函数。这里的createApp
也就是上文代码片段5中const app = ensureRenderer().createApp(...args)
的createApp
。
createAppAPI
我们进入位于core/packages/runtime-core/src/apiCreateApp.ts
中的函数createAppAPI
:
// 代码片段8
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
// 此处省略若干代码...
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
version,
get config() {
return context.config
},
set config(v) {
// 此处省略若干代码...
},
use(plugin: Plugin, ...options: any[]) {
// 此处省略若干代码...
},
mixin(mixin: ComponentOptions) {
// 此处省略若干代码...
},
component(name: string, component?: Component): any {
// 此处省略若干代码...
},
directive(name: string, directive?: Directive) {
// 此处省略若干代码...
},
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
// 此处省略若干代码...
},
unmount() {
// 此处省略若干代码...
},
provide(key, value) {
// 此处省略若干代码...
}
})
// 此处省略若干代码...
return app
}
}
复制代码
从代码片段8中可以看出,createAppAPI
函数返回了一个函数createApp
,而该函数的返回值是一个对象app
,app
其实就是我们创建的Vue
应用,app
上有很多属性和方法,代表了Vue
应用对象所具备的信息和能力。
mount方法
就如代码片段1中所表示的那样,创建一个Vue
应用完成后的第一个操作就是调用mount
方法进行挂载,其他内容我们可以暂时忽略,先关注app
的mount
方法实现:
// 代码片段9
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
vnode.appContext = context
// 此处省略若干代码...
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, isSVG)
}
isMounted = true
app._container = rootContainer
;(rootContainer as any).__vue_app__ = app
// 此处省略若干代码...
return getExposeProxy(vnode.component!) || vnode.component!.proxy
} // 此处省略若干代码...
}
复制代码
代码片段9中省略了很多和开发阶段相关的代码,可以概括为这样几项主要工作:
- 将根组件对象
rootComponent
(代码片段4中的传入的值RootComponent
)转化成虚拟Node; - 调用
render
函数,将这个虚拟Node转化成真实Node并挂载到rootContainer
所指向的元素上。那这里的render
函数来自哪里呢?从代码片段8不难发现,是通过参数传入的,那这个参数从哪里来呢,我们再回到代码片段7发现正是函数baseCreateRenderer
内部声明的render
函数。 - 调用
getExposeProxy
函数得到一个代理对象并返回。
至于如何将组件对象转化成虚拟Node,以及render函数的具体实现,本文都不继续深入,因为这两者都是一个比较大的新的话题,需要新的文章来阐述。下面分析一下这里的getExposeProxy
函数,因为这个函数和我们前面讲的响应式系统相关,而对于响应式系统已经深入掌握过了,理解这个函数应该会比较容易。
getExposeProxy
// 代码片段10
export function getExposeProxy(instance: ComponentInternalInstance) {
if (instance.exposed) {
return (
instance.exposeProxy ||
(instance.exposeProxy = new Proxy(proxyRefs(markRaw(instance.exposed)), {
get(target, key: string) {
if (key in target) {
return target[key]
} else if (key in publicPropertiesMap) {
return publicPropertiesMap[key](instance)
}
}
}))
)
}
}
复制代码
代码片段10的核心就在于这个新创建的Proxy
实例。而这个Proxy
初始化的对象是proxyRefs(markRaw(instance.exposed))
的执行结果。我们先不管instance.exposed
具体是什么含义,但从程序逻辑来看可以这样理解,如果通过instance.exposeProxy
获取数据,只能获取instance.exposed
或publicPropertiesMap
具有的属性,否则就返回undefined
。至于这里为什么先调用markRaw
再调用proxyRefs
,是因为proxyRefs
内部做了条件判断,如果传入的对象本身就是响应式的就直接返回了,所以需要先处理成非响应式的对象。而这里的proxyRefs
是为了访问原始值的响应式对象的值的时候不用再写.value
,这在上一篇文章中已经分析过。
ref的特殊用法
那这个instance.exposed
到底是什么呢?我们先来看看ref
获取子组件的内容的实践应用:
// 代码片段11
<script>
import Child from './Child.vue'
export default {
components: {
Child
},
mounted() {
// this.$refs.child will hold an instance of <Child />
}
}
</script>
<template>
<Child ref="child" />
</template>
复制代码
// 代码片段2,文件名:Child.vue
export default {
expose: ['publicData', 'publicMethod'],
data() {
return {
publicData: 'foo',
privateData: 'bar'
}
},
methods: {
publicMethod() {
/* ... */
},
privateMethod() {
/* ... */
}
}
}
复制代码
关于ref
的这种特殊用法,大家可以在官方文档中查阅出更详细的内容,在这里需要知道,如果子组件给expose
属性设置了值,则父组件只能拿到expose
所声明的这些属性对应的值。这也就是为什么代码片段10中要有这样一个代理对象,反过来我们也知道了保护子组件的内容不被父组件随意访问的机制的实现原理。
总结
本文先抛出一个具体案例,再从createApp
讲起,跟随函数调用栈,提到了编译render
、渲染render
两个函数,分析了createRenderer
、createAppAPI
、mount
、getExposeProxy
等函数实现。到这里大家可以理解创建一个Vue
应用的基本过程。本文为分析渲染render
函数的具体实现打下了基础,关于渲染render
函数的具体实现我将在下一篇文章中正是开始介绍,敬请朋友们期待。
写在最后
读完文章觉得有收获的朋友们,可以做下面几件事情支持:
- 如果
点赞,点在看,转发
可以让文章帮助到更多需要帮助的人; - 如果是微信公众号的作者,可以找我开通
白名单
,转载
我的原创文章;
最后,请朋友们关注我的微信公众号: 杨艺韬
,可以获取我的最新动态。