【Vue3 核心模块源码解析(上)】讲到了 Vue2 与 Vue3的一些区别,Vue3 新特性的使用,以及略微带了一点源码。那么这篇文章就要从
Vue3 模块源码解析
与Vue3 执行逻辑解析
这两个方面去给大家剖析 Vue3 的深层次,一起学习起来吧!
这里还是想多说一点,源码几句话很难去解释清除,下面的核心代码都有写注释,大家按照思路一起走下去吧,本文末尾会有整体的流程图!
为什么要去看源码?可能很多人感觉
你在装X
,事实并不是这样,就像我们在 【上】中讲到 ref 与 reactive 都可以生成响应式数据,为什么更推荐用 reactive 来代替 ref 生成深层次响应式数据结构呢?读读源码,从宏观的设计角度去考虑,可以更快的加速我们的成长!
本篇文章主要从
packages -> compiler-core/reactivity/runtime-core
这三个阶段去学习,Vue3是如何实现响应式的
、Vue3怎么样实现AST模型
、Vue3如何在运行时做一些Diff
开头提到的 MonoRepo 的包管理方式也在packages
的包中提现出来了,一个多包的管理方式
Vue3 的编译核心,作用就是将字符串转换成
抽象语法树AST(Abstract Syntax Tree)
|-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||—— snapshotscodegen.spec.ts.snap
1.2.1 compile.ts
为了方便阅读与理解,把
TS 的类型部分
、环境判断
、断言
等相关内容省略了
compile
这个包主要是实现了什么能力呢?下面就是compiler-core
核心
- 把用户输入的内容做了
AST
的转换,- 转译成
Vue
能够识别的语言或者说Vue
能够识别的语法
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(tempalte);// 2. 给 ast 加点料 --> 做了一些处理transform(ast,Object.assign(options,{ nodeTransforms: [transformElement, transformText, transformExpression] }))// 3. 生成 render 函数return generate(ast)
}
1.2.2 parse.ts
AST 的逻辑
简而言之就是一开始我们是一个模板的语言,之后通过我们的一套解析规则,最后可以生成一个Tree
或者说是一个对象
,对象里面就是对应我们的标签属性,比如type、value、等,可以这么简单的理解AST。
parse.ts 主要就是进行一些 AST 的逻辑处理
import { ElementTypes, NodeTypes ] from "./ast";const enum TagType {start,End ,
}export function baseParse(content: string) {// 创建上下文const context = createParserContext(content);return createRoot(parseChildren(context,[]));
}function createParserContext(content) {console.log("创建 parseContext");return {// 真实源码会有很多参数,这里省略了source: content }
}function parseChildren(context, ancestors) {console.log('开始解析 children');const nodes: any[] = []while (!isEnd(context, ancestors)) {const s = context.sourcelet node = undefinedif (startsWith(s, "{{")) {// '{{'// 看看如果是 {{ 开头的话,那么就是一个插值,那么去解析他node = parseInterpolation(context, mode)} else if (s[0] === '<') {if (s[1] === '/') {// https://html.spec.whatwg.org/multipage/parsing.html#end-tag-open-state// 这里属于 edge case 可以不用关心// 处理结束标签if (/[a-z]/i.test(s[2])) {// 匹配
1.2.3 transform.ts
transform
方法主要做了一下几点事
- 创建 context
- 递归遍历 node, 针对不同的类型(NodeTypes)做不同的处理
- createRootCodegen --> 创建根节点
helper
有点类似于 GC
当中的 引用计数 算法很像,这里维护的是一个 Map 对象
,比如在 unMount
会判断我们当前 count 是否为0,为0时则删除,也是做垃圾回收用的;
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 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;}
}
1.2.4 generate.ts
import { isString } from '@mini-vue/shared';
import { NodeTypes } from './ast';
import { CREATE_ELEMENT_VNODE,
helperNameMap,TO_DISPLAY_STRING } from './runtimeHelpers'; 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,};
}
reactivity
实现了什么样的逻辑呢?
可以看下面index.ts
的引入,基本上就是我们在 Vue3 核心模块源码解析(上)
中讲到的实现响应式的 内容reactive、ref、isRef 、effect等;
export {reactive,readonly,shal1owReadonly,isReadonly,isReactive,isProxy,
} from "./reactive";export { ref, proxyRefs, unRef, isRef } from "./ref";
export { effect, stop, ReactiveEffect } from "./effect";
export { computed } from "./computed";
|-src| |—— index.ts // 所有响应式 API 的暴露,比如ref、unRef、isRef、effect 等| |—— reactive.ts // reactive 响应式 的实现| |—— ref.ts // ref 响应式 的实现| |—— dep.ts // | |—— effect.ts // | |—— baseHandler.ts // | |—— computed.ts // | || ||||——// 测试用例tests|—— xxx.spec.ts|—— xxxx.spec.ts // 对应src目录下
2.2.1 reactive.ts
Vue3
的响应式基本上都是通过weakMap
来实现的,最核心的原因是weakMap
可以使用对象的方式作为键,其次就是弱引用
更好的支持垃圾回收。
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;}
}function createReactiveobject(target, proxyMap, baseHandlers) {// 核心就是 proxy// 目的是可以侦听到用户 get 或者 set 的动作// 如果命中的话就直接返回就好了// 使用缓存做的优化点const existingProxy = proxyMap.get(target);if (existingProxy) {return existingProxy;}const proxy = new Proxy(target, baseHandlers);// 把创建好的 proxy 给存起来,proxyMap.set(target, proxy);return proxy;
}
2.2.2 ref.ts
ref 的大概实现逻辑
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);// 这里会在dep.ts 里面单独声明// 其实就是一个 new Set 的结构this.dep = createDep();}get value() {// 收集依赖// 这里类似于 Vue2 的 watcher 依赖收集// dep.add() 不过相比 Vue2 的数组,这里做了 new Set 的优化// 收集依赖时会 先判断是否收集过trackRefValue(this);return this._value;}set value(newValue) {// 当新的值不等于老的值的话// 那么才需要触发依赖if (hasChanged(newValue, this._rawValue)) {// 更新值this._value = convert(newValue);this._rawValue = newValue;// 执行收集到的所有的依赖 effect 的 run 方法// 类似于 Vue2 中的 Dep.notify()// 内部实际上是用 scheduler 可以让用户自己选择调用时机// 在 runtime-core 中,就是使用了 scheduler 实现在 next ticker 中调用的逻辑triggerRefValue(newValue);}}
}export function ref(value) {return createRef(value);
}
function convert(value) {// 这里 isobject 非常简单,就是用的 Object.isreturn 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);}
}// 这里没有处理 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;
}
2.2.3 baseHandler.ts
baseHandler 对响应式的处理,
get
set
等
问题:为什么是 readonly 的时候不做依赖收集呢?
readonly 的话,是不可以被 set 的,那不可以被 set 就意味着不会触发 trigger,所以就没有收集依赖的必要了
const get = createGetter();
const set = createSetter();
const readonlyGet = createGetter(true);
const shallowReadonlyGet = createGetter(true, true);function createGetter(isReadonly = false, shallow = false) {return function get(target: Target, key: string | symbol, receiver: object) {if (key === ReactiveFlags.IS_REACTIVE) {return !isReadonly;} else if (key === ReactiveFlags.IS_READONLY) {return isReadonly;} else if (key === ReactiveFlags.IS_SHALLOW) {return shallow;} else if (key === ReactiveFlags.RAW &&receiver ===(isReadonly? shallow? shallowReadonlyMap: readonlyMap: shallow? shallowReactiveMap: reactiveMap).get(target)) {return target;}const targetIsArray = isArray(target);if (!isReadonly) {if (targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver);}if (key === "hasOwnProperty") {return hasOwnProperty;}}const res = Reflect.get(target, key, receiver);if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {return res;}// 问题:为什么是 readonly 的时候不做依赖收集呢?// readonly 的话,是不可以被 set 的,那不可以被 set 就意味着不会触发 trigger// 所以就没有收集依赖的必要了if (!isReadonly) {// 在触发get的时候信息依赖收集track(target, TrackOpTypes.GET, key);}if (shallow) {return res;}if (isRef(res)) {// ref unwrapping - skip unwrap for Array + integer key.return targetIsArray && isIntegerKey(key) ? res : res.value;}if (isObject(res)) {// 把内部所有的是 object 的值都用 reactive 包裹,变成响应式// 如果说这个 res 值是一个对象的话,那么我们需要把获取到的 res 也转换成 reactive// res 等于 target[key]return isReadonly ? readonly(res) : reactive(res);}return res;};
}export const mutableHandlers: ProxyHandler
runtime-core
: 整个runtime
的核心,runtime-dom
、runtime-test
等都是为runtime-core提供DOM操作的能力
|-src| |—— index.ts // 主文件,暴露 Vue 运行时所需要的各种方法、enum、VNode等| |—— apiCreateApp.ts // 创建根节点 | |—— vnode.ts // 定义节点的结构并处理这些结构| |—— component.ts // 组件返回的所有内容,暴露给用户 -> getCurrentInstance| |—— componentEmits.ts // emit 方法的处理| |—— componentProps.ts // props 的处理| |—— componentPublicInstance.ts // 共用的 instance| |—— componentSlots.ts // 组件插槽处理| |—— apiInject.ts // inject 方法的处理| |—— apiWatch.ts // watch 方法的处理| |—— renderer.ts // 核心点,diff 的初始化及所有的初始化,下一篇中会详细讲解 Diff| |—— rendererTemplateRef.ts | |—— hmr.ts | |—— h.ts | |—— hydration.ts| |—— profiling.ts| |—— directives.ts| |—— devtools.ts| |—— customFormatter.ts| |—— componentOptions.ts| |—— compat| |—— helpers|——// 测试用例tests|—— xxx.spec.ts|—— xxxx.spec.ts // 对应src目录下
runtime-core 代码片段都比较长,此处挑一些精简过的核心
3.2.1 index.ts
export {// corereactive,ref,readonly,// utilitiesunref,proxyRefs,isRef,toRef,toRefs,isProxy,isReactive,isReadonly,isShallow,// advancedcustomRef,triggerRef,shallowRef,shallowReactive,shallowReadonly,markRaw,toRaw,// effecteffect,stop,ReactiveEffect,// effect scopeeffectScope,EffectScope,getCurrentScope,onScopeDispose
} from '@vue/reactivity'
export { computed } from './apiComputed'
export {watch,watchEffect,watchPostEffect,watchSyncEffect
} from './apiWatch'
3.2.2 apiCreateApp.ts
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;};
}
3.2.3 componentEmits.ts
import { camelize, hyphenate, toHandlerKey } from "@mini-vue/shared";export function emit(instance, event: string, ...rawArgs) {// 1.emit 是基于 props 里面的 onXXX 的函数来进行匹配的// 所以我们先从 props 中看看是否有对应的 event handlerconst props = instance.props;// ex: event -> cick 那么这里取的就是 onclick// 让事情变的复杂一点如果是中划线命名的话,需要转换成change-page -> changePage// 需要得到事件名称let handler = props[toHandlerKey(camelize(event))];// 如果上面没有匹配的话 那么在检测一下 event 是不是 kebab-case 类型if (!handler) {handler = props[toHandlerKey(hyphenate(event))];}if (handler) {handler(...rawArgs);}
}
3.2.4 componentProps.ts
export function initProps(instance, rawProps) {console.log("initProps");// TODO// 应该还有 attrs 的概念//attrs// 如果组件声明了 props 的话,那么才可以进入 props 属性内//// 不然的话是需要存储在 attrs 内// 这里暂时直接赋值给 instance.props 即可instance.props = rawProps;
}
3.2.5 component.ts
import { initProps } from "./componentProps";
import { initslots } from "./componentslots";
import { emit } from "./componentEmits";
import { PublicInstanceProxyHandlers } from "./componentPublicInstance";
import { proxyRefs, shallowReadonly } from "@mini-vue/reactivity";export function createComponentInstance(vnode, parent) {const instance = {type: vnode.type,vnode,next: null, // 需要更新的 ynode,用于更新 component 类型的组件props: (]parent,provides: parent ? parent.provides : {}, // 取 parent 的 provides 作为当前组件的初始化值,这样就可以继承parent.proviedisMounted: 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;
}// 组件 setup 的初始化
export function setupComponent(instance) {// 1.处理 props// 取出存在 vnode 里面的 propsconst { props, children } = instance.vnode;initProps(instance, props);// 2。处理 slotsinitslots(instance, children);// 源码里面有两种类型的 component// 一种是基于 options 创建的// 还有一种是 function 的// 这里处理的是 options 创建的// 叫做 stateful 类型setupStatefulComponent(instance);
}function setupStatefulComponent(instance) {// todo// 1,先创建代理 proxyconsole.log("创建 proxy");// proxy 对象其实是代理了 instance.ctx 对象// 我们在使用的时候需要使用 instance.proxy 对象// 因为 instance.ctx 在 prod 和 dev 坏境下是不同的instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);// 用户声明的对象就是 instance.type// const Component = {setup(),render()} ....const Component = instance.type;// 2,调用 setup// 调用 setup 的时候传入 propsconst { setup } = Component;if (setup) {// 设置当前 currentInstance 的值// 必须要在调用 setup 之前setCurrentInstance(instance);const setupContext = createSetupContext(instance);// 真实的处理场景里面应该是只在 dev 环境才会把 props 设置为只读的const setupResult =setup && setup(shallowReadonly(instance.props), setupContext);setCurrentInstance(null);// 3。处理 setupResulthandleSetupResult(instance, setupResult);} else {finishComponentsetup(instance);}
}function handleSetupResult(instance, setupResult) {// setup 返回值不一样的话,会有不同的处理// 1.看看 setupResult 是个什么if (typeof setupResult === "function") {// 如果返回的是 function 的话,那么绑定到 render 上// 认为是 render 逻辑// setup()f return ()=>(h("div")) }instance.render = setupResult;} else if (typeof setupResult === "object") {// 返回的是一个对象的话// 先存到 setupstate 上// 先使用 @vue/reactivity 里面的 proxyRefs//后面我们自己构建// proxyRefs 的作用就是把 setupResult 对象做一层代理// 方便用户直接访问 ref 类型的值// 比如 setupResult 里面有个 count 是个 ref 类型的对象,用户使用的时候就可以直接使用 count 了, 而不需要在count.value// 这里也就是官网里面说到的自动结构 Ref 类型instance.setupState = proxyRefs(setupResult);}finishComponentsetup(instance);
}
runtime-dom
包:Vue 的底层为什么通过 AST 转换,然后可以在上层供我们的Native、H5、小程序(mpvue)使用;
Vue 是通过 Virtual DOM 实现,runtime-dom
我们可以理解为,给我们VDOM
提供了具有真实DOM一样的能力,就是,比如:createElement、createApp、createRenderer等等
此处只是列举
createElement: (tag, isSVG, is, props): Element => {const el = isSVG? doc.createElementNS(svgNS, tag): doc.createElement(tag, is ? { is } : undefined)if (tag === 'select' && props && props.multiple != null) {;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)}return el},
shared
包主要会返回一些通用的逻辑,比如: isObject()、isString()、camelize()、isOn()等等,实际上和 utils 没什么区别
|-src| |—— index.ts // 核心方法库,类似于 utils| |—— shapeFlags.ts // enum 类型文件| |—— toDiaplayString.ts // 通用转换方法| |
这里面的方法很多,这里只是列举一个
const camelizeRE = /-(\w)/g;
/*** @private* 把中划线命名方式转换成驼峰命名方式*/export const camelize = (str: string): string => {return str.replace(camelizeRE, (_, c) => (c ? c.toupperCase() : ""));
};
结语:到此 【Vue3 核心模块源码解析(中)】结束,本篇主要是以 繁琐的代码块为主,配合上 init 整体的流程图,分享了精简后大致的源码;
当然,最关键的 Diff 还没有讲到,Vue2、Vue3、React 的DIff 有什么区别,Vue3 中的 Diff是如何升级的;
下一篇:分子模拟—Ovito渲染案例教程