深度学习Vue源码-模板编译原理
前言本文主要是手写Vue2.0源码-模板编译原理上一篇主要介绍了Vue数据的响应式原理。对于中高级前端,响应式原则基本就是Vue面试必考的源码基础类。如果你不是很清楚基本上就通过了。今天我们手写的模板编译原理也是Vue面试的常点,复杂度比响应式原理要高。主要涉及AST和大量的正则匹配。学完了可以一起看看思维导图。手写,加深印象。适用人群:没有时间看官方源码或者看源码一头雾水不想看的同学//Vue实例化newVue({el:"#app",data(){return{a:111,};},//render(h){//returnh('div',{id:'a'},'hello')//},//template:`你好
`});上面的代码大家都很熟悉了。根据官网给出的生命周期图,我们可以在options选项中手动配置模板或者渲染。注1:在平时的开发中,我们使用的最重要的是没有编译版本的Vue版本(runtime-only)。在options中直接传入template选项,在开发环境报错。注意2:此处传入的模板选项不要与.vue文件中的
模板混淆。文件组件的模板需要经过vue-loader处理。我们传入的el或者template选项最后会被解析成render函数,从而保持模板解析的一致性。1.模板编译入口//src/init.jsimport{initState}from"./state";import{compileToFunctions}from"./compiler/index";exportfunctioninitMixin(Vue){Vue.prototype._init=function(选项){constvm=这个;//这里的this表示调用_init方法的对象(实例对象)//this.$options是用户newVue时传入的属性vm.$options=options;//初始化状态initState(vm);//如果模板渲染有el属性if(vm.$options.el){vm.$mount(vm.$options.el);}};//这段代码在源码中的位置其实是放在entry-runtime-with-compiler.js中//代表Vue源码中包含compile函数,需要和runtime-only版本的Vue区分开来.prototype.$mount=function(el){constvm=this;constoptions=vm.$options;el=document.querySelector(el);//如果没有渲染属性if(!options.render){//如果有模板属性lettemplate=options.template;if(!template&&el){//如果没有render和template但存在el属性,直接将template赋给el所在的外层HTML结构(即el本身不是父元素)template=el.outerHTML;}//最后,需要将模板转换为渲染函数if(template){constrender=compileToFunctions(template);options.render=渲染;}}};}我们主要关心$mount方法,最后将处理后的template模板转换成render函数相关Vue源码视频讲解:进入学习2.模板转换c的核心方法ompileToFunctions//src/compiler/index.jsimport{parse}from"./parse";import{generate}from"./codegen";exportfunctioncompileToFunctions(template){//我们需要将html字符串转换为渲染function//1.将html代码转换为ast语法树ast用于描述代码本身,形成一个树状结构,不仅可以描述html,还可以描述css和js语法//很多库都使用ast,比如webpackbabeleslint,等letast=parse(template);//2.优化静态节点//有兴趣的可以去看源码,不影响核心功能就不实现了//if(options.optimize!==false){//optimize(ast,选项);//}//3.通过ast重新生成代码//我们最终生成的代码需要和render函数一样//类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))//_c代表创建元素_v代表创建文本_s代表文本Json。stringify--将对象解析成文本letcode=generate(ast);//使用with语法将范围更改为this,然后调用渲染函数。可以使用call来改变this,方便代码中的变量取值letrenderFn=newFunction(`with(this){return${code}}`);returnrenderFn;}新建一个compiler文件夹,表示编译相关的函数。核心exportcompileToFunctions函数主要有三个步骤:1.生成AST2.优化静态节点3.根据AST生成render功能3.解析html并生成ast//src/compiler/parse.js//下面是源码的正则表达式。对正则表达式不清楚的同学可以参考小编写的文章(前端进阶高薪必看正则文章));constncname=`[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;//匹配标签名称如abc-123constqnameCapture=`((?:${ncname}\\:)?${ncname})`;//在abc之前匹配abc:234等特殊标签:optionalconststartTagOpen=newRegExp(`^<${qnameCapture}`);//匹配标签的开始就像/;//匹配标签结束>constendTag=newRegExp(`^<\\/${qnameCapture}[^>]*>`);//匹配以结尾的标签捕获标签名称constattribute=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;//匹配id等属性="app"letroot,currentParent;//表示根节点和当前父节点//栈结构表示开始和结束标签letstack=[];//标记元素和文本类型constELEMENT_TYPE=1;constTEXT_TYPE=3;//生成ast方法functioncreateASTElement(tagName,attrs){return{tag:tagName,type:ELEMENT_TYPE,children:[],attrs,parent:null,};}//处理起始标签函数handleStartTag({tagName,attrs}){letelement=createASTElement(tagName,attrs);如果(!根){根=元素;}currentParent=元素;stack.push(element);}//处理结束标签functionhandleEndTag(tagName){//栈结构[]//比如
会匹配最上面的遇到第一个结束标签时栈的元素取出对应的astletelement=stack.pop();//当前父元素是栈顶的前一个元素,类似于divcurrentParent=stack[stack.length-1];//建立父子关系if(currentParent){element.parent=currentParent;currentParent.children.push(元素);}}//处理文本functionhandleChars(text){//删除空格text=text.replace(/\s/g,"");if(text){currentParent.children.push({type:TEXT_TYPE,text,});}}//解析标签生成ast核心导出函数parse(html){while(html){//find if(textEnd>=0){//获取文本text=html.substring(0,textEnd);}if(text){advance(text.length);处理字符(文本);}}//匹配开始标签functionparseStartTag(){conststart=html.match(startTagOpen);如果(开始){constmatch={tagName:start[1],attrs:[],};//匹配到起始标签时,advance(start[0].length);//开始匹配属性//end代表结束符>如果没有匹配到结束标签//attr代表匹配属性letend,attr;while(!(end=html.match(startTagClose))&&(attr=html.match(属性))){提前(属性[0].length);属性={名称:属性[1],值:属性[3]||属性[4]||attr[5],//这里是因为Regularcapture支持双引号、单引号和不带引号的属性值};匹配.attrs.push(attr);}if(end){//表示标签匹配结束>表示开始标签已被解析advance(1);返回匹配;}}}//拦截html字符串,每次匹配到继续匹配functionadvance(n){html=html.substring(n);}//返回生成的astreturnroot;}使用正则匹配html字符串,满足开始标签、结束标签和文本解析,生成对应的ast并建立对应的父子关系,继续往前拦截剩下的字符串,直到解析完所有html。这里主要写一下开始标签Processing--parseStartTag4中的属性。根据ast重新生成代码//src/compiler/codegen.jsconstdefaultTagRE=/\{\{((?:.|\r?\n)+?)\}\}/g;//匹配花括号{{}}捕获花括号里面的内容functiongen(node){//判断节点类型//主要包括处理文本的核心//源码中包含复杂的处理比如v-oncev-forv-if自定义指令槽等我们只考虑普通文本和变量表达式的处理{{}}//如果是元素类型if(node.type==1){//递归创建return生成(节点);}else{//如果是文本节点lettext=node.text;//没有花括号的变量表达式如果(!defaultTagRE.test(text)){返回`_v(${JSON.stringify(text)})`;}//Regex是全局模式,每次都需要重新设置正则的lastIndex属性,否则会导致匹配bugletlastIndex=(defaultTagRE.lastIndex=0);让标记=[];让匹配,索引;while((match=defaultTagRE.exec(text))){//索引代表匹配的位置index=match.index;if(index>lastIndex){//匹配到的{{位置作为普通文本放在tokens中tokens.push(JSON.stringify(text.slice(lastIndex,index)));}//放入捕获的变量内容tokens.push(`_s(${match[1].trim()})`);//将匹配指针向后移动lastIndex=index+match[0].length;}//如果匹配完成,文本中还有花括号If(lastIndex
{let[key,value]=item.split(":");obj[key]=value;});属性值=obj;}str+=`${attr.name}:${JSON.stringify(attr.value)},`;}return`{${str.slice(0,-1)}}`;}//生成子节点调用gen函数递归创建functiongetChildren(el){constchildren=el.children;if(children){return`${children.map((c)=>gen(c)).join(",")}`;}}//递归创建代码导出函数generate(el){letchildren=getChildren(el);letcode=`_c('${el.tag}',${el.attrs.length?`${genProps(el.attrs)}`:"undefined"}${children?`,${children}`:“”})`;returncode;}得到生成的ast后,需要将ast转换成类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))这样的字符串5.codestringgenerationrenderfunctionexportfunctioncompileToFunctions(template){letcode=generate(ast);//运用智慧h语法将范围更改为此。调用render函数后,可以使用call来改变this,方便代码中的变量取值。例如,name的值变为this.nameletrenderFn=newFunction(`with(this){return${code}}`);returnrenderFn;}总结至此,Vue的模板编译原理就讲完了。大家可以看思维导图,自己写核心代码。需要注意的是,本文大量使用了字符串拼接和正则知识。知道的地方可以查看更多信息欢迎评论留言