从template到ref.render

179 阅读4分钟

vue源码里的compiler+runtim版本,vue.esm.js这份文件里面

如果是vue.runtime.esm.js则没有编译compile的过程方法,并且用下面的方法注册组件是会报错的

现在先注册一个全局组件App1

Vue.component("App",{
  template:'<div>' +
    '<input/>' +
    '<button style="color: blue" @clcik="myClick"><span>{{dataSlot}}</span></button>' +
    '<ul style="list-style: none">Button List<li v-for="item in arr"><button  style="color: red">{{item}}</button></li></ul>' +
    '</div>' +
  '</div>',
    data(){
        return {
            arr:["ARR1","ARR2","ARR3"],
            buttonshow:true,
            dataSlot:"execute myClick"        }
    },
    methods:{
      myclick(){
          console.log("myclick")
      }
    }
    })

   new Vue({
      el: '#app',
      template:"<App/>"
   })

每次执行到Vue.prototype.$mount这里的时候,如果没有render函数且有template,会执行template编译成render函数的过程

     var ref = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines: shouldDecodeNewlines,
        shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this);
      var render = ref.render

compileToFunctions函数里面执行到函数内的方法

var compiled = compile(template, options);

执行到compile函数里面的方法

var compiled = baseCompile(template.trim(), finalOptions);


baseCompile函数主要做了以下的事情

1.解析得到AST
  var ast = parse(template.trim(), options);
2.优化
  if (options.optimize !== false) {
    optimize(ast, options);
  }
3.加上函数
  var code = generate(ast, options);
4.返回
  return {
    ast: ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }

首先看下parse函数

function parse (
  template,
  options
) {
  //parse函数,除去一些判断的方法,其实就这两个主要方法
  parseHTML(template, options)
  return root
}

接着来看一下parseHTML这个方法

 function parseHTML (html, options) {
  //定义栈,用来存放元素
  var stack = [];
  //是否自闭合元素
  var index = 0;
  //目前的元素和是否是最后一个元素  
  var last, lastTag;
  while (html) {
        //这里是判断<这个符号前面有没有文本之类的元素
     var textEnd = html.indexOf('<');
     //第一种情况  
     if (textEnd === 0) { 说明<前面没有内容
        if (comment.test(html)) {
           ...                //匹配comment。<--xxx-->
        }
         if (conditionalComment.test(html)) {
             ...              //匹配这种情况的<![ie>=11>注释        }
        var doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
            ...//匹配<!DOCTYPE XXXX>
        }

        var endTagMatch = html.match(endTag);
        //var endTag = new RegExp(("^<\\/" + qnameCapture + "[^>]*>"));
        if (endTagMatch) { //如果能匹配
             // endTagMatch是个数组,内容如下
            // 比如<div></div>的endTagMatch,
            // 0: "</button>"
            // 1: "button" 
          var curIndex = index;
          advance(endTagMatch[0].length);
          // index在advance里面  index += n; endTagMatch[1],是一个tag   
          parseEndTag(endTagMatch[1], curIndex, index);
          continue
        }
        var startTagMatch = parseStartTag();
        if (startTagMatch) {
            //startTagMatch是个对象
            // attrs: []
            // end: 13
            // start: 5
            // tagName: "input"
            // unarySlash: "/"
          handleStartTag(startTagMatch);
          continue
        }
      }
}
}
}

function advance (n) {
    // 把index完全移动结尾关闭标志的长度,html获取下一个元素
    index += n; 
    // html截取获得n到结尾的内容
    //例如</div><span>xxx</span>,html就是<span>xxx</span>
    html = html.substring(n);
  }

function parseEndTag (tagName, start, end) {
    var pos, lowerCasedTagName;
    if (start == null) { start = index; }
    if (end == null) { end = index; }

    // Find the closest opened tag of the same type
    if (tagName) {
      lowerCasedTagName = tagName.toLowerCase(); //转小写tag
      for (pos = stack.length - 1; pos >= 0; pos--) {//找打最近的同样的tag
        //比如<div><span><a></a></span></div>
        //解析到</a>时候 stack里面是['div','span','a']
        if (stack[pos].lowerCasedTag === lowerCasedTagName) {
          break
        }
      }
    } else {
      // If no tag name is provided, clean shop
        //如果stack里面没有匹配的。pos=0,例如自闭合元素就是找不到匹配的
      pos = 0;
    }

    if (pos >= 0) {
      //如果pos>0,则说明有匹配项
      // Close all the open elements, up the stack
      for (var i = stack.length - 1; i >= pos; i--) {
        if (options.end) {
          //options.end主要是建立元素和前一个元素的父子关系
          options.end(stack[i].tag, start, end);
        }
      }

      // Remove the open elements from the stack
      // 移除最后一个元素
     // Remove the open elements from the stack
      //比如pos是3,就会把数组长度设为3,直接把第三个pop  
      stack.length = pos;
      //获取最后一位
      lastTag = pos && stack[pos - 1].tag;
    } 
    //========
    else if (lowerCasedTagName === 'br') {
      if (options.start) {
        options.start(tagName, [], true, start, end);
      }
    } else if (lowerCasedTagName === 'p') {
      if (options.start) {
        options.start(tagName, [], false, start, end);
      }
      if (options.end) {
        options.end(tagName, start, end);
      }
    }
  }

end: function end (tag, start, end$1) {
      //<div><ul><li><button></button></li></ul></div>  
      //假设tag是button
      //stack里面是 [div,ul,li,button]
      var element = stack[stack.length - 1];
        //此时element是button
      // pop stack
      stack.length -= 1;
       //再获取最后一个元素,li
      currentParent = stack[stack.length - 1];
      closeElement(element);//建立element和currentParent父子关系
    },

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

function parseStartTag () {
    // var startTagOpen = new RegExp(("^<" + qnameCapture));
    var start = html.match(startTagOpen);
    if (start) {
        //start对象
        //0: "<App"
        //1: "App"
        //groups: undefined
        //index: 0
        //input: "<App/>"
        //length: 2
      var match = {
        tagName: start[1],
        attrs: [],
        start: index
      };
          
            }
      advance(start[0].length);

      var end, attr;
      while (!(end = html.match(startTagClose)) 
            && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
        //匹配attr,把attr对象push到上面的match对象
        attr.start = index;
        advance(attr[0].length);
        attr.end = index;
        match.attrs.push(attr);
      }
       //如果 end = html.match(startTagClose),一般是一个数组
       //  0: ">"
           1: ""
        或者
       //  0: "/>"
       //  1: "/"
      if (end) {
        match.unarySlash = end[1]; //这个/是用来确认是否是自闭合元素。例如</input>
        advance(end[0].length);
        match.end = index;
        return match
        //var match = {
        //        tagName:"App",
        //        start:0,
        //        attrs:[]     
        //        unarySlash:"/" ,
        //        end:xx
      }
    }
  }

function handleStartTag (match) {
    var tagName = match.tagName;
    // 确定是否自闭合元素
    var unarySlash = match.unarySlash;
    var unary = isUnaryTag$$1(tagName) || !!unarySlash;
    var l = match.attrs.length;
    var attrs = new Array(l);
    // 处理attrs格式
    for (var i = 0; i < l; i++) {
      var args = match.attrs[i];
      attrs[i] = {
        name: args[1],
        value: decodeAttr(value, shouldDecodeNewlines)
      };
    }
    //如果不是自闭元素,则stack把当前元素对象push进栈
    if (!unary) {
      stack.push({ tag: tagName, 
                   lowerCasedTag: tagName.toLowerCase(), 
                   attrs: attrs, 
                   start: match.start, 
                   end: match.end });
      //设置lastTag为当前元素  
      lastTag = tagName;
    }
    if (options.start) {
      options.start(tagName, attrs, unary, match.start, match.end);
    }
  }

start: function start (tag, attrs, unary, start$1, end) {
      //主要代码如下,1.确定root元素,
      // 2.如果不是自闭合元素,把当前元素入栈
      // 3. 否则,执行closeElement, 建立element和currentParent父子关系
      if (!unary) {
        currentParent = element;
        stack.push(element);
      } else {
        closeElement(element);
      }
    },

// 接着是第二种情况,textEnd >= 0,说明<前面有其他
var text = (void 0), rest = (void 0), next = (void 0);
      if (textEnd >= 0) {
        //把<前面的去掉XXX<div> => <div>
        rest = html.slice(textEnd);
        while (
          !endTag.test(rest) &&
          !startTagOpen.test(rest) &&
          !comment.test(rest) &&
          !conditionalComment.test(rest)
        ) {
           //如果不是开始或结束元素,也不是注释,则把它当文本处理
          // < in plain text, be forgiving and treat it as text
          next = rest.indexOf('<', 1);
          if (next < 0) { break }
          textEnd += next;
          rest = html.slice(textEnd);
        }
        text = html.substring(0, textEnd);
      }

//第三种情况,说明剩下的全是文本
if (textEnd < 0) {
       text = html;
}

// 如果存在文本,则直接把index移到文本的末尾,继续匹配html元素
 if (text) {
   advance(text.length);
}

 if (options.chars && text) {
   options.chars(text, index - text.length, index);
 }

//chart这个函数主要做了以下事情
1、处理文本多空格和换行符的格式
2、定义文本类型,把文本转换为对应的函数,例如遇到{{a}},编译成_s(a)
3、把文本对象插入已有子元素列表
 chars: function chars (text, start, end) {
            var children = currentParent.children;
            //这几个if主要处理了text文本格式
            //文本仅是空格的情况,遇到lineBreak/换行符号的处理,用""还是“ ”代替等分情况处理
            if (inPre || text.trim()) {
                text = isTextTag(currentParent) ? text : decodeHTMLCached(text);
            } else if (!children.length) {
                // remove the whitespace-only node right after an opening tag
                text = '';
            } else if (whitespaceOption) {
                if (whitespaceOption === 'condense') {
                    // in condense mode, remove the whitespace node if it contains
                    // line break, otherwise condense to a single space
                    text = lineBreakRE.test(text) ? '' : ' ';
                } else {
                    text = ' ';
                }
            } else {
                text = preserveWhitespace ? ' ' : '';
            }
            if (text) {
                if (!inPre && whitespaceOption === 'condense') {
                    //多个连续空格转换成单空格
                    // condense consecutive whitespaces into single space
                    text = text.replace(whitespaceRE$1, ' ');
                }
                var res;
                var child;
                if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
                //res = parseText(text, delimiters))主要是看text里面有没有表达式,例如{{}}
                //有的话就把child的type设置成2,
                //如果text不等于空或空格或者该节点的子节点不等于空格或子节点不为空则设置成普通文本,type=3
                    child = {
                        type: 2,
                        expression: res.expression,
                        tokens: res.tokens,
                        text: text
                    };
                } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
                    child = {
                        type: 3,
                        text: text
                    };
                }
                if (child) {
                    //把文本对象push到已有子元素里面
                    children.push(child);
                }
            }
        }

上面的处理逻辑是在lastTag(也就是上一个循环解析的元素)不是script,style,textarea这三个元素

var isPlainTextElement = makeMap('script,style,textarea', true);
if (!lastTag || !isPlainTextElement(lastTag)) //这个判断条件

当html为空时,root就返回给ast。ast就是一个包含父子关系的树

var ast = parse(template.trim(), options);


optimize

if (options.optimize !== false) {
        //这里主要做的是标记静态语法树
        optimize(ast, options);
    }

generate

 var code = generate(ast, options);
 // 这一步是把ast转化成render函数

function generate (
    ast,
    options
) {
    var state = new CodegenState(options);
    var code = ast ? genElement(ast, state) : '_c("div")';
    return {
        render: ("with(this){return " + code + "}"),
        staticRenderFns: state.staticRenderFns
    }
}

 var state = new CodegenState(options);
主要是定义了一些options,
genElement主要是根据ast节点的属性来用不同的方法处理
 if (el.staticRoot && !el.staticProcessed) {
            return genStatic(el, state)
        } else if (el.once && !el.onceProcessed) { //是否是 v-once
            return genOnce(el, state)
        } else if (el.for && !el.forProcessed) { //是否v-for
            return genFor(el, state)
        } else if (el.if && !el.ifProcessed) { //是否v-if
            return genIf(el, state)
        } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
            return genChildren(el, state) || 'void 0'
        } else if (el.tag === 'slot') { //处理插槽
            return genSlot(el, state)
}else{
         var code;
        if (el.component) { //是否为组件
            code = genComponent(el.component, el, state);
        } else {
            var data;
            if (!el.plain || (el.pre && state.maybeComponent(el))) {
                data = genData$2(el, state);
            }

            var children = el.inlineTemplate ? null : genChildren(el, state, true);
            code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
}

function genChildren (
    el,
    state,
    checkSkip,
    altGenElement,
    altGenNode
) {
    var children = el.children;
    if (children.length) {
         ....、、省略代码
        var gen = altGenNode || genNode;
        //map处理children,gen
        return ("[" + (children.map(function (c) { return gen(c, state); }).join(',')) + "]" + (normalizationType$1 ? ("," + normalizationType$1) : ''))
    }
}

var gen = altGenNode || genNode; 
//因为altGenNode为undefined
var gen = genNode; 

关于nodeType的号码代表元素如下
1Element代表元素Element, Text, Comment, ProcessingInstruction, CDATASection, EntityReference2Attr代表属性Text, EntityReference3Text代表元素或属性中的文本内容。None4CDATASection代表文档中的 CDATA 部分(不会由解析器解析的文本)。None5EntityReference代表实体引用。Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference6Entity代表实体。Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference7ProcessingInstruction代表处理指令。None8Comment代表注释。None
function genNode (node, state) {
    if (node.type === 1) {
        return genElement(node, state) //递归调用getElement
    } else if (node.type === 3 && node.isComment) { //node.isComment确定它是comment节点
        return genComment(node) 
    } else {
        return genText(node) //如果是text,则加上_v(node)
    }
}

// 根据不同nodeType和node节点的其他属性,用不同的方法处理,例如
genComponent:return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")")
genComment: return ("_e(" + (JSON.stringify(comment.text)) + ")")
genText:return ("_v(" + (text.type === 2
        ? text.expression // no need for () because already wrapped in _s()
        : transformSpecialNewlines(JSON.stringify(text.text))) + ")")

这样_v,_e,_c都是一些在src/core/instance/render-helpers/index.js定义了的函数,在运行options.render函数的时候会调用
export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

最后返回code,再给code包上一层with函数,使用with语句关联了this对象,
解析时 with代码块的内部把每个变量都认为是this的局部变量;
return {
        render: ("with(this){return " + code + "}"),
        staticRenderFns: state.staticRenderFns
    }



var ref = compileToFunctions(template, {
                outputSourceRange: process.env.NODE_ENV !== 'production',
                shouldDecodeNewlines: shouldDecodeNewlines,
                shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
                delimiters: options.delimiters,
                comments: options.comments
            }, this);
            var render = ref.render;
            var staticRenderFns = ref.staticRenderFns;
            options.render = render; //把上诉过程得到的render函数绑定在options上
//在Vue.prototype._render执行 vnode = render.call(vm._renderProxy, vm.$createElement);
//就是调用这个render函数
//函数类似这个样子
//(function anonymous(){ with(this){return _c('div',[_c('input'),_c('button',{staticStyle:{"color":"blue"},on:{"clcik":myClick}},[_c('span',[_v(_s(dataSlot))])]),_c('ul',{staticStyle:{"list-style":"none"}},[_v("Butt"),_l((arr),function(item){return _c('li',[_c('button',{staticStyle:{"color":"red"}},[_v(_s(item))])])})],2)])}})


本文是个人的阅读代码后的一些见解,如有不妥,请帮忙指出,谢谢各路大神指教。抱拳.png