当前位置: 首页 > article >正文

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 成 ast
  const 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>
  // 先解析开始 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);
}


http://www.kler.cn/a/573938.html

相关文章:

  • *搜索算法(2)
  • mongodb安装教程以及mongodb的使用
  • 记录一个Circle CI出现的错误
  • Android MVI架构模式详解
  • SolidWorks 转 PDF3D 技术详解
  • vue左侧边框点击后让字体高亮
  • 多线程-线程本地变量ThreadLocal
  • 探秘基带算法:从原理到5G时代的通信变革【十】基带算法应用与对比
  • 前端基础之全局事件总线
  • vue表单已经赋值了,但是还是返回async-validator “xxx is required“提示,弹出验证红字而且不能输入
  • supervisord管理Gunicorn进程,使用Nginx作为反向代理运行flask web项目
  • 代理与 hosts 文件冲突问题解决方案
  • uniapp封装路由管理(兼容Vue2和Vue3)
  • 批量对 Word 优化与压缩,减少 Word 文件大小
  • 通信小贾的西天取经之路:从茫然小白到工业互联网售前
  • Java基础知识大全(含答案,面试基础)
  • FX-结构体
  • FusionInsight MRS云原生数据湖
  • 江科大51单片机笔记【8】LED点阵屏显示图形及动画(下)
  • Hive-优化(参数优化篇)