Vue Compile原理分析

Vue中Compile是一个非常复杂的内容,Compile的主要作用是解析模板,生成渲染模板的render, 而render的作用主要是为了生成VNode, Compile主要分为3大块:

  1. parse 接受template原始模板,按着模板的节点和数据生成对应的ast
  2. optimize 遍历ast的每一个节点,标记静态节点,这样就知道哪部分不会变化,于是在页面需要更新时,减少去对比这部分DOM,提升性能
  3. generate 把前两步生成完善的ast,组成render字符串,然后将render字符串通过new Function的方式转换成渲染函数

1. Compile从新建实例到Compile结束的主要流程

Vue编译template, 生成render发生在$mount这个函数中

Vue.prototype.$mount = function(el) {
    var options = this.$options;
    if (!options.render) {
        // 获取template模板
        var tpl = options.template;
        if (tpl) {
            tpl = document.querySelector(tpl).innerHTML;
        }
        if (tpl) {
            var ref = compileToFunctions(tpl, {}, this);
            options.render = ref.render;
            options.staticRenderFns = ref.staticRenderFns;
        }
    }
    // 执行上面生成的render, 生成DOM,挂载DOM
    return mount.call(this, el);
}

compileToFunctions的生成流程

// compileToFunctions是通过createCompiler执行返回的
var ref$1 = createCompiler();
var { compileToFunctions } = ref$1;

createCompiler的生成流程

// createCompiler 是通过 createCompilerCreator 生成的
// createCompilerCreator 会传入一个 baseCompile 的函数
var createCompiler = createCompilerCreator(
    function baseCompile(template, options) {
        var ast = parse(template.trim(), options);
        if (options.optimize !== false) {
            optimize(ast, options);
        }
        var code = generate(ast, options);
        return {
            ast,
            render: code.render,
            staticRenderFns: code.staticRenderFns
        }
    }
);

function createCompilerCreator (baseCompile) {
    return function () {
        // 作用是合并选项,并且调用 baseCompile
        function compile(template) {
            var compiled = baseCompile(template);
            return compiled;
        }

        return {
            compile,
            // compileToFunctions 用来生成 render 和 staticRenderFns
            // compileToFunctions 其内核就是 baseCompile
            compileToFunctions: createCompileToFunctionFn(compile)
        }
    }
}

function createCompileToFunctionFn (compile) {
    // 作为缓存,防止每次都重新编译
    // template 字符串 作为 key
    // 值为 render 和 staticRenderFns
    var cache = Object.create(null);
    return function compileToFunctions(template, options, vm) {
        var key = template;

        // 有缓存的时候直接取出缓存中的结果即可
        if (cache[key]) return cache[key];
        // compile 是 createCompilerCreator 传入的 compile
        var compiled = compile(template, options);
        var res = {
            // 将字符串render解析成函数
            render: new Function(compiled.render),
            staticRenderFns: compiled.staticRenderFns.map(function(code) {
                return new Function(code, fnGenErrors);
            })
        };
        cache[key] = res;
        return cache[key];
    }
}

2. parse主要流程

parse的主要作用就是将template字符串转化为ast,ast为抽象语法树,是一种以树形结果来表示模板语法结构,比如:

{
    tag: 'div',
    type: 1, // 1->节点; 2->表达式,比如{{isShow}}; 3->纯文本
    children: [{
        type: 3,
        text: '11'
    }]
}

parse整个流程非常复杂,需要一步步深入

// pase接收template字符串
function parse(template) {
    // 缓存模板中解析的每个节点的ast
    // 是一个数组存放模板中按顺序从头到尾每个标签的AST
    // 主要用来理清节点父子关系
    var stack = [];
    // 根节点,是ast
    var root;
    // 当前解析标签的父节点
    // 这样才知道当前解析节点的父节点是谁,才把这个节点添加给响应节点的children
    // 根节点没有父节点,所以是undefined
    var currentParent;

    // parseHTML 负责 template 中的标签匹配,再传入 start, end, chars 等方法
    parseHTML(template, {
        start, // 处理头标签
        end, // 处理尾标签
        chars // 处理文本
    });

    return root;
}

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);
        }
    }
}

处理头标签, 当parseHTML匹配到一个首标签,都会把该标签的信息传递给start

function start(tag, attrs, unary) {
    // 创建AST节点
    var element = createASTElement(tag, attrs, currentParent);

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

    // 处理父子关系
    if (currentParent) {
        currentParent.children.push(element);
        element.parent = currentParent;
    }

    // 不是单标签(input, img), 都需要保存到stack
    if (!unary) {
        currentParent = element;
        stack.push(element);
    }
}

function createASTElement(tag, attrs, parent) {
    // 创建一个AST结构,保存数据
    /*
        模板上的属性经过parseHTML解析成一个数组
        [{ name: 'hoho', value: '333'},{ name: 'href', value: '444' }]
        makeAttrMap可以将其转化成如下结构
        { hoho: '333', href: '444' }
    */
    return {
        type: 1,
        tag,
        attrsList: attrs,
        attrsMap: makeAttrsMap(attrs),
        parent,
        children: []
    }
}

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

function end() {
    // 标签解析结束,移除该标签
    /*
        比如有如下一段html:
        <div>
            <section></section>
            <p></p>
        </div>
        stack匹配到两个头标签之后 stack=['div', 'section']
        然后匹配到</section>, 则移除stack中的section, 并且重设 currentParent
        stack=['div']
        currentParent='div'
        再匹配到</p>, p的父节点就是div了,父子顺序就正确了
    */
    stack.length -= 1;
    currentParent = stack[stack.length - 1];
}

处理文本字符串,当parseHTML去匹配<的时候,发现template开头到<还有一段距离,那么这段距离就是文本了,这段文本会给chars方法处理

// chars的主要作用就是为父节点添加文本子节点
// 文本子节点有两种类型
// 1. 普通型,直接存为文本子节点
// 2. 表达式型,需要经过parseText处理
/*
    比如表达式{{isShow}}会被解析成
    {
        expression: toString(isShow),
        tokens: [{@binding: "isShow"}]
    }
    其主要是为了把表达式isShow拿到,方便从实例上获取
*/
function chars(text) {
    // 必须存在根节点,不能用文字开头
    if (!currentParent) return;
    var children = currentParent.children;

    // 通过parseText解析成字符串,判断是否含有双括号表达式,比如{{isShow}}
    // 如果含有表达式,会存放多一点信息
    var res = parseText(text);
    if (res) {
        children.push({
            type: 2,
            expression: res.expression,
            tokens: res.tokens,
            text,
        });
    }
    // 普通字符串,直接存在字符串节点
    else if (!children.length || children[children.length - 1].text !== ' ) {
        children.push({
            type: 3,
            text,
        });
    }
}

下面来看一个完整的Parse流程

<div>11</div>
  1. 开始循环template,匹配到第一个头标签div, 传入start, 生成对应的AST,该div的ast变成根节点root,并设置其为当前父节点currentParent, 保存节点存储数组stack
stack = [{ tag: 'div', children: [] }]

第一轮处理结束,template截断到第一次匹配到的位置11</div>

  1. 开始第二次遍历,开始匹配到<, 发现<不在开头,从开头位置到<有一段普通字符串,调用chars,传入字符串,发现其没有双括号等表达式,直接给父节点添加简单子节点
currentParent.children.push({ type: 3, text: '11' });

此时

stack = [{ tag: 'div', children: [{ type: 3, text: '11' }] }]

第二轮结束,template截断到刚刚匹配完的字符串,此时template=</div>

  1. 开始第三轮遍历,继续寻找<, 发现就在开头,但是这是一个结束标签,标签名是div。这个标签匹配完毕,也会从stack中移除,第3次遍历结束,template继续阶段,此时template为空,遍历结束。
{
    tag: 'div',
    type: 1,
    children: [{ type: 3, text: '11' }]
}

3. 标签解析

上一小节讲到了是通过parseHTML这个方法来对template进行循环遍历的。就是不断的将模板字符串匹配然后截断,直到字符串为空,其中和阶段有关的一个重要函数就是advance

// 截断模板字符串,并保存当前的位置
function advance(n) {
    index += n;
    html = html.substring(n);
}
  • 如果 < 在template开头
    如果是尾标签的<, 那么交给parseEndTag处理
    如果是头标签的>, 那么交给parseStartTag处理

  • 如果 < 不在template开头,那么表明开头到 < 的这段位置是字符串,也需要用到advance去截断字符串。

template = '<div>111</div>';
parseHTML(template);

parseStartTag作用是处理头标签

  1. 把头标签的所有信息及合起来,包括属性,标签名
  2. 匹配完成之后,同样调用advance去截断template
  3. 把标签信息返回
function parseStartTag() {
    // html ='<div name=1>111</div>'
    // start = ["<div", "div", index: 0]
    var start = html.match(startTagOpen);
    if (start) {
        // 存储本次头标签的信息
        var match = {
            tagName: start[1],
            attrs: [],
            start: index
        }
    }

    // start[0]是<div
    // 阶段之后template="name=1 >111</div>"
    advance(start[0].length);

    var end, attr;
    // 循环匹配属性内容,保存属性列表
    // 直到template遍历到头标签的 >
    while (
        // 匹配不到头标签的 > , 开始匹配 属性内容
        !(end = html.match(startTagClose))
        // 开始匹配属性内容
        // attr = ["name=1", "name", "="]
        && (attr = html.match(attribute))
    ) {
        advance(attr[0].length);
        match.attrs.push(attr);
    }

    // 匹配到起始标签的 > , 标签属性那些已经匹配完毕了
    // 返回收集到的标签信息
    if (end) {
        advance(end[0].length);
        // 如果是单标签,那么 unarySlash 的值是 / , 比如 <input />
        match.unarySlash = end[1];
        match.end = index;
        return match;
    }
}

经过以上的parseStartTag会处理返回如下内容

// <div name=1></div>
{
    tagName: 'div',
    // 头标签中的属性信息
    attrs: [
        [" name=1", "name", "=", undefined, undefined, "1"]
    ],
    unarySlash: "", // 表示这个表示是否是单标签
    start: 0, // 头标签的 < 在template中的位置
    end: 12 // 头标签的 > 在template中的位置
}

通过parseStartTag返回的头信息,最后传给了handleStartTag

function handleStartTag(match) {
    var tagName = match.tagName;
    var unarySlash = match.unarySlash;
    // 判断是不是单标签,input, img这些
    var unary = isUnaryTag$$1(tagName) || !!unarySlash;
    var l = match.attrs.length;
    var attrs = new Array(l);

    // 把属性数组转化成对象
    for (let i = 0; i < l; i++) {
        var args = match.attrs[i];
        var value = args[3] || args[4] || args[5] || '';
        attrs[i] = {
            name: args[1],
            value
        }
    }

    // 不是单标签才存到stack
    if (!unary) {
        stack.push({
            tag: tagName,
            attrs
        });
    }

    if (options.start) {
        options.start(
            tagName, attrs, unary,
            match.start, match.end
        );
    }
}

当使用endTag这个正则成功匹配到尾标签时,会调用parseEndTag

function parseEndTag(tagName, start, end) {
    var pos, lowerCasedTagName;
    if (tagName) {
        for (pos = stack.length - 1; pos >= 0; pos--) {
            if (stack[pos].tagName === tagName) break;
        }
    }
    else {
        // 如果没有提供标签签名,那么关闭所有存在stack中的起始标签
        pos = 0;
    }

    // 批量 stack pop 位置后的所有标签
    if (pos >= 0) {
        // 关闭 pos 位置之后所有的起始标签,避免有些标签没有尾标签
        // 比如stack.length = 7, pos = 5, 那么就关闭最后两个
        for (var i = stack.length - 1; i >= pos; i--) {
            if (options.end) {
                options.end(stack[i].tag, start, end);
            }
        }
        // 匹配完闭合标签之后,就把匹配的标签头给移除了
        stack.length = pos;
    }
}

/*
    简要流程如下
    <div>
        <header>
            <span></span>
        </header>
    </div>
    一开始会匹配到3个头标签 stack = [div, header, span]
    然后开始匹配到span, 然后去stack末尾去找span
    确定span在stack中的位置pos后,批量闭合stack的pos之后所有的标签
*/

4.属性解析

处理class分为两种,一种静态的class, 一种动态的class

function transformNode(el, options) {
    var staticClass = getAndRemoveAttr(el, 'class');
    if (staticClass) {
        el.staticClass = JSON.stringify(staticClass);
    }
    // 处理动态class
    var classBinding = getBindingAttr(el, 'class', false);
    if (classBinding) {
        el.classBinding = classBinding;
    }
}

/*
    <span class="a" :class="b"></span>
    转化为下面的
    {    
        classBinding: "b"
        staticClass: ""a""
        tag: "span"
        type: 1
    }
*/

处理style几乎和class一模一样

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;
    }
}

/*
    <span style="width:0" :style="{height: a}"></span>
    转化成下面的
    {
        staticStyle: "{"width":"0"}"
        styleBinding: "{height:a}"
        tag: "span"
        type: 1
    }
*/

解析v-for用的是processFor

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);
        }
    }
}

/*
    <div v-for="(item, index) in arr"></div>
    可以转化为
    {
        alias: "item",    
        for: "arr",    
        iterator1: "index",    
        tag: "div",    
        type: 1,
    }
*/

解析v-if用的是processIf

function processIf(el) {
    var exp = getAndRemoveAttr(el, 'v-if');
    if (exp) {
        el.if = exp;
        (el.ifConditions || el.ifConditions = []).push({
            exp,
            block: el
        })
    } else {
        // 对于 v-else 和 v-else-if 没有做太多的处理
        // 这二者会调用之后的 processIfConditions
        if (getAndRemoveAttr(el, 'v-else') !== null) {
            el.else = true;
        }
        var elseif = getAndRemoveAttr(el, 'v-else-if');
        if (el.elseif) {
            el.elseif = elseif;
        }
    }
}

function processIfConditions(el, parent) {
    var prev = findPrevElement(parent.children);
    if (prev && prev.if) {
        (prev.ifConditions || prev.ifConditions = []).push({
            exp: el.elseif,
            block: el
        });
    }
}

/*
    <div>
        <p></p>
        <div v-if="a"></div>
        <strong v-else-if="b"></strong>
        <span v-else></span>
    </div>
    解析之后生成如下
    {
        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
        }]
    }
*/

slot的解析是通过processSlot来解析的

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
        }
    }
}

/*
    <span>
        <slot name="header" :a="num" :b="num"></slot>
    </span>
    以上模板解析成
    {
        {  
        tag: "span"
        type: 1
        children:[{  
            attrsMap: {name: " header", :a: "num", :b: "num"}
            slotName: "" header""
            tag: "slot"
            type: 1
        }]
    }

    父组件模板
    <div>
        <child >
            <p slot="header" slot-scope="c"> {{ c }}</p>
        </child>
    </div>
    解析成
    {    
        children: [{        
            tag: "child",        
            type: 1,        
            children: [{            
                slotScope: "c",            
                slotTarget: ""header "",            
                tag: "p",            
                type: 1
            }]
        }],    
        tag: "div",    
        type: 1
    }
*/

Vue自带属性v-, :, @三种符号的属性名,会分开处理

先来看:的情况,经过Vue的处理,:开头的属性会被放入el.props或者el.attrs中。

当我们给指令添加了.prop的时候

<!-- 这个属性会被存放到el.props中 -->
<div :name.props="myName"></div>

props是直接添加到DOM属性上的,attrs是直接显示在标签上的。添加props的时候,需要转化成驼峰法,因为DOM元素的props不支持-连接的。

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

<div @click="aaa" @keyup="bbb"></div>
<!-- 
    {
        events: {
            click: { value: 'aaa' },
            keyup: { value: 'bbb' }
        }
    }
 -->

v-开头的会全部保存到el.directives

<div v-a:key="bbb"></div>
<!-- 
    {
        directives: [{
            arg: "key",
            modifiers: undefined,
            name: "a",
            rawName: "v-a:key",
            value: "bbb"
        }]
    }
 -->

普通属性直接存放进el.attrs

<div bbb="ccc"></div>
<!-- 
    {
        attrs: [{
            name: 'bbb',
            value: 'ccc'
        }]
    }
 -->

总体源码如下

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
            });
        }
    }
}

optimize是Compile的三大步骤之一,是一个性能优化的手段

// ... parse
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
    optimize(ast, options);
}
// ...generate

它能遍历AST子树,检测纯静态的子树,即永不需要更改的DOM,Vue内部进行Optimize的方法就是给节点加上static属性。

function optimize(root, options) {
    if (!root) return;
    makeStatic$1(root);
    makeStaticRoots(root);
}

先来看看Vue是如何判断static节点的

function isStatic(node) {
    // 文字表达式
    if (node.type === 2) return false;
    // 纯文本
    if (node.type === 3) return true;
    return node.pre || ( // 如果添加了v-pre指令,表明节点不需要解析了
        !node.hasBindings && // 不能存在指令,事件等
        !node.if && // 不能存在v-if
        !node.for && // 不能存在v-for
        !['slot', 'component'].indexOf(node.tag) > -1 && // 节点名称不能是slot和component
        isPlatformReserverdTag(node.tag) && // 需要时正常的HTML标签
        !isDirectChildOfTemplateFor(node) && // 父辈节点不能是template或者带有v-for
        Object.keys(node).every(isStaticKey) // 该节点所有的属性都需要是静态节点的静态属性
    );
}

上面提到的makeStatic$1方法,主要用来标记节点是否是静态节点

// 标记节点是否是静态节点

function markStatic$1(node) {
    node.static = isStatic(node);    
    if (node.type !== 1) return
    // 不要将组件插槽内容设置为静态。
    // 这就避免了
    // 1、组件无法更改插槽节点
    // 2、静态插槽内容无法热加载
    if (        
        // 正常 thml 标签 才往下处理,组件之类的就不可以
        !isPlatformReservedTag(node.tag) &&
        // 标签名是 slot 才往下处理
        node.tag !== 'slot' &&
        // 有 inline-tempalte 才往下处理
        node.attrsMap['inline-template'] == null
    ) {
        return
    }
    // 遍历所有孩子,如果孩子 不是静态节点,那么父亲也不是静态节点
    var l = node.children.length
    for (var i = 0;i < l; i++) {        
        var child = node.children[i];  
        // 递归设置子节点,子节点再调用子节点
        markStatic$1(child);        
        if (!child.static) {
            node.static = false;
        }
    }
    if (node.ifConditions) {    
        var c = node.ifConditions.length  
        for (var j = 1; j < c; j++) {    
            // block 是 节点的 ast
            var block = node.ifConditions[j].block;
            markStatic$1(block);  
            if (!block.static) {
                node.static = false;
            }
        }
    }
}

第二步就是标记静态根节点

// 标记根节点是否是静态节点

function markStaticRoots(node) {    
    if (node.type === 1) return
    // 要使一个节点符合静态根的条件,它应该有这样的子节点
    // 不仅仅是静态文本。否则,吊装费用将会增加
    // 好处大于坏处,最好总是保持新鲜。
    if (        
        // 静态节点
        node.static &&        
        // 有孩子
        node.children.length &&        
        // 孩子有很多,或者第一个孩子不是纯文本
        ! (node.children.length === 1 && node.children[0].type === 3)
    ) {
        node.staticRoot = true;        
        return
    }
    else {
        node.staticRoot = false;
    }    
    if (node.children) {    
        var l = node.children.length    
        for (var i = 0; i < l; i++) {
            markStaticRoots(
                node.children[i]
            );
        }
    }
}

markStatic$1 这个函数只是为 markStaticRoots 服务的,是为了先把每个节点都处理之后,更加方便快捷静态根节点。

被判断为静态根节点的条件

  1. 该节点所有的子孙节点都是静态节点
  2. 必须存在子节点
  3. 子节点不能是纯文本节点。Vue不会将这种节点标记为静态节点,如果将这种节点也标记为静态节点,会起到负优化的作用,下面讨论为什么给纯文本节点标记为静态节点,是一种负优化

首先标记为静态节点需要维护静态模板存储对象,这个信息存储在_staticTrees中。随着静态根节点的增加,这个存储对象会越来越大,那么占用的内存也会越来越多,势必要增加一些不必要的存储

其实这个问题涉及到 render 和 静态 render 的合作,

<div>
    <span>
        <strong>我是静态文本</strong>
    </span>
    <span v-if="testStaticRender"></span>
</div>

生成的render函数是这样的

with(this) {
    return _c('div', [
        // 这个函数就是去获取静态模板的,这样会产生很多额外的调用
        _m(0),
        (testStaticRender ? _c('span') : _e())
    ])
}

genarate

generate的作用是根据生成的AST节点,拼接成字符串,而这个字符串可以被转化为函数,函数执行后,就会生成VNode

// options用来传入一些判断函数或者指令
function generate(ast, options) {
    // CodegenState 给实例初始化编译状态
    var state = new CodegenState(options);
    // genElement 将AST转化为字符串
    var code = ast ? genElement(ast, state) : '_c("div")';
    return {
        render: "with(this){ return " + code + "}",
        staticRenderFns: state.staticRenderFns
    }
}

function CodegenState(options) {
    this.options = options;      
    // class$1 用于处理AST中的class
    // style$1 用于处理AST中的style
    this.dataGenFns = [ class$1.genData, style$1.genData];      
    this.directives = { on , bind, cloak, model,text ,html]
    // 用来存放静态根节点的render函数
    this.staticRenderFns = [];
}

genElement是AST拼接成字符串的重点函数,主要是处理各种节点,并且拼接起来

// 这个里面主要是一个个的处理函数
function genElement(el, state) {    
    if (
        el.staticRoot && !el.staticProcessed
    ) {
        // 拼接静态节点
        return genStatic(el, state)
    }    
    else if (
        el.for && !el.forProcessed
    ) {        
        return genFor(el, state)
    }    
    else if (
        el.if && !el.ifProcessed
    ) {        
        return genIf(el, state)
    }    
    else if (el.tag === 'slot') {        
        return genSlot(el, state)
    }    
    else {    
        var code;  
        // 处理 is 绑定的组件
        if (el.component) {
            code = genComponent(el.component, el, state);
        }    
        // 上面所有的解析完之后,会走到这一步
        else {  
            // 当 el 不存在属性的时候,el.plain = true
            var data = el.plain ? undefined : genData$2(el, state);  
            // 处理完父节点,遍历处理所有子节点
            var children = genChildren(el, state);
            code = `_c(
                '${el.tag}'
                ${data ?  ("," + data) : ''} 
                ${children ? ("," + children) : ''}
            )`
        }      
        return code
    }
}

拼接静态节点

function genStatic(el, state) {
    el.staticProcessed = true;
    state.staticRenderFns.push(
        "with(this){ return " + genElement(el, state) + "}"
    );
    return `_m(${
        state.staticRenderFns.length - 1
    })`;
}

拼接v-if节点

// el.ifCondition 是用来存放条件数组的
function genIf(el, state) {
    el.isProcessed = true;
    return genIfConditions(
        el.ifConditions.slice(),
        state
    );
}

/*
    <div>
        <p v-if="isShow"></p>
        <span v-else-if="isShow == 2"></span>
        <section v-else></section>
    </div>
    会编译成如下
    {    
        tag:"div",    
        children:[{        
            tag:"p",        
            ifCondition:[{            
                exp: "isShow",            
                block: {..p 的 ast 节点}
            },{            
                exp: "isShow==2",            
                block: {..span 的 ast 节点}
            },{            
                exp: undefined,            
                block: {..section 的 ast 节点}
            }]
        }]
    }
*/

7. 事件拼接

function genData$2(el, state) {
    var data = '{';
    // 组件自定义事件,比如`<div @click="a"></div>`
    if (el.events) {
        data += genHandlers(el.events, false) + ',';
    }
    // 原生DOM事件,比如 `@click.native`
    if (el.nativeEvents) {
        data += genHandlers(el.nativeEvents, true) + ',';
    }
    data = data.replace(/,$/, '') + '}';
    return data;
}

从上面的函数可以知道,不管是组件自定义事件还是原生DOM事件,都是调用的genHandlers

function genHandlers(events, isNative) {
    var res = isNative ? 'nativeOn:{' : 'on:{';
    var handler = events[name];
    for (var name in events) {
        res += ` ${name}:${genHandler(name, handler)}, `
    }
    return res.slice(0, -1) + '}';
}

修饰符内部配置

var modifierCode = {
    stop: '$event.stopPropagation();',
    prevent: '$event.preventDefault();',
    ctrl: genGuard("!$event.ctrlKey"),
    shift: genGuard("!$event.shiftKey"),   
    alt: genGuard("!$event.altKey"),
    meta: genGuard("!$event.metaKey"),
    self: genGuard("$event.target !== $event.currentTarget"),
    left: genGuard("'button' in $event && $event.button !== 0"),
    middle: genGuard("'button' in $event && $event.button !== 1"),
    right: genGuard("'button' in $event && $event.button !== 2")
};
var genGuard = function(condition) {    
    return ` if ( ${ condition } ) return null `
};

/*
    比如添加了stop修饰符的,会这么拼接
    "function($event ){ " + 
        "$event.stopPropagation();" + 
        " return "+ aaa +"($event);" +
    "}"
*/

键盘修饰符

// keys是一个数组,保存的是添加的修饰符,可以是数字,可以是键名
function genKeyFilter(keys) {
    var key = keys.map(genFilterCode).join('&&');
    return `if( !('button' in $event) && ${ key } )
        return null `;
}

function genFilterCode(key) {
    var keyVal = parseInt(key);
    // 如果key是数字,那直接返回字符串
    if (keyVal) {
        return "$event.keyCode!==" + keyVal
    }
    // 如果key是键名,比如`enter`
    // 这个键名可能不在keyCodes, keyNames中,可以支持自定义
    var keyCode = keyCodes[key]; // 获取键值,keyName="Enter"
    var keyName = keyNames[key]; // 获取键名,keyCode=13

    // $event.keyCode 是按下的键的值
    // $event.key 是按下键的名
    // 比如我们按下字母`V`, 那此时的keyCode是86, key是'v'
    return `
        _k(
            $event.keyCode , 
            ${ key } , ${ keyCode }, 
            ${ $event.key } , ${ keyName }
        )
    `
}

// 返回的这个_k本体其实就是`checkKeyCodes`函数
function checkKeyCodes(
    eventKeyCode, key, keyCode, 
    eventKeyName, keyName
) {    
    // 比如 key 传入的是自定义名字  aaaa
    // keyCode 从Vue 定义的 keyNames 获取 aaaa 的实际数字
    // keyName 从 Vue 定义的 keyCode 获取 aaaa 的别名
    // 并且以用户定义的为准,可以覆盖Vue 内部定义的
    var mappedKeyCode = config.keyCodes[key] || keyCode;    
    // 该键只在 Vue 内部定义的 keyCode 中 
    if (keyName && eventKeyName && !config.keyCodes[key]) {        
        return isKeyNotMatch(keyName, eventKeyName)
    }
    // 该键只在 用户自定义配置的 keyCode 中
    else if (mappedKeyCode) {        
        return isKeyNotMatch(mappedKeyCode, eventKeyCode)
    }
    // 原始键名
    else if (eventKeyName) {        
        return hyphenate(eventKeyName) !== key
    }
}

核心的genHandler方法

function genHandler(name, handler) {
    // 没有绑定回调,返回一个空函数
    if (!handler) {
        return 'function(){}';
    }
    // 如果绑定的是数组,则逐个递归一遍
    if (Array.isArray(handler)) {
        return "[" + handler.map(handler => {
            return genHandler(name, handler);
        }).join(",") + "]";
    }
    // 开始解析单个回调
    var isMethodPath = simplePathRE.test(handler.value);    
    var isFunctionExpression = fnExpRE.test(handler.value);

    // 没有modifier
    if (!handler.modifiers) {
        if (isMethodPath || isFunctionExpression) {
            return handler.value;
        }
        // 内连语句,需要包裹一层
        return "function($event){" + handler.value + ";}";
    }
    else {
        var code = "";
        var genModifierCode = ""; // 保存内部修饰符
        for (var key in handler.modifier) {
            if (modifierCode[key]) {
                
            }
            // 精确修饰符
            else if (key === 'exact') {

            }
            // 普通按键
            else {
                keys.push(key);
            }
        }
    }

    // 开始拼接事件回调
    // 拼接Vue定义外的按键修饰符
    if (keys.length) {
        code += genKeyFilter(keys);
    }
    // 把prevent和stop这样的修饰符在按键过滤之后执行
    if (genModifierCode) {
        code += genModifierCode;
    }
    // 事件主体回调
    var handlerCode = isMethodPath ?
        // 执行你绑定的函数
        "return " + handler.value + "($event)" :
        (
            isFunctionExpression
            ? "return " + handler.value + "$event"
            : handler.value
        );
    return `function($event){
        ${code + handlerCode}
    }`
}

拼接事件回调的3个重点

  1. 拼接按键的修饰符
  2. 拼接内置修饰符
  3. 拼接事件回调
  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值