Vue3追本溯源(六)生成render方法之generate函数

513 阅读9分钟

上文解析了transform方法对ast对象进一步转化,依本例为模版,在type=0的根节点和type=1的元素节点对象上生成codegenNode属性。生成新的属性对象;合并相邻两个文本子节点;将创建VNode需要的方法收集起来赋值到root根节点上(例如:createVNodecreateTextVNode等),为generate方法生成render函数字符串(下文简称为renderString)做准备。本文来看下generate函数的具体内部实现

generate函数入口

在调用完transform方法转化ast对象之后,回归到baseCompile方法中

// baseCompile 方法最后return部分
return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
)

调用generate函数,传参ast对象(进一步转化后的模版对象)、options对象。看下generate方法的具体内部实现

export function generate(
  ast: RootNode,
  options: CodegenOptions & {
    onContextCreated?: (context: CodegenContext) => void
  } = {}
): CodegenResult {
    const context = createCodegenContext(ast, options)
    // ...
    const {
        mode,
        push,
        prefixIdentifiers,
        indent,
        deindent,
        newline,
        scopeId,
        ssr
    } = context
    const hasHelpers = ast.helpers.length > 0 // true
    const useWithBlock = !prefixIdentifiers && mode !== 'module' // true
    const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module' // false
    const isSetupInlined = !__BROWSER__ && !!options.inline // false
    
    const preambleContext = isSetupInlined ? createCodegenContext(ast, options) : context
    if (!__BROWSER__ && mode === 'module') {
        // ...
    } else {
        genFunctionPreamble(ast, preambleContext) // 根函数开头
    }
    // ...
}

首先调用createCodegenContext函数生成一个context对象(类似于transform方法中的context对象,也是提供了一些方法,主要是服务于renderString的生成,例如:换行、缩进、字符串拼接等等,同时提供一个变量用于储存renderString,就是contextcode属性,后续使用到具体方法时再详细解析),再根据所运行浏览器环境调用genFunctionPreamble方法生成renderString的头部,传参是astcontext对象,下面解析下genFunctionPreamble函数的内部实现

生成renderString开头部分

function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
    const {
        ssr,
        prefixIdentifiers,
        push,
        newline,
        runtimeModuleName,
        runtimeGlobalName
    } = context
    const VueBinding = !__BROWSER__ && ssr ? `require(${JSON.stringify(runtimeModuleName)})` : runtimeGlobalName
    const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
    if (ast.helpers.length > 0) {
        if (!__BROWSER__ && prefixIdentifiers) {/* ... */}
        else {
            push(`const _Vue = ${VueBinding}\n`)
            // ...
        }
    }
    genHoists(ast.hoists, context) // ast.hoists为空数组,此方法直接return
    newline()
    push(`return `)
}

// context对象中push方法的具体实现
push(code, node) {
    context.code += code
    if (!__BROWSER__ && context.map) {/* ... */}
}

// context对象中newline方法的具体实现
newline() {
    newline(context.indentLevel/* 初始化为0 */)
}
function newline(n: number) {
    context.push('\n' + `  `.repeat(n))
}

首先定义VueBinding变量,值等于context.runtimeGlobalName,在创建context对象时初始化该值为'Vue',再调用contextpush方法,参数为'const _Vue = Vue\n',可以看到push方法就是在contextcode属性上进行字符串的累加。之后调用contextnewline方法进行字符串的添加,newline方法内部也是调用push方法,将'\n' + ' '.repeat(n)(换行符加n个空格) 累加到code中,最后调用push方法将'return '字符串累加到code中。至此,contextcode属性的值为:

// renderString
const _Vue = Vue
return 

生成renderString的return函数

回归到generate方法中,解析genFunctionPreamble函数后的内部实现

// generate方法中执行完genFunctionPreamble的后续
 const functionName = ssr ? `ssrRender` : `render`
 const args = ssr ? ['_ctx', '_push', '_parent', '_attrs'] : ['_ctx', '_cache']
 // ...
 const signature = !__BROWSER__ && options.isTS ? args.map(arg => `${arg}: any`).join(',') : args.join(', ') // args.join(', ')
 // ...
 if (isSetupInlined || genScopeId) {
   // ...
 } else {
   push(`function ${functionName}(${signature}) {`)
 }
 indent()
 // ...
 
// context的 indent方法
indent() {
  newline(++context.indentLevel)
}

首先functionName'render'args['_ctx', '_cache'] (非ssr)。signatureargs.join(', '),所以signature'_ctx, _cache',之后执行push方法累加code字符串,然后调用context.indent方法,可以看到indent方法的实现就是调用newline方法换行并缩进++context.indentLevel个空格(此处传参1), 现在code属性的值为:

// renderString
const _Vue = Vue
return function render(_ctx, _cache) {
 

生成renderString中return函数的函数体

后续解析下renderStringreturn函数的函数体内容形成

// helperNameMap对象key(Symbol类型),value(Vue3暴露的创建节点的方法名称)
export const helperNameMap: any = {
  [TO_DISPLAY_STRING]: `toDisplayString`,
  [CREATE_VNODE]: `createVNode`,
  [CREATE_TEXT]: `createTextVNode`,
  [FRAGMENT]: `Fragment`,
  [OPEN_BLOCK]: `openBlock`,
  [CREATE_BLOCK]: `createBlock`,
  // ...
}

// generate方法中生成renderString中return函数的函数体
if (useWithBlock) {
    push(`with (_ctx) {`)
    indent()
    // function mode const declarations should be inside with block
    // also they should be renamed to avoid collision with user properties
    if (hasHelpers) {
      push(
        `const { ${ast.helpers
          .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
          .join(', ')} } = _Vue`
      )
      push(`\n`)
      newline()
    }
}

// ... add components/directives/temps code
if (!ssr) {
    push(`return `)
}
if (ast.codegenNode) {
    genNode(ast.codegenNode, context)
} else {
    // ...
}
// ...

首先调push('with (_ctx) {')方法累计code字符串,然后indent换行并缩进两个空格。然后将ast.helpers数组([Symbol(toDisplayString), Symbol(createVNode), Symbol(createTextVNode), Symbol(Fragment), Symbol(openBlock), Symbol(createBlock)])通过map循环,根据helperNameMap对象的映射关系替换成新的元素['toDisplayString: _toDisplayString', 'createVNode: _createVNode', 'createTextVNode: _createTextVNode', 'Fragment: _Fragment', 'openBlock: _openBlock', 'createBlock: _createBlock'],然后通过join(', ')构成新的字符串,调push方法累加到code字符串中,最后push('\n')换行,调newline()换行之后缩进两个空格。然后push('return ')。此时code字符串如下:

// renderString
const _Vue = Vue
return function render(_ctx, _cache) {
 with (_ctx) {
  const { toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
  return 

genNode方法解析root对象codegenNode属性

那么接下来就是拼接renderString返回函数的内部返回内容(就是上面js代码中render方法的返回值),调用genNode方法开始解析ast.codegenNode属性,下面详细解析下genNode方法的内部实现

function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
  if (isString(node)) {
    context.push(node)
    return
  }
  if (isSymbol(node)) {
    context.push(context.helper(node))
    return
  }
  switch (node.type) {
      case NodeTypes.VNODE_CALL: // 13
          genVNodeCall(node, context)
          break
      case NodeTypes.TEXT_CALL: // 12
          genNode(node.codegenNode, context)
          break
      case NodeTypes.JS_CALL_EXPRESSION: // 14
          genCallExpression(node, context)
          break
      case NodeTypes.COMPOUND_EXPRESSION: // 8
          genCompoundExpression(node, context)
          break
      case NodeTypes.INTERPOLATION: // 5
          genInterpolation(node, context)
          break
      case NodeTypes.TEXT: // 2
          genText(node, context)
          break
      case NodeTypes.ELEMENT: // 1
          // ...
          genNode(node.codegenNode!, context)
          break
      case NodeTypes.JS_OBJECT_EXPRESSION: // 15
          genObjectExpression(node, context)
          break
      case NodeTypes.SIMPLE_EXPRESSION: // 4
          genExpression(node, context)
          break
  }
}

首先type0codegenNodetype13的对象,所以命中genVNodeCall函数的执行

function genVNodeCall(node: VNodeCall, context: CodegenContext) {
  const { push, helper, pure/* 创建context时定义 - false */ } = context
  const {
    tag,
    props,
    children,
    patchFlag,
    dynamicProps,
    directives,
    isBlock, // type为13的codegenNode - true
    disableTracking // false
  } = node
  if (directives) {}
  if (isBlock) {
    push(`(${helper(OPEN_BLOCK)}(${disableTracking ? `true` : ``}), `)
  }
  if (pure) {}
  push(helper(isBlock ? CREATE_BLOCK : CREATE_VNODE) + `(`, node)
  genNodeList(
    genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
    context
  )
  push(`)`)
  if (isBlock) {
    push(`)`)
  }
  if (directives) {
    push(`, `)
    genNode(directives, context)
    push(`)`)
  }
}

// context的helper方法定义 - '_' + helperNameMap对象映射的value值
helper(key) {
  return `_${helperNameMap[key]}`
}

首先执行push('(_openBlock(), ')('_openBlock'helper(OPEN_BLOCK)的返回值),再调用context.push方法,参数为'_createBlock(',此时code字符串为:

// renderString
const _Vue = Vue
return function render(_ctx, _cache) {
 with (_ctx) {
  const { toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
  return (_openBlock(), _createBlock(

然后调用genNodeList方法对nodetagpropschildrenpatchFlagdynamicProps元素逐步解析并累加code字符串

注意: 这里调用了genNullableArgs方法对上面提到的tag等属性优化,大致实现是: [tag、props、children、patchFlag、dynamicProps]这个数组从最后一项开始循环判断,遇到第一个!=null(属性值存在)的则跳出循环,然后执行args.slice(0, i+1)将最后不存在的属性都剔除,再执行args.map(arg => arg || 'null'),将中间不存在的置为null。换言之将这个由各属性构成的数组从最后一项开始,将属性值不存在的都剔除,中间属性值不存在置为null。依本例解析传入genNodeList方法的第一个参数是[Symbol(Fragment), null, [{...}, {...}], 64]

genNodeList方法依次解析root的codegenNode对象中tag、children等属性

下面详细解析下genNodeList函数的内部实现

function genNodeList(
  nodes: (string | symbol | CodegenNode | TemplateChildNode[])[],
  context: CodegenContext,
  multilines: boolean = false,
  comma: boolean = true
) {
  const { push, newline } = context
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i]
    if (isString(node)) {
      push(node)
    } else if (isArray(node)) {
      genNodeListAsArray(node, context)
    } else {
      genNode(node, context)
    }
    if (i < nodes.length - 1) {
      if (multilines/* false */) {
        comma && push(',')
        newline()
      } else {
        comma && push(', ')
      }
    }
  }
}

该函数内部是一个for循环贯穿,循环传入的nodes数组,依次匹配判断条件对每个元素做出相应的解析。依本例为模版

第一个tagSymbol(Fragment),因此调用genNode方法(此方法在上面有介绍),命中是Symbol类型的判断,执行context.push(context.helper(node)),向code中添加"_Fragment"字符串,然后执行push(', ')添加逗号空格(本质就是分隔_createBlock方法的入参)。
第二个是"null",命中isString,是字符串,所以直接push('null'),在code上累加null,后续push(', ')
第三个是children,因为children是数组,所以执行genNodeListAsArray方法。

此时code字符串为:

// renderString
const _Vue = Vue
return function render(_ctx, _cache) {
 with (_ctx) {
  const { toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
  return (_openBlock(), _createBlock(_Fragment, null, 

genNodeListAsArray解析root的children数组元素

接下来详细解析下genNodeListAsArray方法的内部实现

function genNodeListAsArray(
  nodes: (string | CodegenNode | TemplateChildNode[])[],
  context: CodegenContext
) {
  const multilines =
    nodes.length > 3 ||
    ((!__BROWSER__ || __DEV__) && nodes.some(n => isArray(n) || !isText(n)))
  context.push(`[`)
  multilines && context.indent()
  genNodeList(nodes, context, multilines)
  multilines && context.deindent()
  context.push(`]`)
}

该方法内部首先判断multilines的值,判断条件为children.length > 3 或者 children中存在是数组或者不是文本节点的元素,本例中type=0children满足条件,所以multilines=true。然后执行context.push('[')code添加'['符号,再执行indent()方法换行缩进。

genNode解析type=12子元素的codegenNode

调用genNodeList方法依次解析children数组的每个元素。本例中type=0children的第一个元素是type=12的文本节点,它是个对象所以调用genNode方法。回归到genNode方法中,type=12会继续调用genNode方法,参数是codegenNode属性(本例中type=12codegenNodetype=14的对象),type=14会命中genCallExpression方法的执行。详细看下genCallExpression函数的内部实现

// genCallExpression 方法定义
function genCallExpression(node: CallExpression, context: CodegenContext) {
  const { push, helper, pure } = context
  const callee = isString(node.callee) ? node.callee : helper(node.callee)
  if (pure) {
    push(PURE_ANNOTATION)
  }
  push(callee + `(`, node)
  genNodeList(node.arguments, context)
  push(`)`)
}

genCallExpression解析type=14的文本节点

该方法内部首先判断callee属性是否是字符串,本例为Symbol(createTextVNode),所以callee=_createTextVNode,调用push(callee + '(', node)方法,累加code字符串。之后调用genNodeList方法解析文本节点的arguments属性(本例中为[{type: 8, ...}, '1 /* TEXT */'])。在genNodeList方法中type=8会命中genCompoundExpression函数,看下genCompoundExpression函数的内部实现

// genCompoundExpression方法定义
function genCompoundExpression(
  node: CompoundExpressionNode,
  context: CodegenContext
) {
  for (let i = 0; i < node.children!.length; i++) {
    const child = node.children![i]
    if (isString(child)) {
      context.push(child)
    } else {
      genNode(child, context)
    }
  }
}

该方法内部会循环传入的node.children数组,如果是字符串则直接调用push方法进行字符串累加,否则调用genNode方法依次解析。依本例解析type=8的对象中children[{type:5,...}, ' + ', {type:2,...}]。首先type=5的对象会命中genInterpolation方法

genInterpolation解析type=5动态数据节点

function genInterpolation(node: InterpolationNode, context: CodegenContext) {
  const { push, helper, pure } = context
  if (pure) push(PURE_ANNOTATION)
  push(`${helper(TO_DISPLAY_STRING)}(`)
  genNode(node.content, context)
  push(`)`)
}

genInterpolation方法中首先push累计字符串"_toDisplayString(",然后调用genNode函数解析type=5content属性,本例为"message"字符串,所以直接push('messgae'),最后push(')')添加右括号。回归到genCompoundExpression方法中,children的第二个参数是字符串' + ',所以直接累加。

genText解析type=2纯文本节点

第三个参数是type=2的对象,命中genText方法。该方法内部执行context.push(JSON.stringify(node.content), node),将content属性值累加进code字符串中(本例type=2对象的content' '空格)。

回归到genCallExpression方法中执行的genNodeList函数,type=8的对象执行完后执行push(', ')(原因上面已经详细解析了)。然后解析arguments数组的第二个元素"1 /* TEXT */",因为是字符串所以直接累加。回归到genCallExpression方法中,最后执行push(')')

回归到genNodeListAsArray方法中执行的genNodeList内部(解析的是type=0codegenNode对象的children数组),此时type=12的已经解析完成了,执行comma && push(',');newline()(因为在genNodeListAsArray方法中调用genNodeList时,multilinestrue,上述中已详细解析)添加",",并且换行缩进。此时code字符串为:

// renderString
const _Vue = Vue
return function render(_ctx, _cache) {
 with (_ctx) {
  const { toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
  return (_openBlock(), _createBlock(_Fragment, null, [
   _createTextVNode(_toDisplayString(message) + " ", 1 /* TEXT */),
   

genNode解析type=1的codegenNode

之后开始解析type=1的对象。依然是执行genNode方法,此时会递归调用genNode方法,参数为node.codegenNode(type=13对象)。命中genVNodeCall函数(此函数在解析type=0codegenNode属性时也会命中)。因为isBlock=false,所以执行push('_createVNode(', node)。之后跟上述解析type=0时相同,依次解析tagpropschildrenpatchFlagdynamicProps

1、首先是tag,本例为"\"button"\"字符串,所以直接push("\"button"\"),然后push(', ')

genObjectExpression解析props属性

2、再是props,因为button标签中存在click事件,所以是存在的(type=15的对象)。命中genObjectExpression函数。看下它的内部实现

// genObjectExpression函数定义
function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
  const { push, indent, deindent, newline } = context
  const { properties } = node
  if (!properties.length) {/* ... */}
  const multilines /* false */ =
    properties.length > 1 ||
    ((!__BROWSER__ || __DEV__) &&
      properties.some(p => p.value.type !== NodeTypes.SIMPLE_EXPRESSION))
  push(multilines ? `{` : `{ `)
  multilines/* false */ && indent()
  for (let i = 0; i < properties.length; i++) {
    const { key, value } = properties[i]
    // key
    genExpressionAsPropertyKey(key, context)
    push(`: `)
    // value
    genNode(value, context)
    if (i < properties.length - 1) {
      // will only reach this if it's multilines
      push(`,`)
      newline()
    }
  }
  multilines && deindent()
  push(multilines ? `}` : ` }`)
}

// genExpressionAsPropertyKey定义
function genExpressionAsPropertyKey(
  node: ExpressionNode,
  context: CodegenContext
) {
  const { push } = context
  if (node.type === NodeTypes.COMPOUND_EXPRESSION) {/*...*/} 
  else if (node.isStatic) {
    // only quote keys if necessary
    const text = isSimpleIdentifier(node.content)
      ? node.content
      : JSON.stringify(node.content)
    push(text, node)
  } else {/*...*/}
}

// isSimpleIdentifier
const nonIdentifierRE = /^\d|[^\$\w]/
export const isSimpleIdentifier = (name: string): boolean =>
  !nonIdentifierRE.test(name)

genObjectExpression方法内部首先判断multilines=false(判断propsproperties数组的长度是否>1,或者是否存在value.type !== 4的元素。本例不符合,所以为false),然后push('{ ')。再for循环properties数组,解析每一个属性(本例中只有一个click方法)。每个属性的key值对象调用genExpressionAsPropertyKey方法push(text, node)(这里text的值是在node.content基础上做了一层优化判断,具体是利用正则判断是否是简单表达式,不是则调用JSON.stringifynode.content转成字符串)

回归到genObjectExpression方法中,处理完属性的key值后执行push(': '),再调用genNode(value, context)方法处理value(本例为type=4的对象),命中genExpression方法

// genExpression方法的内部定义
function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
  const { content, isStatic } = node
  context.push(isStatic ? JSON.stringify(content) : content, node)
}

genExpression方法内部就是调用context.push方法进行code字符串的累加,参数由isStatic属性决定,本例中isStaticfalse,所以就是content(modifyMessage)字符串。回归到genObjectExpression方法中,处理完value之后,判断i < properties.length - 1(因为本例中只有一个属性)不成立,最后执行push(' }')。回到genNodeList方法中,执行push(', ')

3、其次解析children,本例中button元素的子元素是type=2的对象,所以执行genNode方法,并命中genText函数,这个函数之前已经解析过了,就是将content属性累加到code字符串(本例为"修改数据"字符串),回归到genNodeList方法中,执行push(', ')

4、再次解析patchFlag,本例为"8 /* PROPS */"字符串,所以直接push累加,然后push(', ')

5、最后解析dynamicProps属性,本例"[\"onClick\"]"仍然是字符串,所以调用push方法直接累加

至此genNodeList方法已经执行完成(tagpropschildrenpatchFlagdynamicProps属性已经解析结束了)。回归到genVNodeCall方法中执行push(')')。回归到genNodeListAsArray函数中type=0children数组已经解析完成了。之后调用multilines && context.deindent()

// context.deindent方法定义
deindent(withoutNewLine = false) {
  if (withoutNewLine) {
    --context.indentLevel
  } else {
    newline(--context.indentLevel)
  }
}

deindent方法的大致作用就是调用push方法累加字符串(\n + context.indentLevel-1个空格),就是换行并缩进n-1个空格。最后执行context.push(']'),添加结尾"]"。此时code字符串为:

// renderString
const _Vue = Vue
return function render(_ctx, _cache) {
 with (_ctx) {
  const { toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
  return (_openBlock(), _createBlock(_Fragment, null, [
   _createTextVNode(_toDisplayString(message) + " ", 1 /* TEXT */),
   _createVNode("button", { onClick: modifyMessage }, "修改数据", 8 /* PROPS */, ["onClick"])
  ]

解析完type=0children属性后(genNodeListAsArray函数执行),执行push(', ')方法。然后解析type=0patchFlag属性,本例为"64 /* STABLE_FRAGMENT */"字符串,所以push直接累加字符串。至此type=0tag, props, children, patchFlag属性已经解析完成,回归到genVNodeCall方法中,执行push(')')。此时type=0codegenNode属性已经解析结束了。回归到 generate方法中

// generate方法的返回部分
if (useWithBlock) {
    deindent()
    push(`}`)
}

deindent()
push(`}`)
// ...
return {
    ast,
    code: context.code,
    preamble: isSetupInlined ? preambleContext.code : ``,
    // SourceMapGenerator does have toJSON() method but it's not in the types
    map: context.map ? (context.map as any).toJSON() : undefined
}

最后因为本例中useWithBlock=true,所以调用deindent方法换行缩进(--context.indentLevel个空格),push('}')累加'}',再调用deindent()push('}')方法换行缩进并累加'}',最终generate函数返回一个对象{ ast, code: 拼接的renderString字符串, ... }。本例为模版生成的renderString如下:

// renderString
const _Vue = Vue
return function render(_ctx, _cache) {
 with (_ctx) {
  const { toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
  return (_openBlock(), _createBlock(_Fragment, null, [
   _createTextVNode(_toDisplayString(message) + " ", 1 /* TEXT */),
   _createVNode("button", { onClick: modifyMessage }, "修改数据", 8 /* PROPS */, ["onClick"])
  ], 64 /* STABLE_FRAGMENT */)
 }
}

总结

整个generate方法类似于一个拨洋葱模型,刚开始利用运行环境、transform解析生成的helpers数组(存放创建节点的函数名称)等拼接renderString的开头部分(变量定义),接着从root根对象的codegenNode属性开始解析,依次解析tag(标签名称)、props(属性)、children(子节点对象)并且进行renderString字符串的拼接。当解析到children数组时会将数组的每个元素依次解析,主要是利用genNode方法中的switch-case命中不同的对象解析方法(例如: 静态文本、动态数据、元素节点等),当子节点完全解析完成之后,回归到上层节点继续解析patchFlag(patch标识位,后续调用patch方法生成DOM时需要用到)和dynamicProps(动态属性),直到root根对象也全部解析结束,那么context.code字符串就是生成的renderString,最终返回{ast, code,...}对象。后续会执行renderString中的render方法,生成VNode对象,再调用patch方法将VNode生成真正的DOM节点。

🍏🍎🍐🍊🍋🍌🍉🍇🍓🫐🍈🍒🍑🥭🍍🥥🥝🍅🍆🥑🥦