Vue进阶之Vue3源码解析(二)
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中进行的是renderconst 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 再次设置 shapeFlagif (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 是新的 vnodeconst { type, shapeFlag } = n2;switch (type) {case Text:processText(n1, n2, container);break;// 其中还有几个类型比如: static fragment commentcase 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 {// todoupdateElement(n1, n2, container, anchor, parentComponent);}}function updateElement(n1, n2, container, anchor, parentComponent) {const oldProps = (n1 && n1.props) || {};const newProps = n2.props || {};// 应该更新 elementconsole.log("应该更新 element");console.log("旧的 vnode", n1);console.log("新的 vnode", n2);// 需要把 el 挂载到新的 vnodeconst el = (n2.el = n1.el);// 对比 propspatchProps(el, oldProps, newProps);// 对比 childrenpatchChildren(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 来更新 keyhostPatchProp(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 更新的时候,把新的值设置为 nullhostPatchProp(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,glet 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. 新老节点都有的,—> 需要 patchfor (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// 基于可扩展的渲染 apiconst 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);}// 处理 propsif (props) {for (const key in props) {// todo// 需要过滤掉vue自身用的key// 比如生命周期相关的 key: beforeMount、mountedconst 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 没有值的话,那么就是 mountif (!n1) {// 初始化 componentmountComponent(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 instanceconst 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 来使用 proxyconst subTree = (instance.subTree = normalizeVNode(instance.render.call(proxyToUse, proxyToUse)));console.log("subTree", subTree);// todoconsole.log(`${instance.type.name}:触发 beforeMount hook`);console.log(`${instance.type.name}:触发 onVnodeBeforeMount hook`);// 这里基于 subTree 再次调用 patch// 基于 render 返回的 vnode ,再次进行渲染// 这里我把这个行为隐喻成开箱// 一个组件就是一个箱子// 里面有可能是 element (也就是可以直接渲染的)// 也有可能还是 component// 这里就是递归的开箱// 而 subTree 就是当前的这个箱子(组件)装的东西// 箱子(组件)只是个概念,它实际是不需要渲染的// 要渲染的是箱子里面的 subTreepatch(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}:调用更新逻辑`);// 拿到最新的 subTreeconst { 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));// 替换之前的 subTreeconst prevTree = instance.subTree;instance.subTree = nextTree;// 触发 beforeUpdated hookconsole.log(`${instance.type.name}:触发 beforeUpdated hook`);console.log(`${instance.type.name}:触发 onVnodeBeforeUpdate hook`);// 用旧的 vnode 和新的 vnode 交给 patch 来处理patch(prevTree, nextTree, prevTree.el, null, instance);// 触发 updated hookconsole.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
相关文章:

Vue进阶之Vue3源码解析(二)
Vue3源码解析 运行runtime-coresrc/createApp.tssrc/vnode.ts.tssrc/renderer.ts runtime-domsrc/index.ts 总结 运行 runtime-core src/createApp.ts vue的创建入口 import { createVNode } from "./vnode";export function createAppAPI(render) {return funct…...

linux的文件系统及文件类型
目录 一、Linux支持的文件系统 二、linux的文件类型 2.1、普通文件 2.2、目录文件 2.3、链接文件 2.4、字符设备文件: 2.5、块设备文件 2.6、套接字文件 2.7、管道文件 三、linux的文件属性 3.1、关于权限部分 四、Linux的文件结构 五、用户主目录 5.1、工作目录…...

如何下载安装 PyCharm?
李升伟 整理 一、下载 PyCharm 访问官网 打开 PyCharm 官网,点击 "Download" 按钮25。 版本选择: 社区版(Community):免费使用,适合个人学习和基础开发。 专业版(Professional&#…...

3D空间曲线批量散点化软件V1.0正式发布,将空间线条导出坐标点,SolidWorks/UG/Catia等三维软件通用
软件下载地址: SolidWorks/UG/Catia等三维软件通用,3D空间曲线批量散点化软件V1.0正式发布,将空间线条导出坐标点 - 陶小桃Blog在三维设计领域,工程师常需将复杂空间曲线转化为离散坐标点以用于逆向工程、有限元分析、数控加工或…...

WPS AI+office-ai的安装、使用
** 说明:WPS AI和OfficeAI是两个独立的AI助手,下面分别简单讲下如何使用 ** WPS AI WPS AI是WPS自带AI工具 打开新版WPS,新建文档后就可以看到菜单栏多了一个“WPS AI”菜单,点击该菜单,发现下方出现很多菜单…...

java后端开发day27--常用API(二)正则表达式爬虫
(以下内容全部来自上述课程) 1.正则表达式(regex) 可以校验字符串是否满足一定的规则,并用来校验数据格式的合法性。 1.作用 校验字符串是否满足规则在一段文本中查找满足要求的内容 2.内容定义 ps:一…...

拼电商客户管理系统
内容来自:尚硅谷 难度:easy 目 标 l 模拟实现一个基于文本界面的 《 拼电商客户管理系统 》 l 进一步掌握编程技巧和调试技巧,熟悉面向对象编程 l 主要涉及以下知识点: 类结构的使用:属性、方法及构造器 对象的创建与…...

华为:Wireshark的OSPF抓包分析过程
一、OSPF 的5包7状态 5个数据包 1.Hello:发现、建立邻居(邻接)关系、维持、周期保活;存在全网唯一的RID,使用IP地址表示 2.DBD:本地的数据库的目录(摘要),LSDB的目录&…...

Android项目优化同步速度
最近项目需要使用ffmpeg,需要gradle配置引入ffmpeg库,发现原来通过google官方的代码仓,下载太慢了,每秒KB级别的速度。(之前下gradle/gradle plugin都不至于这么慢),于是想到配置国内镜像源来提…...

在线教育网站项目第二步 :学习roncoo-education,服务器为ubuntu22.04.05
一、说明 前端技术体系:Vue3 Nuxt3 Vite5 Vue-Router Element-Plus Pinia Axios 后端技术体系:Spring Cloud Alibaba2021 MySQL8 Nacos Seata Mybatis Druid redis 后端系统:roncoo-education(核心框架:S…...

STM32-GPIO详解
目录 一:GPIO基本概念 编辑 二:GPIO的实际应用 三:功能描述 四:GPIO库函数 五:寄存器 GPIO相关寄存器功能 一:GPIO基本概念 GPIO是英文General Purpose Input/Output的缩写,中文翻译为…...

【Framework系列之Client】UIManager和UIPanel模块介绍
今天来介绍一下UIManager和UIPanel模块,话不多说直接开始。 UIManager 功能介绍 UIManager是管理UIPanel的唯一模块,UIManager的主要功能包括: 提供打开、隐藏、关闭UIPanel的相关接口。负责UIPanel对象的创建以及初始化。负责储存UIPanel对…...

阿里云操作系统控制台——ECS操作与性能优化
引言:在数字化时代,云服务器作为强大的计算资源承载平台,为企业和开发者提供了灵活且高效的服务。本文将详细介绍如何一步步操作云服务器 ECS,从开通到组件安装,再到内存全景诊断,帮助快速上手,…...

【长安大学】苹果手机/平板自动连接认证CHD-WIFI脚本(快捷指令)
背景: 已经用这个脚本的记得设置Wifi时候,关闭“自动登录” 前几天实在忍受不了CHD-WIFI动不动就断开,一天要重新连接,点登陆好几次。试了下在网上搜有没有CHD-WIFI的自动连接WIFI自动认证脚本,那样我就可以解放双手&…...

第51天:Web开发-JavaEE应用SpringBoot栈身份验证JWT令牌Security鉴权安全绕过
#知识点 1、安全开发-JavaEE-身份验证-JWT&Security 2、安全开发-JavaEE-安全问题-不安全写法&版本漏洞 #开发框架-SpringBoot 参考:Spring Boot 中文文档 一、身份验证的常见技术: 1、JWT 2、Shiro 3、Spring Security 4、OAuth 2.0 5、SSO 6、…...

中原银行:从“小机+传统数据库”升级为“OceanBase+通用服务器”,30 +系统成功上线|OceanBase DB大咖说(十五)
OceanBase《DB 大咖说》第 15 期,我们邀请到了中原银行金融科技部数据团队负责人,吕春雷。本文为本期大咖说的精选。 吕春雷是一位资历深厚的数据库专家,从传统制造企业、IT企业、甲骨文公司到中原银行,他在数据库技术与运维管理…...

Java面试第八山!《Spring框架》
一、Spring框架概述 Spring是Java企业级应用开发的核心框架,通过控制反转(IoC)和 面向切面编程(AOP)实现模块解耦,简化开发流程。其核心优势包括依赖注入、声明式事务管理、集成主流ORM框架(如…...

LangChain教程 - Agent - 支持 9 种 ReAct 交互
引言 LangChain 总结了 9 种经典的复杂模型交互模式,每种都针对特定任务设计,兼具独特优势与适用场景,内容涵盖: ReAct、Function Call、知识库、搜索等,使用这些模式可以大大简化这些场景开发难度。这些模式可以使用…...

蓝桥杯备赛日记【day1】(c++赛道)
一、裁纸刀问题(2022、规律、思维、省赛) 解法思路: 参考题目给出的例子发现。不管要裁剪多少次。最外围的四次是固定的。然后通过观察发现,我们的行的裁剪次数为(m-1) 次,而每行都需要裁剪列数…...

【大模型基础_毛玉仁】1.4 语言模型的采样方法
【大模型基础_毛玉仁】1.4 语言模型的采样方法 1.4 语言模型的采样方法1.4.1 概率最大化方法1)贪心搜索(GreedySearch)2)波束搜索(BeamSearch) 1.4.2 随机采样方法1)Top-K 采样2)Top…...

[内网安全] Windows 本地认证 — NTLM 哈希和 LM 哈希
关注这个专栏的其他相关笔记:[内网安全] 内网渗透 - 学习手册-CSDN博客 0x01:SAM 文件 & Windows 本地认证流程 0x0101:SAM 文件简介 Windows 本地账户的登录密码是存储在系统本地的 SAM 文件中的,在登录 Windows 的时候&am…...

基于SNR估计的自适应码率LDPC编译码算法matlab性能仿真,对比固定码率LDPC的系统传输性能
目录 1.算法仿真效果 2.算法涉及理论知识概要 2.1 基于序列的SNR估计 2.2 基于SNR估计值进行码率切换 2.3 根据数据量进行码率切换 3.MATLAB核心程序 4.完整算法代码文件获得 1.算法仿真效果 matlab2022a仿真结果如下(完整代码运行后无水印)&…...

opencv 模板匹配方法汇总
在OpenCV中,模板匹配是一种在较大图像中查找特定模板图像位置的技术。OpenCV提供了多种模板匹配方法,通过cv2.matchTemplate函数实现,该函数支持的匹配方式主要有以下6种,下面详细介绍每种方法的原理、特点和适用场景。 1. cv2.T…...

Embedding技术:DeepWalkNode2vec
引言 在推荐系统中,Graph Embedding技术已经成为一种强大的工具,用于捕捉用户和物品之间的复杂关系。本文将介绍Graph Embedding的基本概念、原理及其在推荐系统中的应用。 什么是Graph Embedding? Graph Embedding是一种将图中的节点映射…...

微信小程序注册组件
在微信小程序中注册组件分为自定义组件的创建和全局/局部注册,下面为你详细介绍具体步骤和示例。 自定义组件的创建 自定义组件由四个文件组成,分别是 .js(脚本文件)、.json(配置文件)、.wxml(…...

【docker】安装mysql,修改端口号并重启,root改密
我的docker笔记 【centOS】安装docker环境,替换国内镜像 1. 配置镜像源 使用阿里云镜像加速器,编辑/etc/docker/daemon.json sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-EOF {"registry-mirrors": ["https:/…...

自定义wordpress三级导航菜单代码
首先,在你的主题functions.php文件中,添加以下代码以注册一个新的菜单位置: function mytheme_register_menus() {register_nav_menus(array(primary-menu > __(Primary Menu, mytheme))); } add_action(init, mytheme_register_menus); …...

洛谷 P1480 A/B Problem(高精度详解)c++
题目链接:P1480 A/B Problem - 洛谷 1.题目分析 1:说明这里是高精度除以低精度的形式,为什么不是高精度除以高精度的形式,是因为它很少见,它的模拟方式是用高精度减法来做的,并不能用小学列竖式的方法模拟…...

JAVA入门——网络编程简介
自己学习时的笔记,可能有点水( 以后可能还会补充(大概率不会) 一、基本概念 网络编程三要素: IP 设备在网络中的唯一标识 端口号 应用软件在设备中的唯一标识两个字节表示的整数,0~1023用于知名的网络…...

Ubuntu 合上屏幕 不待机 设置
有时候需要Ubuntu的机器合上屏幕的时候也能正常工作,而不是处于待机状态。 需要进行配置文件的设置,并重启即可。 1. 修改配置文件 /etc/systemd/logind.conf sudo vi /etc/systemd/logind.conf 然后输入i,进入插入状态,修改如…...