前言
在之前的几篇文章中,我们介绍了vue3的响应式模块的源码,接下来将介绍vue3dom渲染相关的源码。vue3的每一个模块代码都可以单独打包的,例如前面的相应式模块就在reactive文件夹中。而现在介绍的dom渲染的相关的源码在runtime-core和runtime-dom这两个文件夹中。其中runtime-core存放的是运行时核心实例相关代码(与平台无关),runtime-dom存放的是操作dom的一些API,及事件处理。
入口
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.mount("#app");
以上是通过vite创建一个vue项目的main.ts文件。带着createApp(App)做了什么?app.mount("#app")又做了什么?这两个问题阅读本文。以下仅为源码核心代码
// rendener.ts文件
const render = (vnode, container) => { // 将虚拟节点 转化成真实节点渲染到容器中
// 后续还有更新 patch 包含初次渲染 还包含更新
patch(null, vnode, container);// 后续更新 prevNode nextNode container
}
return {
createApp: createAppAPI(render), // 创建一个api createApp
render
}
import { createVNode } from "./createVNode";
export function createAppAPI(render) {
return (rootComponent, rootProps) => { // rootComponent:我们传入的App,为组件类型,rootProps:createApp方法的第二个参数,表示根prop参数(一般为空)
let isMounted = false;
const app = { // app对象上有很多方法,如:mount, use, mixin,components,unmount,directive等,这里只介绍mount
mount(container) { // container为传入的#app,表示组件渲染到#app的容器中
// 1.创造组件虚拟节点
let vnode = createVNode(rootComponent, rootProps); // h函数
// 2.挂载的核心就是根据传入的组件对象 创造一个组件的虚拟节点 ,在将这个虚拟节点渲染到容器中
render(vnode, container)
if (!isMounted) {
isMounted = true;
}
}
}
return app
}
}
以上可知,createApp(App)会返回一个提供应用上下文的应用实例,该应用实例挂载的整个组件树共享同一个上下文。运行app.mount('#app')实际上会运行createVNode方法,拿传入的rootComponent(跟组件)去创造相应的虚拟dom。接下来讲解createVNode方法。
createVNode
createVNode方法主要功能就是创造虚拟dom,在介绍之前先看看js中的与、或运算
export const enum ShapeFlags {
ELEMENT = 1, // 元素
FUNCTIONAL_COMPONENT = 1 << 1, // 函数式组件 向左移1位 00000010
STATEFUL_COMPONENT = 1 << 2, // 普通组件 向左移2位 00000100
TEXT_CHILDREN = 1 << 3, // 孩子是文本 向左移3位 00001000
ARRAY_CHILDREN = 1 << 4, // 孩子是数组 向左移4位 00010000
SLOTS_CHILDREN = 1 << 5, // 组件插槽 向左移5位 00100000
TELEPORT = 1 << 6, // teleport组件 向左移6位 01000000
SUSPENSE = 1 << 7, // suspense组件 向左移7位 10000000
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT // 组件 00000100 | 00000010 = 00000110
}
// createVNode.ts文件
export function createVNode(type, props, children = null) { // type: 为对象时,表示类型为组件,为字符串时,表示为元素 props:表示要渲染的这个dom上的属性
// 虚拟节点就是 用一个对象来描述信息的
// & |
const shapeFlag = isObject(type) ?
ShapeFlags.COMPONENT :
isString(type) ?
ShapeFlags.ELEMENT :
0
const vnode = { // 跨平台 虚拟dom的格式
__v_isVNode: true, // 标志这个是一个虚拟dom
type, // 代表要渲染的dom的类型
shapeFlag, // 之后渲染dom的类型的标志
props, // 储存渲染的dom上的属性
children,
key: props && props.key, // key值, dom的唯一标识
component: null, // 如果是组件的虚拟节点要保存组件的实例
el: null, // 虚拟节点对应的真实节点
}
if(children){
// 告诉此节点 是什么样的儿子
// 稍后渲染虚拟节点的时候 可以判断儿子是数组 就循环渲染
vnode.shapeFlag = vnode.shapeFlag | (isString(children) ? ShapeFlags.TEXT_CHILDREN:ShapeFlags.ARRAY_CHILDREN)
}
// vnode 就可以描述出来 当前他是一个什么样的节点 儿子是什么样的
return vnode; // createApp(App)
}
以上createVNode方法就是来创建虚拟dom的。虚拟dom创造好了之后,就要渲染真实dom了,接下来我们回到createAppAPI方法中:
export function createAppAPI(render) {
return (rootComponent, rootProps) => {
let isMounted = false;
const app = {
mount(container) {
// 1.创造组件虚拟节点
let vnode = createVNode(rootComponent, rootProps); // h函数
// 2.挂载的核心就是根据传入的组件对象 创造一个组件的虚拟节点 ,在将这个虚拟节点渲染到容器中
render(vnode, container) // 渲染根据传入的App组件,渲染dom
if (!isMounted) {
isMounted = true;
}
}
}
return app
}
}
// render函数
const render = (vnode, container) => { // 将虚拟节点 转化成真实节点渲染到容器中
// 后续还有更新 patch 包含初次渲染 还包含更新
patch(null, vnode, container);// 后续更新 prevNode nextNode container
}
在运行createVNode方法生成虚拟dom后,调用render函数去渲染dom,而render方法是通过patch(下文将会介绍)方法去渲染dom的。下图为虚拟dom的大致结构:
h方法
熟悉vue的小伙伴肯定对h方法不陌生吧,有时候业务需要的时候,我们会使用它去创造虚拟dom。其实在源码中,h函数也是调用createVNode方法去生成虚拟dom的。看源码之前我们先看看一般我们是怎么使用h函数的。
h('div',{color:red})
h('div',h('span'))
h('div','hello')
h('div',['hello','hello'])
// 从上面示例可知h函数的参数个数是不固定的,且含义是不固定的,只能确定第一个参数是要渲染的dom元素
export function h(type, propsOrChildren, children) {
let l = arguments.length; // 获取h函数参数的个数
if (l === 2) { // 参数个数为2的情况
if (isObject(propsOrChildren) && !Array.isArray(propsOrChildren)) { // 当参数个数为2,判断第二个参数的类型,当为object并且不是数组时,第二个参数代表的含义有两种
if (isVNode(propsOrChildren)) {
return createVNode(type, null, [propsOrChildren])// 第二个参数为虚拟dom时,格式为:h('div',h('span'))
}
return createVNode(type, propsOrChildren); // 当第二个参数为props时, 格式为: h('div',{color:red})
} else {
return createVNode(type, null, propsOrChildren); // 当第二个参数为非对象或者为数组时, 第二个参数代表孩子节点, 格式为: h('div','hello') h('div',['hello','hello'])
}
} else {
if (l > 3) { // 当参数个数大于3的情况
children = Array.prototype.slice.call(arguments, 2); // 获取第三个(包含第三个)参数,并组成一个数组
} else if (l === 3 && isVNode(children)) { // 参数长度为3,并且类型为虚拟dom,把它包装成数组格式
children = [children]
}
return createVNode(type, propsOrChildren, children);
}
}
总结
createVNode是vue3源码中创建虚拟dom的方法,而h函数是暴露给开发者使用来自己创造虚拟dom的,其内部主要是对开发者传入的参数进行转化,最后依然是调用createVNode去创建虚拟dom。