highlight: vs2015
theme: juejin
前言
原文来自 我的个人博客
自上一章我们成功构建了 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
- 生成
- 通过
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
!