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依赖收集track
,set触发依赖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方法,进而触发
track
,trigger
,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 优化点,两个
- 静态标记 + 非全量 Diff:(Vue 3在创建虚拟DOM树的时候,会根据DOM中的内容会不会发生变化,添加一个静态标记。之后在与上次虚拟节点进行对比的时候,就只会对比这些带有静态标记的节点。);
- 使用最长递增子序列优化对比流程,可以最大程度的减少 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]
}