学习vue2.5源码之第八篇——compiler中的codegen

codegen

这部分是compiler的最后一步,将AST转化为render字符串,对应文件是src/compiler/codegen/index.js,然而你打开文件想预览一下整体结构,发现鼠标滚轮滚了几轮都还没到底 [黑人问号脸] ,因为代码有五百多行 [微笑脸] 。咬咬牙还是把它看完了,其实你会发现这代码并不难理解,就是要兼顾的内容,涉及的范围太大了,毕竟vue是个这么庞大的框架,所以。。。。所以还是不能放弃!本篇我就不啰嗦啦,只说一些我觉得有必要分享的地方,大家可以自己慢慢品尝~ ~ ~ ~

开始之前

这部分的学习方法还是一样,善用断点。我们先拿回上次说到的小栗子,输出一下它由AST转化来的render字符串


图片太长,我稍微分段了一下,长这样

render :
    "with(this){return _c('div',{staticClass:"div"},
        [
            _v("\n    我是最普通<的文本\n      "),
            _c('p',[_v(_s(msg))]),_v(" "),
            _c('a',{attrs:{"href":src}},[_v("这是我的简书链接")])
        ]
    )}"

看完玩意儿之后觉得糊里糊涂的,有很多奇奇怪怪的指令,所以我先提前解释一下各个指令的含义,方便等一下我们学习时可以参考~

_c createElement方法,创建一个元素,它的第一个参数是要定义的元素标签名、第二个参数是元素上添加的属性,第三个参数是子元素数组,第四个参数是子元素数组进行归一化处理的级别

_v 本文节点

_s 需解析的文本,之前在parser阶段已经有所修饰

_m 渲染静态内容

_o v-once静态组件

_l v-for节点

_e 注释节点

_t slot节点

接下来就可以开始看代码咯

src/compiler/codegen/index.js

我们从入口函数出发generate

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

喵喵喵?这也太棒了吧,只有十几行。先是定义一个对象CodegenState,里面包含转化时需要的属性和方法,我们看看

export class CodegenState {
  options: CompilerOptions;
  warn: Function;
  transforms: Array<TransformFunction>;
  dataGenFns: Array<DataGenFunction>;
  directives: { [key: string]: DirectiveFunction };
  maybeComponent: (el: ASTElement) => boolean;
  onceId: number;
  staticRenderFns: Array<string>;

  constructor (options: CompilerOptions) {
    this.options = options
    this.warn = options.warn || baseWarn
    this.transforms = pluckModuleFunction(options.modules, 'transformCode')
    this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
    this.directives = extend(extend({}, baseDirectives), options.directives)
    const isReservedTag = options.isReservedTag || no
    this.maybeComponent = (el: ASTElement) => !isReservedTag(el.tag)
    this.onceId = 0
    this.staticRenderFns = []
  }
}

transforms我输出了一下是个空数组,我们先忽略;dataGenFns是对静态类和静态样式的处理,directives是对指令的相关操作,我们以后会再说;isReservedTag保留标签标志;maybeComponent字面意思不是保留标签就可能是组件;onceId使用v-once的递增id;staticRenderFns是对静态根节点的处理。

继续往下看,如果AST为空,code = '_c("div")',即创建一个空的div,否则执行code = genElement(ast)

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      const data = el.plain ? undefined : genData(el, state)

      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // 静态class,style 处理
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

如果该节点是个静态子树,我们就执行genStatic对其进行处理。

function genStatic(el: ASTElement, state: CodegenState, once: ?boolean): string {
  el.staticProcessed = true
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
  return `_m(${
    state.staticRenderFns.length - 1
  },${
    el.staticInFor ? 'true' : 'false'
  },${
    once ? 'true' : 'false'
  })`
}

添加staticProcessed属性,插入staticRenderFns子树中,再执行回genElement,返回提取静态子树_m结构的字符串。

如果该节点有v-once标签,执行genOnce

function genOnce (el: ASTElement, state: CodegenState): string {
  el.onceProcessed = true
  if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.staticInFor) {
    let key = ''
    let parent = el.parent
    while (parent) {
      if (parent.for) {
        key = parent.key
        break
      }
      parent = parent.parent
    }
    if (!key) {
      return genElement(el, state)
    }
    return `_o(${genElement(el, state)},${state.onceId++},${key})`
  } else {
    return genStatic(el, state, true)
  }
}

添加onceProcessed属性,里面分了if和for的情况,前者执行genIf,后者向上查找key,找到的话返回静态内容_o结构的字符串,假如两种情况都不是的话则像静态子树一样处理。

genForgenIf的处理我们先忽略。

如果该节点是template,则执行genChildren,这个函数是对子节点归一化处理的级别标记,后面还会涉及,放到后面再说。

如果该节点是slot,则执行genSlot,返回静态内容_t结构的字符串。

function genSlot (el: ASTElement, state: CodegenState): string {
  const slotName = el.slotName || '"default"'
  const children = genChildren(el, state)
  let res = `_t(${slotName}${children ? `,${children}` : ''}`
  const attrs = el.attrs && `{${el.attrs.map(a => `${camelize(a.name)}:${a.value}`).join(',')}}`
  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

function genComponent (
  componentName: string,
  el: ASTElement,
  state: CodegenState
): string {
  const children = el.inlineTemplate ? null : genChildren(el, state, true)
  return `_c(${componentName},${genData(el, state)}${
    children ? `,${children}` : ''
  })`
}

里面主要就是执行了genChildrengenData,我们再看回genElement中最后一步

const data = el.plain ? undefined : genData(el, state)

const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
    data ? `,${data}` : '' // data
}${
    children ? `,${children}` : '' // children
})`

也是主要执行了genChildrengenData这两个函数,然后返回_c结构的字符串。

先看看genChildren

export function genChildren (
  el: ASTElement,
  state: CodegenState,
  checkSkip?: boolean,
  altGenElement?: Function,
  altGenNode?: Function
): string | void {
  const children = el.children
  if (children.length) {
    const el: any = children[0]
    // optimize single v-for
    if (children.length === 1 &&
      el.for &&
      el.tag !== 'template' &&
      el.tag !== 'slot'
    ) {
      return (altGenElement || genElement)(el, state)
    }
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    const gen = altGenNode || genNode
    return `[${children.map(c => gen(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}

先是对单个v-for子节点进行特别处理,应该是服务端vue方面的优化方案;接着是得到对子元素数组进行归一化处理的处理级别getNormalizationType

function getNormalizationType (
  children: Array<ASTNode>,
  maybeComponent: (el: ASTElement) => boolean
): number {
  let res = 0
  for (let i = 0; i < children.length; i++) {
    const el: ASTNode = children[i]
    if (el.type !== 1) {
      continue
    }
    // el上有`v-for`或标签名是`template`或`slot`
    // 或者el是if块,但块内元素有内容符合上述三个条件的
    if (needsNormalization(el) ||
        (el.ifConditions && el.ifConditions.some(c => needsNormalization(c.block)))) {
      res = 2
      break
    }
    // el是自定义组件
    // 或el是if块,但块内元素有自定义组件的
    if (maybeComponent(el) ||
        (el.ifConditions && el.ifConditions.some(c => maybeComponent(c.block)))) {
      res = 1
    }
  }
  return res
}

该函数返回对子元素数组进行归一化处理的处理级别:

0: 不需处理
1: 需要简单归一化处理
2: 需要深度归一化处理

目前只是进行标记而已,具体的处理在vdom部分才实现,所谓的归一化其实就是将多维的子数组转化为一维的,对于不同的子元素进行不同方式的归一化处理。

完成对归一化处理的标记,继续执行genNode

function genNode (node: ASTNode, state: CodegenState): string {
  if (node.type === 1) {
    return genElement(node, state)
  } if (node.type === 3 && node.isComment) {
    return genComment(node)
  } else {
    return genText(node)
  }
}

对子节点进行分类处理,假如type === 1即是一棵子树的话则执行genElement;假如是注释的话执行genComment;否则就是文本节点,执行genText

export function genComment (comment: ASTText): string {
  return `_e(${JSON.stringify(comment.text)})`
}

export function genText (text: ASTText | ASTExpression): string {
  return `_v(${text.type === 2
    ? text.expression // no need for () because already wrapped in _s()
    : transformSpecialNewlines(JSON.stringify(text.text))
  })`
}

注释节点以_e结构的字符串表示,文本节点以_v结构的字符串拼接,文本节点又分为需要解析的文本,就是parser阶段经过处理的_s字符串,还有就是纯文本节点。

接着我们看看genData,先是对指令进行操作,关于指令的学习我们统一另开篇章讲述。

const dirs = genDirectives(el, state)

然后就是对一些属性的操作,keyrefrefInForpretag,纯粹将他们拼接起来。

  if (el.key) {
    data += `key:${el.key},`
  }
  if (el.ref) {
    data += `ref:${el.ref},`
  }
  if (el.refInFor) {
    data += `refInFor:true,`
  }
  if (el.pre) {
    data += `pre:true,`
  }
  if (el.component) {
    data += `tag:"${el.tag}",`
  }

然后是对attrsprops执行genProps

function genProps (props: Array<{ name: string, value: string }>): string {
  let res = ''
  for (let i = 0; i < props.length; i++) {
    const prop = props[i]
    res += `"${prop.name}":${transformSpecialNewlines(prop.value)},`
  }
  return res.slice(0, -1) // 去掉','
}

也就是遍历数组转化为字符串的形式而已,transformSpecialNewlines是对一些特殊字符的处理

// 2028的字符为行分隔符,2029的字符为段落分隔符会
// 它们被浏览器理解为换行,而在Javascript的字符串表达式中是不允许换行的,从而导致错误
function transformSpecialNewlines (text: string): string {
  return text
    .replace(/\u2028/g, '\\u2028')
    .replace(/\u2029/g, '\\u2029')
}

接着是对eventsnativeEvents执行genHandlersgenHandlers是对v-on事件的处理以及其中修饰符的处理,接下来是对slot,v-model,inline-template以及对v-bind,v-on指令的处理,在这里就不再展开讲述。

这部分只是大概讲述了AST转化为render字符串的大致流程和部分函数学习,没有提及的地方,大家有兴趣的话可以自己学习一下~ 希望这篇文章能让你对vue编译部分的学习有所帮助,下一篇我们就开始一起学习vdom,看看vue是怎么实现虚拟DOM的 : )

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容