从 vue 源码看问题 —— vue 编译器如何生成渲染函数?(下)

简介: 从 vue 源码看问题 —— vue 编译器如何生成渲染函数?(上)

genData() 方法

文件位置:src\compiler\codegen\index.js

/*
  处理节点上的众多属性,最后生成这些属性组成的 JSON 字符串,
  比如 data = { key: xx, ref: xx, ... } 
*/
export function genData(el: ASTElement, state: CodegenState): string {
  // 节点的属性组成的 JSON 字符串
  let data = '{'
  /*
    首先先处理指令,因为指令可能在生成其它属性之前改变这些属性
    执行指令编译方法,如 web 平台的 v-text、v-html、v-model,然后在 el 对象上添加相应的属性,
    如 v-text:el.textContent = _s(value, dir)
       v-html:el.innerHTML = _s(value, dir)
    当指令在运行时还有任务时,比如 v-model,
    则返回 directives: [{ name, rawName, value, arg, modifiers }, ...}] 
  */
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','
  // key,data = { key: xxx }
  if (el.key) {
    data += `key:${el.key},`
  }
  // ref,data = { ref: xxx }
  if (el.ref) {
    data += `ref:${el.ref},`
  }
  // 带有 ref 属性的节点在带有 v-for 指令的节点的内部,data = { refInFor: true }
  if (el.refInFor) {
    data += `refInFor:true,`
  }
  // pre,v-pre 指令,data = { pre: true }
  if (el.pre) {
    data += `pre:true,`
  }
  // 动态组件 <component is="xxx">,data = { tag: 'component' }
  if (el.component) {
    data += `tag:"${el.tag}",`
  }
  /*
    为节点执行模块 (class、style) 的 genData 方法,
    得到 data = { staticClass: xx, class: xx, staticStyle: xx, style: xx }
    module data generation functions
  */
  for (let i = 0; i < state.dataGenFns.length; i++) {
    data += state.dataGenFns[i](el)
  }
  /*
    其它属性,得到 data = { attrs: 静态属性字符串 } 或者 
    data = { attrs: '_d(静态属性字符串, 动态属性字符串)' }
    attributes
  */
  if (el.attrs) {
    data += `attrs:${genProps(el.attrs)},`
  }
  // DOM props,结果 el.attrs 相同
  if (el.props) {
    data += `domProps:${genProps(el.props)},`
  }
  /*
    自定义事件
     - data = { `on${eventName}:handleCode` } 
             或者 
     - { `on_d(${eventName}:handleCode`, `${eventName},handleCode`) }
      event handlers
  */
  if (el.events) {
    data += `${genHandlers(el.events, false)},`
  }
  /* 
    带 .native 修饰符的事件,
     - data = { `nativeOn${eventName}:handleCode` } 
              或者 
     - { `nativeOn_d(${eventName}:handleCode`, `${eventName},handleCode`)
  */
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents, true)},`
  }
  /*
   非作用域插槽,得到 data = { slot: slotName }
   slot target
   only for non-scoped slots
  */
  if (el.slotTarget && !el.slotScope) {
    data += `slot:${el.slotTarget},`
  }
  // scoped slots,作用域插槽,data = { scopedSlots: '_u(xxx)' }
  if (el.scopedSlots) {
    data += `${genScopedSlots(el, el.scopedSlots, state)},`
  }
  /*
    处理 v-model 属性,得到
    data = { model: { value, callback, expression } }
    component v-model
  */
  if (el.model) {
    data += `model:{value:${el.model.value
      },callback:${el.model.callback
      },expression:${el.model.expression
      }},`
  }
  /*
     inline-template,处理内联模版,得到:
     data = { inlineTemplate: { render: function() { render 函数 }, staticRenderFns: [ function() {}, ... ] } }
  */
  if (el.inlineTemplate) {
    const inlineTemplate = genInlineTemplate(el, state)
    if (inlineTemplate) {
      data += `${inlineTemplate},`
    }
  }
  // 删掉 JSON 字符串最后的 逗号,然后加上闭合括号 }
  data = data.replace(/,$/, '') + '}'
  /*
    v-bind 动态参数包装
    必须使用相同的 v-bind 对象应用动态绑定参数
    合并辅助对象,以便正确处理 class/style/mustUseProp 属性。
  */
  if (el.dynamicAttrs) {
    data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
  }
  // v-bind data wrap
  if (el.wrapData) {
    data = el.wrapData(data)
  }
  // v-on data wrap
  if (el.wrapListeners) {
    data = el.wrapListeners(data)
  }
  return data
}
复制代码

genDirectives() 方法

文件位置:src\compiler\codegen\index.js

/**
  运行指令的编译方法,如果指令存在运行时任务,则返回 
  directives: [{ name, rawName, value, arg, modifiers }, ...}] 
*/
function genDirectives(el: ASTElement, state: CodegenState): string | void {
  // 获取指令数组
  const dirs = el.directives
  // 不存在指令,直接结束 
  if (!dirs) return
  // 指令的处理结果
  let res = 'directives:['
  // 用于标记指令是否需要在运行时完成的任务,比如 v-model 的 input 事件
  let hasRuntime = false
  let i, l, dir, needRuntime
  // 遍历指令数组
  for (i = 0, l = dirs.length; i < l; i++) {
    dir = dirs[i]
    needRuntime = true
    // 获取节点当前指令的处理方法,比如 web 平台的 v-html、v-text、v-model
    const gen: DirectiveFunction = state.directives[dir.name]
    if (gen) {
      // 执行指令的编译方法,如果指令还需要运行时完成一部分任务,则返回 true,比如 v-model
      needRuntime = !!gen(el, dir, state.warn)
    }
    if (needRuntime) {
      // 表示该指令在运行时还有任务
      hasRuntime = true
       // res = directives:[{ name, rawName, value, arg, modifiers }, ...]
      res += `{name:"${dir.name}",rawName:"${dir.rawName}"${dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
        }${dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''
        }${dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
        }},`
    }
  }
  // 只有指令存在运行时任务时,才会返回 res
  if (hasRuntime) {
    return res.slice(0, -1) + ']'
  }
}
复制代码

genDirectives() 方法

文件位置:src\compiler\codegen\index.js

/*
  遍历属性数组 props,得到所有属性组成的字符串
  如果不存在动态属性,则返回:'attrName,attrVal,...'
  如果存在动态属性,则返回:'_d(静态属性字符串, 动态属性字符串)' 
 */
function genProps(props: Array<ASTAttr>): string {
  // 静态属性
  let staticProps = ``
  // 动态属性
  let dynamicProps = ``
  // 遍历属性数组
  for (let i = 0; i < props.length; i++) {
    // 属性
    const prop = props[i]
    // 属性值
    const value = __WEEX__
      ? generateValue(prop.value)
      : transformSpecialNewlines(prop.value)
    if (prop.dynamic) {
       // 动态属性,`dAttrName,dAttrVal,...`
      dynamicProps += `${prop.name},${value},`
    } else {
      // 静态属性,'attrName:attrVal,...'
      staticProps += `"${prop.name}":${value},`
    }
  }
  // 闭合静态属性字符串,并去掉静态属性最后的 ','
  staticProps = `{${staticProps.slice(0, -1)}}`
  if (dynamicProps) {
    // 如果存在动态属性则返回:_d(静态属性字符串,动态属性字符串)
    return `_d(${staticProps},[${dynamicProps.slice(0, -1)}])`
  } else {
    // 说明属性数组中不存在动态属性,直接返回静态属性字符串
    return staticProps
  }
}
复制代码

genHandlers() 方法

文件位置:src\compiler\codegen\events.js

/*
  生成自定义事件的代码
  动态:'nativeOn|on_d(staticHandlers, [dynamicHandlers])'
  静态:`nativeOn|on${staticHandlers}`
 */
export function genHandlers (
  events: ASTElementHandlers,
  isNative: boolean
): string {
  // 原生为 nativeOn,否则为 on
  const prefix = isNative ? 'nativeOn:' : 'on:'
  // 静态
  let staticHandlers = ``
  // 动态
  let dynamicHandlers = ``
  /*
    遍历 events 数组
    events = [{ name: { value: 回调函数名, ... } }]
  */ 
  for (const name in events) {
    const handlerCode = genHandler(events[name])
    if (events[name] && events[name].dynamic) {
      // 动态,dynamicHandles = `eventName,handleCode,...,`
      dynamicHandlers += `${name},${handlerCode},`
    } else {
      // staticHandlers = `eventName:handleCode,...,`
      staticHandlers += `"${name}":${handlerCode},`
    }
  }
  // 闭合静态事件处理代码字符串,去除末尾的 ','
  staticHandlers = `{${staticHandlers.slice(0, -1)}}`
  if (dynamicHandlers) {
    // 动态,on_d(statickHandles, [dynamicHandlers])
    return prefix + `_d(${staticHandlers},[${dynamicHandlers.slice(0, -1)}])`
  } else {
    // 静态,`on${staticHandlers}`
    return prefix + staticHandlers
  }
}
复制代码

genStatic() 方法

文件位置:src\compiler\codegen\index.js

/*
  生成静态节点的渲染函数
    1、将当前静态节点的渲染函数放到 staticRenderFns 数组中
    2、返回一个可执行函数 _m(idx, true or '') 
  hoist static sub-trees out
*/
function genStatic(el: ASTElement, state: CodegenState): string {
  // 标记当前静态节点已经被处理过了
  el.staticProcessed = true
  /*
    某些元素(模板)在 v-pre 节点中需要有不同的行为
    所有 pre 节点都是静态根,因此可将其用作包装状态更改并在退出 pre 节点时将其重置
  */
  const originalPreState = state.pre
  if (el.pre) {
    state.pre = el.pre
  }
  /* 
    将静态根节点的渲染函数 push 到 staticRenderFns 数组中,
    比如:[`with(this){return _c(tag, data, children)}`]
  */
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
  state.pre = originalPreState
  /* 
    返回一个可执行函数:_m(idx, true or '')
    idx = 当前静态节点的渲染函数在 staticRenderFns 数组中下标
  */
  return `_m(${state.staticRenderFns.length - 1
    }${el.staticInFor ? ',true' : ''
    })`
}
复制代码

genOnce() 方法

文件位置:src\compiler\codegen\index.js

/*
 处理带有 v-once 指令的节点,结果会有三种:
   1、当前节点存在 v-if 指令,得到一个三元表达式,condition ? render1 : render2
   2、当前节点是一个包含在 v-for 指令内部的静态节点,
      得到 `_o(_c(tag, data, children), number, key)`
   3、当前节点就是一个单纯的 v-once 节点,得到 `_m(idx, true of '')`
  v-once
 */
function genOnce(el: ASTElement, state: CodegenState): string {
  // 标记当前节点的 v-once 指令已经被处理过了
  el.onceProcessed = true
  if (el.if && !el.ifProcessed) {
    /*
     如果含有 v-if 指令 && if 指令没有被处理过
     则处理带有 v-if 指令的节点,最终得到一个三元表达式:
       condition ? render1 : render2 
    */ 
    return genIf(el, state)
  } else if (el.staticInFor) {
    /*
      说明当前节点是被包裹在还有 v-for 指令节点内部的静态节点
      获取 v-for 指令的 key
    */
    let key = ''
    let parent = el.parent
    while (parent) {
      if (parent.for) {
        key = parent.key
        break
      }
      parent = parent.parent
    }
    // key 不存在则给出提示,v-once 节点只能用于带有 key 的 v-for 节点内部
    if (!key) {
      process.env.NODE_ENV !== 'production' && state.warn(
        `v-once can only be used inside v-for that is keyed. `,
        el.rawAttrsMap['v-once']
      )
      return genElement(el, state)
    }
    // 生成 `_o(_c(tag, data, children), number, key)`
    return `_o(${genElement(el, state)},${state.onceId++},${key})`
  } else {
     /*
       上面几种情况都不符合,说明就是一个简单的静态节点,
       和处理静态根节点时的操作一样,得到 _m(idx, true or '')
     */ 
    return genStatic(el, state)
  }
}
复制代码

genFor() 方法

文件位置:src\compiler\codegen\index.js

/*
  处理节点上 v-for 指令  
  得到 `_l(exp, function(alias, iterator1, iterator2){return _c(tag, data, children)})`
*/
export function genFor(
  el: any,
  state: CodegenState,
  altGen?: Function,
  altHelper?: string
): string {
  // v-for 的迭代器,比如 一个数组
  const exp = el.for
  // 迭代时的别名
  const alias = el.alias
  // iterator 为 v-for = "(item ,idx) in obj" 时会有,比如 iterator1 = idx
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
  // 提示,v-for 指令在组件上时必须使用 key
  if (process.env.NODE_ENV !== 'production' &&
    state.maybeComponent(el) &&
    el.tag !== 'slot' &&
    el.tag !== 'template' &&
    !el.key
  ) {
    state.warn(
      `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
      `v-for should have explicit keys. ` +
      `See https://vuejs.org/guide/list.html#key for more info.`,
      el.rawAttrsMap['v-for'],
      true /* tip */
    )
  }
  // 标记当前节点上的 v-for 指令已经被处理过了
  el.forProcessed = true // avoid recursion
  // 返回 `_l(exp, function(alias, iterator1, iterator2){return _c(tag, data, children)})`
  return `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
    `return ${(altGen || genElement)(el, state)}` +
    '})'
}
复制代码

genIf() 方法

文件位置:src\compiler\codegen\index.js

// 处理带有 v-if 指令的节点,最终得到一个三元表达式,condition ? render1 : render2 
export function genIf(
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  // 标记当前节点的 v-if 指令已经被处理过了,避免无效的递归
  el.ifProcessed = true // avoid recursion
  // 得到三元表达式,condition ? render1 : render2
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
function genIfConditions(
  conditions: ASTIfConditions,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  // 长度若为空,则直接返回一个空节点渲染函数
  if (!conditions.length) {
    return altEmpty || '_e()'
  }
  // 从 conditions 数组中拿出第一个条件对象 { exp, block }
  const condition = conditions.shift()
  // 返回结果是一个三元表达式字符串,condition ? 渲染函数1 : 渲染函数2
  if (condition.exp) {
    /*
     如果 condition.exp 条件成立,则得到一个三元表达式,
     如果条件不成立,则通过递归的方式找 conditions 数组中下一个元素,
     直到找到条件成立的元素,然后返回一个三元表达式
    */
    return `(${condition.exp})?${genTernaryExp(condition.block)
      }:${genIfConditions(conditions, state, altGen, altEmpty)
      }`
  } else {
    return `${genTernaryExp(condition.block)}`
  }
  // v-if with v-once should generate code like (a)?_m(0):_m(1)
  function genTernaryExp(el) {
    return altGen
      ? altGen(el, state)
      : el.once
        ? genOnce(el, state)
        : genElement(el, state)
  }
}
复制代码

genIf() 方法

文件位置:src\compiler\codegen\index.js

/*
  生成插槽的渲染函数,得到:_t(slotName, children, attrs, bind)
 */
function genSlot(el: ASTElement, state: CodegenState): string {
   // 插槽名称
  const slotName = el.slotName || '"default"'
  // 生成所有的子节点
  const children = genChildren(el, state)
  // 结果字符串,_t(slotName, children, attrs, bind)
  let res = `_t(${slotName}${children ? `,function(){return ${children}}` : ''}`
  const attrs = el.attrs || el.dynamicAttrs
    ? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
      // slot props are camelized
      name: camelize(attr.name),
      value: attr.value,
      dynamic: attr.dynamic
    })))
    : null
  const bind = el.attrsMap['v-bind']
  if ((attrs || bind) && !children) {
    res += `,null`
  }
  if (attrs) {
    res += `,${attrs}`
  }
  if (bind) {
    res += `${attrs ? '' : ',null'},${bind}`
  }
  return res + ')'
}
复制代码

genComponent() 方法

文件位置:src\compiler\codegen\index.js

/*
  生成动态组件的渲染函数,返回 `_c(compName, data, children)`
  componentName is el.component, take it as argument to shun flow's pessimistic refinement
*/
function genComponent(
  componentName: string,
  el: ASTElement,
  state: CodegenState
): string {
   // 所有的子节点
  const children = el.inlineTemplate ? null : genChildren(el, state, true)
  // 返回 `_c(compName, data, children)`,compName 是 is 属性的值
  return `_c(${componentName},${genData(el, state)}${children ? `,${children}` : ''
    })`
}
复制代码

总结

渲染函数的生成过程是什么?编译器生成的渲染有两类:

  • render 函数,负责生成动态节点的 vnode
  • staticRenderFns 数组中的 静态渲染函数,负责生成静态节点的 vnode

渲染函数的生成过程,其实就是在遍历 AST 节点,通过递归的方式处理每个节点,最后生成格式如:_c(tag, attr, children, normalizationType)

  • tag 是标签名
  • attr 是属性对象
  • children 是子节点组成的数组,其中每个元素的格式都是 _c(tag, attr, children, normalizationTYpe) 的形式,
  • normalization 表示节点的规范化类型,是一个数字 0、1、2

静态节点是怎么处理的?

静态节点的处理分为两步:

  • 将生成静态节点 vnode 函数放到 staticRenderFns 数组中
  • 返回一个 _m(idx) 的可执行函数,即执行 staticRenderFns 数组中下标为 idx 的函数,生成静态节点的 vnode

v-once、v-if、v-for、组件 等都是怎么处理的?

  • 单纯的 v-once 节点处理方式 和 静态节点 一致
  • v-if 节点的处理结果是一个 三元表达式
  • v-for 节点的处理结果是可执行的 _l 函数,该函数负责生成 v-for 节点的 vnode
  • 组件的处理结果和普通元素一样,得到的是形如 _c(compName) 的可执行代码,生成组件的 vnode




目录
相关文章
|
5天前
|
JavaScript
理解 Vue 的 setup 应用程序钩子
【10月更文挑战第3天】`setup` 函数是 Vue 3 中的新组件选项,在组件创建前调用,作为初始化逻辑的入口。它接收 `props` 和 `context` 两个参数,内部定义的变量和函数需通过 `return` 暴露给模板。`props` 包含父组件传入的属性,`context` 包含组件上下文信息。`setup` 可替代 `beforeCreate` 和 `created` 钩子,并提供类似 `data`、`computed` 和 `methods` 的功能,支持逻辑复用和 TypeScript 类型定义。
26 11
|
1天前
|
存储 JavaScript 前端开发
vue尚品汇商城项目-day05【30.登录与注册静态组件(处理公共图片资源问题)+31.注册的业务+登录业务】
vue尚品汇商城项目-day05【30.登录与注册静态组件(处理公共图片资源问题)+31.注册的业务+登录业务】
9 1
|
7天前
|
JavaScript
vue尚品汇商城项目-day07【vue插件-50.(了解)表单校验插件】
vue尚品汇商城项目-day07【vue插件-50.(了解)表单校验插件】
18 4
|
1天前
|
存储 JavaScript API
vue尚品汇商城项目-day05【33.token令牌理解+34.用户登录携带token获取用户信息+35.退出登录】
vue尚品汇商城项目-day05【33.token令牌理解+34.用户登录携带token获取用户信息+35.退出登录】
7 0
|
1天前
|
JavaScript API 数据安全/隐私保护
vue尚品汇商城项目-day05【36.导航守卫理解】
vue尚品汇商城项目-day05【36.导航守卫理解】
7 0
|
1天前
|
JavaScript API
vue尚品汇商城项目-day06【37.获取交易数据+38.用户地址信息展示+39.交易信息展示及交易页面完成+40.提交订单+41.支付组件内获取订单号与展示支付信息】
vue尚品汇商城项目-day06【37.获取交易数据+38.用户地址信息展示+39.交易信息展示及交易页面完成+40.提交订单+41.支付组件内获取订单号与展示支付信息】
6 0
|
1天前
|
JavaScript 前端开发
vue尚品汇商城项目-day06【vue插件-42.支付页面中使用ElementUI以及按需引入】
vue尚品汇商城项目-day06【vue插件-42.支付页面中使用ElementUI以及按需引入】
6 0
|
1天前
|
JavaScript API
vue尚品汇商城项目-day07【44.个人中心二级路由搭建+45.我的订单+46.优化登录跳转+47.独享守卫】
vue尚品汇商城项目-day07【44.个人中心二级路由搭建+45.我的订单+46.优化登录跳转+47.独享守卫】
6 0
|
1天前
|
JavaScript
vue尚品汇商城项目-day07【vue插件-48.(了解)图片懒加载插件】
vue尚品汇商城项目-day07【vue插件-48.(了解)图片懒加载插件】
7 0
|
1天前
|
JavaScript
vue尚品汇商城项目-day07【vue插件-49.(了解)自定义插件】
vue尚品汇商城项目-day07【vue插件-49.(了解)自定义插件】
5 0
下一篇
无影云桌面