18.3 将组件渲染为 HTML 字符串
组件类型的 vnode 没有
type字符串可拼,必须先在服务端执行组件的初始化流程拿到 subTree,再把 subTree 交给统一的分发函数渲染成字符串。
一、最朴素的实现
思路很直接:执行 setup 拿到渲染函数 → 执行渲染函数得到 subTree → 渲染 subTree。
function renderComponentVNode(vnode) {
// 获取 setup 组件选项
let { type: { setup } } = vnode
// 执行 setup 得到渲染函数 render
const render = setup()
// 执行 render 得到 subTree,即组件要渲染的内容
const subTree = render()
// 渲染 subTree 并返回
return renderElementVNode(subTree)
}
这版仍然只演示原理,有两个明显问题:subTree 可能是任意类型的 vnode(含组件),不能直接用
renderElementVNode;组件初始化流程远不止调用setup。
二、用统一的 renderVNode 分发
subTree 本身可能是任意类型的虚拟节点,包括组件类型,因此需要封装一个通用的分发函数。
function renderVNode(vnode) {
const type = typeof vnode.type
if (type === 'string') {
// 标签元素
return renderElementVNode(vnode)
} else if (type === 'object' || type === 'function') {
// 有状态组件 / 函数式组件
return renderComponentVNode(vnode)
} else if (vnode.type === Text) {
// 处理文本节点
} else if (vnode.type === Fragment) {
// 处理片段
} else {
// 其他 VNode 类型
}
}
function renderComponentVNode(vnode) {
// ...前面的初始化逻辑
const subTree = render.call(renderContext, renderContext)
return renderVNode(subTree)
}
三、服务端组件初始化与客户端的差异
服务端渲染时组件的初始化流程与客户端基本一致(同样需要 data、setupContext、setupState、renderContext 等),但有两个重要区别:
| 区别 | 原因 | 结果 |
|---|---|---|
| 数据无须响应式 | 服务端渲染的是应用的当前快照,不存在数据变更后重新渲染 | 不用 reactive(data());props 也无须是浅响应的 |
| 无须创建并执行 render effect | 只需要拿到 subTree,不需要调用渲染器创建真实 DOM | 不挂载、不更新 |
由此可以推出服务端的生命周期表现:
不创建 render effect,就不会有挂载动作,
beforeMount与mounted不会触发。
不存在数据变更后的重新渲染,
beforeUpdate与updated也不会执行。
因此服务端执行渲染函数时不要重复触发响应式 effect,直接调用
render()取 subTree 即可。
四、完整的 renderComponentVNode
服务端版的组件初始化:解析 props/attrs、构造组件实例、实现 emit、构造 setupContext 与 renderContext 代理,最后执行 render 并交给 renderVNode。
function renderComponentVNode(vnode) {
// 兼容函数式组件
const isFunctional = typeof vnode.type === 'function'
let componentOptions = vnode.type
if (isFunctional) {
componentOptions = {
render: vnode.type,
props: vnode.type.props
}
}
let { render, data, setup, beforeCreate, created, props: propsOption } = componentOptions
// 服务端只有 beforeCreate / created 会被执行
beforeCreate && beforeCreate()
// 关键:无须使用 reactive() 创建响应式版本
const state = data ? data() : null
const [props, attrs] = resolveProps(propsOption, vnode.props)
const slots = vnode.children || {}
const instance = {
state,
props, // props 无须 shallowReactive
isMounted: false,
subTree: null,
slots,
mounted: [],
keepAliveCtx: null
}
function emit(event, ...payload) {
const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
const handler = instance.props[eventName]
if (handler) {
handler(...payload)
} else {
console.error('事件不存在')
}
}
// 执行 setup:需要提供 setupContext
let setupState = null
if (setup) {
const setupContext = { attrs, emit, slots }
const prevInstance = setCurrentInstance(instance)
const setupResult = setup(shallowReadonly(instance.props), setupContext)
setCurrentInstance(prevInstance)
if (typeof setupResult === 'function') {
if (render) console.error('setup 函数返回渲染函数,render 选项将被忽略')
render = setupResult
} else {
setupState = setupContext
}
}
vnode.component = instance
// 渲染上下文:组件实例的代理
const renderContext = new Proxy(instance, {
get(t, k) {
const { state, props, slots } = t
if (k === '$slots') return slots
if (state && k in state) {
return state[k]
} else if (k in props) {
return props[k]
} else if (setupState && k in setupState) {
return setupState[k]
} else {
console.error('不存在')
}
},
set(t, k, v) {
const { state, props } = t
if (state && k in state) {
state[k] = v
} else if (k in props) {
props[k] = v
} else if (setupState && k in setupState) {
setupState[k] = v
} else {
console.error('不存在')
}
}
})
// render 的 this 指向 renderContext
created && created.call(renderContext)
const subTree = render.call(renderContext, renderContext)
return renderVNode(subTree)
}
配套的 props 解析:
function resolveProps(options = {}, propsData = {}) {
const props = {}
const attrs = {}
for (const key in propsData) {
// 在 props 选项中有定义,或事件(on 开头)→ props;否则归入 attrs
if (key in options || key.startsWith('on')) {
props[key] = propsData[key]
} else {
attrs[key] = propsData[key]
}
}
return [props, attrs]
}
// 全局变量,存储当前正在被初始化的组件实例
let currentInstance = null
function setCurrentInstance(instance) {
currentInstance = instance
}
服务端把组件渲染成字符串后,浏览器拿到的是一段没有事件绑定的静态 HTML,这正是下一节客户端激活要补上的工作。
相关
- 18.4 客户端激活的原理:客户端如何复用服务端产出的 DOM,并把事件补回去