Vue3 核心模块源码解析

news/2024/9/15 20:49:39/ 标签: javascript, vue.js, 前端

Vue3 核心模块源码解析

  • 1、Vue3 模块源码解析
    • 1.1 compiler-core
      • 1.1.1 目录结构
      • 1.1.2 compile逻辑
    • 1.2 reactivity
      • 1.2.1 目录结构
      • 1.2.2 reactivity逻辑
    • 1.3 runtime-core
      • 1.3.1 目录结构
      • 1.3.2 runtime核心逻辑
    • 1.4 runtime-dom
      • 1.4.1 主要功能
    • 1.5 runtime-test
      • 1.5.1 目录结构
      • 1.5.2 runtime-test核心逻辑
    • 1.6 shared
  • 2、Vue 3 Diff算法
    • 2.1 静态标记 + 非全量 Diff
    • 2.2 最长递增子序列

1、Vue3 模块源码解析

基本核心模块目录结构如下:
source code -> compiler ->compiler code ->runtime->runtime code 浏览器运行

javascript">├─compiler-core
│  │  package.json
│  │
│  ├─src
│  │  │  ast.ts
│  │  │  codegen.ts
│  │  │  compile.ts
│  │  │  index.ts
│  │  │  parse.ts
│  │  │  runtimeHelpers.ts
│  │  │  transform.ts
│  │  │  utils.ts
│  │  │
│  │  └─transforms
│  │          transformElement.ts
│  │          transformExpression.ts
│  │          transformText.ts
│  │
│  └─__tests__
│      │  codegen.spec.ts
│      │  parse.spec.ts
│      │  transform.spec.ts
│      │
│      └─__snapshots__
│              codegen.spec.ts.snap
│
├─reactivity
│  │  package.json
│  │
│  ├─src
│  │      baseHandlers.ts
│  │      computed.ts
│  │      dep.ts
│  │      effect.ts
│  │      index.ts
│  │      reactive.ts
│  │      ref.ts
│  │
│  └─__tests__
│          computed.spec.ts
│          dep.spec.ts
│          effect.spec.ts
│          reactive.spec.ts
│          readonly.spec.ts
│          ref.spec.ts
│          shallowReadonly.spec.ts
│
├─runtime-core
│  │  package.json
│  │
│  ├─src
│  │  │  .pnpm-debug.log
│  │  │  apiInject.ts
│  │  │  apiWatch.ts
│  │  │  component.ts
│  │  │  componentEmits.ts
│  │  │  componentProps.ts
│  │  │  componentPublicInstance.ts
│  │  │  componentRenderUtils.ts
│  │  │  componentSlots.ts
│  │  │  createApp.ts
│  │  │  h.ts
│  │  │  index.ts
│  │  │  renderer.ts
│  │  │  scheduler.ts
│  │  │  vnode.ts
│  │  │
│  │  └─helpers
│  │          renderSlot.ts
│  │
│  └─__tests__
│          apiWatch.spec.ts
│          componentEmits.spec.ts
│          rendererComponent.spec.ts
│          rendererElement.spec.ts
│
├─runtime-dom
│  │  package.json
│  │
│  └─src
│          index.ts
│
├─runtime-test
│  └─src
│          index.ts
│          nodeOps.ts
│          patchProp.ts
│          serialize.ts
│
├─shared
│  │  package.json
│  │
│  └─src
│          index.ts
│          shapeFlags.ts
│          toDisplayString.ts

1.1 compiler-core

Vue3的编译核心,核心作用就是将字符串转换成 抽象对象语法树AST
VDOM->AST
compiler:词法分析,语法分析,代码转换,代码生成

1.1.1 目录结构

javascript">├─src                               
│  │  ast.ts        // ts类型定义,比如type,enum,interface等               
│  │  codegen.ts        // 将生成的ast转换成render字符串                  
│  │  compile.ts // compile统一执行逻辑,有一个 baseCompile ,用来编译模板文件的                
│  │  index.ts // 入口文件 
│  │  parse.ts // 将模板字符串转换成 AST  
│  │  runtimeHelpers.ts // 生成code的时候的定义常量对应关系 
│  │  transform.ts // 处理 AST 中的 vue 特有语法   
│  │  utils.ts                        
│  │                                  
│  └─transforms                       
│          transformElement.ts        
│          transformExpression.ts     
│          transformText.ts           
│                                     
└─__tests__                // 测试用例                      │  codegen.spec.ts                │  parse.spec.ts                  │  transform.spec.ts              │                                 └─__snapshots__                   codegen.spec.ts.snap      

1.1.2 compile逻辑

  • index.ts 入口文件
javascript">// src/index.ts
export { baseCompile } from "./compile";// src/compiler.ts
import { generate } from "./codegen";
import { baseParse } from "./parse";
import { transform } from "./transform";
import { transformExpression } from "./transforms/transformExpression";
import { transformElement } from "./transforms/transformElement";
import { transformText } from "./transforms/transformText";export function baseCompile(template, options) {// 1. 先把 template 也就是字符串 parse 成 astconst ast = baseParse(template);// 2. 给 ast 加点料(- -#)transform(ast,Object.assign(options, {nodeTransforms: [transformElement, transformText, transformExpression],}));// 3. 生成 render 函数代码return generate(ast);
}                     
  • baseParse 将模板字符串转换成 AST
javascript">export function baseParse(content: string) {const context = createParserContext(content);return createRoot(parseChildren(context, []));
}function createParserContext(content) {console.log("创建 paserContext");return {source: content,};
}function createRoot(children) {return {type: NodeTypes.ROOT,children,helpers: [],};
}
//递归解析
function parseChildren(context, ancestors) {console.log("开始解析 children");const nodes: any = [];while (!isEnd(context, ancestors)) {let node;const s = context.source;if (startsWith(s, "{{")) {// 看看如果是 {{ 开头的话,那么就是一个插值, 那么去解析他node = parseInterpolation(context);} else if (s[0] === "<") {if (s[1] === "/") {// 这里属于 edge case 可以不用关心// 处理结束标签if (/[a-z]/i.test(s[2])) {// 匹配 </div>// 需要改变 context.source 的值 -> 也就是需要移动光标parseTag(context, TagType.End);// 结束标签就以为这都已经处理完了,所以就可以跳出本次循环了continue;}} else if (/[a-z]/i.test(s[1])) {node = parseElement(context, ancestors);}}if (!node) {node = parseText(context);}nodes.push(node);}return nodes;
}
  • transform 对一些细节进行加工,代码转换,遵循设计模式,对不同的节点,加入不同的属性或动作
javascript">export function transform(root, options = {}) {// 1. 创建 contextconst context = createTransformContext(root, options);// 2. 遍历 nodetraverseNode(root, context);createRootCodegen(root, context);root.helpers.push(...context.helpers.keys());
}function createTransformContext(root, options): any {const context = {root,nodeTransforms: options.nodeTransforms || [],helpers: new Map(),helper(name) {// 这里会收集调用的次数// 收集次数是为了给删除做处理的, (当只有 count 为0 的时候才需要真的删除掉)// helpers 数据会在后续生成代码的时候用到const count = context.helpers.get(name) || 0;context.helpers.set(name, count + 1);},};return context;
}function traverseNode(node: any, context) {const type: NodeTypes = node.type;// 遍历调用所有的 nodeTransforms// 把 node 给到 transform// 用户可以对 node 做处理const nodeTransforms = context.nodeTransforms;const exitFns: any = [];for (let i = 0; i < nodeTransforms.length; i++) {const transform = nodeTransforms[i];const onExit = transform(node, context);if (onExit) {exitFns.push(onExit);}}switch (type) {case NodeTypes.INTERPOLATION:// 插值的点,在于后续生成 render 代码的时候是获取变量的值context.helper(TO_DISPLAY_STRING);break;case NodeTypes.ROOT:case NodeTypes.ELEMENT:traverseChildren(node, context);break;default:break;}let i = exitFns.length;// i-- 这个很巧妙// 使用 while 是要比 for 快 (可以使用 https://jsbench.me/ 来测试一下)while (i--) {exitFns[i]();}
}function createRootCodegen(root: any, context: any) {const { children } = root;// 只支持有一个根节点// 并且还是一个 single text nodeconst child = children[0];// 如果是 element 类型的话 , 那么我们需要把它的 codegenNode 赋值给 root// root 其实是个空的什么数据都没有的节点// 所以这里需要额外的处理 codegenNode// codegenNode 的目的是专门为了 codegen 准备的  为的就是和 ast 的 node 分离开if (child.type === NodeTypes.ELEMENT && child.codegenNode) {const codegenNode = child.codegenNode;root.codegenNode = codegenNode;} else {root.codegenNode = child;}
}
  • generate 渲染,生成render
javascript">export function generate(ast, options = {}) {// 先生成 contextconst context = createCodegenContext(ast, options);const { push, mode } = context;// 1. 先生成 preambleContextif (mode === "module") {genModulePreamble(ast, context);} else {genFunctionPreamble(ast, context);}const functionName = "render";const args = ["_ctx"];// _ctx,aaa,bbb,ccc// 需要把 args 处理成 上面的 stringconst signature = args.join(", ");push(`function ${functionName}(${signature}) {`);// 这里需要生成具体的代码内容// 开始生成 vnode tree 的表达式push("return ");genNode(ast.codegenNode, context);push("}");return {code: context.code,};
}

1.2 reactivity

负责Vue3中响应式实现的部分,

1.2.1 目录结构

javascript">├─src
│      baseHandlers.ts // 基本处理逻辑
│      computed.ts // computed属性处理
│      dep.ts // effect对象存储逻辑
│      effect.ts // 依赖收集机制
│      index.ts // 入口文件
│      reactive.ts // 响应式处理逻辑
│      ref.ts // ref执行逻辑
│
└─__tests__ // 测试用例computed.spec.tsdep.spec.tseffect.spec.tsreactive.spec.tsreadonly.spec.tsref.spec.tsshallowReadonly.spec.ts

1.2.2 reactivity逻辑

  • index.ts
javascript">export {
reactive,readonly,shallowReadonly,isReadonly,isReactive,isProxy,
} from "./reactive";export { ref, proxyRefs, unRef, isRef } from "./ref";export { effect, stop, ReactiveEffect } from "./effect";export { computed } from "./computed";
  • reactive.ts Proxy 里getter,setter,响应式,get依赖收集trackset触发依赖trigger
javascript">import {mutableHandlers,readonlyHandlers,shallowReadonlyHandlers,
} from "./baseHandlers";export const reactiveMap = new WeakMap();
export const readonlyMap = new WeakMap();
export const shallowReadonlyMap = new WeakMap();export const enum ReactiveFlags {IS_REACTIVE = "__v_isReactive",IS_READONLY = "__v_isReadonly",RAW = "__v_raw",
}export function reactive(target) {return createReactiveObject(target, reactiveMap, mutableHandlers);
}export function readonly(target) {return createReactiveObject(target, readonlyMap, readonlyHandlers);
}export function shallowReadonly(target) {return createReactiveObject(target,shallowReadonlyMap,shallowReadonlyHandlers);
}export function isProxy(value) {return isReactive(value) || isReadonly(value);
}export function isReadonly(value) {return !!value[ReactiveFlags.IS_READONLY];
}export function isReactive(value) {// 如果 value 是 proxy 的话// 会触发 get 操作,而在 createGetter 里面会判断// 如果 value 是普通对象的话// 那么会返回 undefined ,那么就需要转换成布尔值return !!value[ReactiveFlags.IS_REACTIVE];
}export function toRaw(value) {// 如果 value 是 proxy 的话 ,那么直接返回就可以了// 因为会触发 createGetter 内的逻辑// 如果 value 是普通对象的话,// 我们就应该返回普通对象// 只要不是 proxy ,只要是得到了 undefined 的话,那么就一定是普通对象// TODO 这里和源码里面实现的不一样,不确定后面会不会有问题if (!value[ReactiveFlags.RAW]) {return value;}return value[ReactiveFlags.RAW];
}function createReactiveObject(target, proxyMap, baseHandlers) {// 核心就是 proxy// 目的是可以侦听到用户 get 或者 set 的动作// 如果命中的话就直接返回就好了// 使用缓存做的优化点const existingProxy = proxyMap.get(target);if (existingProxy) {return existingProxy;}const proxy = new Proxy(target, baseHandlers); // 重点!!!!getter,setter,响应式,依赖收集track,// 把创建好的 proxy 给存起来,proxyMap.set(target, proxy);return proxy;
}
  • ref.ts ref会触发自定义set和get方法,进而触发 tracktrigger,reactive会触发Proxy里的set和get方法
javascript">import { trackEffects, triggerEffects, isTracking } from "./effect";
import { createDep } from "./dep";
import { isObject, hasChanged } from "@mini-vue/shared";
import { reactive } from "./reactive";export class RefImpl {private _rawValue: any;private _value: any;public dep;public __v_isRef = true;constructor(value) {this._rawValue = value;// 看看value 是不是一个对象,如果是一个对象的话// 那么需要用 reactive 包裹一下this._value = convert(value);this.dep = createDep();}get value() {// 收集依赖trackRefValue(this);return this._value;}set value(newValue) {// 当新的值不等于老的值的话,// 那么才需要触发依赖if (hasChanged(newValue, this._rawValue)) {// 更新值this._value = convert(newValue);this._rawValue = newValue;// 触发依赖triggerRefValue(this);}}
}export function ref(value) {return createRef(value);
}function convert(value) {return isObject(value) ? reactive(value) : value;
}function createRef(value) {const refImpl = new RefImpl(value);return refImpl;
}export function triggerRefValue(ref) {triggerEffects(ref.dep);
}export function trackRefValue(ref) {if (isTracking()) {trackEffects(ref.dep);}
}// 这个函数的目的是
// 帮助解构 ref
// 比如在 template 中使用 ref 的时候,直接使用就可以了
// 例如: const count = ref(0) -> 在 template 中使用的话 可以直接 count
// 解决方案就是通过 proxy 来对 ref 做处理const shallowUnwrapHandlers = {get(target, key, receiver) {// 如果里面是一个 ref 类型的话,那么就返回 .value// 如果不是的话,那么直接返回value 就可以了return unRef(Reflect.get(target, key, receiver));},set(target, key, value, receiver) {const oldValue = target[key];if (isRef(oldValue) && !isRef(value)) {return (target[key].value = value);} else {return Reflect.set(target, key, value, receiver);}},
};// 这里没有处理 objectWithRefs 是 reactive 类型的时候
// TODO reactive 里面如果有 ref 类型的 key 的话, 那么也是不需要调用 ref.value 的
// (but 这个逻辑在 reactive 里面没有实现)
export function proxyRefs(objectWithRefs) {return new Proxy(objectWithRefs, shallowUnwrapHandlers);
}// 把 ref 里面的值拿到
export function unRef(ref) {return isRef(ref) ? ref.value : ref;
}export function isRef(value) {return !!value.__v_isRef;
}
  • effect 依赖收集,触发依赖
javascript">export function effect(fn, options = {}) {const _effect = new ReactiveEffect(fn);// 把用户传过来的值合并到 _effect 对象上去// 缺点就是不是显式的,看代码的时候并不知道有什么值extend(_effect, options);_effect.run();// 把 _effect.run 这个方法返回// 让用户可以自行选择调用的时机(调用 fn)const runner: any = _effect.run.bind(_effect);runner.effect = _effect;return runner;
}export function stop(runner) {runner.effect.stop();
}
  • computed 计算属性
javascript">import { createDep } from "./dep";
import { ReactiveEffect } from "./effect";
import { trackRefValue, triggerRefValue } from "./ref";export class ComputedRefImpl {public dep: any;public effect: ReactiveEffect;private _dirty: boolean;private _valueconstructor(getter) {this._dirty = true;this.dep = createDep();this.effect = new ReactiveEffect(getter, () => {// scheduler// 只要触发了这个函数说明响应式对象的值发生改变了// 那么就解锁,后续在调用 get 的时候就会重新执行,所以会得到最新的值if (this._dirty) return;this._dirty = true;triggerRefValue(this);});}get value() {// 收集依赖trackRefValue(this);// 锁上,只可以调用一次// 当数据改变的时候才会解锁// 这里就是缓存实现的核心// 解锁是在 scheduler 里面做的if (this._dirty) {this._dirty = false;// 这里执行 run 的话,就是执行用户传入的 fnthis._value = this.effect.run();}return this._value;}
}export function computed(getter) {return new ComputedRefImpl(getter);
}

1.3 runtime-core

运行的核心流程,其中包括初始化流程和更新流程

1.3.1 目录结构

javascript">├─src
│  │  apiInject.ts        // 提供provider和inject
│  │  apiWatch.ts        // 提供watch
│  │  component.ts        // 创建组件实例
│  │  componentEmits.ts        // 执行组件props 里面的 onXXX 的函数
│  │  componentProps.ts        // 获取组件props
│  │  componentPublicInstance.ts        // 组件通用实例上的代理,如$el,$emit等
│  │  componentRenderUtils.ts        // 判断组件是否需要重新渲染的工具类
│  │  componentSlots.ts        // 组件的slot
│  │  createApp.ts        // 根据跟组件创建应用
│  │  h.ts        // 创建节点
│  │  index.ts        // 入口文件
│  │  renderer.ts        // 渲染机制,包含diff
│  │  scheduler.ts // 触发更新机制
│  │  vnode.ts        // vnode节点
│  │
│  └─helpers
│          renderSlot.ts        // 插槽渲染实现
│
└─__tests__        // 测试用例apiWatch.spec.tscomponentEmits.spec.tsrendererComponent.spec.tsrendererElement.spec.ts

1.3.2 runtime核心逻辑

  • provide/inject provide全局提供,inject 注入使用
javascript">import { getCurrentInstance } from "./component";export function provide(key, value) {const currentInstance = getCurrentInstance();if (currentInstance) {let { provides } = currentInstance;const parentProvides = currentInstance.parent?.provides;// 这里要解决一个问题// 当父级 key 和 爷爷级别的 key 重复的时候,对于子组件来讲,需要取最近的父级别组件的值// 那这里的解决方案就是利用原型链来解决// provides 初始化的时候是在 createComponent 时处理的,当时是直接把 parent.provides 赋值给组件的 provides 的// 所以,如果说这里发现 provides 和 parentProvides 相等的话,那么就说明是第一次做 provide(对于当前组件来讲)// 我们就可以把 parent.provides 作为 currentInstance.provides 的原型重新赋值// 至于为什么不在 createComponent 的时候做这个处理,可能的好处是在这里初始化的话,是有个懒执行的效果(优化点,只有需要的时候在初始化)if (parentProvides === provides) {provides = currentInstance.provides = Object.create(parentProvides);}provides[key] = value;}
}export function inject(key, defaultValue) {const currentInstance = getCurrentInstance();if (currentInstance) {const provides = currentInstance.parent?.provides;if (key in provides) {return provides[key];} else if (defaultValue) {if (typeof defaultValue === "function") {return defaultValue();}return defaultValue;}}
}
  • watch 触发effect执行(effect里有收集的依赖,可以触发依赖)
javascript">import { ReactiveEffect } from "@mini-vue/reactivity";
import { queuePreFlushCb } from "./scheduler";// Simple effect.
export function watchEffect(effect) {doWatch(effect);
}function doWatch(source) {// 把 job 添加到 pre flush 里面// 也就是在视图更新完成之前进行渲染(待确认?)// 当逻辑执行到这里的时候 就已经触发了 watchEffectconst job = () => {effect.run();};// 这里用 scheduler 的目的就是在更新的时候// 让回调可以在 render 前执行 变成一个异步的行为(这里也可以通过 flush 来改变)const scheduler = () => queuePreFlushCb(job);const getter = () => {source();};const effect = new ReactiveEffect(getter, scheduler);// 这里执行的就是 gettereffect.run();
}
  • component 创建runtime运行时的一些组件属性
javascript">export function createComponentInstance(vnode, parent) {const instance = {type: vnode.type,vnode,next: null, // 需要更新的 vnode,用于更新 component 类型的组件props: {},parent,provides: parent ? parent.provides : {}, //  获取 parent 的 provides 作为当前组件的初始化值 这样就可以继承 parent.provides 的属性了proxy: null,isMounted: false,attrs: {}, // 存放 attrs 的数据slots: {}, // 存放插槽的数据ctx: {}, // context 对象setupState: {}, // 存储 setup 的返回值emit: () => {},};// 在 prod 坏境下的 ctx 只是下面简单的结构// 在 dev 环境下会更复杂instance.ctx = {_: instance,};// 赋值 emit// 这里使用 bind 把 instance 进行绑定// 后面用户使用的时候只需要给 event 和参数即可instance.emit = emit.bind(null, instance) as any;return instance;
}
  • createApp 根节点入口,vue3可以创建多个实例
javascript">import { createVNode } from "./vnode";export function createAppAPI(render) {return function createApp(rootComponent) {const app = {_component: rootComponent,mount(rootContainer) {console.log("基于根组件创建 vnode");const vnode = createVNode(rootComponent);console.log("调用 render,基于 vnode 进行开箱");render(vnode, rootContainer);},};return app;};
}
  • 创建Vnode节点
javascript">import { createVNode } from "./vnode";
export const h = (type: any , props: any = null, children: string | Array<any> = []) => {return createVNode(type, props, children);
};
  • 入口文件
javascript">export * from "./h";
export * from "./createApp";
export { getCurrentInstance, registerRuntimeCompiler } from "./component";
export { inject, provide } from "./apiInject";
export { renderSlot } from "./helpers/renderSlot";
export { createTextVNode, createElementVNode } from "./vnode";
export { createRenderer } from "./renderer";
export { toDisplayString } from "@mini-vue/shared";
export {// corereactive,ref,readonly,// utilitiesunRef,proxyRefs,isReadonly,isReactive,isProxy,isRef,// advancedshallowReadonly,// effecteffect,stop,computed,
} from "@mini-vue/reactivity";
  • render 将对象创建成DOM,涉及到update的Diff(基于vue2做了一些优化)
javascript">
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);
}
  • scheduler 通过微任务nextTick去执行
javascript">// 具体的调度机制见下节课内容
const queue: any[] = [];
const activePreFlushCbs: any = [];const p = Promise.resolve();
let isFlushPending = false;export function nextTick(fn?) {return fn ? p.then(fn) : p;
}export function queueJob(job) {if (!queue.includes(job)) {queue.push(job);// 执行所有的 jobqueueFlush();}
}function queueFlush() {// 如果同时触发了两个组件的更新的话// 这里就会触发两次 then (微任务逻辑)// 但是着是没有必要的// 我们只需要触发一次即可处理完所有的 job 调用// 所以需要判断一下 如果已经触发过 nextTick 了// 那么后面就不需要再次触发一次 nextTick 逻辑了if (isFlushPending) return;isFlushPending = true;nextTick(flushJobs);
}export function queuePreFlushCb(cb) {queueCb(cb, activePreFlushCbs);
}function queueCb(cb, activeQueue) {// 直接添加到对应的列表内就ok// todo 这里没有考虑 activeQueue 是否已经存在 cb 的情况// 然后在执行 flushJobs 的时候就可以调用 activeQueue 了activeQueue.push(cb);// 然后执行队列里面所有的 jobqueueFlush()
}function flushJobs() {isFlushPending = false;// 先执行 pre 类型的 job// 所以这里执行的job 是在渲染前的// 也就意味着执行这里的 job 的时候 页面还没有渲染flushPreFlushCbs();// 这里是执行 queueJob 的// 比如 render 渲染就是属于这个类型的 joblet job;while ((job = queue.shift())) {if (job) {job();}}
}function flushPreFlushCbs() {// 执行所有的 pre 类型的 jobfor (let i = 0; i < activePreFlushCbs.length; i++) {activePreFlushCbs[i]();}
}
  • vnode类型定义及格式规范
javascript">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) {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));} else {return child;}
}// 基于 type 来判断是什么类型的组件
function getShapeFlag(type: any) {return typeof type === "string"? ShapeFlags.ELEMENT: ShapeFlags.STATEFUL_COMPONENT;
}

1.4 runtime-dom

Vue3靠虚拟dom,实现跨平台的能力,runtime-dom提供一个渲染器,这个渲染器可以渲染虚拟dom节点到指定的容器中;

1.4.1 主要功能

javascript">// 源码里面这些接口是由 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 "@vue/runtime-core"

1.5 runtime-test

可以理解成runtime-dom的延伸,,因为runtime-test对外提供的确实是dom环境的测试,方便用于runtime-core的测试;

1.5.1 目录结构

javascript">──src
index.ts
nodeOps.ts
patchProp.ts
serialize.ts

1.5.2 runtime-test核心逻辑

  • index.ts
javascript">// 实现 render 的渲染接口
// 实现序列化
import { createRenderer } from "@mini-vue/runtime-core";
import { extend } from "@vue/shared";
import { nodeOps } from "./nodeOps";
import { patchProp } from "./patchProp";export const { render } = createRenderer(extend({ patchProp }, nodeOps));export * from "./nodeOps";
export * from "./serialize"
export * from '@mini-vue/runtime-core'
- nodeOps,节点定义及操作再runtime-core中的映射
export const enum NodeTypes {ELEMENT = "element",TEXT = "TEXT",
}let nodeId = 0;
// 这个函数会在 runtime-core 初始化 element 的时候调用
function createElement(tag: string) {// 如果是基于 dom 的话 那么这里会返回 dom 元素// 这里是为了测试 所以只需要反正一个对象就可以了// 后面的话 通过这个对象来做测试const node = {tag,id: nodeId++,type: NodeTypes.ELEMENT,props: {},children: [],parentNode: null,};return node;
}function insert(child, parent) {parent.children.push(child);child.parentNode = parent;
}function parentNode(node) {return node.parentNode;
}function setElementText(el, text) {el.children = [{id: nodeId++,type: NodeTypes.TEXT,text,parentNode: el,},];
}export const nodeOps = { createElement, insert, parentNode, setElementText };
  • serialize,序列化: 把Vnode处理成 string
javascript">// 把 node 给序列化
// 测试的时候好对比import { NodeTypes } from "./nodeOps";// 序列化: 把一个对象给处理成 string (进行流化)
export function serialize(node) {if (node.type === NodeTypes.ELEMENT) {return serializeElement(node);} else {return serializeText(node);}
}function serializeText(node) {return node.text;
}export function serializeInner(node) {// 把所有节点变成一个stringreturn node.children.map((c) => serialize(c)).join(``);
}function serializeElement(node) {// 把 props 处理成字符串// 规则:// 如果 value 是 null 的话 那么直接返回 ``// 如果 value 是 `` 的话,那么返回 key// 不然的话返回 key = value(这里的值需要字符串化)const props = Object.keys(node.props).map((key) => {const value = node.props[key];return value == null? ``: value === ``? key: `${key}=${JSON.stringify(value)}`;}).filter(Boolean).join(" ");console.log("node---------", node.children);return `<${node.tag}${props ? ` ${props}` : ``}>${serializeInner(node)}</${node.tag}>`;
}

1.6 shared

公用逻辑

javascript">export * from '../src/shapeFlags';
export * from '../src/toDisplayString';export const isObject = val => {return val !== null && typeof val === 'object';
};export const isString = val => typeof val === 'string';const camelizeRE = /-(\w)/g;
/*** @private* 把中划线命名方式转换成驼峰命名方式*/
export const camelize = (str: string): string => {return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''));
};export const extend = Object.assign;// 必须是 on+一个大写字母的格式开头
export const isOn = key => /^on[A-Z]/.test(key);export function hasChanged(value, oldValue) {return !Object.is(value, oldValue);
}export function hasOwn(val, key) {return Object.prototype.hasOwnProperty.call(val, key);
}/*** @private* 首字母大写*/
export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);/*** @private* 添加 on 前缀,并且首字母大写*/
export const toHandlerKey = (str: string) => (str ? `on${capitalize(str)}` : ``);// 用来匹配 kebab-case 的情况
// 比如 onTest-event 可以匹配到 T
// 然后取到 T 在前面加一个 - 就可以
// \BT 就可以匹配到 T 前面是字母的位置
const hyphenateRE = /\B([A-Z])/g;
/*** @private*/
export const hyphenate = (str: string) => str.replace(hyphenateRE, '-$1').toLowerCase();// 组件的类型
export const enum ShapeFlags {// 最后要渲染的 element 类型ELEMENT = 1,// 组件类型STATEFUL_COMPONENT = 1 << 2,// vnode 的 children 为 string 类型TEXT_CHILDREN = 1 << 3,// vnode 的 children 为数组类型ARRAY_CHILDREN = 1 << 4,// vnode 的 children 为 slots 类型SLOTS_CHILDREN = 1 << 5
}export const toDisplayString = (val) => {return String(val);
};

2、Vue 3 Diff算法

Vue3 diff 优化点,两个

  1. 静态标记 + 非全量 Diff:(Vue 3在创建虚拟DOM树的时候,会根据DOM中的内容会不会发生变化,添加一个静态标记。之后在与上次虚拟节点进行对比的时候,就只会对比这些带有静态标记的节点。);
  2. 使用最长递增子序列优化对比流程,可以最大程度的减少 DOM 的移动,达到最少的 DOM 操作;

2.1 静态标记 + 非全量 Diff

我们看这两段文字

javascript">hello chenghuai
hey chenghuai

我们会发现,这两段文字是有一部分是相同的,这些文字是不需要修改也不需要移动的,真正需要进行修改中间的几个字母,所以diff就变成以下部分

javascript">text1: llo
text2: y

不会进行全部比较,也叫非全量 Diff,静态标记是为了进行非全量 Diff

2.2 最长递增子序列

通过位置数组判断最小化移动次数

那么为什么最长递增子序列就可以保证移动次数最少呢?因为在位置数组中递增就能保证在旧数组中的相对位置的有序性,从而不需要移动,因此递增子序列的最长可以保证移动次数的最少

比如 原数组中的[ b, c, d], 新数组为 [d, b, c],得到的位置数组为 [3, 1, 2],得到最长递增子序列 [1, 2] ,在子序列内的元素不移动,不在此子序列的元素移动即可。对应的实际的节点即 d 节点移动至b, c前面即可。

强烈建议看leetcode原题解法:https://leetcode.cn/problems/longest-increasing-subsequence/

我们以该数组为例:

javascript">[10,9,2,5,3,8,7,13][10,9,2,5,3,8,7,13]

我们可以使用动态规划的思想考虑这个问题。动态规划的思想是将一个大的问题分解成多个小的子问题,并尝试得到这些子问题的最优解,子问题的最优解有可能会在更大的问题中被利用,这样通过小问题的最优解最终求得大问题的最优解
我们先假设只有一个值的数组[13],那么该数组的最长递增子序列就是[13]自己本身,其长度为1。那么我们认为每一项的递增序列的长度值均为1
那么我们这次给数组增加一个值[7, 13], 由于7 < 13,所以该数组的最长递增子序列是[7, 13],那么该长度为2。那么我们是否可以认为,当[7]小于[13]时,以[7]为头的递增序列的长度是,[7]的长度和[13]的长度的和,即1 + 1 = 2。
ok,我们基于这种思想来给计算一下该数组。我们先将每个值的初始赋值为1
在这里插入图片描述

首先 7 < 13 那么7对应的长度就是13的长度再加1,1 + 1 = 2
在这里插入图片描述

继续,我们对比8。我们首先和7比,发现不满足递增,但是没关系我们还可以继续和13比,8 < 13满足递增,那么8的长度也是13的长度在加一,长度为2
[图片]

我们再对比3,我们先让其与8进行对比,3 < 8,那么3的长度是8的长度加一,此时3的长度为3。但是还没结束,我们还需要让3与7对比。同样3 < 7,此时我们需要在计算出一个长度是7的长度加一同样是3,我们对比两个长度,如果原本的长度没有本次计算出的长度值大的话,我们进行替换,反之则我们保留原本的值。由于3 === 3,我们选择不替换。最后,我们让3与13进行对比,同样的3 < 13,此时计算出的长度为2,比原本的长度3要小,我们选择保留原本的值。

[图片]

[图片]

我们从中取最大的值4,该值代表的最长递增子序列的个数。代码如下:

javascript">function lis(arr) {let len = arr.length,dp = new Array(len).fill(1); // 用于保存长度for (let i = len - 1; i >= 0; i--) {let cur = arr[i]for(let j = i + 1; j < len; j++) {let next = arr[j]// 如果是递增 取更大的长度值if (cur < next) dp[i] = Math.max(dp[j]+1, dp[i])}}return Math.max(...dp)
}

在vue3.0中,我们需要的是最长递增子序列在原本数组中的索引。所以我们还需要在创建一个数组用于保存每个值的最长子序列所对应在数组中的index。具体代码如下:

javascript">function lis(arr) {let len = arr.length,res = [],dp = new Array(len).fill(1);// 存默认indexfor (let i = 0; i < len; i++) {res.push([i])}for (let i = len - 1; i >= 0; i--) {let cur = arr[i],nextIndex = undefined;// 如果为-1 直接跳过,因为-1代表的是新节点,不需要进行排序if (cur === -1) continuefor (let j = i + 1; j < len; j++) {let next = arr[j]// 满足递增条件if (cur < next) {let max = dp[j] + 1// 当前长度是否比原本的长度要大if (max > dp[i]) {dp[i] = maxnextIndex = j}}}// 记录满足条件的值,对应在数组中的indexif (nextIndex !== undefined) res[i].push(...res[nextIndex])}let index = dp.reduce((prev, cur, i, arr) => cur > arr[prev] ? i : prev, dp.length - 1)// 返回最长的递增子序列的indexreturn result[index]
}

http://www.ppmy.cn/news/1506080.html

相关文章

力扣——572.另一个树的子树

题目&#xff1a; 思路&#xff1a; 深度优先搜索&#xff0c;遍历root的每一个节点代表的整棵树是否和subroot一样。比较是否一样的时候可以从根节点开始递归&#xff0c;首先查看是否为空&#xff0c;然后值是否一样。 代码&#xff1a; vs可运行代码&#xff1a; &#…

循环依赖问题和Spring三级缓存

产生原因&#xff1a;两个或多个bean之间互相持有对方的引用 解决&#xff1a;spring三级缓存 一级缓存&#xff1a;单例池&#xff0c;存放已经经历了完整的生命周期的bean 二级缓存&#xff1a;存放早期的&#xff0c;还没走完生命周期的bean 三级缓存&#xff1a;存放对…

机械学习—零基础学习日志(python编程)

零基础为了学人工智能&#xff0c;正在艰苦的学习 昨天给高等数学的学习按下暂停键&#xff0c;现在开始学习python编程。 我学习的思路是直接去阿里云的AI学习课堂里面学习。 整体感觉&#xff0c;阿里云的AI课堂还是有一些乱&#xff0c;早期课程和新出内容没有更新和归档…

鸿蒙媒体开发【拼图】拍照和图片

拼图 介绍 该示例通过ohos.multimedia.image和ohos.file.photoAccessHelper接口实现获取图片&#xff0c;以及图片裁剪分割的功能。 效果预览 使用说明&#xff1a; 使用预置相机拍照后启动应用&#xff0c;应用首页会读取设备内的图片文件并展示获取到的第一个图片&#x…

Centos7安装Zabbix5.0的yum安装失败的解决方案

目前由于Centos7停服以及Zabbix官方限制了其5.0版本在Centos7上安装服务版本&#xff0c;因此可能会导致安装Zabbix5.0的一些组件无法正常安装。 zabbix5.0安装参考&#xff1a;一、zabbix 5.0 部署_zabbix5.0部署-CSDN博客 问题现象 当安装到zabbix的GUI包时报如下错误&…

docker上传镜像至阿里云

1、安装wsl2 WSL2安装&#xff08;详细过程&#xff09; 2、安装docker Docker在Windows下的安装及使用 3、创建私人阿里云镜像库 如何创建私人阿里云镜像仓库&#xff1f;&#xff08;保姆级&#xff09; 4、如何删除容器 (1) 查找正在使用该图像的容器 docker ps -a --filte…

ADC模数转换在stm32上的应用

ADC模数转换在stm32上的应用 文章目录 ADC模数转换在stm32上的应用1. 什么是ADC&#xff1f;2. stm32中的ADC3. 12位逐次逼近型ADC1. 工作原理2. 主要特性3. 关键部件4. 优点5. 缺点6. 应用7. 配置和使用 4. stm32的ADC转换模式1. 单次转换模式 (Single Conversion Mode)2. 连续…

SSB概述

SSB的作用 同步广播块 (SSB, Synchronization Signal Block) 在5G NR&#xff08;新无线电&#xff09;中的作用非常重要&#xff0c;主要包括以下几个方面&#xff1a; 时间和频率同步&#xff1a;SSB提供了初始的时间和频率同步信号&#xff0c;帮助用户设备&#xff08;UE…

搭建个人博客需要做哪些事

文章目录 前言搭建步骤站点服务器站点域名注册域名ICP 备案公安备案域名解析 博客图床图床是什么图床搭建 博客站点搭建建站工具本地搭建博客部署 站点运营百度收录百度统计 总结 前言 花了几天时间&#xff0c;搭建了一个个人博客&#xff0c;也算是完成了年初立的一个flag&a…

CV党福音:YOLOv8实现分类

YOLO作为目标检测领域的常青树&#xff0c;如今以及更新到了YOLOv10&#xff0c;并且还有YOLOX、YOLOS等变体&#xff0c;可以说该系列已经在目标检测领域占据了半壁江山&#xff0c;如今&#xff0c;YOLOv8的发行者ultralytics竟有一统江山之意&#xff0c;其在提出的框架中不…

使用 Streamlit 和 Python 构建 Web 应用程序

一.介绍 在本文中&#xff0c;我们将探讨如何使用 Streamlit 构建一个简单的 Web 应用程序。Streamlit 是一个功能强大的 Python 库&#xff0c;允许开发人员快速轻松地创建交互式 Web 应用程序。Streamlit 旨在让 Python 开发人员尽可能轻松地创建 Web 应用程序。以下是一些主…

SQLite批量INSERT

SQLite是一种轻量级、零配置的数据库管理系统。它的数据存储在一个单一的磁盘文件上,使得它非常适合嵌入式系统和移动应用。 在SQLite数据库中进行大批量记录INSERT,有三种方法,三种方法的效率由高低,本文举例说明。 方法一:逐条记录INSERT,这也是效率最低的方法 下面以…

用录制好的视频文件模拟PC电脑摄像头进行无人值守直播/抖音直播/视频号直播/快手直播

现在几乎全面都在刷短视频、看视频直播&#xff0c;现在市场上的数字人直播、无人直播设备也很多&#xff0c;但起步门槛都太高了&#xff0c;对于普通用户或者企业&#xff0c;实际只需要有那么一个简单的软件或者硬件&#xff0c;能将我们已经提前录制的视频作为实时视频源&a…

postgresql 分组合并 字段

postgresql 分组合并 字段 在PostgreSQL中&#xff0c;如果你想要将多个行的字段值合并为一个字段&#xff0c;你可以使用string_agg函数。这个函数可以将同一个组内的指定字段的值连接成一个字符串&#xff0c;并且可以自定义连接符。 下面是一个简单的例子&#xff0c;假设我…

等保测评中的访问控制与用户认证:构建安全的访问管理机制

在当今数字化时代&#xff0c;信息安全已成为企业和组织不可忽视的关键议题。等保测评&#xff0c;作为我国信息安全等级保护制度的重要组成部分&#xff0c;对访问控制与用户认证提出了严格要求&#xff0c;旨在构建安全的访问管理机制&#xff0c;保护信息资产不受未授权访问…

图片转换之heic转jpg(使用ImageMagick)

缘由&#xff1a;iphone的图库&#xff0c;用jpg拍照保存后内存占比较大&#xff0c;heic格式会微缩不少。问题来了&#xff0c;电脑不能直接小图预览heic。 分析&#xff1a;现在就是解决小图预览的问题&#xff08;大图用wps可以看&#xff09; 解决&#xff1a;查找了一些…

IO进程----文件IO

目录 IO进程 文件IO 1. 概念 2. 特点 3. 函数 3.1. 打开文件 3.2. 关闭文件 3.3. 读写文件 read write 3.4. 文件定位操作 文件属性获取 目录操作 IO进程 文件IO 1. 概念 在posix(可移植操作系统接口)中定义的一组输入输出的函数 2. 特点 1. 没有缓冲机制&#xff0c…

数据结构(java实现)——优先级队列,堆

文章目录 优先级队列堆堆的概念堆的模拟实现创建堆入堆判满删除判空获取栈顶元素 创建堆两种方式的时间复杂度堆排序java提供的PriorityQueue类基本的属性关于PriorityQueue类的三个构造方法关于PriorityQueue类中&#xff0c;入堆方法是怎样实现的&#xff1f;PriorityQueue注…

JavaFx中通过线程池运行或者停止多个周期性任务

在JavaFX中&#xff0c;要实现点击按钮启动多个周期性任务并通过多线程执行&#xff0c;并在任务结束后将结果写入多个文本组件中&#xff0c;同时提供另一个按钮来停止这些任务&#xff0c;你可以使用ScheduledExecutorService来管理周期性任务&#xff0c;并使用AtomicBoolea…

智能化的Facebook未来:AI如何重塑社交网络的面貌?

随着人工智能&#xff08;AI&#xff09;技术的飞速发展&#xff0c;社交网络的面貌正在经历深刻的变革。Facebook&#xff08;现Meta Platforms&#xff09;作为全球最大的社交媒体平台之一&#xff0c;正积极探索如何利用AI技术来提升用户体验、优化内容管理并推动平台创新。…