深入浅出MV*框架源码(三):Moon的html->code实现

前言

MV*框架中模板转化成代码的的过程涉及到编译原理,把这个html->code和第二篇文章的code->html连接起来就是完整的一个流程。

词法分析和语法分析

html其实就是一系列的字符串,这个将字符串转化成可执行代码的过程一般就叫做编译或者转译,如果会转成机器代码就是编译,否则就是转译。
很显然,MV*框架做的事情也是转译。而这个转译又主要分为词法分析和语法分析两个过程。

词法分析

词法分析就是将一个完整的句子分割成各个独立的单元的过程,这些单元被称为tokens。
例如一个表达式是“3+2”,那么tokens就是3、+、2。
同理一段html代码<p>Some HTML</p>的tokens就是<p>、Some HTML、</p>

语法分析

语法分析是在词法分析之后的,它的目的是将这个tokens组织起来变成可执行的结构。
在MV*框架里通常就是将tokens转换成一颗AST(抽象语法树)。

compile的使用

Moon的官网提及了Moon.compile的使用:

Moon.compile("<p>Some HTML</p>");

它将会被转成如下code:

function anonymous(m) {
  var instance = this; 
  return m("p", {attrs: {}}, {"shouldRender": false}, [m("#text", {"shouldRender": false}, "Some HTML")]);
}

也就是我们第二篇文章中render函数用到的m函数!之后大家可以自行推测出后续过程了吧~

compile的实现

我们重新new一个Moon实例,打一个断点跟到compile:


Moon-compile.jpg
compile.jpg

实现很简洁,有没有大吃一惊?
其实这就是一个template通过词法分析器lex生成tokens,tokens又通过语法分析器生成ast,最后通过ast生成node的过程。

lex

lex的实现:

var lex = function(input) {
    var state = {
        input: input,
        current: 0,
        tokens: []
    }
    lexState(state);
    return state.tokens;
}

它构造了一个state对象交给lexState分析,分析完后直接拿结果返回。

lexState

它其实也就是对input进行分流处理,有三条分支:不以<开头的转到lexText、以<!--开头的转到lexComment、其余全部给lexTag处理。
也就是说它把tokens分成三种类型:文本、注释、标签。

var lexState = function(state) {
    var input = state.input;
    var len = input.length;
    while (state.current < len) {
        // Check if it is text
        if (input.charAt(state.current) !== "<") {
            lexText(state);
            continue;
        }
        // Check if it is a comment
        if (input.substr(state.current, 4) === "<!--") {
            lexComment(state);
            continue;
        }
        // It's a tag
        lexTag(state);
    }
}
  1. lexTag
var lexTag = function(state) {
    var input = state.input;
    var len = input.length;

    // Lex Starting of Tag
    var isClosingStart = input.charAt(state.current + 1) === "/";
    state.current += isClosingStart === true ? 2 : 1;

    // Lex type and attributes
    var tagToken = lexTagType(state);
    lexAttributes(tagToken, state);

    // Lex ending tag
    var isClosingEnd = input.charAt(state.current) === "/";
    state.current += isClosingEnd === true ? 2 : 1;

    // Check if Closing Start
    if (isClosingStart === true) {
        tagToken.closeStart = true;
    }

    // Check if Closing End
    if (isClosingEnd === true) {
        tagToken.closeEnd = true;
    }
}

lexTag会通过lexTagType生成一个tagToken,然后通过lexAttributes给tagToken补充attr,最后通过isClosingStart判断它是一个开始标签还是一个结束标签。也就是把<p>Some HTML</p>的<p>和</p>这两个token找出来。

1.1 lexTagType
它负责找出标签token的type,例如是p或div:


lexTagType.jpg

它会对模板字符串中每个字符分析直到有"/"或者">"或者" "才结束,然后生成一个tagToken,push到state里,返回。在这个例子里,我们的tagType是div。

1.2 lexAttributes
它负责找出标签token的attr,例如id或class:


lexAttributes.jpg

它会使用双索引滑动窗口方法来判断一个字符串是否是attr,期间声明了一个incrementChar函数用来滑动窗口,在窗口中检测到>或者">就说明这是一个标签的结尾,并且忽略空格,从=拆分,左边一个attr名,右边是一个attr值。然后使用attributes对象来存储所有的attr,每个attr是一个attrValue对象:

{
     name: attrName,
     value: "",
     meta: {}
}
lexAttributes2.jpg

在经过一系列的查找后,lexAttributes成功地把tagToken的attrs悉数找出,然后设置到tagToken.attributes里去。

  1. lexText
    它负责找出文本类型的token:


    lexText.jpg

很有趣的一点是:它是根据一个正则来获取当前文本的结尾位置的。

var tagOrCommentStartRE = /<\/?(?:[A-Za-z]+\w*)|<!--/;

在找到文本token后还分两种情况:纯文本和部分文本。如果是纯文本就直接从current这个索引截取剩下的部分,如果是部分文本就只截取滑动窗口的内容。

  1. lexComment
    它负责找出注释类型的token:


    lexComment.jpg

    实现方法和lexText类似,只不过注释结尾比较好找,发现-->就行了。

  2. 结果
    最初的html:
<div id="app" class="container">
    <h2 class="text-center" m-on:click="haha()">{{msg}}</h2>
    <h2 class="text-center" m-on:click="haha()">{{computeData}}</h2>
    <!-- 这是注释 -->
</div>

最后得到这个结果:


lex-result.jpg

parse的实现

parse函数的实现同样很简洁:


parse.jpg

它建立了一个root对象来存储ast,每个子元素通过parseWalk得出。

parseWalk

parseWalk是用来遍历tokens的:


parseWalk-1.jpg

它会构造token、previousToken、nextToken三个指针,然后声明了一个move函数用来移动这三个指针,有点类似链表的操作。
在当前token的类型是文本或者注释的时候,会执行move,只不过一个返回previousToken.value一个返回null,因为文本的前面肯定是标签,而注释就没有处理的意义了。下面进入正题:type是tag的时候:


parseWalk-2.jpg

它会先获取tagType、closeStart、closeEnd,还会判断这个标签是不是SVG或者空元素,之后通过createParseNode创建一个parseNode(也就是ast树的一个node),接着又兵分三路,只重点处理非空非svg无closeStart的标签。
这里有个难琢磨的地方就是递归parseWalk生成子节点,这里需要自己多跟一下代码。

createParseNode

其实就是创建一个ast的node对象:


createParseNode.jpg

parse结果0.

parse-result.jpg

generate的实现

generate一改之前lexer和parse的简洁,变成很难读的样子了:


generate.jpg

不过还好,增加点耐心就没事。
可以看出ast这颗树实际上是挂载在root的children上,这里声明了一个state对象,里面包括了attr、directive、dep,接着通过generateNode生成若干个m函数,用dependencies把dep拿出来生成dependenciesCode最终拼接成最终的执行代码,然后返回根据这段代码生成的render函数。

generateNode

这个函数负责生成vnode


generateNode1.jpg

可以看出来这里还是兵分三路:

  1. node是string类型
  2. node是slot类型
  3. 其他情况
    它们分别做了什么呢?第一种情况直接编译模板和meta,第二种情况是对slot进行处理,最后是一般情况对meta、prop、directive、children等进行处理,最终返回生成好的调用代码。


    generateNode2.jpg

defaultMetadata

它负责生成默认渲染配置的对象:


defaultMetadata.jpg

generateProps

generateProps是个大函数,涉及到很多小函数


generateProps.jpg

它先获取props,然后把这个props丢进vnode的attrs,接着处理指令。接着开始生成props代码,和指令匹配,如果匹配到就进入beforeGenerate、afterGenerate、duringGenerate里。

  1. beforeGenerate
    这个函数是存在于各个内置指令对象里的,除了它之外,还有duringPropGenerate和afterGenerate方法。
    这里以m-on为例,它会获取事件名、事件回调,然后编译模板表达式,生成修饰符,最后加上事件监听代码:


    m-on-beforeGenerate.jpg
  2. compileTemplateExpression
    它负责编译模板里的依赖表达式:


    compileTemplateExpression.jpg

    它会获取模板里的dependencies,或者说{{}}里面的表达式。

  3. addEventListenerCodeToVNode
    它负责给vnode添加事件监听:


    addEventListenerCodeToVNode.jpg

    它会从vnode.meta获取eventListeners,最终取出eventHandlers给它加入handler来给vnode添加上事件监听。

  4. compileTemplate->compileTemplateState
    compileTemplate负责编译一个模板,具体工作交给compileTemplateState做:


    compileTemplate.jpg

    compileTemplateState对模板做了什么呢?它会通过escapeString先进行一些转义工作:


    escapeString.jpg

    如果是{{}}表达式,会通过scanTemplateStateUntil扫描依赖和scanTemplateStateForWhitespace去除空格:
    compileTemplateState.jpg

    这样就得到了{{}}里面的依赖,接着调用compileTemplateExpression编译这个表达式,迭代进行下去这个过程把dependencies收集过来。
  5. scanTemplateStateUntil
    这其实就是个扫描器,返回{{}}里面真正的表达式:


    scanTemplateStateUntil.jpg
  6. scanTemplateStateForWhitespace
    它同样是个扫描器,不过它的作用是扫描空格然后略过去:


    scanTemplateStateForWhitespace.jpg

generateMeta和generateEventlisteners

它们分别生成meta执行代码和事件执行代码,在generateMeta中对meta里的eventListeners进行特殊处理进到generateEventlisteners里去。

var generateMeta = function(meta) {
    var metaCode = "{";
    for (var key in meta) {
        if (key === "eventListeners") {
            metaCode += generateEventlisteners(meta[key])
        } else {
            metaCode += "\"" + key + "\": " + (meta[key]) + ", ";
        }
    }

    metaCode = metaCode.substring(0, metaCode.length - 2) + "}, ";
    return metaCode;
}
var generateEventlisteners = function(eventListeners) {
    var eventListenersCode = "\"eventListeners\": {";
    for (var type in eventListeners) {
        var handlers = eventListeners[type];
        eventListenersCode += "\"" + type + "\": [";

        for (var i = 0; i < handlers.length; i++) {
            eventListenersCode += (handlers[i]) + ", ";
        }

        eventListenersCode = eventListenersCode.substring(0, eventListenersCode.length - 2) + "], ";
    }

    eventListenersCode = eventListenersCode.substring(0, eventListenersCode.length - 2) + "}, ";
    return eventListenersCode;
}

总结

html->code总体经历了三个阶段:lex语法分析、compile词法分析、generate生成可执行代码,实际过程就是html->tokens->ast->code。

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

推荐阅读更多精彩内容