组件渲染
vnode 本质是用来描述 DOM 的 JavaScript 对象,它在 Vue 中可以描述不同类型的节点,比如:普通元素节点、组件节点等。
vnode 的优点:
抽象:引入 vnode,可以把渲染过程抽象化,从而使得组件的抽象能力也得到提升
跨平台:因为 patch vnode 的过程不同平台可以有自己的实现,基于 vnode 再做服务端渲染、weex 平台、小程序平台的渲染
组件的渲染流程:
创建 vnode
createVNode 主要做了四件事:
- 处理 props,标准化 class 和 style
- 对 vnode 类型信息编码
- 创建 vnode 对象
- 标准化子节点
/**
* 创建 vnode
*/
function createVNode(type, props = null, children = null) {
// 1、处理 props,标准化 class 和 style
if (props) {
// ...
}
// 2、对 vnode 类型信息编码
const shapeFlag = isString(type)
? 1 /* ELEMENT */
: isSuspense(type)
? 128 /* SUSPENSE */
: isTeleport(type)
? 64 /* TELEPORT */
: isObject(type)
? 4 /* STATEFUL_COMPONENT */
: isFunction(type)
? 2 /* FUNCTIONAL_COMPONENT */
: 0
// 3、创建 vnode 对象
const vnode = {
type,
props,
shapeFlag,
// 一些其他属性
}
// 4、标准化子节点,把不同数据类型的 children 转成数组或者文本类型
normalizeChildren(vnode, children)
return vnode
}
渲染 vnode
组件自身通过 render 进行渲染,子组件通过 patch 进行渲染。
render 主要做了几件事:
- 检查是否存在 vnode
- 如果之前有,现在没有,则销毁
- 如果现在有,则创建或更新
- 缓存 vnode,用于判断是否已经渲染
/**
* 渲染 vnode
*/
const render = (vnode, container) => {
// vnode 为 null,则销毁组件
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
}
// 否则创建或者更新组件
else {
patch(container._vnode || null, vnode, container)
}
// 缓存 vnode 节点,表示已经渲染
container._vnode = vnode
}
patch 主要做了两件事:
- 判断是否销毁节点
- 挂载新节点
/**
* 更新 DOM
* @param {vnode} n1 - 旧的 vnode(为 null 时表示第一次挂载)
* @param {vnode} n2 - 新的 vnode
* @param {DOM} container - DOM 容器,vnode 渲染生成 DOM 后,会挂载到 container 下面
*/
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {
// 如果存在新旧节点,且新旧节点类型不同,则销毁旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// 挂载新 vnode
const { type, shapeFlag } = n2
switch (type) {
case Text:
// 处理文本节点
break
case Comment:
// 处理注释节点
break
case Static:
// 处理静态节点
break
case Fragment:
// 处理 Fragment 元素
break
default:
if (shapeFlag & 1/* ELEMENT */) {
// 处理普通 DOM 元素
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
} else if (shapeFlag & 6/* COMPONENT */) {
// 处理 COMPONENT
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
} else if (shapeFlag & 64/* TELEPORT */) {
// 处理 TELEPORT
} else if (shapeFlag & 128/* SUSPENSE */) {
// 处理 SUSPENSE
}
}
}
处理组件
在 props、data、methods、computed 等 options 中定义一些变量,在组件初始化阶段,Vue3 内部会处理这些 options,即把定义的变量添加到了组件实例上等模板编译成 render 函数的时候,内部通过 with(this){}
的语法去访问在组件实例中的变量。
组件的渲染流程:
/**
* 处理 COMPONENT
*/
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 旧节点为 null,表示不存在旧节点,则直接挂载组件
if (n1 == null) {
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
// 旧节点存在,则更新组件
else {
updateComponent(n1, n2, parentComponent, optimized)
}
}
/**
* 挂载组件
* mountComponent 做了三件事:
* 1、创建组件实例
* 2、设置组件实例
* 3、设置并运行带副作用的渲染函数
*/
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 1、创建组件实例,内部也通过对象的方式去创建了当前渲染的组件实例
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
// 2、设置组件实例,instance 保留了很多组件相关的数据,维护了组件的上下文包括对 props、插槽,以及其他实例的属性的初始化处理
setupComponent(instance)
// 3、设置并运行带副作用的渲染函数
setupRenderEffet(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
}
/**
* 创建组件实例
*/
function createComponentInstance(vnode, parent, suspense) {
// 继承父组件实例上的 appContext,如果是根组件,则直接从跟 vnode 中取
const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
// 定义组件实例
const instance = {
uid: uid++, // 组件唯一id
vnode, // 组件 vnode
parent, // 父组件实例
appContext, // app 上下文
type: vnode.type, // vnode 节点类型
root: null, // 根组件实例
next: null, // 新的组件 vnode
subTree: null, // 子节点 vnode
update: null, // 带副作用更新函数
render: null, // 渲染函数
proxy: null, // 渲染上下文代理
withProxy: null, // 带有 with 区块的渲染上下文代理
effects: null, // 响应式相关对象
provides: parent ? parent.provide : Object.create(appContext.provides), // 依赖注入相关
accessCache: null, // 渲染代理的属性访问缓存
renderCache: [], // 渲染缓存
ctx: EMPTY_OBJ, // 渲染上下文
data: EMPTY_OBJ, // data 数据
props: EMPTY_OBJ, // props 数据
attrs: EMPTY_OBJ, // 普通属性
slots: EMPTY_OBJ, // 插槽相关
refs: EMPTY_OBJ, // 组件或者 DOM 的 ref 引用
setupState: EMPTY_OBJ, // setup 函数返回的响应式结果
setupContext: null, // setup 函数上下文数据
components: Object.create(appContext.components), // 注册的组件
directives: Object.create(appContext.directives), // 注册的指令
suspense, // suspense 相关
asyncDep: null, // suspense 异步依赖
asyncResolved: false, // suspense 异步依赖是否都已处理
isMounted: false, // 是否挂载
isUnmounted: false, // 是否卸载
isDeactivated: false, // 是否激活
bc: null, // 生命周期 before create
c: null, // 生命周期 created
bm: null, // 生命周期 before mount
m: null, // 生命周期 mounted
bu: null, // 生命周期 before update
u: null, // 生命周期 update
um: null, // 生命周期 unmounted
bum: null, // 生命周期 before unmounted
da: null, // 生命周期 deactivated
a: null, // 生命周期 activated
rtg: null, // 生命周期 render triggered
rtc: null, // 生命周期 render tracked
ec: null, // 生命周期 err captured
emit: null // 派发事件方法
}
// 初始化渲染上下文
instance.ctx = { _: instance }
// 初始化根组件指针
instance.root = parent ? parent.root : instance
// 初始化派发事件方法
instance.emit = emit.bind(null, instance)
return instance
}
/**
* 创建和设置组件实例
*/
function setupComponent(instance, isSSR = false) {
const { props, children, shapeFlag } = instance.vnode
// 判断是否是一个有状态的组件
const isStateful = shapeFlag & 4
// 初始化 props
initProps(instance, props, isStateful, isSSR)
// 初始化插槽
initSlots(instance, children)
// 如果是一个有状态的组件,则设置有状态的组件实例
const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) : undefined
return setupResult
}
/**
* 设置有状态的组件实例
*/
function setupStatefulComponent(instance, isSSR) {
const Component = instance.type
// 创建渲染代理的属性访问缓存
instance.accessCache = []
// 1、创建渲染上下文代理
/**
* 为什么需要代理?
* 答:在执行组件渲染函数的时候,直接访问渲染上下文 instance.ctx 中的属性,做一层 proxy 对渲染上下文 instance.ctx 属性的访问和修改代理到对 setupState、ctx、data、props 中的数据的访问和修改(简单来说:通过 proxy 拦截对组件实例上下文的修改,代理为对 data 等的修改)
*/
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
// 2、判断处理 setup 函数
const { setup } = Component
// 3、组件实例的设置
if (setup) {
// 3.1、如果 setup 函数带参数,则创建一个 setupContext
const setupContext = (instance.setupContext = setup.length > 1 ? createSetupContext(instance) : null)
// 3.2、执行 setup 函数,获取结果
const setupResult = callWithErrorHandling(setup, instance, 0/* SETUP_FUNCTION */, [instance.props, setupContext])
// 3.3、处理 setup 返回结果
handleSetupResult(instance, setupResult)
} else {
// 完成组件实例设置
finishComponentSetup(instance)
}
}
// 渲染上下文代理的处理器
const PublicInstanceProxyHandlers = {
get({ _: instance }, key) {
const { ctx, setupState, data, props, accessCache, type, appContext } = instance
// 判断 key 不已 $ 开头的情况(!!!注意优先级) - setupState(setup 函数返回的数据) | data | props | ctx
if (key[0] !== '$') {
// 在缓存中访问渲染代理的属性
const n = accessCache[key]
// 如果缓存中存在该属性
if (n !== undefined) {
switch (n) {
case 0: /* SETUP */
return setupState[key]
case 1: /* DATA */
return data[key]
case 3: /* CONTEXT */
return ctx[key]
case 2: /* PROPS */
return props[key]
}
}
// 从 setupState 中取数据
else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
accessCache[key] = 0
return setupState[key]
}
// 从 data 中取数据
else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
accessCache[key] = 1
return data[key]
}
// 从 props 中取数据
else if (type.props && hasOwn(normalizePropsOptions(type.props)[0], key)) {
accessCache[key] = 2
return props[key]
}
// 从 ctx 中取数据
else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
accessCache[key] = 3
return ctx[key]
}
// 都取不到
else {
accesssCache[key] = 4
}
}
const publicGetter = publicPropertiesMap[key]
let cssModul, globalProperties
// 公开的 $xxx 属性或方法
if (publicGetter) {
return publicGetter(instance)
}
// css 模块,通过 vue-loader 编译的时候注入
else if ((cssModule = type.__cssModules) && (cssModule = cssModule[key])) {
return cssModule
}
// 用户的自定义的属性,也用 $ 开头
else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
accessCache[key] = 3
return ctx[key]
}
// 全局定义的属性
else if (((globalProperties = appContext.config.globalProperties), hasOwn(globalProperties, key))) {
return globalProperties[key]
} else if ((process.env.NODE_ENV !== 'production') && currentRenderingInstance && key.indexOf('__v') !== 0) {
if (data !== EMPTY_OBJ && key[0] === '$' && hasOwn(data, key)) {
// 如果在 data 中定义的数据以 $ 开头,会报警告,因为 $ 是保留字符,不会做代理
warn(/* ... */)
} else {
// 在模板中使用的变量如果没有定义,报警告
warn(/* ... */)
}
}
},
set({ _: instance }, key, value) {
const { data, setupState, ctx } = instance
// 给 setupState 赋值
if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
setupState[key] = value
}
// 给 data 赋值
else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
data[key] = value
}
// 不能直接给 props 赋值,因为不符合单向数据流的设计思想
else if (key in instance.props) {
(process.env.NODE_ENV !== 'production') && warn(/* ... */)
return false
}
// 不能给 Vue 内部以 $ 开头的保留属性赋值
if (key[0] === '$' && key.slice(1) in instance) {
(process.env.NODE_ENV !== 'production') && warn(/* ... */)
return false
}
// 用户自定义数据赋值
else {
ctx[key] = value
}
return true
},
has({ _: { data, setupState, accessCache, ctx, type, appContext } }, key) {
// 依次判断 key 是否存在于 缓存、data、setupState、props、ctx(渲染上下文)、publicPropertiesMap(公开属性)、appContext.config.globalProperties(应用全局属性)
return (accessCache[key] !== undefined
|| (data !== EMPTY_OBJ && hasOwn(data, key))
|| (setupState !== EMPTY_OBJ && hasOwn(setupState, key))
|| (type.props && hasOwn(normalizePropsOptions(type.props)[0], key))
|| hasOwn(ctx, key)
|| hasOwn(publicPropertiesMap, key)
|| hasOwn(appContext.config.globalProperties, key))
}
}
/**
* 创建 setupContext - setup 函数的第二个参数
*/
function createSetupContext(instance) {
return {
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit
}
}
/**
* 对 setup 进行封装,处理错误的情况
*/
function callWithErrorHandling(fn, instance, type, args) {
let res
try {
res = args ? fn(...args) : fn()
} catch (err) {
handleError(err, instance, type)
}
return res
}
/**
* 处理 setupResult - setup 返回结果
*/
function handleSetupResult(instance, setupResult) {
// 当 setupResult 为函数,则把该函数设置为组件实例的渲染函数
if (isFunction(setupResult)) {
instance.render = setupResult
}
// 当 setupResult 为对象,则把 setupResult 变成响应式
else if (isObject(setupResult)) {
instance.setupState = reactive(setupResult)
}
// 完成组件实例设置
finishComponentSetup(instance)
}
/**
* 完成组件实例设置
* finishComponentSetup 主要做了两件事:
* 1、标准化模板或者渲染函数
* 2、兼容 Options API
*/
function finishComponentSetup(instance) {
const Component = instance.type
// 1、标准化模板或者渲染函数
/**
* Vue 的两种写法:
* 1、引入 Vue.js - 直接在组件对象的 template 属性中编写组件的模板(对应 runtime-compiled)
* 2、SFC - 通过编写组件的 tempalte 模板去描述一个组件的 DOM 结构(对应 runtime-only)
* Vue 在 Web 端有两种编译方式:
* 1、runtime-compiled(古老写法)
* 2、runtime-only(推荐:体积更小、运行时不用编译、耗时更少、性能更优秀)
* 区别在于是否注册了 compile 方法
*/
if (!instance.render) {
// runtime-compiled 编译方式,存在 template 不存在 render,则在 JavaScript 运行时进行模板编译,生成 render 函数
if (compile && Component.template && !Component.render) {
Component.render = compile(Component.template, {
isCustomElement: instance.appContext.config.isCustomElement || No
})
Component.render._rc = true
}
if ((process.env.NODE_ENV !== 'production') && !Component.render) {
// runtime-only 编译模式,存在 tempalte 不存在 render,则警告如果想要在运行时编译应使用 runtime-compiled 模式
if (!compile && Component.template) {
warn(/* ... */)
}
// 既没有写 render,也没有 template,则警告缺少 template 或 render
else {
warn(/* ... */)
}
}
// 把组件的 render 函数赋值给 instance.render
instance.render = (Component.render || NOOP)
if (instance.render._rc) {
// 对于使用 with 块的运行时编译的渲染函数,使用 RuntimeCompiledPublicInstanceProxyHandlers 代理
instance.withProxy = new Proxy(instance.ctx, RuntimeCompiledPublicInstanceProxyHandlers)
}
}
// 2、兼容 Options API
{
currentInstance = instance
applyOptions(instance, Component)
currentInstance = null
}
}
// 使用 with 块运行时编译的渲染函数的代理处理器,在之前渲染上下文代理 PublicInstanceProxyHandlers 的基础上进行扩展
const RuntimeCompiledPublicInstanceProxyHandlers = {
...PublicInstanceProxyHandlers,
get(target, key) {
if (key === Symbol.unscopables) return
return PublicInstanceProxyHandlers.get(target, key, target)
},
has(_, key) {
// 如果 key 以 _ 开头或者 key 在全局变量白名单内,则 has 为 false
const has = key[0] !== '_' && !isGloballyWhitelisted(key)
if ((process.env.NODE_ENV !== 'production') && !has && PublicInstanceProxyHandlers.has(_, key)) {
warn(/* ... */)
}
return has
}
}
/**
* 兼容 Option API
*/
function applyOptions(instance, options, deferredData = [], deferredWatch = [], asMixin = false) {
const {
mixins, // 混入
extends: extendsOptions, // 继承
props: propsOptions, // 父组件传参
data: dataOptions, // 状态
computed: computedOptions, // 计算属性
methods, // 方法
watch: watchOptions, // 监视属性
provide: provideOptions, // 依赖
inject: injectOptions, // 注入
components, // 组件
directives, // 指令
// 生命周期
beforeMount,
mounted,
beforeUpdata,
updated,
activated,
deactivated,
beforeUnmount,
unmounted,
renderTracked,
renderTriggered,
errorCaptured
} = options
// instance.proxy 作为 this
const publicThis = instance.proxy
const ctx = instance.ctx
// 处理全局 mixin、extend、本地 mixins、inject、methods、data、computed、watch、provide、components、directives、生命周期 option
// ...
}
/**
* setup 渲染副作用函数
* 副作用:当组件数据发生变化时,effect 函数包裹的内部渲染函数 componentEffect 会重新执行一遍,从而达到重新渲染组件的目的
*/
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
// 创建响应式的副作用渲染函数
instance.update = effect(function componentEffect() {
// 如果组件实例 instance 上的 isMounted 属性为 false,说明是初次渲染
/**
* 初始化渲染主要做两件事情:
* 1、渲染组件生成子树 subTree
* 2、把 subTree 挂载到 container 中
*/
if (!instance.isMounted) {
// 1、渲染组件生成子树 vnode
const subTree = (instance.subTree = renderComponentRoor(instance))
// 2、把子树 vnode 挂载到 container 中
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
// 保留渲染生成的子树根 DOM 节点
initialVNode.el = subTree.el
instance.isMounted = true
}
// 更新组件
else {
// ...
}
}, prodEffectOptions)
}
处理普通元素
/**
* 处理 ELEMENT
*/
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
isSVG = isSVG || n2.type === 'svg'
// 旧节点为 null,说明没有旧节点,为第一次渲染,则挂载元素节点
if (n1 == null) {
mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
// 否则更新元素节点
else {
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}
/**
* 挂载元素
* mountElement 主要做了四件事:
* 1、创建 DOM 元素节点
* 2、处理 props
* 3、处理子节点
* 4、把创建的 DOM 元素节点挂载到 container 上
*/
const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
let el
const { type, props, shapeFlag } = vnode
// 1、创建 DOM 元素节点
el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is)
// 2、处理 props,比如 class、style、event 等属性
if (props) {
// 遍历 props,给这个 DOM 节点添加相关的 class、style、event 等属性,并作相关的处理
for (const key in props) {
if (!isReservedProp(key)) {
hostPatchProp(el, key, null, props[key], isSVG)
}
}
}
// 3、处理子节点
// 子节点是纯文本的情况
if (shapeFlag & 8/* TEXT_CHILDREN */) {
hostSetElementText(el, vnode.children)
}
// 子节点是数组的情况
else if (shapeFlag & 16/* ARRAY_CHILDREN */) {
mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren)
}
// 4、把创建的 DOM 元素节点挂载到 container 上
hostInsert(el, container, anchor)
}
/**
* 创建元素
*/
function createElement(tag, isSVG, is) {
// 在 Web 环境下的方式
isSVG ? document.createElementNS(svgNS, tag) : document.createElement(tag, is ? { is } : undefined)
// 如果是其他平台就不是操作 DOM 了,而是平台相关的 API,这些相关的方法是在创建渲染器阶段作为参数传入的
}
/**
* 处理子节点是纯文本的情况
*/
function setElementText(el, text) {
// 在 Web 环境下通过设置 DOM 元素的 textContent 属性设置文本
el.textContent = text
}
/**
* 处理子节点是数组的情况
*/
function mountChildren(children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start = 0) {
// 遍历 chidren,获取每一个 child,递归执行 patch 方法挂载每一个 child
for (let i = start; i < children.length; i++) {
// 预处理 child
const child = (children[i] = optimized ? cloneIfMounted(children[i]) : normalizeVNode(children[i]))
// 执行 patch 挂载 child
// 执行 patch 而非 mountElement 的原因:因为子节点可能有其他类型的 vnode,比如 组件 vnode
patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
}
/**
* 把创建的 DOM 元素节点挂载到 container 下
* 因为 insert 的执行是在处理子节点后,所以挂载的顺序是先子节点,后父节点,最终挂载到最外层的容器上
*/
function insert(child, parent, anchor) {
// 如果有参考元素 anchor,则把 child 插入到 anchor 前
if (anchor) {
parent.insertBefore(child, anchor)
}
// 否则直接通过 appendChild 插入到父节点的末尾
else {
parent.appendChild(child)
}
}
扩展:嵌套组件
组件 vnode 主要维护着组件的定义对象,组件上的各种 props,而组件本身是一个抽象节点,它自身的渲染其实是通过执行组件定义的 render 渲染函数生成的子树 vnode 来完成,然后再通过 patch 这种递归的方式,无论组件的嵌套层级多深,都可以完成整个组件树的渲染。