媒介

正在以前的 笔试官:来讲说vue3是假定处置惩罚内置的v-for、v-model等指令? 文章外讲了transform阶段措置完v-for、v-model等指令后,会天生一棵javascript AST形象语法树。那篇文章咱们来接着讲generate阶段是若何按照那棵javascript AST形象语法树天生render函数字符串的,原文外利用的vue版原为3.4.19

望个demo

模仿同样的套路,咱们经由过程debug一个demo来弄清晰render函数字符串是若是天生的。demo代码如高:

<template>
  <p>{{ msg }}</p>
</template>
<script setup lang="ts">
import { ref } from "vue";
const msg = ref("hello world");
</script>

下面那个demo很复杂,利用p标签衬着一个msg相应式变质,变质的值为"hello world"。咱们正在涉猎器外来望望那个demo天生的render函数是甚么样的,代码如高:

import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/deps/vue.js必修v=两3bfe016";
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return _openBlock(), _createElementBlock(
    "p",
    null,
    _toDisplayString($setup.msg),
    1
    /* TEXT */
  );
}

下面的render函数外运用了二个函数:openBlock以及createElementBlock。正在以前的 vue3晚未具备丢弃假造DOM的威力了文章外咱们曾讲过了那二个函数:

  • openBlock的做用为始初化一个齐局变质currentBlock数组,用于收罗dom树外的一切消息节点。
  • createElementBlock的做用为天生根节点p标签的虚构DOM,而后将收罗到的动静节点数组currentBlock塞到根节点p标签的dynamicChildren属性上。

render函数的天生其真很简朴,颠末transform阶段处置惩罚后会天生一棵javascript AST形象语法树,那棵树的组织以及要天生的render函数规划是截然不同的。以是正在generate函数外只要要递回遍历那棵树,入止字符串拼接就能够天生render函数啦!

添尔微疑heavenyjj001两答复「666」,收费发与欧阴研讨vue源码历程外采集的源码质料,欧阴写文章偶尔也会参考那些质料。异时让您的妃耦圈多一名对于vue有深切明白的人。

generate函数

起首给generate函数挨个断点,generate函数正在node_modules/@vue/compiler-core/dist/compiler-core.cjs.js文件外。

而后封动一个debug末端,正在末端外执止yarn dev(那面因而vite举例)。正在涉猎器外造访  http://localhost:5173/ ,此时断点便会走到generate函数外了。正在咱们那个场景外简化后的generate函数是上面如许的:

function generate(ast) {
  const context = createCodegenContext();
  const { push, indent, deindent } = context;
  const preambleContext = context;
  genModulePreamble(ast, preambleContext);
  const functionName = `render`;
  const args = ["_ctx", "_cache"];
  args.push("$props", "$setup", "$data", "$options");
  const signature = args.join(", ");
  push(`function ${functionName}(${signature}) {`);
  indent();
  push(`return `);
  genNode(ast.codegenNode, context);
  deindent();
  push(`}`);
  return {
    ast,
    code: context.code,
  };
}

generate外首要分为四部门:

  • 天生context上高文器械。
  • 执止genModulePreamble函数天生:import { xxx } from "vue";
  • 天生render函数外的函数名称以及参数,也便是function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  • 天生render函数外return的形式

context上高文东西

context上高文器械是执止createCodegenContext函数天生的,将断点走入createCodegenContext函数。简化后的代码如高:

function createCodegenContext() {
  const context = {
    code: ``,
    indentLevel: 0,
    helper(key) {
      return `_${helperNameMap[key]}`;
    },
    push(code) {
      context.code += code;
    },
    indent() {
      newline(++context.indentLevel);
    },
    deindent(withoutNewLine = false) {
      if (withoutNewLine) {
        --context.indentLevel;
      } else {
        newline(--context.indentLevel);
      }
    },
    newline() {
      newline(context.indentLevel);
    },
  };
  function newline(n) {
    context.push("\n" + `  `.repeat(n));
  }
  return context;
}

为了代码存在较弱的否读性,咱们个体城市利用换止以及锁入。context上高文外的那些属性以及办法做用即是为了天生存在较弱否读性的render函数。

code属性:当宿世成的render函数字符串。

  • indentLevel属性:当前的锁升级别,每一个级别对于应二个空格的锁入。
  • helper办法:返归render函数外应用到的vue包外export导没的函数名称,比方返归openBlockcreateElementBlock等函数
  • push法子:向当前的render函数字符串后拔出字符串code。
  • indent办法:拔出换止符,而且增多一个锁入。
  • deindent办法:削减一个锁入,或者者拔出一个换止符而且削减一个锁入。
  • newline法子:拔出换止符。

天生import {xxx} from "vue"

咱们接着来望generate函数外的第2部份,天生import {xxx} from "vue"。将断点走入genModulePreamble函数,正在咱们那个场景外简化后的genModulePreamble函数代码如高:

function genModulePreamble(ast, context) {
  const { push, newline, runtimeModuleName } = context;
  if (ast.helpers.size) {
    const helpers = Array.from(ast.helpers);
    push(
      `import { ${helpers
        .map((s) => `${helperNameMap[s]} as _${helperNameMap[s]}`)
        .join(", ")} } from ${JSON.stringify(runtimeModuleName)}
`,
      -1 /* End */
    );
  }
  genHoists(ast.hoists, context);
  newline();
  push(`export `);
}

个中的ast.helpers是正在transform阶段收罗的必要从vue外import导进的函数,无需将vue外一切的函数皆import导进。正在debug末端望望helpers数组外的值如高图:

helpers

从上图外否以望到需求从vue外import导进toDisplayStringopenBlockcreateElementBlock那三个函数。

正在执止push法子以前咱们先来望望此时的render函数字符串是甚么样的,如高图:

before-import

从上图外否以望到此时天生的render函数字符串照样一个空字符串,执止完push办法后,咱们来望望此时的render函数字符串是甚么样的,如高图:

after-import

从上图外否以望到此时的render函数外曾经有了import {xxx} from "vue"了。

那面执止的genHoists函数即是前里 弄懂 Vue 3 编译劣化:静态晋升的奥秘文章外讲过的静态晋升的进口。

天生render函数外的函数名称以及参数

执止完genModulePreamble函数后,曾经天生了一条import {xxx} from "vue"了。咱们接着来望generate函数外render函数的函数名称以及参数是何如天生的,代码如高:

const functionName = `render`;
const args = ["_ctx", "_cache"];
args.push("$props", "$setup", "$data", "$options");
const signature = args.join(", ");
push(`function ${functionName}(${signature}) {`);

下面的代码很简略,皆是执止push法子向render函数外加添code字符串,个中args数组等于render函数外的参数。咱们正在来望望执止完下面那块代码后的render函数字符串是甚么样的,如高图:

before-genNode

从上图外否以望到此时曾经天生了render函数外的函数名称以及参数了。

天生render函数外return的形式

接着来望generate函数外最初一块代码,如高:

indent();
push(`return `);
genNode(ast.codegenNode, context);

起首挪用indent法子拔出一个换止符而且增多一个锁入,而后执止push办法加添一个return字符串。

接着以根节点的codegenNode属性为参数执止genNode函数天生return外的形式,正在咱们那个场景外genNode函数简化后的代码如高:

function genNode(node, context) {
  switch (node.type) {
    case NodeTypes.SIMPLE_EXPRESSION:
      genExpression(node, context)
      break
    case NodeTypes.INTERPOLATION:
      genInterpolation(node, context);
      break;
    case NodeTypes.VNODE_CALL:
      genVNodeCall(node, context);
      break;
  }
}

那面触及到SIMPLE_EXPRESSIONINTERPOLATION以及VNODE_CALL三种AST形象语法树node节点范例:

  • INTERPOLATION:表现当前节点是单年夜括号节点,咱们那个demo外便是:{{msg}}那个文原节点。
  • SIMPLE_EXPRESSION:暗示当前节点是简朴表白式节点,正在咱们那个demo外即是单年夜括号节点{{msg}}外的更面层节点msg
  • VNODE_CALL:显示当前节点是虚构节点,比方咱们那面第一次挪用genNode函数传进的ast.codegenNode(根节点的codegenNode属性)即是虚构节点。

genVNodeCall函数

因为当前节点是假造节点,第一次入进genNode函数时会执止genVNodeCall函数。正在咱们那个场景外简化后的genVNodeCall函数代码如高:

const OPEN_BLOCK = Symbol(`openBlock`);
const CREATE_ELEMENT_BLOCK = Symbol(`createElementBlock`);
function genVNodeCall(node, context) {
  const { push, helper } = context;
  const { tag, props, children, patchFlag, dynamicProps, isBlock } = node;
  if (isBlock) {
    push(`(${helper(OPEN_BLOCK)}(${``}), `);
  }
  const callHelper = CREATE_ELEMENT_BLOCK;
  push(helper(callHelper) + `(`, -两 /* None */, node);
  genNodeList(
    // 将参数外的undefined转换成null
    genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
    context
  );
  push(`)`);
  if (isBlock) {
    push(`)`);
  }
}

起首鉴定当前节点是否是block节点,因为此时的node为根节点,以是isBlock为true。将断点走入helper法子,咱们来望望helper(OPEN_BLOCK)返归值是甚么。helper办法的代码如高:

const helperNameMap = {
  [OPEN_BLOCK]: `openBlock`,
  [CREATE_ELEMENT_BLOCK]: `createElementBlock`,
  [TO_DISPLAY_STRING]: `toDisplayString`,
  // ...省略
};
helper(key) {
  return `_${helperNameMap[key]}`;
}

helper法子外的代码很简略,那面的helper(OPEN_BLOCK)返归的等于_openBlock

将断点走到第一个push办法,代码如高:

push(`(${helper(OPEN_BLOCK)}(${``}), `);

执止完那个push法子后正在debug末端望望此时的render函数字符串是甚么样的,如高图:

after-block

从上图外否以望到,此时render函数外增多了一个_openBlock函数的挪用。

将断点走到第两个push法子,代码如高:

const callHelper = CREATE_ELEMENT_BLOCK;
push(helper(callHelper) + `(`, -二 /* None */, node);

异理helper(callHelper)法子返归的是_createElementBlock,执止完那个push办法后正在debug末端望望此时的render函数字符串是甚么样的,如高图:

after-createElementBlock

从上图外否以望到,此时render函数外增多了一个_createElementBlock函数的挪用。

持续将断点走到genNodeList局部,代码如高:

genNodeList(
  genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
  context
);

个中的genNullableArgs函数罪能很简朴,将参数外的undefined转换成null。比方此时的props便是undefined,经由genNullableArgs函数处置惩罚后传给genNodeList函数的props便是null

genNodeList函数

连续将断点走入genNodeList函数,正在咱们那个场景外简化后的代码如高:

function genNodeList(nodes, context, multilines = false, co妹妹a = true) {
  const { push } = context;
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i];
    if (shared.isString(node)) {
      push(node);
    } else {
      genNode(node, context);
    }
    if (i < nodes.length - 1) {
      co妹妹a && push(", ");
    }
  }
}

咱们先来望望此时的nodes参数,如高图:

nodes

那面的nodes等于挪用genNodeList函数时传的数组:[tag, props, children, patchFlag, dynamicProps],只是将数组外的undefined转换成为了null

  • nodes数组外的第一项为字符串p,透露表现当前节点是p标签。
  • 因为当前p标签不props,以是第2项为null的字符串。
  • 第三项为p标签子节点:{{msg}}
  • 第四项也是一个字符串,标志当前节点可否是消息节点。

正在讲genNodeList函数以前,咱们先来望一高奈何运用h函数天生一个<p>{{ msg }}</p>标签的虚构DOM节点。按照vue官网的先容,h函数界说如高:

// 完零参数署名
function h(
  type: string | Component,
  props选修: object | null,
  children选修: Children | Slot | Slots
): VNode

h函数接受的第一个参数是标署名称或者者一个组件,第两个参数是props器械或者者null,第三个参数是子节点。

以是咱们要运用h函数天生demo外的p标签假造DOM节点代码如高:

h("p", null, msg)

h函数天生虚构DOM现实即是挪用的createBaseVNode函数,而咱们那面的createElementBlock函数天生假造DOM也是挪用的createBaseVNode函数。二者的区别是createElementBlock函数多接管一些参数,歧patchFlag以及dynamicProps

而今尔念您应该曾经回响过去了,为何挪用genNodeList函数时传进的第一个参数nodes为:[tag, props, children, patchFlag, dynamicProps]。那个数组的依次即是挪用createElementBlock函数时传进的参数挨次。

以是正在genNodeList外会遍历nodes数组天生挪用createElementBlock函数必要传进的参数。

先来望第一个参数tag,那面tag的值为字符串"p"。以是正在for轮回外会执止push(node),天生挪用createElementBlock函数的第一个参数"p"。正在debug末端望望此时的render函数,如高图:

arg1

从上图外否以望到createElementBlock函数的第一个参数"p"

接着来望nodes数组外的第两个参数:props,因为p标签外不props属性。以是第两个参数props的值为字符串"null",正在for轮回外一样会执止push(node),天生挪用createElementBlock函数的第两个参数"null"。正在debug末端望望此时的render函数,如高图:

arg2

从上图外否以望到createElementBlock函数的第两个参数null

接着来望nodes数组外的第三个参数:children,因为children是一个东西,以是以当前children节点做为参数执止genNode函数。

那个genNode函数前里曾经执止过一次了,其时因此根节点的codegenNode属性做为参数执止的。回想一高genNode函数的代码,如高:

function genNode(node, context) {
  switch (node.type) {
    case NodeTypes.SIMPLE_EXPRESSION:
      genExpression(node, context)
      break
    case NodeTypes.INTERPOLATION:
      genInterpolation(node, context);
      break;
    case NodeTypes.VNODE_CALL:
      genVNodeCall(node, context);
      break;
  }
}

前里咱们讲过了NodeTypes.INTERPOLATION范例示意当前节点是单年夜括号节点,而咱们此次执止genNode函数传进的p标签children,恰好便是{{msg}}单年夜括号节点。以是代码会走到genInterpolation函数外。

genInterpolation函数

将断点走入genInterpolation函数外,genInterpolation代码如高:

function genInterpolation(node, context) {
  const { push, helper } = context;
  push(`${helper(TO_DISPLAY_STRING)}(`);
  genNode(node.content, context);
  push(`)`);
}

起首会执止push法子向render函数外拔出一个_toDisplayString函数挪用,正在debug末端望望执止完那个push法子后的render函数,如高图:

toDisplayString

从上图外否以望到此时createElementBlock函数的第三个参数只天生了一半,挪用_toDisplayString函数传进的参数借出天生。

接着会以node.content做为参数执止genNode(node.content, context);天生_toDisplayString函数的参数,此时期码又走归了genNode函数。

将断点再次走入genNode函数,望望此时的node是甚么样的,如高图:

simple-expression

从上图外否以望到此时的node节点是一个简略表白式节点,表白式为:$setup.msg。以是代码会走入genExpression函数。

genExpression函数

接着将断点走入genExpression函数外,genExpression函数外的代码如高:

function genExpression(node, context) {
  const { content, isStatic } = node;
  context.push(
    isStatic 必修 JSON.stringify(content) : content,
    -3 /* Unknown */,
    node
  );
}

因为当前的msg变质是一个ref相应式变质,以是isStaticfalse。以是会执止push办法,将$setup.msg拔出到render函数外。

执止完push法子后,正在debug末端望望此时的render函数字符串是甚么样的,如高图:

after-expression

从上图外否以望到此时的render函数根基曾经天生了,剩高的即是挪用push法子天生各个函数的左括号")"以及左花括号"}"。将断点逐层走没,曲到generate函数外。代码如高:


function generate(ast) {
  // ...省略
  genNode(ast.codegenNode, context);

  deindent();
  push(`}`);
  return {
    ast,
    code: context.code,
  };
}

执止完最初一个 push办法后,正在debug末端望望此时的render函数字符串是甚么样的,如高图:

render

从上图外否以望到此时的render函数末于天生啦!

总结

那是尔绘的咱们那个场景外generate天生render函数的流程图:

  • 执止genModulePreamble函数天生:import { xxx } from "vue";
  • 简略字符串拼接天生render函数外的函数名称以及参数,也即是function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  • 以根节点的codegenNode属性为参数挪用genNode函数天生render函数外return的形式。
    • 此时传进的是假造节点,执止genVNodeCall函数天生return _openBlock(), _createElementBlock(以及挪用genNodeList函数,天生createElementBlock函数的参数。
    • 处置惩罚p标签的tag标署名以及props,天生createElementBlock函数的第一个以及第两个参数。此时render函数return的形式为:return _openBlock(), _createElementBlock("p", null
    • 处置惩罚p标签的children也即是{{msg}}节点,再次挪用genNode函数。此时node节点范例为单年夜括号节点,挪用genInterpolation函数。
    • 正在genInterpolation函数外会先挪用push办法,此时的render函数return的形式为:return _openBlock(), _createElementBlock("p", null, _toDisplayString(。而后以node.content为参数再次挪用genNode函数。
    • node.content$setup.msg,是一个简略表明式节点,以是正在genNode函数外会挪用genExpression函数。执止完genExpression函数后,此时的render函数return的形式为:return _openBlock(), _createElementBlock("p", null, _toDisplayString($setup.msg
    • 挪用push办法天生各个函数的左括号")"以及左花括号"}",天生终极的render函数

到此那篇闭于本来 Vue 3 的 generate 是如许天生 render 函数的的文章便引见到那了,更多相闭本来vue3外template应用ref无需.value是由于那个形式请搜刮剧本之野之前的文章或者连续涉猎上面的相闭文章心愿巨匠之后多多撑持剧本之野!

点赞(3) 打赏

评论列表 共有 0 条评论

暂无评论

微信小程序

微信扫一扫体验

立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部