Vue3源码解析
- 运行
- runtime-core
- src/createApp.ts
- src/vnode.ts.ts
- src/renderer.ts
- runtime-dom
- src/index.ts
- 总结
运行
runtime-core
src/createApp.ts
vue的创建入口
import { createVNode } from "./vnode";
export function createAppAPI(render) {
return function createApp(rootComponent) {
// 将根节点组件返回过来,然后进行一个mount,mount中进行的是render
const app = {
_component: rootComponent,
mount(rootContainer) {
console.log("基于根组件创建 vnode");
const vnode = createVNode(rootComponent);
console.log("调用 render,基于 vnode 进行开箱");
// render进行页面的渲染,render针对vnode去执行的
render(vnode, rootContainer);
},
};
return app;
};
}
src/vnode.ts.ts
vnode处理
import { ShapeFlags } from "@mini-vue/shared";
export { createVNode as createElementVNode }
export const createVNode = function (
type: any,
props?: any,
children?: string | Array<any>
) {
// 注意 type 有可能是 string 也有可能是对象
// 如果是对象的话,那么就是用户设置的 options
// type 为 string 的时候
// createVNode("div")
// type 为组件对象的时候
// createVNode(App)
const vnode = {
el: null,
component: null,
key: props?.key,
type,
props: props || {},
children,
shapeFlag: getShapeFlag(type),
};
// 基于 children 再次设置 shapeFlag
if (Array.isArray(children)) {
vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
} else if (typeof children === "string") {
vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;
}
normalizeChildren(vnode, children);
return vnode;
};
export function normalizeChildren(vnode, children) {
// 如果是对象的话,就针对数组做兼容处理
// 针对children类型,除了element类型,增加一个当前节点vnode的处理
if (typeof children === "object") {
// 暂时主要是为了标识出 slots_children 这个类型来
// 暂时我们只有 element 类型和 component 类型的组件
// 所以我们这里除了 element ,那么只要是 component 的话,那么children 肯定就是 slots 了
if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
// 如果是 element 类型的话,那么 children 肯定不是 slots
} else {
// 这里就必然是 component 了,
vnode.shapeFlag |= ShapeFlags.SLOTS_CHILDREN;
}
}
}
// 用 symbol 作为唯一标识
export const Text = Symbol("Text");
export const Fragment = Symbol("Fragment");
/**
* @private
*/
export function createTextVNode(text: string = " ") {
return createVNode(Text, {}, text);
}
// 标准化 vnode 的格式
// 其目的是为了让 child 支持多种格式
export function normalizeVNode(child) {
// 暂时只支持处理 child 为 string 和 number 的情况
if (typeof child === "string" || typeof child === "number") {
return createVNode(Text, null, String(child)); //怎样创建一个vnode
} else {
return child;
}
}
// 基于 type 来判断是什么类型的组件
function getShapeFlag(type: any) {
return typeof type === "string"
? ShapeFlags.ELEMENT
: ShapeFlags.STATEFUL_COMPONENT;
}
src/renderer.ts
进行一系列 ast 的动作,然后交由给patch,进行diff比较,针对diff处理完成后,最后将其转化为dom,dom是交由runtime-dom去做的
import { ShapeFlags } from "@mini-vue/shared";
import { createComponentInstance } from "./component";
import { queueJob } from "./scheduler";
import { effect } from "@mini-vue/reactivity";
import { setupComponent } from "./component";
import { Fragment, normalizeVNode, Text } from "./vnode";
import { shouldUpdateComponent } from "./componentRenderUtils";
import { createAppAPI } from "./createApp";
export function createRenderer(options) {
const {
createElement: hostCreateElement,
setElementText: hostSetElementText,
patchProp: hostPatchProp,
insert: hostInsert,
remove: hostRemove,
setText: hostSetText,
createText: hostCreateText,
} = options;
const render = (vnode, container) => {
console.log("调用 path")
patch(null, vnode, container);
};
// vue3中的diff算法
function patch(
n1,
n2,
container = null,
anchor = null,
parentComponent = null
) {
// 基于 n2 的类型来判断
// 因为 n2 是新的 vnode
const { type, shapeFlag } = n2;
switch (type) {
case Text:
processText(n1, n2, container);
break;
// 其中还有几个类型比如: static fragment comment
case Fragment:
processFragment(n1, n2, container);
break;
default:
// 这里就基于 shapeFlag 来处理
if (shapeFlag & ShapeFlags.ELEMENT) {
console.log("处理 element");
processElement(n1, n2, container, anchor, parentComponent);
} else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
console.log("处理 component");
processComponent(n1, n2, container, parentComponent);
}
}
}
function processFragment(n1: any, n2: any, container: any) {
// 只需要渲染 children ,然后给添加到 container 内
if (!n1) {
// 初始化 Fragment 逻辑点
console.log("初始化 Fragment 类型的节点");
mountChildren(n2.children, container);
}
}
function processText(n1, n2, container) {
console.log("处理 Text 节点");
if (n1 === null) {
// n1 是 null 说明是 init 的阶段
// 基于 createText 创建出 text 节点,然后使用 insert 添加到 el 内
console.log("初始化 Text 类型的节点");
hostInsert((n2.el = hostCreateText(n2.children as string)), container);
} else {
// update
// 先对比一下 updated 之后的内容是否和之前的不一样
// 在不一样的时候才需要 update text
// 这里抽离出来的接口是 setText
// 注意,这里一定要记得把 n1.el 赋值给 n2.el, 不然后续是找不到值的
const el = (n2.el = n1.el!);
if (n2.children !== n1.children) {
console.log("更新 Text 类型的节点");
hostSetText(el, n2.children as string);
}
}
}
function processElement(n1, n2, container, anchor, parentComponent) {
if (!n1) {
mountElement(n2, container, anchor);
} else {
// todo
updateElement(n1, n2, container, anchor, parentComponent);
}
}
function updateElement(n1, n2, container, anchor, parentComponent) {
const oldProps = (n1 && n1.props) || {};
const newProps = n2.props || {};
// 应该更新 element
console.log("应该更新 element");
console.log("旧的 vnode", n1);
console.log("新的 vnode", n2);
// 需要把 el 挂载到新的 vnode
const el = (n2.el = n1.el);
// 对比 props
patchProps(el, oldProps, newProps);
// 对比 children
patchChildren(n1, n2, el, anchor, parentComponent);
}
function patchProps(el, oldProps, newProps) {
// 对比 props 有以下几种情况
// 1. oldProps 有,newProps 也有,但是 val 值变更了
// 举个栗子
// 之前: oldProps.id = 1 ,更新后:newProps.id = 2
// key 存在 oldProps 里 也存在 newProps 内
// 以 newProps 作为基准
for (const key in newProps) {
const prevProp = oldProps[key];
const nextProp = newProps[key];
if (prevProp !== nextProp) {
// 对比属性
// 需要交给 host 来更新 key
hostPatchProp(el, key, prevProp, nextProp);
}
}
// 2. oldProps 有,而 newProps 没有了
// 之前: {id:1,tId:2} 更新后: {id:1}
// 这种情况下我们就应该以 oldProps 作为基准,因为在 newProps 里面是没有的 tId 的
// 还需要注意一点,如果这个 key 在 newProps 里面已经存在了,说明已经处理过了,就不要在处理了
for (const key in oldProps) {
const prevProp = oldProps[key];
const nextProp = null;
if (!(key in newProps)) {
// 这里是以 oldProps 为基准来遍历,
// 而且得到的值是 newProps 内没有的
// 所以交给 host 更新的时候,把新的值设置为 null
hostPatchProp(el, key, prevProp, nextProp);
}
}
}
function patchChildren(n1, n2, container, anchor, parentComponent) {
const { shapeFlag: prevShapeFlag, children: c1 } = n1;
const { shapeFlag, children: c2 } = n2;
// 如果 n2 的 children 是 text 类型的话
// 就看看和之前的 n1 的 children 是不是一样的
// 如果不一样的话直接重新设置一下 text 即可
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
if (c2 !== c1) {
console.log("类型为 text_children, 当前需要更新");
hostSetElementText(container, c2 as string);
}
} else {
// 如果之前是 array_children
// 现在还是 array_children 的话
// 那么我们就需要对比两个 children 啦
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
patchKeyedChildren(c1, c2, container, anchor, parentComponent);
}
}
}
}
function patchKeyedChildren(
c1: any[],
c2: any[],
container,
parentAnchor,
parentComponent
) {
let i = 0;
const l2 = c2.length;
let e1 = c1.length - 1;
let e2 = l2 - 1;
const isSameVNodeType = (n1, n2) => {
return n1.type === n2.type && n1.key === n2.key;
};
while (i <= e1 && i <= e2) {
const prevChild = c1[i];
const nextChild = c2[i];
if (!isSameVNodeType(prevChild, nextChild)) {
console.log("两个 child 不相等(从左往右比对)");
console.log(`prevChild:${prevChild}`);
console.log(`nextChild:${nextChild}`);
break;
}
console.log("两个 child 相等,接下来对比这两个 child 节点(从左往右比对)");
patch(prevChild, nextChild, container, parentAnchor, parentComponent);
i++;
}
while (i <= e1 && i <= e2) {
// 从右向左取值
const prevChild = c1[e1];
const nextChild = c2[e2];
if (!isSameVNodeType(prevChild, nextChild)) {
console.log("两个 child 不相等(从右往左比对)");
console.log(`prevChild:${prevChild}`);
console.log(`nextChild:${nextChild}`);
break;
}
console.log("两个 child 相等,接下来对比这两个 child 节点(从右往左比对)");
patch(prevChild, nextChild, container, parentAnchor, parentComponent);
e1--;
e2--;
}
if (i > e1 && i <= e2) {
// 如果是这种情况的话就说明 e2 也就是新节点的数量大于旧节点的数量
// 也就是说新增了 vnode
// 应该循环 c2
// 锚点的计算:新的节点有可能需要添加到尾部,也可能添加到头部,所以需要指定添加的问题
// 要添加的位置是当前的位置(e2 开始)+1
// 因为对于往左侧添加的话,应该获取到 c2 的第一个元素
// 所以我们需要从 e2 + 1 取到锚点的位置
const nextPos = e2 + 1;
const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor;
while (i <= e2) {
console.log(`需要新创建一个 vnode: ${c2[i].key}`);
patch(null, c2[i], container, anchor, parentComponent);
i++;
}
} else if (i > e2 && i <= e1) {
// 这种情况的话说明新节点的数量是小于旧节点的数量的
// 那么我们就需要把多余的
while (i <= e1) {
console.log(`需要删除当前的 vnode: ${c1[i].key}`);
hostRemove(c1[i].el);
i++;
}
} else {
// 左右两边都比对完了,然后剩下的就是中间部位顺序变动的
// 例如下面的情况
// a,b,[c,d,e],f,g
// a,b,[e,c,d],f,g
let s1 = i;
let s2 = i;
const keyToNewIndexMap = new Map();
let moved = false;
let maxNewIndexSoFar = 0;
// 先把 key 和 newIndex 绑定好,方便后续基于 key 找到 newIndex
// 时间复杂度是 O(1)
for (let i = s2; i <= e2; i++) {
const nextChild = c2[i];
keyToNewIndexMap.set(nextChild.key, i);
}
// 需要处理新节点的数量
const toBePatched = e2 - s2 + 1;
let patched = 0;
// 初始化 从新的index映射为老的index
// 创建数组的时候给定数组的长度,这个是性能最快的写法
const newIndexToOldIndexMap = new Array(toBePatched);
// 初始化为 0 , 后面处理的时候 如果发现是 0 的话,那么就说明新值在老的里面不存在
for (let i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0;
// 遍历老节点
// 1. 需要找出老节点有,而新节点没有的 -> 需要把这个节点删除掉
// 2. 新老节点都有的,—> 需要 patch
for (i = s1; i <= e1; i++) {
const prevChild = c1[i];
// 优化点
// 如果老的节点大于新节点的数量的话,那么这里在处理老节点的时候就直接删除即可
if (patched >= toBePatched) {
hostRemove(prevChild.el);
continue;
}
let newIndex;
if (prevChild.key != null) {
// 这里就可以通过key快速的查找了, 看看在新的里面这个节点存在不存在
// 时间复杂度O(1)
newIndex = keyToNewIndexMap.get(prevChild.key);
} else {
// 如果没key 的话,那么只能是遍历所有的新节点来确定当前节点存在不存在了
// 时间复杂度O(n)
for (let j = s2; j <= e2; j++) {
if (isSameVNodeType(prevChild, c2[j])) {
newIndex = j;
break;
}
}
}
// 因为有可能 nextIndex 的值为0(0也是正常值)
// 所以需要通过值是不是 undefined 或者 null 来判断
if (newIndex === undefined) {
// 当前节点的key 不存在于 newChildren 中,需要把当前节点给删除掉
hostRemove(prevChild.el);
} else {
// 新老节点都存在
console.log("新老节点都存在");
// 把新节点的索引和老的节点的索引建立映射关系
// i + 1 是因为 i 有可能是0 (0 的话会被认为新节点在老的节点中不存在)
newIndexToOldIndexMap[newIndex - s2] = i + 1;
// 来确定中间的节点是不是需要移动
// 新的 newIndex 如果一直是升序的话,那么就说明没有移动
// 所以我们可以记录最后一个节点在新的里面的索引,然后看看是不是升序
// 不是升序的话,我们就可以确定节点移动过了
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex;
} else {
moved = true;
}
patch(prevChild, c2[newIndex], container, null, parentComponent);
patched++;
}
}
// 利用最长递增子序列来优化移动逻辑
// 因为元素是升序的话,那么这些元素就是不需要移动的
// 而我们就可以通过最长递增子序列来获取到升序的列表
// 在移动的时候我们去对比这个列表,如果对比上的话,就说明当前元素不需要移动
// 通过 moved 来进行优化,如果没有移动过的话 那么就不需要执行算法
// getSequence 返回的是 newIndexToOldIndexMap 的索引值
// 所以后面我们可以直接遍历索引值来处理,也就是直接使用 toBePatched 即可
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: [];
let j = increasingNewIndexSequence.length - 1;
// 遍历新节点
// 1. 需要找出老节点没有,而新节点有的 -> 需要把这个节点创建
// 2. 最后需要移动一下位置,比如 [c,d,e] -> [e,c,d]
// 这里倒循环是因为在 insert 的时候,需要保证锚点是处理完的节点(也就是已经确定位置了)
// 因为 insert 逻辑是使用的 insertBefore()
for (let i = toBePatched - 1; i >= 0; i--) {
// 确定当前要处理的节点索引
const nextIndex = s2 + i;
const nextChild = c2[nextIndex];
// 锚点等于当前节点索引+1
// 也就是当前节点的后面一个节点(又因为是倒遍历,所以锚点是位置确定的节点)
const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;
if (newIndexToOldIndexMap[i] === 0) {
// 说明新节点在老的里面不存在
// 需要创建
patch(null, nextChild, container, anchor, parentComponent);
} else if (moved) {
// 需要移动
// 1. j 已经没有了 说明剩下的都需要移动了
// 2. 最长子序列里面的值和当前的值匹配不上, 说明当前元素需要移动
if (j < 0 || increasingNewIndexSequence[j] !== i) {
// 移动的话使用 insert 即可
hostInsert(nextChild.el, container, anchor);
} else {
// 这里就是命中了 index 和 最长递增子序列的值
// 所以可以移动指针了
j--;
}
}
}
}
}
function mountElement(vnode, container, anchor) {
const { shapeFlag, props } = vnode;
// 1. 先创建 element
// 基于可扩展的渲染 api
const el = (vnode.el = hostCreateElement(vnode.type));
// 支持单子组件和多子组件的创建
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 举个栗子
// render(){
// return h("div",{},"test")
// }
// 这里 children 就是 test ,只需要渲染一下就完事了
console.log(`处理文本:${vnode.children}`);
hostSetElementText(el, vnode.children);
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 举个栗子
// render(){
// Hello 是个 component
// return h("div",{},[h("p"),h(Hello)])
// }
// 这里 children 就是个数组了,就需要依次调用 patch 递归来处理
mountChildren(vnode.children, el);
}
// 处理 props
if (props) {
for (const key in props) {
// todo
// 需要过滤掉vue自身用的key
// 比如生命周期相关的 key: beforeMount、mounted
const nextVal = props[key];
hostPatchProp(el, key, null, nextVal);
}
}
// todo
// 触发 beforeMount() 钩子
console.log("vnodeHook -> onVnodeBeforeMount");
console.log("DirectiveHook -> beforeMount");
console.log("transition -> beforeEnter");
// 插入
hostInsert(el, container, anchor);
// todo
// 触发 mounted() 钩子
console.log("vnodeHook -> onVnodeMounted");
console.log("DirectiveHook -> mounted");
console.log("transition -> enter");
}
function mountChildren(children, container) {
children.forEach((VNodeChild) => {
// todo
// 这里应该需要处理一下 vnodeChild
// 因为有可能不是 vnode 类型
console.log("mountChildren:", VNodeChild);
patch(null, VNodeChild, container);
});
}
function processComponent(n1, n2, container, parentComponent) {
// 如果 n1 没有值的话,那么就是 mount
if (!n1) {
// 初始化 component
mountComponent(n2, container, parentComponent);
} else {
updateComponent(n1, n2, container);
}
}
// 组件的更新
function updateComponent(n1, n2, container) {
console.log("更新组件", n1, n2);
// 更新组件实例引用
const instance = (n2.component = n1.component);
// 先看看这个组件是否应该更新
if (shouldUpdateComponent(n1, n2)) {
console.log(`组件需要更新: ${instance}`);
// 那么 next 就是新的 vnode 了(也就是 n2)
instance.next = n2;
// 这里的 update 是在 setupRenderEffect 里面初始化的,update 函数除了当内部的响应式对象发生改变的时候会调用
// 还可以直接主动的调用(这是属于 effect 的特性)
// 调用 update 再次更新调用 patch 逻辑
// 在update 中调用的 next 就变成了 n2了
// ps:可以详细的看看 update 中 next 的应用
// TODO 需要在 update 中处理支持 next 的逻辑
instance.update();
} else {
console.log(`组件不需要更新: ${instance}`);
// 不需要更新的话,那么只需要覆盖下面的属性即可
n2.component = n1.component;
n2.el = n1.el;
instance.vnode = n2;
}
}
function mountComponent(initialVNode, container, parentComponent) {
// 1. 先创建一个 component instance
const instance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent
));
console.log(`创建组件实例:${instance.type.name}`);
// 2. 给 instance 加工加工
setupComponent(instance);
setupRenderEffect(instance, initialVNode, container);
}
function setupRenderEffect(instance, initialVNode, container) {
// 调用 render
// 应该传入 ctx 也就是 proxy
// ctx 可以选择暴露给用户的 api
// 源代码里面是调用的 renderComponentRoot 函数
// 这里为了简化直接调用 render
// obj.name = "111"
// obj.name = "2222"
// 从哪里做一些事
// 收集数据改变之后要做的事 (函数)
// 依赖收集 effect 函数
// 触发依赖
function componentUpdateFn() {
if (!instance.isMounted) {
// 组件初始化的时候会执行这里
// 为什么要在这里调用 render 函数呢
// 是因为在 effect 内调用 render 才能触发依赖收集
// 等到后面响应式的值变更后会再次触发这个函数
console.log(`${instance.type.name}:调用 render,获取 subTree`);
const proxyToUse = instance.proxy;
// 可在 render 函数中通过 this 来使用 proxy
const subTree = (instance.subTree = normalizeVNode(
instance.render.call(proxyToUse, proxyToUse)
));
console.log("subTree", subTree);
// todo
console.log(`${instance.type.name}:触发 beforeMount hook`);
console.log(`${instance.type.name}:触发 onVnodeBeforeMount hook`);
// 这里基于 subTree 再次调用 patch
// 基于 render 返回的 vnode ,再次进行渲染
// 这里我把这个行为隐喻成开箱
// 一个组件就是一个箱子
// 里面有可能是 element (也就是可以直接渲染的)
// 也有可能还是 component
// 这里就是递归的开箱
// 而 subTree 就是当前的这个箱子(组件)装的东西
// 箱子(组件)只是个概念,它实际是不需要渲染的
// 要渲染的是箱子里面的 subTree
patch(null, subTree, container, null, instance);
// 把 root element 赋值给 组件的vnode.el ,为后续调用 $el 的时候获取值
initialVNode.el = subTree.el;
console.log(`${instance.type.name}:触发 mounted hook`);
instance.isMounted = true;
} else {
// 响应式的值变更后会从这里执行逻辑
// 主要就是拿到新的 vnode ,然后和之前的 vnode 进行对比
console.log(`${instance.type.name}:调用更新逻辑`);
// 拿到最新的 subTree
const { next, vnode } = instance;
// 如果有 next 的话, 说明需要更新组件的数据(props,slots 等)
// 先更新组件的数据,然后更新完成后,在继续对比当前组件的子元素
if (next) {
// 问题是 next 和 vnode 的区别是什么
next.el = vnode.el;
updateComponentPreRender(instance, next);
}
const proxyToUse = instance.proxy;
const nextTree = normalizeVNode(
instance.render.call(proxyToUse, proxyToUse)
);
// 替换之前的 subTree
const prevTree = instance.subTree;
instance.subTree = nextTree;
// 触发 beforeUpdated hook
console.log(`${instance.type.name}:触发 beforeUpdated hook`);
console.log(`${instance.type.name}:触发 onVnodeBeforeUpdate hook`);
// 用旧的 vnode 和新的 vnode 交给 patch 来处理
patch(prevTree, nextTree, prevTree.el, null, instance);
// 触发 updated hook
console.log(`${instance.type.name}:触发 updated hook`);
console.log(`${instance.type.name}:触发 onVnodeUpdated hook`);
}
}
// 在 vue3.2 版本里面是使用的 new ReactiveEffect
// 至于为什么不直接用 effect ,是因为需要一个 scope 参数来收集所有的 effect
// 而 effect 这个函数是对外的 api ,是不可以轻易改变参数的,所以会使用 new ReactiveEffect
// 因为 ReactiveEffect 是内部对象,加一个参数是无所谓的
// 后面如果要实现 scope 的逻辑的时候 需要改过来
// 现在就先算了
instance.update = effect(componentUpdateFn, {
scheduler: () => {
// 把 effect 推到微任务的时候在执行
// queueJob(effect);
queueJob(instance.update);
},
});
}
function updateComponentPreRender(instance, nextVNode) {
// 更新 nextVNode 的组件实例
// 现在 instance.vnode 是组件实例更新前的
// 所以之前的 props 就是基于 instance.vnode.props 来获取
// 接着需要更新 vnode ,方便下一次更新的时候获取到正确的值
nextVNode.component = instance;
// TODO 后面更新 props 的时候需要对比
// const prevProps = instance.vnode.props;
instance.vnode = nextVNode;
instance.next = null;
const { props } = nextVNode;
console.log("更新组件的 props", props);
instance.props = props;
console.log("更新组件的 slots");
// TODO 更新组件的 slots
// 需要重置 vnode
}
return {
render,
createApp: createAppAPI(render),
};
}
function getSequence(arr: number[]): number[] {
const p = arr.slice();
const result = [0];
let i, j, u, v, c;
const len = arr.length;
for (i = 0; i < len; i++) {
const arrI = arr[i];
if (arrI !== 0) {
j = result[result.length - 1];
if (arr[j] < arrI) {
p[i] = j;
result.push(i);
continue;
}
u = 0;
v = result.length - 1;
while (u < v) {
c = (u + v) >> 1;
if (arr[result[c]] < arrI) {
u = c + 1;
} else {
v = c;
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1];
}
result[u] = i;
}
}
}
u = result.length;
v = result[u - 1];
while (u-- > 0) {
result[u] = v;
v = p[v];
}
return result;
}
runtime-dom
根据vnode节点,其实是要转换成dom结构的,根据 ast 和 vnode节点 来区分 web 和 weex的节点,创建好vnode后,拿着这个节点把他转换成真实能够消费的属性
src/index.ts
调用到原生的API去进行dom维度的操作
// 源码里面这些接口是由 runtime-dom 来实现
// 这里先简单实现
import { isOn } from "@mini-vue/shared";
import { createRenderer } from "@mini-vue/runtime-core";
// 后面也修改成和源码一样的实现
function createElement(type) {
console.log("CreateElement", type);
const element = document.createElement(type);
return element;
}
function createText(text) {
return document.createTextNode(text);
}
function setText(node, text) {
node.nodeValue = text;
}
function setElementText(el, text) {
console.log("SetElementText", el, text);
el.textContent = text;
}
function patchProp(el, key, preValue, nextValue) {
// preValue 之前的值
// 为了之后 update 做准备的值
// nextValue 当前的值
console.log(`PatchProp 设置属性:${key} 值:${nextValue}`);
console.log(`key: ${key} 之前的值是:${preValue}`);
if (isOn(key)) {
// 添加事件处理函数的时候需要注意一下
// 1. 添加的和删除的必须是一个函数,不然的话 删除不掉
// 那么就需要把之前 add 的函数给存起来,后面删除的时候需要用到
// 2. nextValue 有可能是匿名函数,当对比发现不一样的时候也可以通过缓存的机制来避免注册多次
// 存储所有的事件函数
const invokers = el._vei || (el._vei = {});
const existingInvoker = invokers[key];
if (nextValue && existingInvoker) {
// patch
// 直接修改函数的值即可
existingInvoker.value = nextValue;
} else {
const eventName = key.slice(2).toLowerCase();
if (nextValue) {
const invoker = (invokers[key] = nextValue);
el.addEventListener(eventName, invoker);
} else {
el.removeEventListener(eventName, existingInvoker);
invokers[key] = undefined;
}
}
} else {
if (nextValue === null || nextValue === "") {
el.removeAttribute(key);
} else {
el.setAttribute(key, nextValue);
}
}
}
function insert(child, parent, anchor = null) {
console.log("Insert");
parent.insertBefore(child, anchor);
}
function remove(child) {
const parent = child.parentNode;
if (parent) {
parent.removeChild(child);
}
}
let renderer;
function ensureRenderer() {
// 如果 renderer 有值的话,那么以后都不会初始化了
return (
renderer ||
(renderer = createRenderer({
createElement,
createText,
setText,
setElementText,
patchProp,
insert,
remove,
}))
);
}
export const createApp = (...args) => {
return ensureRenderer().createApp(...args);
};
export * from "@mini-vue/runtime-core"
总结
- vue模板化的语法 => compiler的过程
- compiler的结果 vnode => AST
- vnode 进行 patch,执行 diff 比较 => runtime的过程
- 更新后的vnode -> 调用 runtime-dom,进行 DOM 渲染 render