资讯专栏INFORMATION COLUMN

【Vue原理】Compile - 源码版 之 Parse 属性解析

tinna / 1165人阅读

摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理源码版之属性解析哈哈哈,今天终

写文章不容易,点个赞呗兄弟  


专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
研究基于 Vue版本 【2.5.17】

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

【Vue原理】Compile - 源码版 之 属性解析

哈哈哈,今天终于到了属性解析的部分了,之前已经讲过了 parse 流程,标签解析,最后就只剩下 属性解析了 (´・ᴗ・`)

如果你对 compile 不感兴趣的就先不看把,毕竟不会马上起到什么作用~~ヾ(●´∀`●)

如果你们没看过前面两篇文章的,十分建议看一下~

Compile 之 Parse 主要流程
Compile 之 标签解析

如果看了,你们应该知道《属性解析》在哪部分中,没错,在处理 头标签的 部分 parse-start 中

那么我们就来到 parse - start 这个函数中!

看到下面的源码中,带有 process 的函数都是用于处理 属性的

function parse(template){
    
    parseHTML(template,{        

        start:(...抽出放下面)

    })
}



function start(tag, attrs, unary) {    



    // 创建 AST 节点

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



    // 节点需要解析,并没有还没有处理

    if (!element.processed) {

        processFor(element);
        processIf(element);
        processSlot(element);        



        for (var i = 0; i < transforms.length; i++) {

            element = transforms[i](element, options) || element;
        }

        processAttrs(element);
    }
    
    .... 省略部分不重要代码  



    // 父节点就是上一个节点,直接放入 上一个节点的 children 数组中

    if (currentParent) {      



        // 说明前面节点有 v-if

        if (element.elseif || element.else) {
            processIfConditions(element, currentParent);

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

看完了吧,上面处理属性的函数大概有几个

没啥难的,就是内容多了点

1、processFor,解析 v-for

2、processIf,解析 v-if

3、processSlot,解析 slot

4、processAttrs,解析其他属性

5、transforms,解析样式属性

并且只有 element.processed 为 false 的时候,才会进行解析

因为 element.processed 表示属性已经解析完毕,一开始 element.processed 的值是 undefined

下面就会逐个说明上面的方法

先明确下 element 是什么?

parse 流程中说过了,element 是 通过解析得到的 tag 信息,生成的 ast

下面会逐个分析下上面的四个函数,并会附上相应的 element 例子作为参考

其实还有很多其他处理函数,为了维持文章的长度,所以我去掉了

开篇之前,大家需要先了解 getAndRemoveAttr 这个函数,下面很多地方都会使用到

作用就是从 el.attrList 中查找某个属性,返回返回属性值

function getAndRemoveAttr(el, name, removeFromMap) {  



    var val =el.attrsMap[name];  



    if (removeFromMap) {        

        delete el.attrsMap[name];

    }    



    return val

}
parse-start 中的 tramsforms

在parse -start 这个函数的 开头,我们看到有一个 transfroms 的东西

transforms 是一个数组,存放两个函数,一个是处理 动静态的 class,一个处理 动静态的 style

两种处理都很简单的,我们来简单看看处理结果就好了

处理 class
function transformNode(el, options) {    



    var staticClass = getAndRemoveAttr(el, "class");    



    if (staticClass) {

        el.staticClass = JSON.stringify(staticClass);
    }    



    // :class="b" 直接返回 b
    var classBinding = getBindingAttr(el, "class", false);    



    if (classBinding) {

        el.classBinding = classBinding;
    }
}

{    

    classBinding: "b"

    staticClass: ""a""
    tag: "span"
    type: 1

}
处理 style
function transformNode$1(el, options) {    



    var staticStyle = getAndRemoveAttr(el, "style");    



    if (staticStyle) {        

        // 比如绑定 style="height:0;width:0"

        // parseStyleText 解析得到对象 { height:0,width:0 }
        el.staticStyle = JSON.stringify(parseStyleText(staticStyle));
    }  



    // :style="{height:a}" 解析得 {height:a}
    var styleBinding = getBindingAttr(el, "style", false);    

    if (styleBinding) {

        el.styleBinding = styleBinding;
    }
}

{    

    staticStyle: "{"width":"0"}"

    styleBinding: "{height:a}"
    tag: "span"
    type: 1

}
解析 v-for

在 parse - start 这个函数中,看到了 processFor,没错,就是解析 v-for 指令的!

function processFor(el) {    



    var exp = getAndRemoveAttr(el, "v-for")    



    if (exp) {        

        // 比如指令是 v-for="(item,index) in arr"

        // res = {for: "arr", alias: "item", iterator1: "index"}
        var res = parseFor(exp);        

        if (res) {            

            // 把 res 和 el 属性合并起来

            extend(el, res);
        } 
    }
}

没有什么难度,直接看模板 和最终结果好了

{    

    alias: "item",    

    for: "arr",    

    iterator1: "index",    

    tag: "div",    

    type: 1,

}
解析 v-if

在 parse - start 这个函数中,看到了 processFor,没错,就是解析 v-if 指令的!

function processIf(el) {    



    var exp = getAndRemoveAttr(el, "v-if");    



    if (exp) {


        el.if = exp;
        
        (el.ifConditions || el.ifConditions=[])
        .push({            

            exp: exp,            

            block: el

        })
    } else {        



        if (getAndRemoveAttr(el, "v-else") != null) {

            el.else = true;
        }        



        var elseif = getAndRemoveAttr(el, "v-else-if");        



        if (elseif) {

            el.elseif = elseif;
        }
    }
}

处理 v-if 上是这样的,需要把 v-if 的 表达式 和 节点都保存起来

而 v-else ,只需要设置 el.else 为 true,v-else-if 同样需要保存 表达式

在这里 v-else 和 v-else-if 并没有做太多处理,而是在最前面的 parse-start 中有处理

if (element.elseif || element.else) {
    processIfConditions(element, currentParent);
} 

当经过 processIf 之后,该属性存在 elseif 或 else

那么会调用一个方法,如下

function processIfConditions(el, parent) {    



    var prev = findPrevElement(parent.children);    



    if (prev && prev.if) {    

    
        (prev.ifConditions ||prev.ifConditions=[])
        .push({            
            exp: el.elseif,            

            block: el

        })
    }
} 

这个方法主要是把 带有 v-else-if 和 v-else 的节点挂靠在 带有 v-if 的节点上

先来看挂靠后的结果

{    

    tag: "header",    

    type: 1,    

    children:[{        

        tag: "header",        

        type: 1,        

        if: "a",        

        ifCondition:[

            {exp: "a", block: {header的ast 节点}}
            {exp: "b", block: {strong的ast 节点}}
            {exp: undefined, block: {span的ast节点}}
        ]
    },{        

        tag: "p"

        type: 1
    }]
} 

我们可以看到,原来写的两个子节点,strong 和 span 都不在 div 的children 中

而是跑到了 header 的 ifCondition 里面

现在看看 processIfConditions , 这个方法是只会处理 带有 v-else-if 和 v-else 的节点的

并且需要找到 v-if 的节点挂靠,怎么找的呢?你可以看到一个方法

function findPrevElement(children) {    



    var i = children.length;    



    while (i--) {        

        if (children[i].type === 1) {            

            return children[i]

        } else {
            children.pop();
        }
    }
} 

从同级子节点中结尾开始找,当type ==1 的时候,这个节点就是带有 v-if 的节点

那么 v-else 那两个就可以直接挂靠在上面了

你会问,为什么从结尾不是返回 span 节点,为什么 type ==1 就是带有 v-if?

首先,你并不能从正常解析完的角度去分析,要从标签逐个解析的角度去分析

比如现在已经解析完了 v-if 的节点,并且添加进了 父节点的 children

然后解析下一个节点,比如这个节点是带有 v-else-if 的节点,此时,再去 parent.children 找最后一个节点(也就是刚刚添加进去的 v-if 节点)

肯定返回的是 v-if 的节点,自然能正确挂靠了

v-else 同理

如果你说 v-if 和 v-else-if 隔了一个其他节点,那 v-else-if 就无法挂靠在 v-if 了呢

那你肯定是刁民,v-else-if 必须跟着 v-if 的,否则都会报错,错误就不讨论了

解析 slot

在 parse - start 这个函数中,看到了 processSlot,没错,就是解析 slot 相关

function processSlot(el) {    



    if (el.tag === "slot") {

        el.slotName = el.attrsMap.name



    } else {        

        

        var slotScope = getAndRemoveAttr(el, "slot-scope")

        el.slotScope = slotScope;     

   

        // slot 的名字
        var slotTarget = el.attrsMap.slot        



        if (slotTarget) {

            el.slotTarget = 
                slotTarget === """" 

                ? ""default""

                : slotTarget;

        }
    }
}

这个好像也没什么好讲的,就简单记录一下 解析的结果好了

子组件模板


    
    

解析成

{  
    tag: "span"
    type: 1
    children:[{  
        attrsMap: {name: " header", :a: "num", :b: "num"}
        slotName: "" header""
        tag: "slot"
        type: 1
    }]
}

父组件模板

{{ c }}

解析成

{    

    children: [{        

        tag: "child",        

        type: 1,        

        children: [{            

            slotScope: "c",            

            slotTarget: ""header "",            

            tag: "p",            

            type: 1

        }]
    }],    

    tag: "div",    

    type: 1

}

下面内容很多,但是不难

解析其他属性

这一块内容很多,但是总的来说没有难度,就是看得烦了一些,然后把源码放到了最后,打算先写解析

这里集中处理了剩下的其他类型的属性,大致分了两种情况

1Vue 自带属性

比如 带有 "v-" , ":" , " @" 三种符号的属性名,这三种每种都会分开处理

而在这三种属性开始处理前,会把属性名带有的 modifiers 给提取出来

比如带有 modifiers 的指令

v-bind.a.b.c = "xxxx"

经过处理,会提取出 modifiers 对象,如下

{a: true, b: true, c: true}

以供指令使用

之后就开始处理三种类型属性

1 " : "

我们都知道 " : " 等于 "v-bind" ,所有当匹配到这种属性名的时候,会进入这里的处理

大致看一遍之后,可以看到,经过这部分的处理

属性会存放进 el.props 或者 el.attrs

那么问题来了?

怎么判断属性放入 el.props 还是 el.attrs 呢?

有两种条件

1、modifiers.prop

当你给指令添加了 .prop 的时候,比如

那么 sex 这个属性,就会被存放到 el.props

2、表单

你看到这一句代码

!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)

第一,不能是组件

第二,是表单元素,且是表单重要属性

来看看 platformMustUseProp 吧,很容易

当元素是 input,textarea,option,select,progress

属性是 selected ,checked ,value 等之类的话

都要存放到 el.props 中

function a(tag, type, attr) {    



    return (

        (attr === "value" && "input,textarea,option,select,progress".indexOf(tag)>-1) 
        && type !== "button" 
        || (attr === "selected" && tag === "option") 
        || (attr === "checked" && tag === "input") 
        || (attr === "muted" && tag === "video")
    )
};

或许你会问

el.props 和 el.attrs 有什么区别呢?

props 是直接添加到 dom 属性上的,而不会显示在标签上


attrs 则是用于显示到到 标签属性上的


还有一个问题

添加进 el.props 的属性,为什么要转换成驼峰命名?

你看到的,所有属性名,都会通过一个 camelize 的方法,为什么呢?

因为 DOM 的属性都是驼峰命名的,不存在横杆的命名

所以要把 a-b 的命名都转成 aB,随便截了一张图

然而 innerHTML 比较特殊,驼峰都不行,所以做了特殊处理,你也看到的

驼峰的方法应该挺有用的,放上来吧

var camelize = function(str) {    



    return str.replace(/-(w)/g, function(_, c) { 

        return c ? c.toUpperCase() : ""; 
    })
})

modifiers.sync

之后,你应该还发现了一块宝藏,没错就是 sync

相信你应该用过吧,用于父子通信的,子组件想修改父组件传入的 prop

通过事件的方式,间接修改 父组件的数据,从而更新 props

为了避免大家不记得了,在这里贴一个使用例子

父组件 给 子组件 传入 name ,加入 sync 可以双向修改

子组件想修改 父组件传入的 name,直接触发事件并传入参数就可以了 this.$emit("update:name", 222)

于是现在我们来看他在属性解析时是怎么实现的

addHandler(el, 
    "update:" + camelize(name),
    genAssignmentCode(value, "$event")
);

看看这段代码做了什么

首先

camelize(name)

把名字变成驼峰写法,比如 get-name,转换成 getName

然后下面这段代码 执行

genAssignmentCode(value, "$event")

解析返回 "value = $event"

然后 addHandler 就是把 事件名和事件回调保存到 el.events 中,如下

保存的 events 后面会被继续解析,value 会被包一层 function

相当于给子组件监听事件

@update:name ="function($event){ xxx = $event }"

$event 就是子组件触发事件时 传入的值

xxx 是 父组件的数据,赋值之后,就相当于子组件修改父组件数据了

要是想了解 event 的内部原理,可以看 Event - 源码版 之 绑定组件自定义事件

2 " @ "

当匹配到 @ 或者 v-on 的时候,属于添加事件,这里没有太多处理

addHandler 就是把所有事件保存到 el.events

3 " v- "

剩下 带有 v- 的属性,都会放到这里处理

匹配参数的,源码中注释也说清楚了,这里不解释了

然后统统保存到 el.directives 中

2普通属性

没啥说的,普通属性,直接存放进 el.attrs

下面就是处理其他属性的源码,你别看很长,其实很简单的!

var onRE = /^@|^v-on:/;

var dirRE = /^v-|^@|^:/;

var bindRE = /^:|^v-bind:/;

var modifierRE = /.[^.]+/g;

var argRE = /:(.*)$/;



function processAttrs(el) {    



    var list = el.attrsList;    

    var i, l, name, rawName, value, modifiers, isProp;   



    for (i = 0, l = list.length; i < l; i++) {


        name = rawName = list[i].name;
        value = list[i].value;        



        // 判断属性是否带有 "v-" , "@" , ":"

        if (dirRE.test(name)) {            



            // mark element as dynamic

            el.hasBindings = true;       

     

            // 比如 v-bind.a.b.c = "xxzxxxx"

            // 那么 modifiers = {a: true, b: true, c: true}
            modifiers = parseModifiers(name);            



            // 抽取出纯名字

            if (modifiers) {    

            

                // name = "v-bind.a.b.c = "xxzxxxx" "

                // 那么 name= v-bind
                name = name.replace(modifierRE, "");
            }        

    

            // 收集动态属性,v-bind,可能是绑定的属性,可能是传入子组件的props

            // bindRE = /^:|^v-bind:/
            if (bindRE.test(name)) {   

            

                // 抽取出纯名字,比如 name= v-bind

                // 替换之后,name = bind
                name = name.replace(bindRE, "");
                isProp = false;      

          

                if (modifiers) {   

                 

                    // 直接添加到 dom 的属性上
                    if (modifiers.prop) {
                        isProp = true;    

                    

                        // 变成驼峰命名

                        name = camelize(name);                        

                        if (name === "innerHtml")   

                            name = "innerHTML"; 

                    }      

              

                    // 子组件同步修改
                    if (modifiers.sync) {
                        addHandler(el,      

                            // 得到驼峰命名                      

                            "update:" + camelize(name), 

                            // 得到 "value= $event"

                            genAssignmentCode(value, "$event")
                        );
                    }
                }  

              

                // el.props 的作用上面有说,这里有部分是 表单的必要属性都要保存在 el.props 中
                if (

                     isProp ||

                     // platformMustUseProp 判断这个属性是不是要放在 el.props 中

                     // 比如表单元素 input 等,属性是value selected ,checked 等

                     // 比如 tag=input,name=value,那么value 属性要房子啊 el.props 中

                     (!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name))

                ) {


                    (el.props || (el.props = [])).push({ 
                        name, 
                        value
                    });
                } 

                // 其他属性放在 el.attrs 中

                else {


                    (el.attrs || (el.attrs = [])).push({ 
                        name, 
                        value
                    });
                }
            }            



            // 收集事件,v-on , onRE = /^@|^v-on:/

            else if (onRE.test(name)) {    

            

                // 把 v-on 或者 @ 去掉,拿到真正的 指令名字
                // 比如 name ="@click" , 替换后 name = "click"
                name = name.replace(onRE, "");
                addHandler(el, name, value, modifiers, false);
            }            



            // 收集其他指令,比如 "v-once",

            else { 

                // 把v- 去掉,拿到真正的 指令名字
                name = name.replace(dirRE, "");                



                // name = "bind:key" , argMatch = [":a", "a"]

                var argMatch = name.match(argRE);                

                var arg = argMatch && argMatch[1];    

            

                if (arg) {                    

                    // 比如 name = "bind:key" ,去掉 :key

                    // 然后 name = "bind"
                    name = name.slice(0, -(arg.length + 1));
                }

                (el.directives || (el.directives = [])).push({ 
                    name, 
                    rawName, 
                    value, 
                    arg, 
                    modifiers
                });
            }

        } else {

            (el.attrs || (el.attrs = [])).push({ 
                name, 
                value
            });
        }
    }
}
最后

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/106587.html

相关文章

  • Vue原理Compile - 源码 Parse 标签解析

    摘要:当字符串开头是时,可以匹配匹配尾标签。从结尾,找到所在位置批量闭合。 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧 【Vue原理】Compile - 源码版 之 标签解析...

    loostudy 评论0 收藏0
  • Vue原理Compile - 源码 Parse 主要流程

    写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧 【Vue原理】Compile - 源码版 之 Parse 主要流程 本文难度较繁琐,需要耐心观看,如果你对 compile 源码暂时...

    Forest10 评论0 收藏0
  • Vue原理Compile - 源码 generate 节点数据拼接

    摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理源码版之节点数据拼接上一篇我们 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究...

    fizz 评论0 收藏0
  • Vue原理Compile - 源码 从新建实例到 compile结束的主要流程

    摘要:页面这个实例,按理就需要解析两次,但是有缓存之后就不会理清思路也就是说,其实内核就是不过是经过了两波包装的第一波包装在中的内部函数中内部函数的作用是合并公共和自定义,但是相关代码已经省略,另一个就是执行第二波包装在中,目的是进行缓存 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 ...

    CODING 评论0 收藏0
  • Vue原理Compile - 源码 optimize 标记静态节点

    摘要:一旦我们检测到这些子树,我们可以把它们变成常数,这样我们就不需要了在每次重新渲染时为它们创建新的节点在修补过程中完全跳过它们。否则,吊装费用将会增加好处大于好处,最好总是保持新鲜。 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版难看,...

    Soarkey 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<