Vue3的Dom渲染
runtime-dom
- Vue的dom操作模块放在runtime-dom中
体验下
- 核心api是h与createRenderer
- h创建虚拟dom对象(h不再runtime-dom里,在runtime-core中)
- createRenderer接收一些固定参数,返回的结果有一个render方法,这个方法会执行dom插入
<script src="../../../node_modules/@vue/runtime-dom/dist/runtime-dom.global.js"></script>
const app = document.querySelector('#app');
let {
createRenderer,
h
} = VueRuntimeDOM;
let renderer = createRenderer({
createElement(element) {
return document.createElement(element)
},
setElementText(el, text) {
el.innerHTML = text;
},
insert(el, container) {
container.appendChild(el)
},
patchProp(el, key, prevValue, nextValue) {}
});
renderer.render(h('h1', {
style: {
color: 'red'
}
}, 'hello word'), app)
实现
index.ts
import { nodeOps } from "./nodeOps";
import { patchProp } from "./patchProp";
const renderOptions = Object.assign(nodeOps, { patchProp });
console.log(renderOptions);
nodeOps.ts
export const nodeOps = {
insert(child, parent, anchor = null) {
parent.insertBefore(child, anchor);
},
remove(child) {
let parentNode = child.parentNode;
if (parentNode) {
parentNode.removeChild(child);
}
},
setElementText(el, text) {
el.textContent = text;
},
setText(node, text) {
node.nodeValue = text;
},
querySelector(selector) {
return document.querySelector(selector);
},
parentNode(node) {
return node.parentNode;
},
nextSibling(node) {
return node.nextSibling;
},
createElement(tagName) {
return document.createElement(tagName);
},
createText(text) {
return document.createTextNode(text);
}
}
pathProp.ts
import { patchAttr } from "./modules/attr";
import { patchClass } from "./modules/class";
import { patchEvent } from "./modules/event";
import { patchStyle } from "./modules/style";
export function patchProp(el, key, prevValue, nextValue) {
if (key === 'class') {
patchClass(el, nextValue);
} else if (key === 'style') {
patchStyle(el, prevValue, nextValue);
} else if (/^on[^a-z]/.test(key)) {
patchEvent(el, key, nextValue);
} else {
patchAttr(el, key, nextValue);
}
}
modules/
- 将pathProp.ts使用到的公共方法抽离出来,每个方法一个文件,保证代码的可读性
attr.ts
export function patchAttr(el, key, nextValue) {
if (nextValue) {
el.setAttribute(key, nextValue)
} else {
el.removeAttribute(key);
}
}
class.ts
export function patchClass(el, nextValue) {
if (nextValue == null) {
el.removeAttribute('class');
} else {
el.className = nextValue;
}
}
event.ts
function createInvoker(preValue) {
const invoker = (e) => { invoker.value(e) };
invoker.value = preValue;
return invoker;
}
export function patchEvent(el, eventName, nextValue) {
const invokers = el._vei || (el._vel = {});
const exitingInvoker = invokers[eventName];
if (exitingInvoker && nextValue) {
exitingInvoker.value = nextValue;
} else {
const eName = eventName.slice(2).toLowerCase();
if (nextValue) {
const invoker = createInvoker(nextValue);
invokers[eventName] = invoker;
el.addEventListener(eName, invoker)
} else if (exitingInvoker) {
el.removeEventListene(eName, exitingInvoker);
invokers[eventName] = null;
}
}
}
style.ts
export function patchStyle(el, prevValue, nextValue) {
prevValue = prevValue || {};
nextValue = nextValue || {};
for (let key in nextValue) {
el.style[key] = nextValue[key];
}
for (let key in prevValue) {
if (nextValue[key] == null) {
el.style[key] = null;
}
}
}
runtime-core
- 核心api是h,作用为创建一个虚拟dom对象
- h函数内部依赖于createVNode
h
- 传入固定的值,返回虚拟dom对象
- 子节点只要不是文本,会转成数组
- 使用方式:
- h(元素,{属性},文本)
- h(元素,{属性},[儿子,儿子])
- h(元素,null,[儿子,儿子])
实现h
import { isArray, isObject } from "@vue/shared";
import { isVNode, createVNode } from "./createVNode";
export function h(type, propsOrChildren, children = null) {
let l = arguments.length;
if (l === 2) {
if (isObject(children) && !isArray(children)) {
if (isVNode(propsOrChildren)) {
return createVNode(type, null, [propsOrChildren]);
}
return createVNode(type, propsOrChildren);
} else {
return createVNode(type, null, propsOrChildren);
}
} else {
if (l > 3) {
children = Array.from(arguments).slice(2);
}
else if (l === 3 && isVNode(children)) {
children = [children];
}
return createVNode(type, propsOrChildren, children);
}
}
createVNode
import { isArray, isString } from "@vue/shared";
import { isArray, isString } from "@vue/shared";
export const Text = Symbol('Text');
export function isSameVnode(n1, n2) {
return (n1.type === n2.type) && (n1.key === n2.key)
}
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
}
export function isVNode(value: any) {
return value ? value.__v_isVNode === true : false
}
export function createVNode(type, props = null, children = null) {
let shapeFlag = isString(type) ? ShapeFlags.ELEMENT : 0;
const vnode = {
__v_isVNode: true,
type,
props,
children,
key: props && props.key,
el: null,
shapeFlag
}
if (children) {
let temp = 0;
if (isArray(children)) {
temp = ShapeFlags.ARRAY_CHILDREN;
} else {
children = String(children);
temp = ShapeFlags.TEXT_CHILDREN;
}
vnode.shapeFlag |= temp;
}
return vnode;
}
runderer.ts
import { isString } from "@vue/shared";
import { ShapeFlags, Text, createVNode, isSameVnode } from "./createVNode";
export function createRenderer(renderOptions) {
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
} = renderOptions;
const normlize = (children, i) => {
if (isString(children[i])) {
let vnode = createVNode(Text, null, children[i]);
children[i] = vnode;
}
return children[i];
}
const mountChildren = (children, container) => {
for (let i = 0; i < children.length; i++) {
let child = normlize(children, i);
patch(null, child, container);
}
}
const unmount = (vnode) => {
hostRemove(vnode.el)
}
const processText = (n1, n2, container) => {
if (n1 == null) {
hostInsert(n2.el = hostCreateText(n2.children), container)
} else {
const el = n2.el = n1.el;
hostSetText(el, n2.children)
}
}
const mountElement = (vnode, container, anchor = null) => {
let { type, props, shapeFlag, children } = vnode;
let el = vnode.el = hostCreateElement(type);
hostInsert(el, container, anchor);
if (props) {
for (let key in props) {
hostPatchProp(el, key, null, props[key]);
}
}
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, children);
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(children, el);
}
}
const patchProps = (oldProps, newProps, el) => {
for (let key in newProps) {
hostPatchProp(el, key, oldProps[key], newProps[key]);
}
for (let key in oldProps) {
if (newProps[key] == null) {
hostPatchProp(el, key, oldProps[key], null);
}
}
}
const unmountChildren = (children) => {
for (let i = 0; i < children.length; i++) {
unmount(children[i])
}
}
const patchKeyChildren = (c1, c2, el) => {
let i = 0;
let e1 = c1.length - 1;
let e2 = c2.length - 1;
while (i <= e1 && i <= e2) {
const n1 = c1[i];
const n2 = c2[i];
if (isSameVnode(n1, n2)) {
patch(n1, n2, el);
} else {
break;
}
i++;
}
while (i <= e1 && i <= e2) {
const n1 = c1[e1];
const n2 = c2[e2];
if (isSameVnode(n1, n2)) {
patch(n1, n2, el);
} else {
break;
}
e1--;
e2--;
}
if (i > e1) {
if (i <= e2) {
while (i <= e2) {
const nextPos = e2 + 1;
const anchor = nextPos < c2.length ? c2[nextPos].el : null;
patch(null, c2[i], el, anchor);
i++;
}
}
} else if (i > e2) {
if (i <= e1) {
while (i <= e1) {
unmount(c1[i]);
i++;
}
}
}
let s1 = i;
let s2 = i;
let keyToNewIndexMap = new Map();
for (let i = s2; i <= e2; i++) {
keyToNewIndexMap.set(c2[i].key, i);
}
const toBePatched = e2 - s2 + 1;
const newIndexToOldIndex = new Array(toBePatched).fill(0);
for (let i = s1; i < e1; i++) {
const oldChild = c1[i];
let newIndex = keyToNewIndexMap.get(oldChild.key);
if (!newIndex) {
unmount(oldChild);
} else {
newIndexToOldIndex[newIndex - s2] = i + 1;
patch(oldChild, c2[newIndex], el);
}
}
for (let i = toBePatched - 1; i >= 0; i--) {
let index = i + s2;
let current = c2[index];
let anchor = (index + 1) < c2.length ? c2[index + 1].el : null;
if (newIndexToOldIndex[i] === 0) {
patch(null, current, el, anchor);
} else {
hostInsert(current.el, el, anchor);
}
}
}
const patchChildren = (n1, n2, el) => {
const c1 = n1.children;
const c2 = n2.children;
const preShafeFlag = n1.shapeFlag;
const shafeFlag = n2.shapeFlag;
if (shafeFlag & ShapeFlags.TEXT_CHILDREN) {
if (preShafeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(c1);
}
if (c1 !== c2) {
hostSetElementText(el, c2)
}
} else {
if (preShafeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (shafeFlag & ShapeFlags.ARRAY_CHILDREN) {
patchKeyChildren(c1, c2, el)
} else {
unmountChildren(c1);
}
} else {
if (preShafeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, '');
}
if (shafeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(c2, el);
}
}
}
}
const patchElement = (n1, n2) => {
let el = n2.el = n1.el;
let oldProps = n1.props || {};
let newProps = n2.props || {};
patchProps(oldProps, newProps, el);
patchChildren(n1, n2, el);
}
const processElement = (n1, n2, container, anchor = null) => {
if (n1 === null) {
mountElement(n2, container, anchor);
} else {
patchElement(n1, n2)
}
}
const patch = (n1, n2, container, anchor = null) => {
if (n1 === n2) return;
if (n1 && !isSameVnode(n1, n2)) {
unmount(n1);
n1 = null;
}
const { type, shapeFlag } = n2;
switch (type) {
case Text:
processText(n1, n2, container);
break;
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor);
}
}
}
const render = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode);
}
} else {
patch(container._vnode || null, vnode, container);
}
container._vnode = vnode;
}
return { render };
}
index.ts
export { createVNode,Text } from './createVNode';
export { createRenderer } from './renderer'
export * from '@vue/reactivity'
export { h } from './h';