自上一章我们成功构建了 h
函数创建 VNode
后,这一章的目标就是要在 VNode
的基础上构建 renderer
渲染器。
根据上一章的描述,我们知道在 packages/runtime-core/src/renderer.ts
中存放渲染器相关的内容。
Vue
提供了一个 baseCreateRenderer
的函数(这个函数很长有 2000
多行代码~),它会返回一个对象,我们把返回的这个对象叫做 renderer
渲染器。
对于该对象而言,提供了三个方法:
render
:渲染函数hydrate
:服务端渲染相关createApp
:初始化方法因为这里代码实在太长了,所以我们将会以下面两个思想来阅读以及实现:
接下来就让我们开始吧,Here we go~
我们依然从上一章的测试案例开始将:
html<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
的。至于其内部究竟是如何工作的,又到了我们的源码阅读时间~
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
来判定触发 挂载 还是 更新 的操作
div
textContent
props
dom
container._vnode
= vnode
赋值 旧 VNode整个 基本架构 应该分为 三部分 进行处理:
renderer
渲染器本身,我们需要构建出 baseCreateRenderer
方法dom
的操作都是与 core
分离的,而和 dom
的操作包含了 两部分:
Element
操作:比如 insert
、createElement
等,这些将被放入到 runtime-dom
中props
操作:比如 设置类名,这些也将被放入到 runtime-dom
中renderer 渲染器本身
packages/runtime-core/src/renderer.ts
文件:tsimport { 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
对象:tsconst 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
方法:tsconst 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
方法:ts/**
* 为 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
方法:tsconst onRE = /^on[^a-z]/
/**
* 是否 on 开头
*/
export const isOn = (key: string) => onRE.test(key)
三大块 全部完成,标记着整个 renderer
架构设计完成。
packages/runtime-core/src/renderer.ts
中,创建 processElement
方法:ts/**
* 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
树我们知道,在源码中,我们可以直接:
jsconst { render } = Vue
render(vnode, document.querySelector('#app'))
但是在我们现在的代码,发现是 不可以 直接这样导出并使用的。
所以这就是本小节要做的 得到可用的 render
函数
packages/runtime-dom/src/index.ts
:tsimport { 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
:`
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
!
本文作者:叶继伟
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!