Vue 编译之parse的思想探讨

楔子

笔者公司的前端小组掀起了Vue源码学习小组,前后几个月的共同学习,让小组成员都已经对Vue对大致框架有了个模糊对轮廓。
现在已经进入第二阶段:整理。

我们将小组分为四个部分,vue对整理也分为三个大模块:数据绑定、从template到vnode、vnode转化为dom对patch。

笔者对小组被分到了template到vnode对部分,拿到手后,感觉内容比较多,就先将内容根据源码对布局分为两小块:parse 和 render过程。

再者,古人云:兵马未动,粮草先行。笔者打算先介绍思想和思路,然后在具体到一些细枝末节。

本文准备到就是parse部分思想。

正文

一、整个编译过程的目的

我们都知道使用vue到时候,我们使用定制样式到几种方式大致为:

  • 1.options template。
  • 2.el。(挂载的节点)
  • 3.render 方法。

我们需要明白到是无论是哪种方式,我们最终的目的都是
生成以vnode为单位vdom树

而生成的vdom树,最终是用于patch过程中,生成真实dom节点。

所以我们的编译的最终目的是获得:

Virtue dom。

形式大致如下图:

vnode的样式

暂时不用管每个属性的作用,我们已经知道我们的目标是怎样的了。那么还需要知道的是 起始点和过程

正常情况下,如果我们这样初始化的话:

new Vue({
      el: '#el',
      template: `
        <div>

        <div v-for="(item,index) in options" :key="item.id">
          {{item.id}}
          <div>{{item.text}}</div>
        </div>
        
      </div>
      `,
      data: {
        name:"dinglei",
        options: [
          { id: 1, text: 'Hello' },
          { id: 2, text: 'World' }
        ]
      }
    })

那我们的初始化的状态则是 template的 一串 String。

当然初始化为String的写法还是满多的。 比如:

  • 不传template,直接使用使用$el。
  • template,使用一个模版。

所以我们从的转换过程,就是一串String 到 virtue dom的过程。

但是值得一提的是,vue 从模版到vdom的过程并非是直接一次性到位的过程。可能是因为尤大的设计的vnode和原生的dom属性差距过大,直接编译成vnode不好完成。其次是考虑到性能优化等方面。

所以vue的编译过程其实是分为三个过程:

  • parse
  • optimize
  • codegen for render And render

分别对应三个过程:

  • 从模版到 astElement
  • 优化添加标记staticRoot,当然这个层面的static是用于codegen里面的。
  • codegen 生成 render函数,render 绑定实例后执行生成vnode。

接下来我们将进入本文的主题

二、parse、optimize、codegen的核心思想解读。

1)parse解读

首先需要明白astElement包括哪些属性。在vue源码 flow文件目录下的compiler.js可以找到astElement的模型

declare type ASTElement = {
  type: 1;
  tag: string;
  attrsList: Array<ASTAttr>;
  attrsMap: { [key: string]: any };
  rawAttrsMap: { [key: string]: ASTAttr };
  parent: ASTElement | void;
  children: Array<ASTNode>;

  start?: number;
  end?: number;

  processed?: true;

  static?: boolean;
  staticRoot?: boolean;
  staticInFor?: boolean;
  staticProcessed?: boolean;
  hasBindings?: boolean;

  text?: string;
  attrs?: Array<ASTAttr>;
  dynamicAttrs?: Array<ASTAttr>;
  props?: Array<ASTAttr>;
  plain?: boolean;
  pre?: true;
  ns?: string;

  component?: string;
  inlineTemplate?: true;
  transitionMode?: string | null;
  slotName?: ?string;
  slotTarget?: ?string;
  slotTargetDynamic?: boolean;
  slotScope?: ?string;
  scopedSlots?: { [name: string]: ASTElement };

  ref?: string;
  refInFor?: boolean;

  if?: string;
  ifProcessed?: boolean;
  elseif?: string;
  else?: true;
  ifConditions?: ASTIfConditions;

  for?: string;
  forProcessed?: boolean;
  key?: string;
  alias?: string;
  iterator1?: string;
  iterator2?: string;

  staticClass?: string;
  classBinding?: string;
  staticStyle?: string;
  styleBinding?: string;
  events?: ASTElementHandlers;
  nativeEvents?: ASTElementHandlers;

  transition?: string | true;
  transitionOnAppear?: boolean;

  model?: {
    value: string;
    callback: string;
    expression: string;
  };

  directives?: Array<ASTDirective>;

  forbidden?: true;
  once?: true;
  onceProcessed?: boolean;
  wrapData?: (code: string) => string;
  wrapListeners?: (code: string) => string;

  // 2.4 ssr optimization
  ssrOptimizability?: number;

  // weex specific
  appendAsTree?: boolean;
};

整个流程我们需要的归纳为:

  • 识别器(parseHTML) (1)
  • 存储栈(stack) (2)
  • 创建函数(createASTElement) (3)
  • 上下文(currentParent)(4)

整个工作方式:

  • 1.就是识别器(1)利用正则从前到后识别所有敏感字段。如(标签、事件、迭代、数据绑定、插槽等等)
  • 2.open标签 如<div>{{test}}</div>的<div>,识别器识别到此类标签,就会将div 存放到stack当中,利用创建函数(3)创建createASTElement并处理大部分非组件属性(具体如v-model、v-if、v-for)。需要注意到是:
    • 1.一元标签如:img等,会直接走3处理。
    • p标签会做特殊处理,目的是为了和浏览器等识别方式达到目标一致。具体点击此处
  • 3.识别到闭合标签,则处理下原生属性如:id 、 class等等、还有ref、slot、component、key等等,并跟上下文(4)建立起父子关系。最终形成了树的结构。

其中的细节特别多,如果有兴趣请关注我们的动态,我们会在下期给出详细过程讲解。

2) optimize思想解读。

我们在上面已经给出了astElement的详细属性,其中有两个属性叫做staticRoot 和 staticInFor。而optimize 的过程,就是给astElement打上这两个标记。这里是为了让这类静态节点,在render过程,能够走缓存的方式,只渲染一次。

好处很明显,能够减少重复对比和渲染的过程,提高性能。

3)code generate解读

这个过程其实还没有生成vnode,而是生成一个执行函数,且包含了this的执行code,其格式如下:

with(this) { _c('div'...xxx...xxx) }

我们在强调下,这次又是从astElement直接到另一个String。
上面astElement是个树状结构。

image

然后在这个过程,基本一个astElement就对应一个短函数。
最基本短函数是createElement 也就是_c。
最终的树状结构,会以函数的形式表现处理函数如下

_c(
    'div',
    {
        key:'xxx',
        ref:'xx',
        pre:'xxx',
        domPro:xxx,
        ....
    },
    [ // chidren
        _v(_s('ding')),
        _c('p',{model:'isshow',}, [ ...xxx ])
    ]
)

可以清晰的看到,最终形成的string,依然是一个树状形式,是以function形式展示的树状,而且所有属性都已经抽离成createElement的第二个参数。

一句话概括下code generate做的事情就是:

生成vnode的前置工作,抽离astElement所有的属性,形成短函数链。

短函数对应大致如下:

export function installRenderHelpers (target: any) {
  target._o = markOnce // v-once
  target._n = toNumber
  target._s = toString
  target._l = renderList // v-for
  target._t = renderSlot // slot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic // static 
  target._f = resolveFilter 
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots // scopeSlot
  target._g = bindObjectListeners // linsners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

4) render 函数。

这个过程是直接执行,就会得到vnode,其细节包括了component的处理,会有单独一节去介绍。

这里展示下vnode的格式。

declare interface VNodeData {
  key?: string | number;
  slot?: string;
  ref?: string;
  is?: string;
  pre?: boolean;
  tag?: string;
  staticClass?: string;
  class?: any;
  staticStyle?: { [key: string]: any };
  style?: string | Array<Object> | Object;
  normalizedStyle?: Object;
  props?: { [key: string]: any };
  attrs?: { [key: string]: string };
  domProps?: { [key: string]: any };
  hook?: { [key: string]: Function };
  on?: ?{ [key: string]: Function | Array<Function> };
  nativeOn?: { [key: string]: Function | Array<Function> };
  transition?: Object;
  show?: boolean; // marker for v-show
  inlineTemplate?: {
    render: Function;
    staticRenderFns: Array<Function>;
  };
  directives?: Array<VNodeDirective>;
  keepAlive?: boolean;
  scopedSlots?: { [key: string]: Function };
  model?: {
    value: any;
    callback: Function;
  };
};

总结

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

推荐阅读更多精彩内容