Vue3源码解读 - Vue3 渲染原理
1. Vue3 自定义渲染器
自定义渲染器 的作用是把虚拟 DOM 渲染为特定平台上的真实元素 。在浏览器中,渲染器会把虚拟 DOM
渲染成真实 DOM
元素。
ts
import { createRenderer } from "vue";
const { render, createApp } = createRenderer<Node, Element>({
patchProp,
...nodeOps,
});
定义渲染器可以传入特定于平台的类型,如下所示:
js
const { createRenderer, h } = Vue;
const renderer = createRenderer({
createElement(element) {
return document.createElement(element);
},
setElementText(el, text) {
el.innerHTML = text;
},
insert(el, container) {
container.appendChild(el);
},
});
renderer.render(h("h1", "hello world"), document.getElementById("app"));
2. 节点操作 和 比对属性 实现
Vue3 中将
runtime
模块分为runtime-core
核心代码 及 其他平台对应的运行时 ,那么VueRuntimeDOM
无疑就是解决浏览器运行时 的问题,此包中提供了 DOM 属性操作 和节点操作一系列接口。
2.1 创建 runtime-dom 包
runtime-dom
针对浏览器运行时,包括 DOM API
、属性、事件处理等
runtime-dom/package.json
json
{
"name": "@vue/runtime-dom",
"main": "index.js",
"module": "dist/runtime-dom.esm-bundler.js",
"unpkg": "dist/runtime-dom.global.js",
"buildOptions": {
"name": "VueRuntimeDOM",
"formats": ["esm-bundler", "cjs", "global"]
}
}
bash
pnpm install @vue/shared@workspace --filter @vue/runtime-dom
2.2 实现节点常用操作 nodeOps
runtime-dom/src/nodeOps
这里存放常见 DOM 操作 API ,不同运行时提供的具体实现不一样,最终将操作方法传递到runtime-core
中,所以runtime-core
不需要关心平台相关代码~
nodeOps
这里存放着所有的节点操作的方法
js
export const nodeOps = {
// 添加节点
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null);
},
// 删除节点
remove: (child) => {
const parent = child.parentNode;
if (parent) {
parent.removeChild(child);
}
},
// 创建节点
createElement: (tag, isSVG, is, props): Element => {
const el = isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined)
if (tag === 'select' && props && props.multiple != null) {
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
}
return el
},
// 创建文本
createText: text => doc.createTextNode(text),
// 创建文本
createComment: text => doc.createComment(text),
// 设置文本节点内容
setText: (node, text) => {
node.nodeValue = text
},
// 设置元素中的内容
setElementText: (el, text) => {
el.textContent = text
},
// 父亲节点
parentNode: node => node.parentNode as Element | null,
// 下一个节点
nextSibling: node => node.nextSibling,
// 搜索元素
querySelector: selector => doc.querySelector(selector),
// 设置一个作用域id
setScopeId(el, id) {
el.setAttribute(id, '')
},
};
设置一个作用域 id
作用域 id 通常是一个随机生成的字符串,用于确保每个组件的样式都是唯一的,不会影响到其他组件。
js
setScopeId(el, id) {
el.setAttribute(id, '')
},
这个就是我们平常看到的标签上面的自定义属性,只有 key(scopeId
),没有 value
2.3 比对属性方法 patchProp
此方法主要针对不同的属性提供不同的
patch
操作
js
import { patchClass } from "./modules/class"; // 类名处理
import { patchStyle } from "./modules/style"; // 样式处理
import { patchEvent } from "./modules/events"; // 事件处理
import { patchAttr } from "./modules/attrs"; // 属性处理
import { isOn } from "@vue/shared";
export const patchProp = (el, key, prevValue, nextValue) => {
switch (key) {
// class类名
case "class":
patchClass(el, nextValue);
break;
// 样式
case "style":
patchStyle(el, prevValue, nextValue);
break;
default:
if (isOn(key)) {
// 如果是事件
patchEvent(el, key, nextValue);
} else {
// html 标签属性
patchAttr(el, key, nextValue);
}
break;
}
};
export const patchProp: DOMRendererOptions['patchProp'] = (
el,
key,
prevValue,
nextValue,
isSVG = false,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
) => {
if (key === 'class') {
// class类名
patchClass(el, nextValue, isSVG)
} else if (key === 'style') {
// 样式
patchStyle(el, prevValue, nextValue)
} else if (isOn(key)) {
// 如果是事件,并且忽略v-model的监听事件:onModelUpdate
if (!isModelListener(key)) {
patchEvent(el, key, prevValue, nextValue, parentComponent)
}
} else if (
key[0] === '.'
? ((key = key.slice(1)), true)
: key[0] === '^'
? ((key = key.slice(1)), false)
: shouldSetAsProp(el, key, nextValue, isSVG)
) {
patchDOMProp(
el,
key,
nextValue,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
)
} else {
// 处理特殊情况下的input元素的v-model指令
// 因为当input元素的type属性为checkbox时,v-model指令还可以接受两个可选的属性:true-value和false-value
// 由于非字符串的属性值在DOM中会被强制转换为字符串,所以这里需要将属性值存储到DOM元素的属性中,以便在后续处理中使用
if (key === 'true-value') {
;(el as any)._trueValue = nextValue
} else if (key === 'false-value') {
;(el as any)._falseValue = nextValue
}
// html 标签属性:将属性值应用到DOM元素上
patchAttr(el, key, nextValue, isSVG, parentComponent)
}
}
2.3.1 patchClass(操作类名)
如果当前元素正在进行过渡动画,就需要将过渡动画中使用的临时 class 也加入到 class 属性中。
如果 class 属性的值为null或undefined
,就将 class 属性从 DOM 元素上移除。
如果当前元素为 SVG 元素,则需要使用 setAttribute 方法将 class 属性应用到 DOM 元素上。都不是的话,就直接设置 DOM 元素的 className 属性就好了。
js
function patchClass(el: Element, value: string | null, isSVG: boolean) {
const transitionClasses = (el as ElementWithTransition)[vtcKey]
if (transitionClasses) {
value = (
value ? [value, ...transitionClasses] : [...transitionClasses]
).join(' ')
}
if (value == null) {
el.removeAttribute('class')
} else if (isSVG) {
el.setAttribute('class', value)
} else {
el.className = value
}
}
2.3.2 patchStyle(操作样式)
js
function patchStyle(el: Element, prev: Style, next: Style) {
const style = (el as HTMLElement).style
const isCssString = isString(next)
// 更新style
if (next && !isCssString) {
if (prev && !isString(prev)) {
for (const key in prev) {
if (next[key] == null) {
// 老的有,新的没有,那就清空掉
setStyle(style, key, '')
}
}
}
// 再用最新的直接覆盖
for (const key in next) {
setStyle(style, key, next[key])
}
} else {
const currentDisplay = style.display
if (isCssString) {
if (prev !== next) {
style.cssText = next as string
}
} else if (prev) {
// 老的有新的没有删除
el.removeAttribute('style')
}
// v-show这个指令是用于控制元素的显示和隐藏,是通过控制元素的display属性来实现
// 但是有个特殊情况:如果元素的display属性已经被其他样式规则控制,那么v-show指令就需要将控制权交给其他样式规则,将display属性设置为当前的display属性值
if (vShowOldKey in el) {
style.display = currentDisplay
}
}
}
2.3.3 patchEvent(操作事件)
根据事件的旧值(prevValue
)和 新值(nextValue
),来添加、删除或更新事件监听器
js
function createInvoker(initialValue) {
const invoker = (e) => invoker.value(e);
invoker.value = initialValue;
return invoker;
}
function patchEvent(
el: Element & { [veiKey]?: Record<string, Invoker | undefined> },
rawName: string,
prevValue: EventValue | null,
nextValue: EventValue | null,
instance: ComponentInternalInstance | null = null
) {
// 更新事件
const invokers = el[veiKey] || (el[veiKey] = {})
// 如果绑定过,则替换为新的
const existingInvoker = invokers[rawName]
if (nextValue && existingInvoker) {
// 将旧监听器对象的value属性更新为新
existingInvoker.value = nextValue
} else {
// 转化事件为小写的
const [name, options] = parseName(rawName)
if (nextValue) {
// 增加事件监听
const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
// 添加该事件的监听器
addEventListener(el, name, invoker, options)
} else if (existingInvoker) {
// 需要删除该事件的监听器
removeEventListener(el, name, existingInvoker, options)
invokers[rawName] = undefined
}
}
}
看看这里的
createInvoker
: 在绑定事件的时候,绑定一个伪造的事件处理函数invoker
,把真正的事件处理函数设置为invoker.value
属性的值
2.3.4 patchAttr(操作属性)
js
function patchAttr(
el: Element,
key: string,
value: any,
isSVG: boolean,
instance?: ComponentInternalInstance | null
) {
if (isSVG && key.startsWith("xlink:")) {
if (value == null) {
el.removeAttributeNS(xlinkNS, key.slice(6, key.length));
} else {
// 说明属于XML命名空间中的属性,此时需要使用setAttributeNS方法来设置属性
el.setAttributeNS(xlinkNS, key, value);
}
} else {
if (__COMPAT__ && compatCoerceAttr(el, key, value, instance)) {
return;
}
// 在Vue2中,如果一个布尔属性的值为true,则会将该属性的名称作为属性值应用到元素上。
// 但是Vue3中,布尔属性的值为true时,只需要将该属性设置为空字符串即可
const isBoolean = isSpecialBooleanAttr(key);
if (value == null || (isBoolean && !includeBooleanAttr(value))) {
el.removeAttribute(key);
} else {
el.setAttribute(key, isBoolean ? "" : value);
}
}
}
3. Runtime-Dom 实现
runtime-dom
主要提供dom
操作的方法
3.1 createApp
用户调用的
createApp
函数就在这里被声明
js
import { extend } from "@vue/shared";
import { patchProp } from "./patchProp";
import { nodeOps } from "./nodeOps";
// runtimeDom中对dom操作的所有选项
const rendererOptions = extend({ patchProp }, nodeOps);
// 用户调用的createApp方法,此时才会创建渲染器
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
if (__DEV__) {
injectNativeTagCheck(app)
injectCompilerOptionsCheck(app)
}
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
// 这里需要注意的是,由于innerHTML属性可以包含JavaScript代码,因此在使用innerHTML属性时需要注意安全性问题
// 如果这些模板是由服务器渲染的,那么应该确保它们不包含任何用户数据。
// 这有点像react的 dangerouslySetInnerHTML 属性
component.template = container.innerHTML
if (__COMPAT__ && __DEV__) {
for (let i = 0; i < container.attributes.length; i++) {
const attr = container.attributes[i]
if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
compatUtils.warnDeprecation(
DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
null
)
break
}
}
}
}
// 清空容器内容
container.innerHTML = ''
// 执行挂载逻辑
const proxy = mount(container, false, container instanceof SVGElement)
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
return proxy
}
return app
}) as CreateAppFunction<Element>
更多关于createApp
的原理可以移步Vue3 源码解读之 createApp
3.2 createRenderer(创建渲染器)
最终我们在
index.js
中引入写好的方法,渲染选项就准备好了。 稍后将虚拟 DOM 转化成真实 DOM 会调用这些方法
js
// -----------这些逻辑移动到core中与平台代码无关--------------
function createRenderer(rendererOptions) {
return {
createApp(rootComponent, rootProps) {
// 用户创建app的参数
const app = {
mount(container) {
// 挂载的容器
},
};
return app;
},
};
}
createRenderer
接收渲染所需的方法,h
方法为创建虚拟节点 的方法。这两个方法和平台无关,所以我们将这两个方法在runtime-core
中实现。
js
import { nodeOps } from "./nodeOps";
import { patchProp } from "./patchProp";
// 准备好所有渲染时所需要的的属性
const renderOptions = Object.assign({ patchProp }, nodeOps);
createRenderer(renderOptions).render(
h("h1", "james"),
document.getElementById("app")
);
4. Runtime-Core 实现
4.1 创建 runtime-core 包
runtime-core
不关心运行平台。
runtime-core/package.json
json
{
"name": "@vue/runtime-core",
"module": "dist/runtime-core.esm-bundler.js",
"types": "dist/runtime-core.d.ts",
"files": ["index.js", "dist"],
"buildOptions": {
"name": "VueRuntimeCore",
"formats": ["esm-bundler", "cjs"]
}
}
runtime-core
中需要依赖@vue/shared
及@vue/reactivity
bash
pnpm install @vue/shared@workspace @vue/reactivity@workspace --filter @vue/runtime-core
最后我们将开发环境下的打包入口改为 runtime-dom
4.2 createRenderer
renderer.ts
js
import { createAppAPI } from "./apiCreateApp";
export function createRenderer(rendererOptions) {
// 渲染时所到的api
const render = (vnode, container) => {
// 核心渲染方法
// 将虚拟节点转化成真实节点插入到容器中
};
return {
createApp: createAppAPI(render),
};
}
4.3 createAppAPI
js
export function createAppAPI(render) {
return function createApp(rootComponent, rootProps = null) {
const app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent, // 组件
_props: rootProps, // 属性
_container: null,
_context: context,
_instance: null,
mount(rootContainer) {
// 1.通过rootComponent 创建vnode
// 2.调用render方法将vnode渲染到rootContainer中
},
};
return app;
};
}
4.4 虚拟节点的实现
4.4.1 形状标识
通过组合可以描述虚拟节点的类型
js
// vue3使用位运算符提供的形状标识
export const enum ShapeFlags {
ELEMENT = 1,
FUNCTIONAL_COMPONENT = 1 << 1,
STATEFUL_COMPONENT = 1 << 2,
TEXT_CHILDREN = 1 << 3,
ARRAY_CHILDREN = 1 << 4,
SLOTS_CHILDREN = 1 << 5,
TELEPORT = 1 << 6,
SUSPENSE = 1 << 7,
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
COMPONENT_KEPT_ALIVE = 1 << 9,
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
4.4.2 createVNode
实现
js
export function isVNode(value: any) {
return value ? value.__v_isVNode === true : false;
}
export const createVNode = (type, props, children = null) => {
const shapeFlag = isString(type) ? ShapeFlags.ELEMENT : 0;
const vnode = {
__v_isVNode: true,
type,
props,
key: props && props["key"],
el: null,
children,
shapeFlag,
};
if (children) {
let type = 0;
if (Array.isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN;
} else {
children = String(children);
type = ShapeFlags.TEXT_CHILDREN;
}
vnode.shapeFlag |= type;
// 如果shapeFlag为9 说明元素中包含一个文本
// 如果shapeFlag为17 说明元素中有多个子节点
}
return vnode;
};
export const createVNode = (
__DEV__ ? createVNodeWithArgsTransform : _createVNode
) as typeof _createVNode
createVNode
的写法比较死板,我们让他变的更灵活些
4.4.3 h
函数的实现
js
export function h(type, propsOrChildren?, children?) {
const l = arguments.length;
if (l === 2) {
// 只有属性,或者一个元素子节点的时候
if (isObject(propsOrChildren) && !Array.isArray(propsOrChildren)) {
if (isVNode(propsOrChildren)) {
// h('div',h('span'))
return createVNode(type, null, [propsOrChildren]);
}
return createVNode(type, propsOrChildren); // h('div',{style:{color:'red'}});
} else {
// 传递子节点列表的情况
return createVNode(type, null, propsOrChildren); // h('div',null,[h('span'),h('span')])
}
} else {
if (l > 3) {
// 超过3个除了前两个都是子节点
children = Array.prototype.slice.call(arguments, 2);
} else if (l === 3 && isVNode(children)) {
children = [children]; // 子节点是元素,就将其包装成 h('div',null,[h('span')])
}
return createVNode(type, propsOrChildren, children); // h('div',null,'james')
}
}
// 注意子节点是:数组、文本、null
4.5 createRenderer
实现
render
方法就是采用 runtime-dom
中提供的方法将虚拟节点 转化成真实的DOM 节点渲染到指定容器中。
js
export function createRenderer(options) {
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
} = options;
const patch = (n1, n2, container) => {
// 初始化节点和节点的diff算法都在这里
};
const render = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
} // 卸载
} else {
patch(container._vnode || null, vnode, container); // 初始化和更新
}
container._vnode = vnode;
};
return {
render,
};
}
4.6 创建真实 DOM
源代码的实现考虑的非常全面,下面只跳出重要的几个地方来解释下:
js
const mountChildren: MountChildrenFn = (
children,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
start = 0
) => {
for (let i = start; i < children.length; i++) {
const child = (children[i] = optimized
? cloneIfMounted(children[i] as VNode)
: normalizeVNode(children[i]))
patch(
null,
child,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
const mountElement = (vnode, container) => {
const { type, props, shapeFlag } = vnode;
let el = (vnode.el = hostCreateElement(type)); // 创建真实元素,挂载到虚拟节点上
if (props) {
// 处理属性
for (const key in props) {
// 更新元素属性
hostPatchProp(el, key, null, props[key]);
}
}
// 文本节点
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 文本
hostSetElementText(el, vnode.children);
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 多个子节点
mountChildren(vnode.children, el);
}
hostInsert(el, container); // 插入到容器中
};
const patch = (n1, n2, container) => {
// 初始化节点和节点的diff算法都在这里
if (n1 == n2) {
return;
}
if (n1 == null) {
// 初始化的情况
mountElement(n2, container);
} else {
// diff算法
}
};
4.7 卸载 DOM
js
createRenderer(renderOptions).render(null, document.getElementById("app"));
js
const unmount = (vnode) => {
hostRemove(vnode.el);
};
const render = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
// 卸载
unmount(container._vnode); // 找到对应的真实节点将其卸载
}
} else {
patch(container._vnode || null, vnode, container); // 初始化和更新
}
container._vnode = vnode;
};
总结
本文我们学习了 Vue3 自定义渲染器,作用是将虚拟 DOM 渲染为真实的DOM元素。
然后渲染成展示真实的DOM元素 之前,涉及到节点操作 和比对属性
节点操作包括dom节点的创建,更新和删除等方法
比对属性包括节点的class,style和事件
还有就是Runtime-Dom包
提供了createApp
的方法
Runtime-Core
包则是提供创建渲染器createRenderer
、createAppAPI
、以及虚拟节点的实现、创建真实 DOM
和 卸载 DOM
等方法