深入浅出--模板编译原理

第9章 模板编译

模板 -> 模板编译 -> 渲染函数 -> vnode -> 视图
template中可以用{{}}加入变量和js表达式,可以使用一些v-指令,都由于模板编译赋予模板强大的功能,将这些功能都变成渲染函数的一部分,从而渲染视图。
将模板编译成渲染函数可以分为3个步骤:
1.将模板解析成AST => 解析器
2.遍历AST标记静态节点 => 优化器
3.使用AST生成渲染函数 => 代码生成器

解析器

通过一条主线将不同类型的解析器(文本解析器、HTML解析器、过滤器解析器)组装在一起,监听HTML解析到HTML标签的开始位置、结束位置、文本或注释时,触发钩子函数,生成一个对应的AST节点。文本解析器用于解析带变量的文本,过滤器解析器用于解析过滤器。

优化器

遍历AST,检测出所有静态子树,并给其打标记。当静态节点被标记后,每次重新渲染时,会跳过更新节点的过程,而是直接克隆已经存在的虚拟节点。静态节点除了首次渲染,后续不需要任何重新渲染操作,优化器可以避免无用渲染,从而提升性能。

代码生成器

把AST转换成渲染函数中的代码字符串。代码字符串最终导出到外界使用时,会被放到渲染函数中,当渲染函数被导出,模板编译的任务就完成了。代码字符串中的_c和_v函数调用都是创建vnode的方法,_c创建元素类型的,_v创建文本类型的。

第9章 解析器

模板 -> AST

运行原理

解析器分为HTML解析器、文本解析器、过滤器解析器,最主要的是HTML解析器。在解析HTML的过程中,不断触发开始标签、结束标签、文本及注释钩子函数,在这些钩子函数里,可以构建AST节点。

层级关系通过栈来实现。解析HTML时,从前往后解析,每次触发start钩子函数时,如果当前节点有子节点,就推入栈中;当触发end钩子函数时,就从栈中弹出一个节点。这样可以保证,每当触发钩子函数start时,栈的最底节点就是当前正在构建节点的父节点。

当HTML解析器不再触发钩子函数时,说明所有模板都已经解析完毕,AST构建完成。

parseHTML(template, {
    start (tag, attrs, unary) {
        let element = createASTElement(tag, attrs, currentParent)
    }
    chars (text) {
        let element = { type: 3, text}
    }
    comment (text) {
        let element = { type: 3, text, isComment: true}
    }
    end () {}
})

function createASTElement (tag, attrs, parent) {
    return {
        type: 1,
        tag,
        attrsList: attrs,
        parent,
        children: []
    }
}

HTML解析器

export function parseHTML (html, options) {
    while (html) {
        if (!lastTag || !isPlainTextElement(lastTag)) {
            // 父元素为正常元素
            let textEnd = html.indexOf('<')
            // 以<开头
            if (textEnd === 0) {
                const comment = /^<!--/
                const conditionalComment = /^<!\]/
                const doctype = /^<!DOCTYPE [^>]+>/i

                const ncname = '[a-zA-Z_][\\w\\-\\.]*'
                const qnameCapture = `((?:${ncname}\\:)?${ncname})`
                const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
                const startTagOpen = new RegExp(`^<${qnameCapture}`)
                const startTagClose = /^\s*(\/?)>/
                const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)' + |([^\s"'=<>`]+)))?/
                // 注释
                if (comment.test(html)) {
                    const commentEnd = html.indexOf('-->')
                    if (commentEnd >= 0) {
                        if (options.shouldKeepComment) {
                            options.comment(html.substring(4, commentEnd))
                        }
                        html = html.substring(commentEnd + 3)
                        continue
                    }
                }
                // 条件注释
                if (conditionalComment.test(html)) {
                    const conditionalEnd = html.indexOf(']>')
                    if (conditionalEnd > 0) {
                        html = html.substring(conditionalEnd + 2)
                        continue
                    }
                }
                // DOCTYPE
                const doctypeMatch = html.match(doctype)
                if (doctypeMatch) {
                    html = html.substring(doctypeMatch[0].length)
                    continue
                }
                // 结束标签
                const endTagMatch = html.match(endTag)
                if (endTagMatch) {
                    html = html.substring(endTagMatch[0].length)
                    options.end(endTagMatch[1])
                    continue
                }
               // 开始标签
                const startTagMatch = parseStartTag()
                if (startTagMatch) {
                    handleStartTag(startTagMatch) // 将tagName、attrs和unary取出来,然后调用start钩子将这些数据放到参数中
                    continue
                }

                function advance (n) {
                    html = html.substring(n)
                }
                function parseStartTag() {
                    const start = html.match(startTagOpen)
                    if (start) {
                        const match = {
                            tagName: start[1],
                            attrs: []
                        }
                        advance(start[0].length)
                        // 解析标签属性
                        let end, attr
                        while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
                            advance(attr[0].length)
                            math.attrs.push(attr)
                        }
                        // 是否自闭和
                        if (end) {
                            match.unarySlash = end[1]
                            advance(end[0].length)
                            return match
                        }
                    }
                }
            }
           let text, rest, next
           if (textEnd >= 0) {
               rest = html.slice(textEnd)
               while (
                   !endTag.test(rest) &&
                   !startTagOpen.test(rest) &&
                   !comment.test(rest) &&
                   !conditionalComment.test(rest)
               ) {
                   // 如果<在文本中,且不符合任何需要被解析的片段的类型,那就属于文本 
                   next = rest.indexOf('<', 1)
                   if (next < 0) break
                   textEnd += next
                   rest = html.slice(textEnd)
               }
               text = html.substring(0, textEnd)
               html = html.substring(textEnd)
           }
           // 整个模板找不到<,说明全是文本
           if (textEnd < 0) {
               text = html
               html = '' 
           }
           if (options.chars && text) {
               options.chars(text)
           }
        } else {
            // 父元素为script、style、textarea,标签内所有内容当作文本处理
           const stackedTag = lastTag.toLowerCase()
           const reStackedTag = reCache[stachedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
           const rest = html.replace(reStackedTag, function(all, text) {
               if (options.chars) {
                   options.chars(text)
               }
               return ''
           })
           html = rest
           options.end(stackedTag)
        }
    }
}

文本解析器

对HTML解析器解析出来的文本进行二次加工,纯文本不用进行任何处理,主要是用于进一步解析带变量的文本。

parseHTML(template, {
    start (tag, attrs, unary) {},
    end () {},
    chars (text) {
        text = text.trim()
        if (text) {
            const children = currentParent.children
            let expression
            if (expression = parseText(text)) {
                children.push({
                    type: 2,
                    expression,
                    text
                })
            } else {
                children.push({
                    type: 3,
                    text
                })
            }
        }
    }
})

// "Hello {{name}}" => '"Hello" +_s(name)'
parseText (text) {
    const tagRE = /\{\{((?:.|\n)+?)\}\}/g
    // 如果没匹配到{{}},则为纯文本,直接return
    if (!tagRE.test(text)) {
        return
    }
    const tokens = []
    let lastIndex = tagRE.lastIndex = 0
    let match, index
    while ((match = tagRE.exec(text))) {
        index = match.index // 匹配到{{}}的位置
        // 把{{前面的文本添加到tokens中
        if (index > lastIndex) { 
            tokens.push(JSON.stringify(text.slice(lastIndex, index)))
        }
        // 把匹配到的变量改成_s(x)形式,也添加到数组中
        tokens.push(`_s(${match[1].trim()})`)
        // 更新lastIndex,保证下一轮循环不会重复匹配已经解析过的文本
        lastIndex = index + match[0].length
    }
    // 所有变量都处理完,如果右边还有文本,将文本添加到数组中
    if (lastIndex < text.length) {
        tokens.push(JSON.stringify(text.slice(lastIndex)))
    }
    // 将数组中每一位拼回字符串
    return tokens.join('+')
}

第10章 优化器

目的是找到AST中的静态子树,并打上标记。
静态子树: 在AST中永远不会发生变化的节点,eg:纯文本节点。
标记静态子树有两点好处:

  1. 每次重新渲染时,不需要为静态子树创建新的节点,而是直接克隆已存在的子树
  2. 在虚拟dom patching过程中,如果新旧节点都是静态子树,就无需进行patching,可以直接跳过

优化器内部实现过程

主要分为两个步骤:

  1. 找出AST中所有静态节点并打标记--static属性为true
  2. 找出AST中所有静态根节点并打标记--staticRoot为true(所有子节点都是静态节点,并且父级是动态节点)
    优化的过程,其实是给ASTnode添加static和staticRoot属性。
    export function optimize(root) {
        if (!root) return
        // 标记所有静态节点
        markStatic()
       // 标记所有静态根节点
        markStaticRoots(root)

        function isStatic (node) {
            if (node.type === 2) return false // 带变量的文本节点
            if (node.type === 3) return true // 不带变量的纯文本节点 type === 1为元素节点
            return !!(node.pre || ( // v-pre说明是静态节点
              !node.hasBindings && // 没有动态绑定: v-, @
              !node.if && !node.for && // 没有使用v-if v-for
              !isBuiltInTag(node.tag) && // 不能是内置标签,slot或component
              isPlatformReservedTag(node.tag) && // 不是组件,标签名必须是保留标签(分为HTML保留标签和SVG保留标签)
              !isDirectChildOfTemplateFor(node) && // 当前节点的父节点不能是带v-for的template标签
              Object.keys(node).every(isStaticKey) // 节点中不存在动态节点才会有的属性,静态节点的属性范围是type\tag\attrsList\attrsMap\plain\parent\children\attrs\staticClass\staticStyle,如果有属性名不在这之内,就不是静态节点
            ))
        }

        function markStatic (node) {
            node.static = isStatic(node)
            if (node.type === 1) {
                for (let i=0; i<node.children.length; i++) {
                    const child = node.children[i]
                    markStatic(child)
                    // 重新校对,如果子节点是动态,那此节点也是动态
                    if (!child.static) {
                        node.static = false
                    }
                }
            }
        }

        function markStaticRoots (node) {
            if (node.type === 1) {
                if (node.static && node.children.length && !(
                    node.children.length === 1  &&
                    node.children[0].type === 3
                )) { // 第一个找到的静态节点,其子节点一定也为静态,所以标记为静态根节点,直接return不再递归判断子节点
                    node.staticRoot = true
                    return
                } else  { // 只有一个文本节点,即使是静态节点也不会被标记,因为其优化成本大于收益
                    node.staticRoot = false
                }
                if (node.children) { // 递归
                    for (let i=0; i<node.children.length; i++) {
                        markStaticRoots(node.children[i])
                    }
                }
            }
        }
    }

第11章 代码生成器

将AST转换成渲染函数中的代码字符串,渲染函数被执行后,生成vnode,虚拟dom通过vnode来渲染视图。

// <div id="el">Hello {{name}}</div>
// 生成的代码字符串如下
with(this) {
    return _c( // _c其实是createElement,创建虚拟dome元素节点
        "div",
        {
            attrs: {"id": "el"}
         },
         [
             _v("Hello " + _s(name)) 
         ]
    )
}

生成原理

递归,从顶向下依次处理每一个AST节点。每处理一个节点,就会生成一个与节点类型相对应的代码字符串。

// 元素节点对应的代码字符串
_c(<tagname>, <data>, <children>)

处理子节点时,创建出来的代码字符串会放在<children>位置。

<div id="el">
    <div>
         <p>Hello {{name}}</p>
    </div>
</div>
// 创建出的代码字符串
_c('div', 
    {
        attrs: { "id": "el"}
    },
    [
        _c('div',
            [
                _c('p',
                    [_v("Hello"+_s(name))]
                )
            ]
        )
    ]
)
// 代码字符串会被包裹在with语句中的code中
`with(this){return ${code}}`

生成元素节点

_c

function genElement (el, state) {
    const data = el.plain ? undefined : genData(el, state)
    const children = genChildren(el, state)
    code = `_c('${el.tag}'${
        data ? `,${data}` : ''
    }${
        children ? `,${children}` : ''
    })`
    return code
}

function genData (el: ASTElement, state: CodegenState): string {
    let data = '{'
    if (el.key) {
        data += `key:${el.key},`
    }
   if (el.ref) {
        data += `ref:${el.ref},`
    }
    if (el.pre) {
        data += `pre:${el.pre},`
    }
    // ...
    data = data.replace(/,$/, '') + '}'
    return data
}

function genChildren (el, state) {
    const children = el.children
    if (children.length) {
        return `[${children.map(c => genNode(c, state)).join(',')}]`
    }
}

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

生成文本节点

function genText (text) {
    return `_v(${text.type === 2
        ? text.expression // 带变量文本
        :  JSON.stringify(text.text) // 静态纯文本 Hello => "'Hello'"
    })`
}

生成注释节点

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