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指令在什么情况下执行,根据这些执行,后续进行发布和更新
  • .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包

  1. “.”,是默认引用这个包的时候
    比如: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”
  2. ./*:就是通配符,比如: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 成 astconst ast = baseParse(template); // 2. 给 ast 加点料(- -#)transform(ast,Object.assign(options, {//针对不同的节点的类型,通过调用策略方式,依次来调用nodeTransforms: [transformElement, transformText, transformExpression],}));// 3. 生成 render 函数代码return generate(ast);
}
  1. 生成 AST -> 将模板转化成对象,这个对象为js对象

AST explorer
在这里插入图片描述

也就是像这样,将左侧内容转化为右侧对象的过程
type:层级的关系
tag:元素
attrsList:属性列表,id
attrsMap:class,style的映射
parent:父节点
children:子节点
expression:表达式的类型
tokens:向量,向量:变量的含义,这里是通过 greeting 绑定了 @binding 变量,将绑定的变量转化为data

  1. 添油加醋,针对不同的节点有不同的策略来处理
  2. 拿着第二步添油加醋后的内容生成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>// 先解析开始 tagconst 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];// 移动光标// <divadvanceBy(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. 遍历 nodetraverseNode(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.childrenparent.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 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;}
}
几种策略模式
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);// 把当前的节点放到容器内, 然后删除掉jchildren.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 = {}) {// 先生成 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,};
}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) {// Implementconst { 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,要么是reactivereturn 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,要么返回falseif (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 赋值// 利用全局属性来获取当前的 effectactiveEffect = this as any;// 执行用户传入的 fnconsole.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> = [];// depconst 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 内部存储的 effecteffects.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的方法,也就是刚说的proxyreturn 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 _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);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/29813.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

servlet tomcat

在spring-mvc demo程序运行到DispatcherServlet的mvc处理 一文中&#xff0c;我们实践了浏览器输入一个请求&#xff0c;然后到SpringMvc的DispatcherServlet处理的整个流程. 设计上这些都是tomcat servlet的处理 那么究竟这是怎么到DispatcherServlet处理的&#xff0c;本文将…

【我的待办(MyTodolists)-免费无内购的 IOS 应用】

我的待办&#xff08;MyTodolists&#xff09; 我的待办&#xff1a;智能任务管理助手应用说明主要功能为什么选择"我的待办"&#xff1f;隐私保障使用截图 我的待办&#xff1a;智能任务管理助手 应用说明 "我的待办"是一款智能化的任务管理应用&#x…

GCC RISCV 后端 -- C语言语法分析过程

在 GCC 编译一个 C 源代码时&#xff0c;先会通过宏处理&#xff0c;形成 一个叫转译单元&#xff08;translation_unit&#xff09;&#xff0c;接着进行语法分析&#xff0c;C 的语法分析入口是 static void c_parser_translation_unit(c_parser *parser); 接着就通过类似递…

Vim复制内容到系统剪切板

参考链接 【Vim】Vim 中将文件内容复制到系统剪切板的方法_vi 复制到系统剪贴板-CSDN博客 [转]vim如何复制到系统剪贴板 - biiigwang - 博客园 1. 确定Vim是否支持复制到系统剪切板 输入命令 vim --version | grep clipboard 如果是开头&#xff0c;说明支持系统剪切板&…

测试用大模型组词

已经把hanzi-writer的js的调用、hanzi-writer调用的数千个汉字的json文件&#xff0c;全都放在本地了。虽然用的办法还是比较笨的。我注意到 大模型也可以部署本地&#xff0c;虽然使用频率低的情况下不划算。 尝试直接通过html的javascript通过api key调用大语言模型&#x…

华为eNSP:配置单区域OSPF

一、什么是OSPF&#xff1f; OSPF&#xff08;Open Shortest Path First&#xff0c;开放最短路径优先&#xff09;是一种链路状态路由协议&#xff0c;属于内部网关协议&#xff08;IGP&#xff09;&#xff0c;主要用于在单一自治系统&#xff08;AS&#xff09;内部动态发现…

P62 线程

这篇文章我们来讲一下线程。截止到目前&#xff0c;我们的代码都是在单线程上运行的&#xff0c;现在看起来没有什么问题&#xff0c;但是目前所有的计算机几乎都不只有一个逻辑线程&#xff0c;所以如果我们一直使用单线程运行&#xff0c;这样的话效率会很低。尤其是如果我们…

Android AudioFlinger(五)—— 揭开AudioMixer面纱

前言&#xff1a; 在 Android 音频系统中&#xff0c;AudioMixer 是音频框架中一个关键的组件&#xff0c;用于处理多路音频流的混音操作。它主要存在于音频回放路径中&#xff0c;是 AudioFlinger 服务的一部分。 上一节我们讲threadloop的时候&#xff0c;提到了一个函数pr…

im即时聊天客服系统SaaS还是私有化部署:成本、安全与定制化的权衡策略

随着即时通讯技术的不断发展&#xff0c;IM即时聊天客服系统已经成为企业与客户沟通、解决问题、提升用户体验的重要工具。在选择IM即时聊天客服系统时&#xff0c;企业面临一个重要决策&#xff1a;选择SaaS&#xff08;软件即服务&#xff09;解决方案&#xff0c;还是进行私…

DeepSeek系列模型技术报告的阅读笔记

DeepSeek系列模型技术报告的阅读笔记 之前仔细阅读了DeepSeek系列模型的主要技术方面内容与发展脉络&#xff0c;以下是DeepSeek系列模型技术报告的笔记&#xff0c;有错误的地方欢迎指正&#xff01; 文章目录 DeepSeek系列模型技术报告的阅读笔记GQADeepseek MoEAbstractIn…

【VUE】第二期——生命周期及工程化

目录 1 生命周期 1.1 介绍 1.2 钩子 2 可视化图表库 3 脚手架Vue CLI 3.1 使用步骤 3.2 项目目录介绍 3.3 main.js入口文件代码介绍 4 组件化开发 4.1 组件 4.2 普通组件注册 4.2.1 局部注册 4.2.2 全局注册 1 生命周期 1.1 介绍 Vue生命周期&#xff1a;就是…

Spring-framework源码编译

版本统一&#xff08;搭配其他版本会遇到不可知错误&#xff09;&#xff1a; 1&#xff09;spring 5.2.X&#xff08;5.5.26&#xff09; 2&#xff09;JDK8 3&#xff09;Gradle:5.6.4 可以在gradle-wrapper.properties中修改 https\://services.gradle.org/distribution…

使用 Deepseek + kimi 快速生成PPT

前言 最近看到好多文章和视频都在说&#xff0c;使用 Deepseek 和 kimi 能快速生成精美的 ppt&#xff0c;毕竟那都是别人说的&#xff0c;只有自己尝试一次才知道结果。 具体操作 第一步&#xff1a;访问 deepseek 我们访问 deepseek &#xff0c;把我们想要输入的内容告诉…

火绒终端安全管理系统V2.0--纵深防御体系(分层防御)之规则拦截层

火绒终端安全管理系统V2.0--多层次主动防御系统。 率先将单步防御和多步恶意监控相结合&#xff0c;监控百个防御点&#xff08;包含防火墙&#xff09;&#xff0c;有效阻止各种恶意程序对系统的攻击和篡改&#xff0c;保护终端脆弱点。 ✅ 内容拦截层&#xff08;Content-B…

如何在WPS中接入DeepSeek并使用OfficeAI助手(超细!成功版本)

目录 第一步&#xff1a;下载并安装OfficeAI助手 第二步&#xff1a;申请API Key 第三步:两种方式导入WPS 第一种:本地大模型Ollama 第二种APIKey接入 第四步&#xff1a;探索OfficeAI的创作功能 工作进展汇报 PPT大纲设计 第五步&#xff1a;我的使用体验(体验建议) …

力扣35.搜索插入位置-二分查找

class Solution:def searchInsert(self, nums: List[int], target: int) -> int:# 初始化左右指针left, right 0, len(nums) - 1# 当左指针小于等于右指针时&#xff0c;继续循环while left < right:# 计算中间位置mid (left right) // 2# 如果中间元素等于目标值&…

云计算专业必考三大证书,助你抢占职业发展先机!【云计算认证学习资料分享(考试大纲、培训教材、实验手册等等)】

随着云计算技术的飞速发展和广泛应用&#xff0c;云计算行业对专业人才的需求也日益旺盛。拥有权威的云计算认证&#xff0c;不仅能够证明你的技术实力&#xff0c;更能为你的职业发展增添砝码&#xff0c;赢得高薪offer&#xff01; 本文将为大家介绍云计算专业最值得考的三大…

Redis - 核心原理深度解析:线程模型、持久化与高可用性

文章目录 概述一、Redis线程模型演进1. 经典单线程模型&#xff08;Redis 4.0前&#xff09;2. 多线程优化演进 二、数据持久化机制1. AOF&#xff08;Append Only File&#xff09;2. RDB&#xff08;Redis Database&#xff09;3. 混合持久化&#xff08;Redis 4.0&#xff0…

腾讯云对象存储服务(COS)

腾讯云对象存储服务&#xff08;COS&#xff09; 安全、可扩展、低成本的云存储解决方案 腾讯云 对象存储服务&#xff08;COS&#xff0c;Cloud Object Storage&#xff09; 是一种高可靠、高性能、可扩展的云存储服务&#xff0c;专为海量非结构化数据&#xff08;如图片、…

K8S学习之基础十:k8s中初始化容器和主容器

init容器和主容器 init容器和主容器的区别 初始化容器不支持 Readinessprobe&#xff0c;因为他们必须在pod就绪之前运行完成每个init容器必须运行成功&#xff0c;下一个才能够运行 # 定义两个初始化容器&#xff0c;完成后再运行主容器 vi pod-init.yaml apiVersion: v1 …