Vue进阶之Vue3源码解析(一)
Vue3源码解析
- 目录结构
- 编译
- compiler-core
- package.json
- src/index.ts 入口文件
- src/compile.ts
- 生成AST
- src/parse.ts
- 代码转换
- src/transform.ts
- 几种策略模式
- src/transforms/transformElement.ts
- src/transforms/transformText.ts
- src/transforms/transformExpression.ts
- 代码生成
- src/codegen.ts
- 响应式
- reactivity
- 入口,src/index.ts
- reactive方法
- src/reactive.ts
- src/baseHanlders.ts
- src/effect.ts
- ref方法
- src/ref.ts
- ref,reactive,readonly的区别
- computed
目录结构
所有的点文件夹都是配置化
或集成工作流
中的内容
- rollup.config.js
也是使用 rollup配置,与vue2内容类似
根据 buildConfig 导出不同的目录结构 - .github
- workflows 在 vue2 中也讲过
- ci.yml CI 结合 push指令在什么情况下执行,根据这些执行,后续进行发布和更新
- workflows 在 vue2 中也讲过
- .vscode
- extensions.json 根据vsCode建议下载这样的插件
- launch.json 将这部分配置加到config里
- setttings.json 针对JS,TS的代码格式化,基于esbenp.prettier-vscode这样的配置化去做的
- .well-known 维护者的内容,可以进行一些捐助
- changelogs 对应的版本的更新
- packages-private 与整个打包是没关系的,它就是私有化的一些包,包含 dts,单文件的vue运行时的页面,通过这个页面可以进行vue的开发
- packages 核心
- scripts 在package的过程中,基于node执行对应的脚本
- build.js 基于 rollup 或其他工具 进行打包,然后执行 run,run的过程就是往下执行,pnpm run build-dts 生成类型,buildAll 基于对应的指令,针对 target,最后去打包这里的内容
- size-report.js 每个包的大小分析
- setup-vitest.ts 初始化的配置
- utils.js 工具化
- netlify.toml 打包工具,线上的CI工具,能够进行自定义化,定制化部署的
- pnpm-workspace.yaml 基于多包的形式
- vitest.config.ts 类似于 jest, 基于 vue3 的特性中,能够进行自动化测试,或者测试用例执行的部分
这些体现了工程化,所谓的工程化,就是怎么样将上述这一系列功能聚合起来,能够在项目中引用到。
- packages Vue3也需要编译compiler
- compiler-core 这个是编译时的核心
- compiler-sfc
- compiler-dom 以上这三个是相关联的包
- compiler-ssr 将 ssr 的语法 转换成 vue能够识别的语法
- reactivity vue3中的核心
- runtime-core vue2源码中说了runtime过程中基于生命周期做了什么事情,这里重点基于runtime-core展开讲述,在运行时的过程中,具体是怎么做的
- runtime-dom
- runtime-test
- server-renderer 服务端渲染的内容
- shared 在大多数的仓库里,针对一些相对来说比较通用的分享/配置型的文件,这里做单独子包的方式去引用的
- vue 这里做单文件的vue导出的
- index.js 入口,具体引用的vue是打包的文件
编译
compiler-core
完整的编译的核心,packages/compiler-core
package.json
"main": "index.js",
"module": "dist/compiler-core.esm-bundler.js",
"types": "dist/compiler-core.d.ts",
这三点是现在作为主流开发方式都是基于ts去做的
types是作为ts的入口
module是作为ECMAScript module(esModule),正常通过 import xxx from xxx,引用的就是这个路径
main 默认引用这个包的时候,作为UMD的方式去引用
"files": [
"index.js",
"dist"
],
最后引用的路径
"exports": {
".": { //.是默认引用这个包的时候,
"types": "./dist/compiler-core.d.ts",
"node": {
"production": "./dist/compiler-core.cjs.prod.js",
"development": "./dist/compiler-core.cjs.js",
"default": "./index.js"
},
"module": "./dist/compiler-core.esm-bundler.js",
"import": "./dist/compiler-core.esm-bundler.js",
"require": "./index.js"
},
"./*": "./*"
},
npm上@vue/compiler-core包
- “.”,是默认引用这个包的时候
比如:import xxx from ‘’@vue/compiler-core"这个引用的就是,exports下的"."下的 import 内容,也就是:“import”: “./dist/compiler-core.esm-bundler.js”, 这部分
要是通过 require 的方式引用,引的内容就是:“require”: “./index.js” 这个
引用这个包类型的时候,引用的内容就是:“types”: "./dist/compiler-core.d.ts"这个
node就是当前node的env的环境
比如,process.env.NODE_ENV=‘development’,当是开发环境的时候,引用的就是node下的这个:“development”: “./dist/compiler-core.cjs.js”
当是生产环境下的,也就是 process.env.NODE_ENV=‘production’,引用的就是这个:“production”: “./dist/compiler-core.cjs.prod.js”
默认的时候,没有设置环境变量的时候,引用的就是这个:“default”: “./index.js” - ./*:就是通配符,比如:import xxx from ‘’@vue/compiler-core/index.js"就是引用的compiler-core下的index.js文件
这个就是在 package.json 中就算是,怎样去具体约束 exports导出的产物,尤其在公用的库里,经常会这样去用,其实是在子包里做了一个产物的分发。
编译时,其实指的是
编译原理的实现
,AST(抽象语法树),其实是完整的编译时的思路原理,这里关于编译时的过程,其实就算是关于AST的具体应用
src/index.ts 入口文件
主要是这个:
export { baseCompile } from './compile'
做了compile.ts文件的导出
src/compile.ts
概括:从0-1创建出最后运行时能够识别的模板
export function baseCompile(template, options) { //template:vue中模板化的部分,可以理解为字符串模板,options:参数
// 1. 先把 template 也就是字符串 parse 成 ast
const ast = baseParse(template);
// 2. 给 ast 加点料(- -#)
transform(
ast,
Object.assign(options, {
//针对不同的节点的类型,通过调用策略方式,依次来调用
nodeTransforms: [transformElement, transformText, transformExpression],
})
);
// 3. 生成 render 函数代码
return generate(ast);
}
- 生成 AST -> 将模板转化成对象,这个对象为js对象
AST explorer
也就是像这样,将左侧内容转化为右侧对象的过程
type:层级的关系
tag:元素
attrsList:属性列表,id
attrsMap:class,style的映射
parent:父节点
children:子节点
expression:表达式的类型
tokens:向量,向量:变量的含义,这里是通过 greeting 绑定了 @binding 变量,将绑定的变量转化为data
- 添油加醋,针对不同的节点有不同的策略来处理
- 拿着第二步添油加醋后的内容生成render函数代码,也就是vue在运行时的产物
生成AST
src/parse.ts
import { ElementTypes, NodeTypes } from "./ast";
const enum TagType {
Start,
End,
}
export function baseParse(content: string) {
const context = createParserContext(content); //转换成最最基础的格式, 就像这个:"text": "{{ greeting }} World!",
return createRoot(parseChildren(context, []));
}
function createParserContext(content) {
console.log("创建 paserContext");
return {
source: content,
};
}
// 递归的遍历,最后返回一个数组
// DFS depth first search 深度优先遍历的过程
function parseChildren(context, ancestors) { //ancestors:第一层:[],
console.log("开始解析 children");
const nodes: any = [];
// 类似 <div><span>{{ greeting }} World!</span><span>456</span></div> 这样的一个字符串,当没到最后一位的时候,执行这个循环
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);
}
}
// 兜底,既不是 {{abc}},也不是<div></div>,那么就是字符串结构
if (!node) {
node = parseText(context);
}
nodes.push(node);
}
return nodes;
}
function isEnd(context: any, ancestors) {
// 检测标签的节点
// 如果是结束标签的话,需要看看之前有没有开始标签,如果有的话,那么也应该结束
// 这里的一个 edge case 是 <div><span></div>
// 像这种情况下,其实就应该报错
const s = context.source;
if (context.source.startsWith("</")) {
// 从后面往前面查
// 因为便签如果存在的话 应该是 ancestors 最后一个元素
for (let i = ancestors.length - 1; i >= 0; --i) {
if (startsWithEndTagOpen(s, ancestors[i].tag)) {
return true;
}
}
}
// 看看 context.source 还有没有值
return !context.source;
}
function parseElement(context, ancestors) { //ancestors 祖先节点
// 应该如何解析 tag 呢
// <div></div>
// 先解析开始 tag
const element = parseTag(context, TagType.Start);
ancestors.push(element);
const children = parseChildren(context, ancestors);
ancestors.pop();
// 解析 end tag 是为了检测语法是不是正确的
// 检测是不是和 start tag 一致
if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, TagType.End);
} else {
throw new Error(`缺失结束标签:${element.tag}`);
}
element.children = children;
return element;
}
function startsWithEndTagOpen(source: string, tag: string) {
// 1. 头部 是不是以 </ 开头的
// 2. 看看是不是和 tag 一样
return (
startsWith(source, "</") &&
source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase()
);
}
function parseTag(context: any, type: TagType): any {
// 发现如果不是 > 的话,那么就把字符都收集起来 ->div
// 正则
const match: any = /^<\/?([a-z][^\r\n\t\f />]*)/i.exec(context.source);
const tag = match[1];
// 移动光标
// <div
advanceBy(context, match[0].length);
// 暂时不处理 selfClose 标签的情况 ,所以可以直接 advanceBy 1个坐标 < 的下一个就是 >
advanceBy(context, 1);
if (type === TagType.End) return; //end类型就return掉
let tagType = ElementTypes.ELEMENT;
return {
type: NodeTypes.ELEMENT,
tag,
tagType,
};
}
function parseInterpolation(context: any) {
// 1. 先获取到结束的index
// 2. 通过 closeIndex - startIndex 获取到内容的长度 contextLength
// 3. 通过 slice 截取内容
// }} 是插值的关闭
// 优化点是从 {{ 后面搜索即可
const openDelimiter = "{{"; //开始的
const closeDelimiter = "}}"; //结束的
const closeIndex = context.source.indexOf(
closeDelimiter,
openDelimiter.length
);
// TODO closeIndex -1 需要报错的
// 让代码前进2个长度,可以把 {{ 干掉
advanceBy(context, 2);
const rawContentLength = closeIndex - openDelimiter.length;
const rawContent = context.source.slice(0, rawContentLength);
const preTrimContent = parseTextData(context, rawContent.length); //将这个变量拿出来
const content = preTrimContent.trim(); //并且把空格剔除掉,得到的这个content就是变量名字,比如 {{ greeting }} World!,得到的就是greeting
// 最后在让代码前进2个长度,可以把 }} 干掉
advanceBy(context, closeDelimiter.length); //将右边的 }} 跳过去
return {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION, //插值的格式,变量名的定义
content, //就是greeting
},
};
}
function parseText(context): any {
console.log("解析 text", context);
// endIndex 应该看看有没有对应的 <
// 比如 hello</div>
// 像这种情况下 endIndex 就应该是在 o 这里
// {
const endTokens = ["<", "{{"];
let endIndex = context.source.length;
for (let i = 0; i < endTokens.length; i++) {
const index = context.source.indexOf(endTokens[i]);
// endIndex > index 是需要要 endIndex 尽可能的小
// 比如说:
// hi, {{123}} <div></div>
// 那么这里就应该停到 {{ 这里,而不是停到 <div 这里
if (index !== -1 && endIndex > index) {
endIndex = index;
}
}
const content = parseTextData(context, endIndex);
return {
type: NodeTypes.TEXT,
content,
};
}
function parseTextData(context: any, length: number): any {
console.log("解析 textData");
// 1. 直接返回 context.source
// 从 length 切的话,是为了可以获取到 text 的值(需要用一个范围来确定)
const rawText = context.source.slice(0, length);
// 2. 移动光标
advanceBy(context, length);
return rawText;
}
function advanceBy(context, numberOfCharacters) {
console.log("推进代码", context, numberOfCharacters);
context.source = context.source.slice(numberOfCharacters);
}
function createRoot(children) {
return {
type: NodeTypes.ROOT,
children,
helpers: [],
};
}
function startsWith(source: string, searchString: string): boolean {
return source.startsWith(searchString);
}
context:为了维持dom的关系去定义的上下文层级的变量
比如,像这样的内容执行baseParse后
<div><span>{{ greeting }} World!</span><span>456</span></div>
因此第一步,baseParse返回的内容就是createRoot返回的对象:
{
type: NodeTypes.ROOT,
children:[
{
type: NodeTypes.ELEMENT,
tag: "div",
tagType,
children:[
{
type: NodeTypes.ELEMENT,
tag: "span",
tagType,
children:[
{
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION, //插值的格式,变量名的定义
content, //就是greeting
},
},
{
type: NodeTypes.TEXT,
content:"World!",
};
]
},
{
type: NodeTypes.ELEMENT,
tag: "span",
tagType,
children:[
{
type: NodeTypes.TEXT,
content:"456",
};
]
}
]
},
],
helpers: [],
};
代码转换
第二步,代码转换,针对不同Node节点做加工
针对每个节点做定制化的过程,也就是对数组进行遍历,遍历后递归,递归后根据策略模式执行里面的方法,然后进行匹配
像webpack中的一些插件也是这种思路,在webpack具体源码执行过程中,其实也是定义了webpackPlugin的name,然后往后做这些事情
// 2. 给 ast 加点料(- -#)
transform(
ast,
Object.assign(options, {
nodeTransforms: [transformElement, transformText, transformExpression],
})
);
src/transform.ts
import { NodeTypes } from "./ast";
import { TO_DISPLAY_STRING } from "./runtimeHelpers";
export function transform(root, options = {}) {
// 1. 创建 context 上下文
const context = createTransformContext(root, options);
// 2. 遍历 node
traverseNode(root, context);
createRootCodegen(root, context);
root.helpers.push(...context.helpers.keys());
}
function traverseNode(node: any, context) {
const type: NodeTypes = node.type;
// 遍历调用所有的 nodeTransforms
// 把 node 给到 transform
// 用户可以对 node 做处理
const nodeTransforms = context.nodeTransforms; //这里的nodeTransforms其实就是外层调用transform函数传递的第二个参数nodeTransforms,也就是策略模式
const exitFns: any = [];
for (let i = 0; i < nodeTransforms.length; i++) {
const transform = nodeTransforms[i];
// 策略模式匹配节点,根据节点的类型node.type来匹配执行,达到定制化的目的
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 traverseChildren(parent: any, context: any) {
// node.children
parent.children.forEach((node) => {
// TODO 需要设置 context 的值
traverseNode(node, context);
});
}
function createTransformContext(root, options): any {
const context = {
root,
nodeTransforms: options.nodeTransforms || [],
helpers: new Map(),
// 主要是helper的处理,helper是最后生成代码时,类似于辅助函数
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 node
const 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;
}
}
几种策略模式
src/transforms/transformElement.ts
根据节点的类型,NodeTypes.type,符合预期的话就执行,不符合预期就不执行
import { createVNodeCall, NodeTypes } from "../ast";
export function transformElement(node, context) {
if (node.type === NodeTypes.ELEMENT) {
return () => {
// 没有实现 block 所以这里直接创建 element
// TODO
// 需要把之前的 props 和 children 等一系列的数据都处理
const vnodeTag = `'${node.tag}'`;
// TODO props 暂时不支持
const vnodeProps = null;
let vnodeChildren = null;
if (node.children.length > 0) {
if (node.children.length === 1) {
// 只有一个孩子节点 ,那么当生成 render 函数的时候就不用 [] 包裹
const child = node.children[0];
vnodeChildren = child;
}
}
// 创建一个新的 node 用于 codegen 的时候使用
node.codegenNode = createVNodeCall(
context,
vnodeTag,
vnodeProps,
vnodeChildren
);
};
}
}
src/transforms/transformText.ts
import { NodeTypes } from "../ast";
import { isText } from "../utils";
export function transformText(node, context) {
if (node.type === NodeTypes.ELEMENT) {
// 在 exit 的时期执行
// 下面的逻辑会改变 ast 树
// 有些逻辑是需要在改变之前做处理的
return () => {
// hi,{{msg}}
// 上面的模块会生成2个节点,一个是 text 一个是 interpolation 的话
// 生成的 render 函数应该为 "hi," + _toDisplayString(_ctx.msg)
// 这里面就会涉及到添加一个 “+” 操作符
// 那这里的逻辑就是处理它
// 检测下一个节点是不是 text 类型,如果是的话, 那么会创建一个 COMPOUND 类型
// COMPOUND 类型把 2个 text || interpolation 包裹(相当于是父级容器)
const children = node.children;
let currentContainer;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (isText(child)) {
// 看看下一个节点是不是 text 类
for (let j = i + 1; j < children.length; j++) {
const next = children[j];
if (isText(next)) {
// currentContainer 的目的是把相邻的节点都放到一个 容器内
if (!currentContainer) {
currentContainer = children[i] = {
type: NodeTypes.COMPOUND_EXPRESSION,
loc: child.loc,
children: [child],
};
}
currentContainer.children.push(` + `, next);
// 把当前的节点放到容器内, 然后删除掉j
children.splice(j, 1);
// 因为把 j 删除了,所以这里就少了一个元素,那么 j 需要 --
j--;
} else {
currentContainer = undefined;
break;
}
}
}
}
};
}
}
src/transforms/transformExpression.ts
import { NodeTypes } from "../ast";
export function transformExpression(node) {
if (node.type === NodeTypes.INTERPOLATION) {
node.content = processExpression(node.content);
}
}
function processExpression(node) {
node.content = `_ctx.${node.content}`;
return node
}
代码生成
第三步,拿着代码转换的结果做真正的代码生成,真正的代码生成,可以理解为,拿着封装好的ast对象,转换成能够识别到的语法模板
// 3. 生成 render 函数代码
return generate(ast);
src/codegen.ts
import { isString } from "@mini-vue/shared";
import { NodeTypes } from "./ast";
import {
CREATE_ELEMENT_VNODE,
helperNameMap,
TO_DISPLAY_STRING,
} from "./runtimeHelpers";
// 最后能够让运行时识别的模板生成的动作,将刚刚生成的ast做了一个模板生成的动作
export function generate(ast, options = {}) {
// 先生成 context
const context = createCodegenContext(ast, options);
const { push, mode } = context;
// 1. 先生成 preambleContext
if (mode === "module") {
genModulePreamble(ast, context);
} else {
genFunctionPreamble(ast, context);
}
const functionName = "render";
const args = ["_ctx"];
// _ctx,aaa,bbb,ccc
// 需要把 args 处理成 上面的 string
const signature = args.join(", ");
push(`function ${functionName}(${signature}) {`);
// 这里需要生成具体的代码内容
// 开始生成 vnode tree 的表达式
push("return ");
genNode(ast.codegenNode, context);
push("}");
return {
code: context.code,
};
}
function genFunctionPreamble(ast: any, context: any) {
const { runtimeGlobalName, push, newline } = context;
const VueBinging = runtimeGlobalName;
const aliasHelper = (s) => `${helperNameMap[s]} : _${helperNameMap[s]}`;
if (ast.helpers.length > 0) {
push(
`
const { ${ast.helpers.map(aliasHelper).join(", ")}} = ${VueBinging}
`
);
// => const {} = 'xxx'
}
newline();
push(`return `);
}
function genNode(node: any, context: any) {
// 生成代码的规则就是读取 node ,然后基于不同的 node 来生成对应的代码块
// 然后就是把代码快给拼接到一起就可以了
switch (node.type) {
case NodeTypes.INTERPOLATION:
genInterpolation(node, context);
break;
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context);
break;
case NodeTypes.ELEMENT:
genElement(node, context);
break;
case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context);
break;
case NodeTypes.TEXT:
genText(node, context);
break;
default:
break;
}
}
function genCompoundExpression(node: any, context: any) {
const { push } = context;
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if (isString(child)) {
push(child);
} else {
genNode(child, context);
}
}
}
function genText(node: any, context: any) {
// Implement
const { push } = context;
push(`'${node.content}'`);
}
function genElement(node, context) {
const { push, helper } = context;
const { tag, props, children } = node;
push(`${helper(CREATE_ELEMENT_VNODE)}(`);
genNodeList(genNullableArgs([tag, props, children]), context);
push(`)`);
}
function genNodeList(nodes: any, context: any) {
const { push } = context;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (isString(node)) {
push(`${node}`);
} else {
genNode(node, context);
}
// node 和 node 之间需要加上 逗号(,)
// 但是最后一个不需要 "div", [props], [children]
if (i < nodes.length - 1) {
push(", ");
}
}
}
function genNullableArgs(args) {
// 把末尾为null 的都删除掉
// vue3源码中,后面可能会包含 patchFlag、dynamicProps 等编译优化的信息
// 而这些信息有可能是不存在的,所以在这边的时候需要删除掉
let i = args.length;
// 这里 i-- 用的还是特别的巧妙的
// 当为0 的时候自然就退出循环了
while (i--) {
if (args[i] != null) break;
}
// 把为 falsy 的值都替换成 "null"
return args.slice(0, i + 1).map((arg) => arg || "null");
}
function genExpression(node: any, context: any) {
context.push(node.content, node);
}
function genInterpolation(node: any, context: any) {
const { push, helper } = context;
push(`${helper(TO_DISPLAY_STRING)}(`);
genNode(node.content, context);
push(")");
}
function genModulePreamble(ast, context) {
// preamble 就是 import 语句
const { push, newline, runtimeModuleName } = context;
if (ast.helpers.length) {
// 比如 ast.helpers 里面有个 [toDisplayString]
// 那么生成之后就是 import { toDisplayString as _toDisplayString } from "vue"
const code = `import {${ast.helpers
.map((s) => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(", ")} } from ${JSON.stringify(runtimeModuleName)}`;
push(code);
}
newline();
push(`export `);
}
function createCodegenContext(
ast: any,
{ runtimeModuleName = "vue", runtimeGlobalName = "Vue", mode = "function" }
): any {
const context = {
code: "",
mode,
runtimeModuleName,
runtimeGlobalName,
helper(key) {
return `_${helperNameMap[key]}`;
},
push(code) {
context.code += code;
},
newline() {
// 换新行
// TODO 需要额外处理缩进
context.code += "\n";
},
};
return context;
}
响应式
响应式是vue3中核心的亮点
reactivity
入口,src/index.ts
export {
reactive,
readonly,
shallowReadonly,
isReadonly,
isReactive,
isProxy,
} from "./reactive";
export { ref, proxyRefs, unRef, isRef } from "./ref";
//effect 触发依赖回收的动作,track和trigger的动作
export { effect, stop, ReactiveEffect } from "./effect";
export { computed } from "./computed";
reactive方法
src/reactive.ts
import {
mutableHandlers,
readonlyHandlers,
shallowReadonlyHandlers,
} from "./baseHandlers";
// WeakMap 访问时在内存中本身Vue能够识别的语言最后被资源回收掉,WeakMap能够帮助我们把它的代码内容能够进行长期保存下去(不让我们进行垃圾回收)
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",
}
// reactive,readonly,shallowReadonly都是调用的createReactiveObject这个方法,都是分别的WeakMap
export function reactive(target) {
return createReactiveObject(target, reactiveMap, mutableHandlers); //这里的handler其实是baseHandlers里面的get和set,也就是proxy的定义
}
export function readonly(target) {
return createReactiveObject(target, readonlyMap, readonlyHandlers);
}
export function shallowReadonly(target) {
return createReactiveObject(
target,
shallowReadonlyMap,
shallowReadonlyHandlers
);
}
export function isProxy(value) {
// 要么是readonly,要么是reactive
return isReactive(value) || isReadonly(value);
}
// isReadonly的区分,第一次创建时候,会标识出唯一的值,唯一的枚举
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);
// 把创建好的 proxy 给存起来,
proxyMap.set(target, proxy);
return proxy;
}
src/baseHanlders.ts
reactive,shallowReactive,readonly,shallowReadonly等等方法的处理,响应式的核心
import { ReactiveEffect, track, trigger } from "./effect";
import {
reactive,
ReactiveFlags,
reactiveMap,
readonly,
readonlyMap,
shallowReadonlyMap,
} from "./reactive";
import { isObject } from "@mini-vue/shared";
const get = createGetter();
const set = createSetter();
const readonlyGet = createGetter(true);
const shallowReadonlyGet = createGetter(true, true);
// 默认都是false
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
// 判断是否在原本map中有值,有值就直接返回
const isExistInReactiveMap = () =>
key === ReactiveFlags.RAW && receiver === reactiveMap.get(target);
const isExistInReadonlyMap = () =>
key === ReactiveFlags.RAW && receiver === readonlyMap.get(target);
const isExistInShallowReadonlyMap = () =>
key === ReactiveFlags.RAW && receiver === shallowReadonlyMap.get(target);
// 第一次的时候,是没有值的,所以这里直接跳过,在下一次创建好的话会加上这个属性的
// get的时候,要么返回true,要么返回false
if (key === ReactiveFlags.IS_REACTIVE) {
//如果是响应式的
return !isReadonly;
} else if (key === ReactiveFlags.IS_READONLY) {
// 如果是只读的
return isReadonly;
} else if (
isExistInReactiveMap() ||
isExistInReadonlyMap() ||
isExistInShallowReadonlyMap()
) {
return target;
}
// es6中proxy的属性
const res = Reflect.get(target, key, receiver);
// 问题:为什么是 readonly 的时候不做依赖收集呢
// readonly 的话,是不可以被 set 的, 那不可以被 set 就意味着不会触发 trigger
// 所有就没有收集依赖的必要了
// reactive和readonly的区别:readonly不会进行依赖回收
if (!isReadonly) { //reactive的话,会进行get的动作
// 在触发 get 的时候进行依赖收集
track(target, "get", key);
}
// shallow和非shallow的区别,是否要进行递归回收
// 如果是 shallow 的话,那么就不进行递归了
if (shallow) {
return res;
}
// 不是 shallow 的话,那么就进行递归
if (isObject(res)) {
// 把内部所有的是 object 的值都用 reactive 包裹,变成响应式对象
// 如果说这个 res 值是一个对象的话,那么我们需要把获取到的 res 也转换成 reactive
// res 等于 target[key]
return isReadonly ? readonly(res) : reactive(res);
}
return res;
};
}
// set动作
function createSetter() {
return function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
// 在触发 set 的时候进行触发依赖
trigger(target, "set", key);
return result;
};
}
// 只能读,不能改
export const readonlyHandlers = {
get: readonlyGet,
set(target, key) {
// readonly 的响应式对象不可以修改值
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
);
return true;
},
};
// 动态的
export const mutableHandlers = {
get,
set,
};
// 也不能set,这里做拦截
export const shallowReadonlyHandlers = {
get: shallowReadonlyGet,
set(target, key) {
// readonly 的响应式对象不可以修改值
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
);
return true;
},
};
src/effect.ts
import { createDep } from "./dep";
import { extend } from "@mini-vue/shared";
let activeEffect = void 0;
let shouldTrack = false;
const targetMap = new WeakMap();
// 用于依赖收集
export class ReactiveEffect {
active = true;
deps = [];
public onStop?: () => void;
constructor(public fn, public scheduler?) {
console.log("创建 ReactiveEffect 对象");
}
run() {
console.log("run");
// 运行 run 的时候,可以控制 要不要执行后续收集依赖的一步
// 目前来看的话,只要执行了 fn 那么就默认执行了收集依赖
// 这里就需要控制了
// 是不是收集依赖的变量
// 执行 fn 但是不收集依赖
if (!this.active) {
return this.fn();
}
// 执行 fn 收集依赖
// 可以开始收集依赖了
shouldTrack = true;
// 执行的时候给全局的 activeEffect 赋值
// 利用全局属性来获取当前的 effect
activeEffect = this as any;
// 执行用户传入的 fn
console.log("执行用户传入的 fn");
const result = this.fn();
// 重置
shouldTrack = false;
activeEffect = undefined;
return result;
}
stop() {
if (this.active) {
// 如果第一次执行 stop 后 active 就 false 了
// 这是为了防止重复的调用,执行 stop 逻辑
cleanupEffect(this);
if (this.onStop) {
this.onStop();
}
this.active = false;
}
}
}
function cleanupEffect(effect) {
// 找到所有依赖这个 effect 的响应式对象
// 从这些响应式对象里面把 effect 给删除掉
effect.deps.forEach((dep) => {
dep.delete(effect);
});
effect.deps.length = 0;
}
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();
}
export function track(target, type, key) {
if (!isTracking()) {
return;
}
console.log(`触发 track -> target: ${target} type:${type} key:${key}`);
// 1. 先基于 target 找到对应的 dep
// 如果是第一次的话,那么就需要初始化
let depsMap = targetMap.get(target);
if (!depsMap) {
// 初始化 depsMap 的逻辑
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = createDep();
depsMap.set(key, dep);
}
trackEffects(dep);
}
export function trackEffects(dep) {
// 用 dep 来存放所有的 effect
// TODO
// 这里是一个优化点
// 先看看这个依赖是不是已经收集了,
// 已经收集的话,那么就不需要在收集一次了
// 可能会影响 code path change 的情况
// 需要每次都 cleanupEffect
// shouldTrack = !dep.has(activeEffect!);
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
(activeEffect as any).deps.push(dep);
}
}
export function trigger(target, type, key) {
// 1. 先收集所有的 dep 放到 deps 里面,
// 后面会统一处理
let deps: Array<any> = [];
// dep
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 暂时只实现了 GET 类型
// get 类型只需要取出来就可以
const dep = depsMap.get(key);
// 最后收集到 deps 内
deps.push(dep);
const effects: Array<any> = [];
deps.forEach((dep) => {
// 这里解构 dep 得到的是 dep 内部存储的 effect
effects.push(...dep);
});
// 这里的目的是只有一个 dep ,这个dep 里面包含所有的 effect
// 这里的目前应该是为了 triggerEffects 这个函数的复用
triggerEffects(createDep(effects));
}
export function isTracking() {
return shouldTrack && activeEffect !== undefined;
}
export function triggerEffects(dep) {
// 执行收集到的所有的 effect 的 run 方法
for (const effect of dep) {
if (effect.scheduler) {
// scheduler 可以让用户自己选择调用的时机
// 这样就可以灵活的控制调用了
// 在 runtime-core 中,就是使用了 scheduler 实现了在 next ticker 中调用的逻辑
effect.scheduler();
} else {
effect.run();
}
}
}
ref方法
src/ref.ts
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) {
// 调用了reactive的方法,也就是刚说的proxy
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;
}
ref,reactive,readonly的区别
三者都是通过proxy
去关联到的,并且reactive本身是能够进行深层次递归处理的
ref:官方提供的用来创建响应式的数据的,reactive没有办法针对基础类型,ref 是能够针对任何类型添加响应式,
computed
返回一个构造函数实例
类似 react 中的 useMemo,当computed的值发生变化时,才会触发它的依赖回收
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 _value
constructor(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 的话,就是执行用户传入的 fn
this._value = this.effect.run();
}
return this._value;
}
}
export function computed(getter) {
return new ComputedRefImpl(getter);
}