1. 程式人生 > >Vue源碼後記-其余內置指令(2)

Vue源碼後記-其余內置指令(2)

model 歸類 cli undefined bject 調用 默認 inpu plain

  ……

  指令這個講起來還有點復雜,先把html弄上來:

    <body>
        <div id=‘app‘>
            <div v-if="vIfIter" v-bind:style="styleObject">
                <input v-show="vShowIter" v-model=‘vModel‘ />
                <span v-once>{{msg}}</span>
                <div v-html="html"
></div> </div> <div class=‘on‘>empty Node</div> </div> </body> <script src=‘./vue.js‘></script> <script> var app = new Vue({ el: #app, data: { vIfIter: true
, vShowIter: true, vModel: 1, styleObject: { color: red }, msg: Hello World, html: <span>v-html</span> }, }); </script>

  上一節是解析完了input標簽的2個屬性,並將其打包進了directives屬性中返回。

  接著會繼續跑genData函數,如下:

    function genData(el) {
        var data = ‘{‘;

        // 從這出來
        var dirs = genDirectives(el);
        if (dirs) {
            data += dirs + ‘,‘;
        }

        // code...

        // DOM props
        if (el.props) {
            data += "domProps:{" + (genProps(el.props)) + "},";
        }
        // event handlers
        if (el.events) {
            data += (genHandlers(el.events, false, warn$3)) + ",";
        }
        
        // code...

        data = data.replace(/,$/, ‘‘) + ‘}‘;
        // v-bind data wrap
        if (el.wrapData) {
            data = el.wrapData(data);
        }
        return data
    }

  由於在處理v-model的時候給el添加了props與events屬性,所以之後會跑進兩個處理函數。

  首先看看genProps:

    // props => {name:value,value:(vModel)}
    function genProps(props) {
        var res = ‘‘;
        for (var i = 0; i < props.length; i++) {
            var prop = props[i];
            res += "\"" + (prop.name) + "\":" + (transformSpecialNewlines(prop.value)) + ",";
        }
        return res.slice(0, -1)
    }

    // 替換新換行符
    function transformSpecialNewlines(text) {
        return text
            .replace(/\u2028/g, ‘\\u2028‘)
            .replace(/\u2029/g, ‘\\u2029‘)
    }

  比較簡單,直接看返回的字符串:技術分享

  

  接下來是處理添加的events:

    // event handlers
    if (el.events) {
        data += (genHandlers(el.events, false, warn$3)) + ",";
    }

    function genHandlers(events, native, warn) {
        var res = native ? ‘nativeOn:{‘ : ‘on:{‘;
        for (var name in events) {
            var handler = events[name];

            // click.right => contextmenu
            // 右鍵並不能觸發click事件 建議使用H5新屬性 然而這個屬性兼容性捉急

            res += "\"" + name + "\":" + (genHandler(name, handler)) + ",";
        }
        return res.slice(0, -1) + ‘}‘;
    }

    // name => inpout
    // handler => {modifiers => null,value:‘if(...)...‘}
    function genHandler(name, handler) {
        if (!handler) {
            return ‘function(){}‘
        }

        if (Array.isArray(handler)) {
            return ("[" + (handler.map(function(handler) {
                return genHandler(name, handler);
            }).join(‘,‘)) + "]")
        }

        // simplePathRE => /^\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[‘.*?‘]|\[".*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*\s*$/;
        // 這是匹配的啥玩意啊!
        var isMethodPath = simplePathRE.test(handler.value);
        // fnExpRE => /^\s*([\w$_]+|\([^)]*?\))\s*=>|^function\s*\(/;
        // 匹配箭頭函數或function
        var isFunctionExpression = fnExpRE.test(handler.value);

        // 匹配完 兩個都是false
        // 包裝成函數形式返回
        if (!handler.modifiers) {
            return isMethodPath || isFunctionExpression ?
                handler.value :
                ("function($event){" + (handler.value) + "}") // inline statement
        }
        // 處理事件後綴
        else {
            // code...
        }
    }

  事件處理方面也很明了,首先判斷是原生事件還是自定義,然後根據表達式的形式進行處理返回,本例會被包裝為一個普通的函數返回,如圖:

  技術分享

  字符串代表DOM上有一個input事件,值為"on:..."

  至此,input標簽AST轉換完畢,返回的code字符串如圖:

    "_c(‘input‘,
    {directives:[{name:"show",rawName:"v-show",value:(vShowIter),expression:"vShowIter"},{name:"model",rawName:"v-model",value:(vModel),expression:"vModel"}],
    domProps:{"value":(vModel)},
    on:{"input":function($event){if($event.target.composing)return;vModel=$event.target.value}}})"

  包含tag名、內置指令v-model/v-show、props、events。

  下面處理span標簽,包含一個v-once:

    <span v-once>{{msg}}</span>

  這個el有個once屬性,會進入genOnce函數:

    function genOnce(el) {
        el.onceProcessed = true;
        // 優先處理v-if
        if (el.if && !el.ifProcessed) {
            return genIf(el)
        } else if (el.staticInFor) {
            // 同時出現v-once與v-for
        } else {
            return genStatic(el)
        }
    }

    // hoist static sub-trees out
    function genStatic(el) {
        el.staticProcessed = true;
        // 該節點屬於靜態dom
        staticRenderFns.push(("with(this){return " + (genElement(el)) + "}"));
        return ("_m(" + (staticRenderFns.length - 1) + (el.staticInFor ? ‘,true‘ : ‘‘) + ")")
    }

    function genElement(el) {
        if (el.staticRoot && !el.staticProcessed) {
            // once/for/if/templte/slot
        } else {
            // component or element
            var code;
            if (el.component) {
                code = genComponent(el.component, el);
            } else {
                var data = el.plain ? undefined : genData(el);

                var children = el.inlineTemplate ? null : genChildren(el, true);
                code = "_c(‘" + (el.tag) + "‘" + (data ? ("," + data) : ‘‘) + (children ? ("," + children) : ‘‘) + ")";
            }
            // module transforms => return code
        }
    }

  由於v-once是一次性渲染,所以會被歸類於靜態渲染,render函數會被彈入staticRenderFns數組。

  這裏還有一個值得註意的是,由於span節點只有v-once,並沒有其余的屬性,沒有必要進入genData進行屬性切割,所以早在parseHTML階段,該el的plain屬性被置為true,跳過genData階段。

    // parseHTML => processRawAttrs
    function processRawAttrs(el) {
        var l = el.attrsList.length;
        if (l) {
            // code...
        } else if (!el.pre) {
            el.plain = true;
        }
    }

  呃……

  沒有屬性處理會genChildren,子節點是一個表達式,會返回一個如圖字符串:技術分享,最開始跑源碼的時候見過_v、_s,不用的是該表達式被彈入了staticRenderFns數組。

  至此,span標簽解析完畢。

  下面開始解析下一個div標簽:

    <div v-html="html"></div>

  該標簽包含一個內置指令,所以會進入genDirectives分支,並調用對應的gen函數處理v-html:

    function html(el, dir) {
        // dir.value => html
        if (dir.value) {
            addProp(el, ‘innerHTML‘, ("_s(" + (dir.value) + ")"));
        }
    }

  代碼很簡單,將v-html對應的值用_s包裹起來,添加到props屬性中,如圖:技術分享

  這裏需要註意的是,該函數並未返回任何值,因為v-html不需要弄進directives屬性,所以默認返回的是undefined,直接會跳過後面條件判斷返回。

  因為設置了props屬性,所以後面的genProps也要跑,過程不看了直接看結果:技術分享

  至此,v-html的標簽解析完了,返回一個render函數:技術分享

  下面解析根元素的下一個子元素,靜態div:

    <div class=‘on‘>empty Node</div>

  調用流程是這樣的:generate => genChildren => genNode => genElement => genData && genChildren

  節點包含一個靜態的類以及字符串子節點,所以會調用最後的genData與genChildren返回一個render字符串,流程比較簡單,所以直接放結果:技術分享

 

  到此為止,所有的AST都被轉化為render函數,可以看一下結構:

    _c(‘div‘ /*<div id=‘app‘>*/ , {
        attrs: {
            "id": "app"
        }
    }, [(vIfIter) /*v-if條件*/ ?
        // 條件為真渲染下面的DOM
        _c(‘div‘ /*<div v-if="vIfIter" v-bind:style="styleObject">*/ , {
            style: (styleObject)
        }, [_c(‘input‘ /*<input v-show="vShowIter" v-model=‘vModel‘ />*/ , {
                directives: [{
                    name: "show",
                    rawName: "v-show",
                    value: (vShowIter),
                    expression: "vShowIter"
                }, {
                    name: "model",
                    rawName: "v-model",
                    value: (vModel),
                    expression: "vModel"
                }],
                domProps: {
                    "value": (vModel)
                },
                on: {
                    "input": function($event) {
                        if ($event.target.composing) return;
                        vModel = $event.target.value
                    }
                }
            }),
            _v(" ") /*這些是回車換行符*/ ,
            _m(0) /*<span v-once>{{msg}}</span>*/ , _v(" "),
            _c(‘div‘ /*<div v-html="html"></div>*/ , {
                domProps: {
                    "innerHTML": _s(html)
                }
            })
        ]) :
        // 否則渲染一個空的div
        _e() /*<div></div>*/ ,
        _v(" "),
        _c(‘div‘ /*<div class=‘on‘>empty Node</div>*/ , {
            staticClass: "on"
        }, [_v("empty Node")])
    ])

  這樣看起來結構就非常清晰了,render函數針對v-if是使用一個三元表示式處理的,該函數的整體框架結構為_c(tagName,attrs,childrens),其中childrens可能嵌套多個_c。

  其中有個小點需要註意的是裏面並沒有v-once的那個span標簽,取而代之的是一個_m(0),這是因為span標簽是靜態節點,所以被彈入staticRenderFns數組並作為第一個元素,這裏將來會將其取出來並渲染。

  這個patch下次再講,最近被項目搞的腦袋疼。

Vue源碼後記-其余內置指令(2)