前往小程序,Get更优阅读体验!
立即前往
社区首页 >专栏 >【Vue原理】Compile - 源码版 之 Parse 主要流程

【Vue原理】Compile - 源码版 之 Parse 主要流程

作者头像
神仙朱
修改于 2019-08-04 13:01:05
修改于 2019-08-04 13:01:05
7840
举报

写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧

研究基于 Vue版本 【2.5.17】

如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧

【Vue原理】Compile - 源码版 之 Parse 主要流程

本文难度较繁琐,需要耐心观看,如果你对 compile 源码暂时不感兴趣可以先移步白话版 Compile - 白话版,

parse 是 渲染三巨头的老大,其作用是把 template 字符串模板,转换成 ast

其涉及源码也是多得一批,达到了 一千多行,想想如果我把全部源码放到文章里面来简直不能看,所以我打算只保留主要部分,就是正常流程可以走通,去掉那些特殊处理的地方

大部分源码都是特殊处理,比如 script ,style,input ,pre 等标签,这次全部都去掉,只留下通用元素的处理流程,留下一个骨架

因为 parse 的内容非常的多,除了精简源码之外,我还通过不同内容划分文章去记录

今天,要记录的就是 parse 解析 template 成 ast 的大致流程,而怎么解析标签名,怎么解析标签属性会暂时忽略,而独立成文。当有解析标签名和解析属性的地方会直接出结果。比如当我说在 模板 "<div></div>" 匹配出头标签时,直接就得到 div ,而不会去考究是如何匹配出来的

好的,到底 template 是怎么变成 ast 的呢?跟着我去探索把~


AST

先来说说 ast 吧,这种复杂的概念,反正是需要查的。所以本文根本不需要解释太多

直接说我的理解吧

抽象语法树,以树状形式表现出语法结构

直接使用例子去直观感受就好了

代码语言:txt
复制
<div>111</div>

用 ast 去描述这个模板就是

代码语言:txt
复制
{ 
    tag:'div',    

    type :1 , 

    children:[ { 
        type:3, 
        text:'11' 
    } ] 
}

简单得一批把,复杂的这里也不提了,反正跟 parse 没多大关系我觉得

另外记一下,节点的 type 表示的意思

type:1,节点

type:2,表达式,比如 {{isShow}}

type:3,纯文本

现在就开始 parse 的内容了,那么就看 parse 的源码


Parse

parse 是渲染三巨头的老大,同时它也是一个函数,源码如下

代码语言:txt
复制
function parse(template) {    



    var stack = []; // 缓存模板中解析的每个节点的 ast

    var root;   // 根节点,是 ast
    var currentParent; // 当前解析的标签的父节点

    /**
    * parseHTML 处理 template 匹配标签,再传入 start,end,chars 等方法
    **/
    parseHTML(template, {        

        start: (..被抽出,在后面)

         end: (..被抽出,在后面), // 为 起始标签 开启闭合节点
         chars: (..被抽出,在后面) // 文字节点
    });    



    return root

}

parse 接收 template 字符串,使用 parseHTML 这个函数在 template 中匹配标签

并传入 start,end,chars 三个函数 供 parseHTML 处理标签等内容

start,end,chars 方法都已经被我抽出来,放在后面逐个说明

下面来看下其中声明的三个变量

1 stack

是一个数组存放模板中按顺序 从头到尾 每个标签的 ast

注:不会存放单标签的 ast ,比如 input,img 这些

比如 stack 是这样的

代码语言:txt
复制
stack=[{ 
    tag:'div',    

    type :1 , 

    children:[ { 
        type:3, 
        text:'11' 
    } ] 
}]

主要作用是帮助理清节点父子关系

2 root

每个模板都必须有一个根节点。写过 Vue 项目的都知道了,所以一般解析到第一个标签的时候,会直接设置这个标签为 根节点

并且最后返回的也是 root

不可以存在两个根节点(有 v-if 的不讨论)

3 currentParent

在解析标签的时候,必须要知道这个标签的 父节点时谁

这样才知道 这个标签是谁的子节点,才能把这个节点添加给相应的 节点的 children

注:根节点 没有 父节点,所以就是 undefined

parse 源码已经被我精简得很简单了,主要内容其实就在 其中涉及的四个方法中

parseHTML,start,end,chars

parseHTML 是处理 template 的主力,其他三个函数是功能类型的,负责处理相应的内容。 例如,start 是处理头标签的,end 是处理尾标签的,chars 是处理文本的

先来看看 parseHTML


处理 template

parseHTML 作为处理 template,匹配标签的函数,是十分庞大的,其中兼顾了非常多情况的处理

而本次在不影响流程的情况下,我去掉了下面这些处理,优化阅读

1、没有结束标签的处理

2、文字中包含 < 的处理

3、注释的处理

4、忽略首尾空白字符,默认起始和结尾都是标签

个人认为主要内容为三个

1、循环 template 匹配标签

2、把匹配到的内容,传给相应的方法处理

3、截断 template

来看源码,已经简化得不行了,但是还是要花点心思看看

代码语言:txt
复制
function parseHTML(html, options) {    



    while (html) {       



         // 寻找 < 的起始位置

        var textEnd = html.indexOf('<'),
            text ,rest ,next;        



        // 模板起始位置是标签开头 <

        if (textEnd === 0) {   

               

            /**
             * 如果是尾标签的 <
             * 比如 html = '</div>' , 匹配出 endTagMatch =["</div>", "div"]
             */
            var endTagMatch = html.match(endTag);            



            if (endTagMatch) {      

          

                // endTagMatch[0]="</a>"

                html = html.substring(endTagMatch[0].length); 

              

                // 处理尾标签,方法后面有记录
                options.end();                



                continue

            }   

                

            /**
             * 如果是起始标签的 <
             * parseStartTag 作用是,匹配标签存在的属性,截断 template
             * html = '<div></div>', 

             * parseStartTag 处理之后,startTagMatch = {tagName: "div", attrs: []}

             */
            var startTagMatch = parseStartTag();   

       

            // 匹配到 起始标签之后
            if (startTagMatch) {  

             

                // 处理起始标签,后面有介绍
                options.start(起始标签的信息);                



                continue

            }
        }        



        // 模板起始位置不是 <,而是文字

        if (textEnd >= 0) {
            text = html.substring(0, textEnd);
            html = html.substring(n);
        }       



        // 处理文字,后面有介绍
        if (options.chars && text) {
            options.chars(text);
        }
    }
}

思路如下

1匹配 < 这个符号

因为他是标签的开头(已经排除了文字中含有 < 的处理,不做讨论)

2如果 template 开头是 <

那么可能是 尾标签,可能是 头标签,那么就需要判断到底是哪个

1、先匹配尾标签,如果匹配到,那么就是尾标签,使用 end 方法处理。

2、如果不是,使用 parseStartTag 函数匹配得到首标签,并把 首标签信息传给 start 处理

parseStartTag 就是使用正则在template 中匹配出 首标签信息,其中包括标签名,属性等

比如 template 是

代码语言:txt
复制
html = '<div name="22">111</div>;'

parseStartTag 处理匹配之后得到

代码语言:txt
复制
{    

    tagName: "div", 

    attrs: [{name:"22"}]
}

3 如果 template 开头不是 <

那么证明 开头 到 < 的位置这一段,是字符串,那么就是文本了

传给 chars 方法处理

每次处理一次,就会截断到匹配的位置,然后 template 越来越短,直接为空,退出 while,于是处理完毕

对于截断呢,使用 substring,可能忘了怎么作用的,写个小例子

传入数字,表示这个位置前面的字符串都不要

image
image

然后,就到了我们其他三个方法的闪亮登场了


处理头标签

每当 parseHTML 匹配到一个 首标签,都会把该标签的信息传给 start 方法,让他来处理

代码语言:txt
复制
function start(tag, attrs, unary) {    



    // 创建 AST 节点

    var element = createASTElement(tag, attrs, currentParent);      



    /**
     * ...省略了一段处理 vFor,vIf,解析 @ 等属性指令的代码
     **/

    // 设置根节点,一个模板只有一个根节点
    if (!root) root = element;    



    // 处理父子关系

    if (currentParent) {
        currentParent.children.push(element);
        element.parent = currentParent;
    }    



    // 不是单标签(input,img 那些),就需要保存 stack

    if (!unary) {
        currentParent = element;
        stack.push(element);
    }
}

精简得一目了然(面目全非),看得极度舒适

看看 start 方法都做了哪些恶呢

1、创建 ast

2、解析 attrs,并存放到 ast (已省略属性解析)

3、设置根节点,父节点,把节点添加进父节点的 children

4、ast 保存进 stack

好像不用解释太多,肯定都看得懂啊,除了一个 创建 ast 的函数

这就来源码

代码语言:txt
复制
function createASTElement(tag, attrs, parent) {    



    return {        

        type: 1,        

        tag: tag,        

        attrsList: attrs,        

        // 把 attrs 数组 转成 对象

        attrsMap: makeAttrsMap(attrs),        

        parent: parent,        

        children: []

    }
}

创建一个 ast 结构,保存数据

直接返回一个对象,非常明了,包含的各种属性,应该也能看懂

其中有一个 makeAttrsMap 函数,举个栗子

模板上的属性,经过 parseHTML 解析成一个数组,如下

代码语言:txt
复制
[{    

    name:"hoho" ,value:"333"

},{    

    name:"href" ,value:"444"

}]

makeAttrMap 转成对象成这样

代码语言:txt
复制
{ hoho:"333",   href:"444"}

然后就保存在 ast 中


处理尾标签

每当 parseHTML 匹配到 尾标签 ,比如 "</div>" 的时候,就会调用传入的 end 方法

来看看吧

代码语言:txt
复制
function end() {    

    // 标签解析结束,移除该标签

    stack.length -= 1;
    currentParent = stack[stack.length - 1];
}

乍一看,很简单啊!这么少(都是精简...)

作用有两个

1从 stack 数组中移除这个节点

stack 保存的是匹配到的头标签,如果标签已经匹配结束了,那么就需要移除

stack 就是为了明确各节点间父子关系而存在的

保证 stack 中最后一个节点,永远是下次匹配的节点的父节点

举个栗子,存在下面模板

公众号
公众号

stack 匹配两个 头标签之后

代码语言:txt
复制
stack = [ 'div' , 'section']

看看 start 可以知道,此时 currentParent = section

然后匹配到 </section>,则移除 stack 中的 section,并且重设 currentParent

代码语言:txt
复制
stack = ['div']

currentParent = 'div'

再匹配到 p 的时候,p 的父节点就是 div,父子顺序就是正确的了

2重新设置 stack 最后一个节点为父节点


处理文本字符串

当 parseHTML 去匹配 < 的时候,发现 template 不是 <,template开头 到 < 还有一段距离

那么这段距离的内容就是 文本了,那么就会把这段文本传给 chars 方法处理

来看看源码

代码语言:txt
复制
function chars(text) {    



    // 必须存在根节点,不可能用文字开头

    if(!currentParent) return



    var children = currentParent.children;    



    // 通过 parseText 解析成字符串,判断是否含有双括号表达式,比如 {{item}}

    // 如果是有表达式,会存放多一些信息,
    var res = parseText(text)    



    if(res) {

        children.push({            

            type: 2,            

            expression: res.expression,            

            tokens: res.tokens,            

            text: text

        });
    }    



    // 普通字符串,直接存为 字符串子节点

    else if(
      !children.length ||
      children[children.length - 1].text !== ' '
    ) {
        children.push({            

            type: 3,            

            text: text

        });
    }
}

这段代码主要作用就是,为 父节点 添加 文本子节点

而文本子节点分为两种类型

1、普通型,直接存为文本子节点

2、表达式型,需要经过 parseText 处理

直接以结果来定义吧

比如处理这段文本

{{isShow}}

代码语言:txt
复制
{    

    expression: toString(isShow)

    tokens: [{@binding: "isShow"}]
}

主要是为了把表达式 isShow 拿到,方便后面从实例上获取值

好的,现在,template 处理流程所涉及的主要方法都讲完了

现在用上面这些函数来走一个流程

现在有一个模板

代码语言:txt
复制
<div>11 </div>

1 开始循环 tempalte

匹配到第一个 头标签 (<div>),传入 parse-start,生成 对应的 ast

该 div 的 ast 变成根节点 root,并设置其为当前父节点 currentParent,保存进节点缓存数组 stack

此时

代码语言:txt
复制
stack = [ { tag:'div' , children:[ ] } ]

第一轮处理结束,template 截断到第一次匹配到的位置

此时,template = 11 </div>

2 开始第二次遍历

开始匹配 <,发现 < 不在开头,而 开头位置 到 < 有一段普通字符串

调用 parse-char,传入字符串

发现其没有 双括号表达式,直接给父节点添加简单子节点

代码语言:txt
复制
currentParent.children.push({ type:3 , text:'11' })

此时

代码语言:txt
复制
stack =[ { tag:'div' , children:[ { type:3 , text:'11' } ] } ]

第二轮处理结束,template 截断到刚刚匹配完的字符串

此时,template = </div>

3 开始第三轮遍历

继续寻找 <,发现就在开头,但是这是一个结束标签,标签名是 div

因为 stack 是节点顺序存入的,这个结束标签肯定属于 stack 最后一个 标签

由于 该标签匹配完毕,所以从 stack 中移除

并且设置 当前父节点 currentParent 为 stack 倒数第二个

第三次遍历结束,template 继续截断

此时 template 为空了,结束所有遍历

返回此次 tempalte 解析的 root

代码语言:txt
复制
{ 
    tag:'div',type :1 , 
    children:[ { type:3 , text:'11' } ] 
}

于是 parse 就成功把 tempalte 解析成了 ast ,就是 root


总结

本问讲的是 parse 的主要流程,忽略了内部的处理细节,比如怎么解析标签,怎么解析属性,其他内容都会独立成文章

在 parse 的流程中,大致有五个函数,我们屡一下,如下

parse,parseHTML,start,end,chars

parse 是整个 parse 流程的总函数

parseHTML 是 parse 处理的主力函数

start,end,chars 是 在 parse 中传给 parseHTML ,用来帮助处理 匹配的标签信息的函数,这三个函数会在 parseHTML 中被调用


最后

鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,有重谢

公众号
公众号
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-07-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 神仙朱 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
梯度提升树(GBDT)原理小结
地址:https://www.cnblogs.com/pinard/p/6140514.html
机器学习算法工程师
2018/07/26
8440
梯度提升树(GBDT)原理小结
梯度提升树(GBDT)原理小结
    在集成学习之Adaboost算法原理小结中,我们对Boosting家族的Adaboost算法做了总结,本文就对Boosting家族中另一个重要的算法梯度提升树(Gradient Boosting Decison Tree, 以下简称GBDT)做一个总结。GBDT有很多简称,有GBT(Gradient Boosting Tree), GTB(Gradient Tree Boosting ), GBRT(Gradient Boosting Regression Tree), MART(Multiple Additive Regression Tree),其实都是指的同一种算法,本文统一简称GBDT。GBDT在BAT大厂中也有广泛的应用,假如要选择3个最重要的机器学习算法的话,个人认为GBDT应该占一席之地。
刘建平Pinard
2018/08/14
4960
机器学习(23)之GBDT详解
关键字全网搜索最新排名 【机器学习算法】:排名第一 【机器学习】:排名第一 【Python】:排名第三 【算法】:排名第四 前言 在(机器学习(20)之Adaboost算法原理小结)中,我们对Boosting家族的Adaboost算法做了总结,本文就对Boosting家族中另一个重要的算法梯度提升树(Gradient Boosting Decison Tree, 以下简称GBDT)做一个总结。GBDT有很多简称,有GBT(Gradient Boosting Tree), GTB(Gradient Tree
昱良
2018/04/04
1.5K0
机器学习(23)之GBDT详解
【算法】GBDT算法
小编邀请您,先思考: 1 GBDT算法的原理是什么? 2 GBDT算法如何做正则化处理? 本文对Boosting家族中另一个重要的算法梯度提升树(Gradient Boosting Decison Tree, 以下简称GBDT)做一个总结。GBDT有很多简称,有GBT(Gradient Boosting Tree), GTB(Gradient Tree Boosting ), GBRT(Gradient Boosting Regression Tree), MART(Multiple Additive Re
陆勤_数据人网
2018/03/27
1.3K0
【算法】GBDT算法
从决策树到GBDT梯度提升决策树和XGBoost
决策树可以转换成if-then规则的集合,也可以看作是定义在特征空间划分类的条件概率分布。决策树学习算法包括三部分:特征选择,数的生成和数的剪枝。最大优点: 可以自学习。在学习的过程中,不需要使用者了解过多背景知识,只需要对训练实例进行较好的标注,就能够进行学习。显然,属于有监督学习。 常用有一下三种算法:
大鹅
2021/06/15
1.2K0
从决策树到GBDT梯度提升决策树和XGBoost
GBDT梯度提升树
GBDT的全称是Gradient boosting decision tree,它是通过拟合负梯度Gradient boosting和决策回归树decision tree组合而成,该算法由多颗决策树构成,多颗决策树的结果加起来作为最终结论。让损失函数沿着梯度方向的下降。这个就是GDBT 的 GB的核心。GBDT 每轮迭代的时候,都去拟合损失函数在当前模型下的负梯度。(如果损失函数使用的是平方误差损失函数,则这个损失函数的负梯度就可以用残差来代替,以下所说的残差拟合,便是使用了平方误差损失函数)。
opprash
2019/09/02
1.6K0
机器学习入门:硬核拆解GBDT
Boosting是集成学习的一种基分类器(弱分类器)生成方式,核心思想是通过迭代生成了一系列的学习器,给误差率低的学习器高权重,给误差率高的学习器低权重,结合弱学习器和对应的权重,生成强学习器。
统计学家
2020/07/02
1.1K0
最常用的决策树算法!Random Forest、Adaboost、GBDT 算法
本文主要介绍基于集成学习的决策树,其主要通过不同学习框架生产基学习器,并综合所有基学习器的预测结果来改善单个基学习器的识别率和泛化性。
Datawhale
2019/11/06
1.2K0
AI面试题之GBDT梯度提升树
【Boost】就是让多个弱分类器,通过不同的集成方式,来让多个弱分类器变成一个强分类器。
机器学习炼丹术
2020/07/14
1.4K0
AI面试题之GBDT梯度提升树
GBDT(梯度提升决策树)总结笔记
数据:对于输入数据 $$$x_i \in R^d$$$,训练数据里的第i个样本。 模型:如何对于给定的 $$$x_i$$$预测 $$$\hat{y}_i$$$。
用户1332428
2018/07/30
7940
GBDT(梯度提升决策树)总结笔记
深入理解GBDT回归算法
Boosting、Bagging和Stacking是集成学习(Ensemble Learning)的三种主要方法。Boosting是一族可将弱学习器提升为强学习器的算法,不同于Bagging、Stacking方法,Boosting训练过程为串联方式,弱学习器的训练是有顺序的,每个弱学习器都会在前一个学习器的基础上进行学习,最终综合所有学习器的预测值产生最终的预测结果。
OpenCV学堂
2019/10/30
2.7K0
深入理解GBDT回归算法
GBDT(梯度提升决策树)算法(详细版)
一、前言 通过之前的文章GBDT算法(简明版)对GBDT的过程做了大概的讲解,我们可以了解到GBDT是一种迭代的决策树算法,由多棵决策树组成,所有树的结论累加起来做最终答案。GBDT是一个应用很广泛的算法,可以用于分类,回归和特征选择,特别是用于和其他算法进行模型组成时,如logistic+GBDT,该算法在很多数据上都有不错的效果,GBDT还有其他的名字,如MART,GBRT和Tree Net等。 二、基础知识 2.1 决策树(DT) 决策树这种算法有着很多良好的特性,比如说训练时间复杂度较低,预测的过程
智能算法
2018/04/03
5.5K0
GBDT(梯度提升决策树)算法(详细版)
[白话解析] 通俗解析集成学习之GBDT
本文将为大家讲解GBDT这个机器学习中非常重要的算法。因为这个算法属于若干算法或者若干思想的结合,所以很难找到一个现实世界的通俗例子来讲解,所以只能少用数学公式来尽量减少理解难度。
罗西的思考
2020/09/07
2K0
GBDT算法总结
在上一节中,我们介绍了GBDT的基本思路,但是没有解决损失函数拟合方法的问题。针对这个问题,大牛Freidman提出了用损失函数的负梯度来拟合本轮损失的近似值,进而拟合一个CART回归树。第t轮的第i个样本的损失函数的负梯度表示为
全栈程序员站长
2022/11/04
8030
GBDT算法总结
从决策树到XGBOOST
XGBoost在机器学习领域可谓风光无限,作为从学术界来的模范生,帮助工业界解决了许多实际问题,真可谓:
西西木木
2020/06/07
1.5K0
集成学习综述-从决策树到XGBoost
在之前缅怀金大侠的文章“永远的金大侠-人工智能的江湖”中提到:集成学习是机器学习中一种特殊的存在,自有其深厚而朴实的武功哲学,能化腐朽为神奇,变弱学习为强学习,虽不及武当和少林那样内力与功底深厚。其门下两个主要分支-Bagging和Boosting,各有成就,前者有随机森林支撑门面,后者有AdaBoost,GBDT,XGBoost一脉传承。门下弟子近年来在Kaggle大赛中获奖无数,体现了实用主义的风格,为众多习武之人所喜爱,趋之若鹜。
SIGAI学习与实践平台
2018/12/18
1.1K0
最全!两万字带你完整掌握八大决策树!
决策树是一个非常常见并且优秀的机器学习算法,它易于理解、可解释性强,其可作为分类算法,也可用于回归模型。
AI科技评论
2020/07/08
1.9K0
机器学习 学习笔记(18) 提升树
提升树是以分类树或回归树为基本分类器的提升方法,提升树被认为是统计学习中性能最好的方法之一。
2018/09/04
9320
机器学习 学习笔记(18) 提升树
GBDT与XGBOOST串讲
最近,一直被GBDT和XGBOOST烦恼,产生了如下的问题,由此产生了这篇文章。
张小磊
2020/09/21
6860
GBDT与XGBOOST串讲
如果你还不了解GBDT,不妨看看这篇文章
这是来自读者的一篇投稿,因为公众号对 Latex 公式支持不是很好,所以可以点击文末 “阅读原文“ 进行阅读。同时也希望觉得有帮助的欢迎到作者的 Github 上 star !
kbsc13
2019/08/16
8150
相关推荐
梯度提升树(GBDT)原理小结
更多 >
LV.1
这个人很懒,什么都没有留下~
目录
  • AST
  • Parse
    • 1 stack
    • 2 root
    • 3 currentParent
  • 处理 template
    • 1匹配 < 这个符号
    • 2如果 template 开头是 <
    • 3 如果 template 开头不是 <
  • 处理头标签
  • 处理尾标签
    • 1从 stack 数组中移除这个节点
    • 2重新设置 stack 最后一个节点为父节点
  • 处理文本字符串
    • 1 开始循环 tempalte
    • 2 开始第二次遍历
    • 3 开始第三轮遍历
  • 总结
  • 最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档