vue3 源码学习,实现一个 mini-vue(七):构建 renderer 渲染器之 ELEMENT 节点的挂载
前言
原文来自 我的个人博客
自上一章我们成功构建了 h 函数创建 VNode 后,这一章的目标就是要在 VNode 的基础上构建 renderer 渲染器。
根据上一章的描述,我们知道在 packages/runtime-core/src/renderer.ts 中存放渲染器相关的内容。
Vue 提供了一个 baseCreateRenderer 的函数(这个函数很长有 2000 多行代码~),它会返回一个对象,我们把返回的这个对象叫做 renderer 渲染器。

对于该对象而言,提供了三个方法:
render:渲染函数hydrate:服务端渲染相关createApp:初始化方法
因为这里代码实在太长了,所以我们将会以下面两个思想来阅读以及实现:
- 阅读:没有使用的代码就当做不存在
- 实现:用最少的代码来实现
接下来就让我们开始吧,Here we go~
1. 案例分析
我们依然从上一章的测试案例开始讲:
<script>
const { h, render } = Vue
const vnode = h(
'div',
{
class: 'test'
},
'hello render'
)
console.log(vnode)
render(vnode, document.querySelector('#app'))
</script>
上一章中我们跟踪了 h 函数的创建,但是并没有提 render 函数。
实际上在 h 函数创建了 VNode 后,就是通过 render 渲染函数将 VNode 渲染成真实 DOM 的。至于其内部究竟是如何工作的,我们从源码中去找答案吧~
2. 源码阅读:初见 render 函数,ELEMENT 的挂载操作
- 我们直接到源码
packages/runtime-core/src/renderer.ts的第2327行进行debugger:

-
可以看到
render函数内部很简单,对vnode进行判断是否为null,此时我们的vnode是从h函数得到的vnode肯定不为空,所以会执行patch方法,最后将vnode赋值到container._vnode上。我们进入到patch方法。 -
patch的是贴片、补丁的意思,在这里patch表示 更新 节点。这里传递的参数我们主要关注 前三个。container._vnode表示 旧节点(n1),vnode表示 新节点(n2),container表示 容器。我们进入patch方法:

- 上图讲得很明白了,我们进入
processElement方法:

- 因为当前为 挂载操作,所以 没有旧节点,即:
n1 === null,进入mountElement方法:

- 在
mountElement方法中,代码首先会进入到hostCreateElement方法中,根据上图我们也知道,hostCreateElement方法实际上就是调用了document.createElement方法创建了Element并返回,但是有个点可以提的是,这个方法在packages/runtime-dom/src/nodeOps.ts,我们之前调试的代码都在packages/runtime-core/src/renderer.ts。这是因为vue为了保持兼容性,把所有和浏览器相关的API封装到了runtime-dom中。此时el和vnode.el的值为createElement生成的div实例。我们代码接着往下跑:

- 进入
hostSetElementText,而hostSetElementText实际上就是执行el.textContent = text,hostSetElementText同样 在packages/runtime-dom/src/nodeOps.ts中(和浏览器有关的API都在runtime-dom,下面不再将)。我们接着调试:

-
因为此时我们的
prop有值, 所以会进入这个for循环,看上面的图应该很明白了,就是添加了class属性,接着程序跳出patchClass,跳出patchProp,跳出for循环,if结束。如果此时触发div的outerHTML方法,就会得到<div class="test">hello render</div> -
到现在
dom已经构建好了,最后就只剩下** 挂载** 操作了 -
继续执行代码将进入
hostInsert(el, container, anchor)方法:

-
可以看到
hostInsert方法就是执行了insertBefore,而我们知道insertBefore可以将 ·dom· 插入到执行节点 -
那么到这里,我们已经成功的把
div插入到了dom树中,执行完成hostInsert方法之后,浏览器会出现对应的div. -
至此,整个
render执行完成
总结:
由以上代码可知:
- 整个挂载
Element | Text_Children的过程分为以下步骤:- 触发
patch方法 - 根据
shapeFlag的值,判定触发processElement方法 - 在
processElement中,根据 是否存在旧VNode来判定触发 挂载 还是 更新 的操作- 挂载中分成了4大步:
- 生成
div - 处理
textContent - 处理
props - 挂载
dom
- 生成
- 挂载中分成了4大步:
- 通过
container._vnode=vnode赋值 旧 VNode
- 触发
3. 代码实现:构建 renderer 基本架构
整个 基本架构 应该分为 三部分 进行处理:
renderer渲染器本身,我们需要构建出baseCreateRenderer方法- 我们知道所有和
dom的操作都是与core分离的,而和dom的操作包含了 两部分:Element操作:比如insert、createElement等,这些将被放入到runtime-dom中props操作:比如 设置类名,这些也将被放入到runtime-dom中
renderer 渲染器本身
- 创建
packages/runtime-core/src/renderer.ts文件:
import { ShapeFlags } from 'packages/shared/src/shapeFlags'
import { Fragment } from './vnode'
/**
* 渲染器配置对象
*/
export interface RendererOptions {
/**
* 为指定 element 的 prop 打补丁
*/
patchProp(el: Element, key: string, prevValue: any, nextValue: any): void
/**
* 为指定的 Element 设置 text
*/
setElementText(node: Element, text: string): void
/**
* 插入指定的 el 到 parent 中,anchor 表示插入的位置,即:锚点
*/
insert(el, parent: Element, anchor?): void
/**
* 创建指定的 Element
*/
createElement(type: string)
}
/**
* 对外暴露的创建渲染器的方法
*/
export function createRenderer(options: RendererOptions) {
return baseCreateRenderer(options)
}
/**
* 生成 renderer 渲染器
* @param options 兼容性操作配置对象
* @returns
*/
function baseCreateRenderer(options: RendererOptions): any {
/**
* 解构 options,获取所有的兼容性方法
*/
const {
insert: hostInsert,
patchProp: hostPatchProp,
createElement: hostCreateElement,
setElementText: hostSetElementText
} = options
const patch = (oldVNode, newVNode, container, anchor = null) => {
if (oldVNode === newVNode) {
return
}
const { type, shapeFlag } = newVNode
switch (type) {
case Text:
// TODO: Text
break
case Comment:
// TODO: Comment
break
case Fragment:
// TODO: Fragment
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// TODO: Element
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// TODO: 组件
}
}
}
/**
* 渲染函数
*/
const render = (vnode, container) => {
if (vnode == null) {
// TODO: 卸载
} else {
// 打补丁(包括了挂载和更新)
patch(container._vnode || null, vnode, container)
}
container._vnode = vnode
}
return {
render
}
}
封装 Element 操作
- 创建
packages/runtime-dom/src/nodeOps.ts模块,对外暴露nodeOps对象:
const doc = document
export const nodeOps = {
/**
* 插入指定元素到指定位置
*/
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
/**
* 创建指定 Element
*/
createElement: (tag): Element => {
const el = doc.createElement(tag)
return el
},
/**
* 为指定的 element 设置 textContent
*/
setElementText: (el, text) => {
el.textContent = text
}
}
封装 props 操作
- 创建
packages/runtime-dom/src/patchProp.ts模块,暴露patchProp方法:
const doc = document
export const nodeOps = {
/**
* 插入指定元素到指定位置
*/
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
/**
* 创建指定 Element
*/
createElement: (tag): Element => {
const el = doc.createElement(tag)
return el
},
/**
* 为指定的 element 设置 textContent
*/
setElementText: (el, text) => {
el.textContent = text
}
}
- 创建
packages/runtime-dom/src/modules/class.ts模块,暴露patchClass方法:
/**
* 为 class 打补丁
*/
export function patchClass(el: Element, value: string | null) {
if (value == null) {
el.removeAttribute('class')
} else {
el.className = value
}
}
- 在
packages/shared/src/index.ts中,写入isOn方法:
const onRE = /^on[^a-z]/
/**
* 是否 on 开头
*/
export const isOn = (key: string) => onRE.test(key)
三大块 全部完成,标记着整个 renderer 架构设计完成。
4. 代码实现:基于 renderer 完成 ELEMENT 节点挂载
- 在
packages/runtime-core/src/renderer.ts中,创建processElement方法:
/**
* Element 的打补丁操作
*/
const processElement = (oldVNode, newVNode, container, anchor) => {
if (oldVNode == null) {
// 挂载操作
mountElement(newVNode, container, anchor)
} else {
// TODO: 更新操作
}
}
/**
* element 的挂载操作
*/
const mountElement = (vnode, container, anchor) => {
const { type, props, shapeFlag } = vnode
// 创建 element
const el = (vnode.el = hostCreateElement(type))
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 设置 文本子节点
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// TODO: 设置 Array 子节点
}
// 处理 props
if (props) {
// 遍历 props 对象
for (const key in props) {
hostPatchProp(el, key, null, props[key])
}
}
// 插入 el 到指定的位置
hostInsert(el, container, anchor)
}
const patch = (oldVNode, newVNode, container, anchor = null) => {
if (oldVNode === newVNode) {
return
}
const { type, shapeFlag } = newVNode
switch (type) {
case Text:
// TODO: Text
break
case Comment:
// TODO: Comment
break
case Fragment:
// TODO: Fragment
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(oldVNode, newVNode, container, anchor)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// TODO: 组件
}
}
}
根据源码的逻辑,在这里主要做了五件事情:
- 区分挂载、更新
- 创建
Element - 设置
text - 设置
class - 插入
DOM树
5. 代码实现:合并渲染架构
我们知道,在源码中,我们可以直接:
const { render } = Vue
render(vnode, document.querySelector('#app'))
但是在我们现在的代码,发现是 不可以 直接这样导出并使用的。
所以这就是本小节要做的 得到可用的 render 函数
- 创建
packages/runtime-dom/src/index.ts:
import { createRenderer } from '@vue/runtime-core'
import { extend } from '@vue/shared'
import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp'
const rendererOptions = extend({ patchProp }, nodeOps)
let renderer
function ensureRenderer() {
return renderer || (renderer = createRenderer(rendererOptions))
}
export const render = (...args) => {
ensureRenderer().render(...args)
}
-
在
packages/runtime-core/src/index.ts中导出createRenderer -
在
packages/vue/src/index.ts中导出render -
创建测试实例
packages/vue/examples/runtime/render-element.html:`
<script>
const { h, render } = Vue
const vnode = h(
'div',
{
class: 'test'
},
'hello render'
)
console.log(vnode)
render(vnode, document.querySelector('#app'))
</script>
成功渲染出 hello render!

转载自:https://juejin.cn/post/7184850658963488829