跳到主要内容

官方模拟版

2023年06月11日
柏拉文
越努力,越幸运

一、packages/runtime-core/src/renderer.ts


import { ShapeFlags } from 'packages/shared/src/shapeFlags';
import { Fragment, isSameVNodeType, normalizeVNode } from './vnode';
import { EMPTY_OBJ, isString } from '@vue/shared';
import { Text, Comment } from './vnode';
import { createComponentInstance, setupComponent } from './component';
import { ReactiveEffect } from 'packages/reactivity/src/effect';
import { queueJob } from './scheduler';
import { renderComponentRoot } from './componentRenderUtils';

export function createRenderer(options) {
return baseCreateRenderer(options);
}

export function baseCreateRenderer(options) {
const {
insert: hostInsert,
remove: hostRemove,
setText: hostSetText,
patchProp: hostPatchProp,
createText: hostCreateText,
createComment: hostCreateComment,
createElement: hostCreateElement,
setElementText: hostSetElementText
} = options;

const patch = (n1, n2, container, anchor = null) => {
if (n1 === n2) {
return;
}

if (n1 && !isSameVNodeType(n1, n2)) {
unmount(n1);
n1 = null;
}

const { type, shapeFlag } = n2;

switch (type) {
case Text:
processText(n1, n2, container, anchor);
break;
case Comment:
processCommentNode(n1, n2, container, anchor);
break;
case Fragment:
processFragment(n1, n2, container, anchor);
break;
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor);
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(n1, n2, container, anchor);
}
}
};

const processText = (n1, n2, container, anchor) => {
if (n1 == null) {
n2.el = hostCreateText(n2.children);
hostInsert(n2.el, container, anchor);
} else {
const el = (n2.el = n1.el);
if (n2.children !== n1.children) {
hostSetText(el, n2.children);
}
}
};

const processCommentNode = (n1, n2, container, anchor) => {
if (n1 == null) {
n2.el = hostCreateComment(n2.children);
hostInsert(n2.el, container, anchor);
} else {
n2.el = n1.el;
}
};

const processFragment = (n1, n2, container, anchor) => {
if (n1 == null) {
mountChildren(n2.children, container, anchor);
} else {
patchChildren(n1, n2, container, anchor);
}
};

const processElement = (n1, n2, container, anchor) => {
if (n1 == null) {
mountElement(n2, container, anchor);
} else {
patchElement(n1, n2);
}
};

const processComponent = (n1, n2, container, anchor) => {
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
} else {
mountComponent(n2, container, anchor);
}
} else {
updateComponent(n1, n2);
}
};

const mountComponent = (initialVNode, container, anchor) => {
const componentMountInstance = initialVNode.component;
const instance =
componentMountInstance ||
(initialVNode.component = createComponentInstance(initialVNode));

setupComponent(instance);
setupRenderEffect(instance, initialVNode, container, anchor);
};

const updateComponent = (n1, n2) => {};

const setupRenderEffect = (instance, initialValue, container, anchor) => {
const componentUpdateFn = () => {
if (!instance.isMounted) {
const { bm, m } = instance;
if (bm) {
bm();
}
const subTree = (instance.subTree = renderComponentRoot(instance));
patch(null, subTree, container, anchor);
initialValue.el = subTree.el;
if (m) {
m();
}
instance.isMounted = true;
} else {
let { next, vnode } = instance;
if (!next) {
next = vnode;
}
const nextTree = renderComponentRoot(instance);
const prevTree = instance.subTree;
instance.subTree = nextTree;
patch(prevTree, nextTree, container, anchor);
next.el = nextTree.el;
}
};

const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => {
queueJob(update);
}
));

const update = (instance.update = () => effect.run());

update();
};

const mountElement = (vnode, container, anchor) => {
let el;
const { type, props, shapeFlag } = vnode;
// 1. 创建 Element
el = vnode.el = hostCreateElement(type);
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 2. 设置 Text
hostSetElementText(el, vnode.children);
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
}
if (props) {
// 3. 设置 Props
for (const key in props) {
hostPatchProp(el, key, null, props[key]);
}
}
// 4. 插入
hostInsert(el, container, anchor);
};

const patchElement = (n1, n2) => {
const el = (n2.el = n1.el!);
const oldProps = n1.props || EMPTY_OBJ;
const newProps = n2.props || EMPTY_OBJ;
patchChildren(n1, n2, el, null);
patchProps(el, n2, oldProps, newProps);
};

const mountChildren = (children, container, anchor) => {
if (isString(children)) {
children = children.split('');
}
for (let i = 0; i < children.length; i++) {
const child = (children[i] = normalizeVNode(children[i]));
patch(null, child, container, anchor);
}
};

const patchChildren = (n1, n2, container, anchor) => {
const c1 = n1 && n1.children;
const prevShapFlag = n1 ? n1.shapeFlag : 0;
const c2 = n2 && n2.children;
const { shapeFlag } = n2;

if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 新节点为文本节点
if (prevShapFlag & ShapeFlags.ARRAY_CHILDREN) {
// 新节点为文本节点 && 旧节点为数组 则卸载旧子节点
}

if (c2 !== c1) {
// 挂载新子节点文本
hostSetElementText(container, c2);
}
} else {
// 新节点不为文本节点
if (prevShapFlag & ShapeFlags.ARRAY_CHILDREN) {
// 新节点不为文本节点 && 旧节点为数组节点
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 新节点为数组节点 && 旧节点为数组节点 则 Diff 运算
} else {
// 新节点为数组节点 & 旧节点不为数组节点 则卸载旧子节点
}
} else {
if (prevShapFlag & ShapeFlags.TEXT_CHILDREN) {
// 新节点不为文本节点 && 旧节点为文本节点 则下载旧子节点
hostSetElementText(container, '');
}
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
}
}
}
};

const patchProps = (el, vnode, oldProps, newProps) => {
if (oldProps !== newProps) {
if (oldProps !== EMPTY_OBJ) {
for (const key in oldProps) {
if (!(key in newProps)) {
hostPatchProp(el, key, oldProps[key], null);
}
}
}
for (const key in newProps) {
const next = newProps[key];
const prev = oldProps[key];
if (next !== prev) {
hostPatchProp(el, key, prev, next);
}
}
}
};

const unmount = vnode => {
hostRemove(vnode.el as Node);
};

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
};
}

二、packages/runtime-core/src/component.ts


import { ShapeFlags } from 'packages/shared/src/shapeFlags';
import { applyOptions } from './componentOptions';
import { isFunction } from '@vue/shared';

let uid = 0;

export function createComponentInstance(vnode) {
const type = vnode.type;
const instance = {
uid: uid++,
type,
vnode,
effect: null,
render: null,
update: null,
subTree: null,

// lifecycle hooks
isMounted: false,
isUnmounted: false,
isDeactivated: false,
bc: null,
c: null,
bm: null,
m: null,
bu: null,
u: null,
um: null,
bum: null,
da: null,
a: null,
rtg: null,
rtc: null,
ec: null,
sp: null
};
return instance;
}

export function setupComponent(instance) {
const isStateful = isStatefulComponent(instance);
const setupResult = isStateful ? setupStatefulComponent(instance) : undefined;
return setupResult;
}

export function isStatefulComponent(instance) {
return instance.vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT;
}

export function setupStatefulComponent(instance) {
const Component = instance.type;
const { setup } = Component;
if (setup) {
const setupResult = setup();
handleSetupResult(instance, setupResult);
} else {
finishComponentSetup(instance);
}
}

export function handleSetupResult(instance, setupResult) {
if (isFunction(setupResult)) {
instance.render = setupResult;
}

finishComponentSetup(instance);
}

export function finishComponentSetup(instance) {
const Component = instance.type;
if (!instance.render) {
instance.render = Component.render;
}
applyOptions(instance);
}

三、packages/runtime-core/src/componentOptions.ts


import { isArray, isObject } from '@vue/shared';
import { reactive } from 'vue';
import { onBeforeMount, onMounted } from './apiLifecycle';

export function callHook(hook, proxy) {
hook.bind(proxy)();
}

export function applyOptions(instance) {
const {
data: dataOptions,
beforeCreate,
created,
beforeMount,
mounted
} = instance.type;

if (beforeCreate) {
callHook(beforeCreate, instance.data);
}

if (dataOptions) {
const data = dataOptions();
if (isObject(data)) {
instance.data = reactive(data);
}
}

if (created) {
callHook(created, instance.data);
}

function registerLifecycleHook(register, hook) {
if (isArray(hook)) {
hook.forEach(_hook => register(_hook.bind(instance.data), instance));
} else if (hook) {
register(hook.bind(instance.data), instance);
}
}

registerLifecycleHook(onBeforeMount, beforeMount);
registerLifecycleHook(onMounted, mounted);
}

四、packages/runtime-core/src/apiLifecycle.ts


import { LifecycleHooks } from './enums';

export function injectHook(type, hook, target) {
if (target) {
target[type] = hook;
return hook;
}
}

export const createHook = lifecycle => {
return (hook, target) => injectHook(lifecycle, hook, target);
};

export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT);
export const onMounted = createHook(LifecycleHooks.MOUNTED);

五、packages/runtime-core/src/componentRenderUtils.ts


import { ShapeFlags } from 'packages/shared/src/shapeFlags';
import { normalizeVNode } from './vnode';

export function renderComponentRoot(instance) {
const { type, vnode, render, data } = instance;
let result;
try {
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
result = normalizeVNode(render.call(data));
}
} catch (error) {
console.log(error);
}
return result;
}

六、packages/runtime-core/src/enums.ts


export const enum LifecycleHooks {
BEFORE_CREATE = 'bc',
CREATED = 'c',
BEFORE_MOUNT = 'bm',
MOUNTED = 'm',
BEFORE_UPDATE = 'bu',
UPDATED = 'u',
BEFORE_UNMOUNT = 'bum',
UNMOUNTED = 'um',
DEACTIVATED = 'da',
ACTIVATED = 'a',
RENDER_TRIGGERED = 'rtg',
RENDER_TRACKED = 'rtc',
ERROR_CAPTURED = 'ec',
SERVER_PREFETCH = 'sp'
}

测试用例


render h Component

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue3-Next-Mini Render Component</title>
<script type="importmap">
{
"imports": {
"vue": "../packages/vue/dist/vue.js"
}
}
</script>
</head>
<body>
<div id="app"></div>
<script type="module">
import { h, render } from 'vue';

const component1 = {
render() {
return h('div', 'hello component mount');
}
};

const vnode1 = h(component1);
render(vnode1, document.querySelector('#app'));

setTimeout(() => {
const component2 = {
render() {
return h('div', 'hello component update');
}
};

const vnode2 = h(component2);
render(vnode2, document.querySelector('#app'));
}, 3000);
</script>
</body>
</html>

render h Component data state

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue3-Next-Mini Render Component Data State</title>
<script type="importmap">
{
"imports": {
"vue": "../packages/vue/dist/vue.js"
}
}
</script>
</head>
<body>
<div id="app"></div>
<script type="module">
import { h, render } from 'vue';

const component1 = {
data() {
return {
msg: 'hello component mount'
};
}
};

const vnode1 = h(component1);
render(vnode1, document.querySelector('#app'));
</script>
</body>
</html>

render h Component data lifecycle

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue3-Next-Mini Render Component Data State</title>
<script type="importmap">
{
"imports": {
"vue": "../packages/vue/dist/vue.js"
}
}
</script>
</head>
<body>
<div id="app"></div>
<script type="module">
import { h, render } from 'vue';

const component1 = {
data() {
return {
msg: 'hello component mount'
};
},
render() {
return h('div', this.msg);
},
beforeCreate() {
console.log('beforeCreate');
},
created() {
console.log('created');
},
beforeMount() {
console.log('beforeMount');
},
mounted() {
console.log('mounted');
}
};

const vnode1 = h(component1);
render(vnode1, document.querySelector('#app'));
</script>
</body>
</html>

render h Component data state lifecycle

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue3-Next-Mini Render Component Data State</title>
<script type="importmap">
{
"imports": {
"vue": "../packages/vue/dist/vue.js"
}
}
</script>
</head>
<body>
<div id="app"></div>
<script type="module">
import { h, render } from 'vue';

const component1 = {
data() {
return {
msg: 'hello component mount'
};
},
render() {
return h('div', this.msg);
},
beforeCreate() {
console.log('beforeCreate', this);
},
created() {
console.log('created', this);
},
beforeMount() {
console.log('beforeMount', this);
},
mounted() {
setTimeout(() => {
this.msg = 'hello component update';
}, 3000);
console.log('mounted', this);
}
};

const vnode1 = h(component1);
render(vnode1, document.querySelector('#app'));
</script>
</body>
</html>

render h Component setup

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue3-Next-Mini Render Component setup</title>
<script type="importmap">
{
"imports": {
"vue": "../packages/vue/dist/vue.js"
}
}
</script>
</head>
<body>
<div id="app"></div>
<script type="module">
import { h, render, reactive } from 'vue';

const component1 = {
setup() {
const obj = reactive({
msg: 'hello component mount'
});

setTimeout(() => {
obj.msg = 'hello component update';
}, 3000);

return () => h('div', obj.msg);
}
};

const vnode1 = h(component1);
render(vnode1, document.querySelector('#app'));
</script>
</body>
</html>