详解Vue.js3.0组件是如何渲染为DOM的
本⽂主要是讲述 Vue.js 3.0 中⼀个组件是如何转变为页⾯中真实 DOM 节点的。对于任何⼀个基于 Vue.js 的应⽤来说,⼀切的故事都要从应⽤初始化「根组件(通常会命名为 APP)挂载到 HTML 页⾯ DOM 节点(根组件容器)上」说起。所以,我们可以从应⽤的根组件为切⼊点。
主线思路:聚焦于⼀个组件是如何转变为 DOM 的。
辅助思路:
涉及到源代码的地⽅,需要明确标记源码所在⽂件,同时将 TS 简化为 JS 以便于直观理解
思路每前进⼀步要能够得出结论
尽量总结归纳出流程图
应⽤初始化
在 Vue.js 3.0 中,初始化⼀个应⽤的⽅式和 Vue.js 2.x 有差别但是差别不⼤(本质上都是把 App 组件挂载到 id 为 app 的DOM 节点上),在 Vue.js 3.0 中⽤法如下:
import { createApp } from 'vue'
import App from './app'
const app = createApp(App)
createApp 简化版源码
// packages/runtime-dom/src/index.ts
// 创建应⽤
const createApp = ((...args) => {
// 1. 创建 app 对象
const app = ensureRenderer().createApp(...args)
const { mount } = app
/
/ 2. 重写 mount ⽅法
// ...
}
return app
})
createApp ⽅法中主要做了两件事:
创建 app 对象
重写 unt ⽅法
接下来会分别看⼀下这两个过程都做了什么事情。
创建 app 对象
从 ensureRenderer() 着⼿。在 Vue.js 3.0 中有⼀个「渲染器」的概念,我们先对渲染器有⼀个初步的印象:**渲染器可以⽤于跨平台渲染,是⼀个包含了平台渲染核⼼逻辑的 JavaScript 对象。**接下来,我们通过简化版源码来验证这个结论:
// packages/runtime-dom/src/index.ts
// 定义渲染器变量
let renderer
// 创建⼀个渲染器对象
// 惰性创建渲染器(当⽤户只依赖响应式包的时候可以通过 tree-shaking 的⽅式移除核⼼渲染逻辑相关的代码)
function ensureRenderer() {
return renderer || (renderer = createRenderer(rendererOptions))
}
/
/ packages/runtime-core/src/renderer.ts
export function createRenderer(options) {
}
// 创建不同平台渲染器的函数,在其内部都会调⽤ baseCreateRenderer
function baseCreateRenderer(options, createHydrationFns) {
// ⼀系列内部函数
const render = (vnode, container) => {
// 组件渲染的核⼼逻辑
}
// 返回渲染器对象
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
可以看出渲染器最终由 baseCreateRenderer 函数⽣成,是⼀个包含 render 和createApp 函数的 JS 对象。其中 createApp 函数是由 createAppAPI 函数返回的。那 createApp 接收的参数有哪些呢?为了寻求答案,我们需要看⼀下 createAppAPI 做了什么事情。
// packages/runtime-core/src/apiCreateApp.ts
// 接收⼀个渲染器 render 作为参数,接收⼀个可选参数 hydrate,返回⼀个⽤于创建 app 的函数
export function createAppAPI(render, hydrate) {
// createApp 接收两个参数:根组件对象和根组件的prop
return function createApp(rootComponent, rootProps = null) {
const context = createAppContext()
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent,
_props: rootProps,
_container: null,
_context: context,
version,
get 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): any {
// 创建根组件的 vnode
const vnode = createVNode(rootComponent, rootProps)
// 利⽤函数参数传⼊的渲染器渲染 vnode
render(vnode, rootContainer)
app._container = rootContainer
return vnodeponent.proxy
},
unmount() {},
provide(key, value) {}
}
return app
}
}
渲染器对象的 createApp ⽅法接收两个参数:根组件对象和根组件的prop。这和应⽤初始化 demo 中 createApp(App) 的使⽤⽅式是吻合的。还可以看到的是:createApp 返回的 app 对象在最初定义时包含了 _uid 、 use 、 mixin 、 component 、mount 等属性。
此时,我们可以得出结论:在应⽤层调⽤的 createApp ⽅法内部,⾸先会⽣成⼀个渲染器,然后调⽤渲染器的 createApp ⽅法创建 app 对象。app 对象中具有⼀系列我们在⽇常开发应⽤时已经很熟悉的属性。
在应⽤层调⽤的 createApp ⽅法内部创建好 app 对象后,接下来便是对 unt ⽅法重写。
重写 unt ⽅法
先看⼀下简化版的 unt 源码:
// packages/runtime-dom/src/index.ts
const { mount } = app
// 1. 标准化容器(将传⼊的 DOM 对象或者节点选择器统⼀为 DOM 对象)
if (!container) return
const component = app._component
// 2. 标准化组件(如果根组件不是函数,并且没有 render 函数和 template 模板,则把根组件 innerHTML 作为 template)
if (!isFunction(component) && !der && !plate) {
}
// 3. 挂载前清空容器的内容
container.innerHTML = ''
// 4. 执⾏渲染器创建 app 对象时定义的 mount ⽅法(在后⽂中称之为「标准 mount 函数」)来渲染根组件
const proxy = mount(container)
return proxy
}
浏览器平台 unt ⽅法重写主要做了 4 件事情:
1. 标准化容器
2. 标准化组件
3. 挂载前清空容器的内容
4. 执⾏标准 mount 函数渲染组件
此时可能会有⼈思考⼀个问题:为什么要重写unt 呢?答案是因为 Vue.js 需要⽀持跨平台渲染。
⽀持跨平台渲染的思路:不同的平台具有不同的渲染器,不同的渲染器中会调⽤标准的 baseCreateRenderer 来保证核⼼(标准)的渲染流程是⼀致的。
以浏览器端和服务端渲染的代码实现为例:
createApp 流程图
在分别了解了创建 app 对象和重写 unt 过程后,我们来以整体的视⾓看⼀下 createApp 函数的实现:
⽬前为⽌,只是对应⽤的初始化有了⼀个初步的印象,但是还没有涉及到具体的组件渲染过程。可以看到根组件的渲染是在标准 mount 函数中进⾏的。所以接下来需要去深⼊了解标准 mount 函数。
标准 mount 函数
简化版源码
// packages/runtime-core/src/apiCreateApp.ts
// createAppAPI 函数内部返回的 createApp 函数中定义了 app 对象,mount 函数是 app 对象的⽅法之⼀
mount(rootContainer, isHydrate) {
// 1. 创建根组件的 vnode
const vnode = createVNode(rootComponent, rootProps)
// 2. 利⽤函数参数传⼊的渲染器渲染 vnode
render(vnode, rootContainer)
app._container = rootContainer
return vnodeponent.proxy
},
createVNode ⽅法做了两件事:
1. 基于根组件「创建 vnode」
2. 在根组件容器中「渲染 vnode」
vnode ⼤致可以理解为 Virtual DOM(虚拟 DOM)概念的⼀个具体实现,是⽤普通的 JS 对象来描述 DOM 对象。因为不是真实的 DOM 对象,所以叫做 Virtual DOM。
我们来⼀起看⼀下创建 vnode 和渲染 vnode 的具体过程。
创建 vnode:createVNode(rootComponent, rootProps)
简化版源码(已经把分⽀逻辑拿掉)
// packages/runtime-core/src/vnode.ts
function _createVNode(type, props, children,
// 1. 对 VNodeTypes 或 ClassComponent 类型的 type 进⾏各种标准化处理:规范化 vnode、规范化 component、规范化 CSS 类和样式
// 2. 将 vnode 类型信息编码为位图
const shapeFlag = isString(type)
ShapeFlags.ELEMENT
: __FEATURE_SUSPENSE__ && isSuspense(type)
ShapeFlags.SUSPENSE
: isTeleport(type)
ShapeFlags.TELEPORT
: isObject(type)
ShapeFlags.STATEFUL_COMPONENT
: isFunction(type)
ShapeFlags.FUNCTIONAL_COMPONENT
:
0
// 3. 创建 vnode 对象
const vnode = {
__v_isVNode: true,
[ReactiveFlags.SKIP]: true,
type, // 把函数⼊参 type 赋值给 vnode
props,
children: null,
component: null,
staticCount: 0,
shapeFlag, // 把 vnode 类型信息赋值给 vnode
/
/ 还有很多属性
}
// 4. 标准化⼦节点 children
normalizeChildren(vnode, children)
return vnode
}
createVNode 做了 4 件事
1. 对 VNodeTypes 或 ClassComponent 类型的 type 进⾏各种标准化处理
2. 将 vnode 类型信息编码为位图
3. 创建 vnode 对象
4. 标准化⼦节点 children
细⼼的同学会发现:在标准 mount 函数中执⾏ createVNode(rootComponent, rootProps) 时,参数是根组件 rootComponent 和根组件属性 rootProps,但是在 _createVNode 在定义时函数签名的前两个参数确实 type 和 props。rootComponent 与 type 的关系是什么呢?函数名为什么差了⼀个 _ 呢?
⾸先函数名的差异,是由于在定义函数时,基于代码运⾏环境做了⼀个判断:
export const createVNode = (__DEV__
createVNodeWithArgsTransform
: _createVNode) as typeof _createVNode
其次,rootComponent 与 type 的关系我们可以从 type 的类型定义中得到答案:
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null
): VNode { }
当 createVNode把这 4 件事情做好后,会返回已经创建好 vnode,接下来做的事情是渲染 vnode。
渲染 vnode:render(vnode, rootContainer)
即使不看具体源码实现,我们其实⼤致可以⽤⼀句话总结出渲染 vnode 过程做了什么事情:把 vnode 转化为真实 DOM。
前⽂我们提过,**渲染器是⼀个包含了平台渲染核⼼逻辑的 JavaScript 对象。**渲染 vnode 正是通过调⽤渲染器的 render ⽅法做的。
// 返回渲染器对象
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
我们来看⼀下 render 函数的定义(简化版源码):**
// packages/runtime-core/src/renderer.ts
const render = (vnode, container) => {
if (vnode == null) {
// 如果 vnode 为 null,但是容器中有 vnode,则销毁组件
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 创建或更新组件
patch(container._vnode || null, vnode, container)
}
/
/ packages/runtime-core/src/scheduler.ts
flushPostFlushCbs()
// 缓存 vnode 节点(标识该 vnode 已经完成渲染)
container._vnode = vnode
}
抽象来看, render 做的事情是:如果传⼊的 vnode 为空,则销毁组件,否则就创建或者更新组件。其中有两个关键函数:patch 和 unmount(patch、unmount 和 render 都是在baseCreateRenderer函数内部的⽅法)。
可以从 patch 着⼿,看⼀下是如何将 vnode 转化为 DOM 的。
patch
// packages/runtime-core/src/renderer.ts
const patch = (
vuejs流程图插件n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
optimized = false
) => {
// 1. 如果是更新 vnode 并且新旧 vnode 类型不⼀致,则销毁旧的 vnode
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// 2. 处理不同类型节点的渲染
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
// 处理⽂本节点
processText(n1, n2, container, anchor)
break
case Comment:
// 处理注释节点
break
case Static:
// 处理静态节点
break
case Fragment:
// 处理 Fragment 元素(/guide/migration/fragments.html#fragments)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
/
/ 处理普通 DOM 元素
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 处理组件
} else if (shapeFlag & ShapeFlags.TELEPORT) {
// 处理 TELEPORT
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// 处理 SUSPENSE
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论