当前位置: 首页 > Web前端 > vue.js

手写Vue2系列编译器

时间:2023-03-31 19:16:15 vue.js

当学习成为习惯,知识就成为常识。感谢您的关注、点赞、收藏和评论。有新视频和文章会第一时间发到微信公众号。欢迎关注:李永宁lyn的文章已收录到github仓库liyongning/blog。欢迎收看星空。前言接下来正式进入手写Vue2系列。这里不会从头开始,而是直接基于lyn-vue升级,所以如果你还没有看过手写Vue系列的Vue1.x,请先从本文开始,按顺序学习。我们都知道Vue1的问题在于大型应用中Watcher过多。不懂原理的可以参考手写Vue系列之Vue1.x。所以在Vue2中引入了VNode和diff算法来解决这个问题。通过降低Watcher的粒度,一个组件对应一个Watcher(渲染Watcher),这样就不会出现大页面上Watcher过多导致性能下降的问题。在Vue1中,Watcher与页面中的响应式数据一一对应。当响应式数据发生变化时,Dep通知Watcher完成相应的DOM更新。但是在Vue2中,一个组件对应一个Watcher。当响应式数据发生变化时,Watcher并不知道响应式数据在组件中的什么位置,那么如何完成更新呢?看了前面的源码系列,大家肯定知道Vue2引入了VNode和diff算法来将组件编译成VNode。每次响应数据发生变化,都会产生一个新的VNode。通过diff算法比较新旧VNode,找出哪里有变化,然后执行相应的DOM操作,完成更新。所以这里大家可以理解,Vue1和Vue2核心数据响应部分其实没有变化,主要变化在编译器部分。目标是完成一个Vue2编译器的简化实现,从字符串模板解析开始,最后得到render函数。当编译器在手写Vue1时,编译器使用DOMAPI来遍历模板的DOM结构。Vue2中不再使用该方法,与官方相同,直接编译组件的模板字符串生成AST。然后从AST生成渲染函数。先备份Vue1的编译目录,然后新建一个编译目录作为Vue2的编译目录mvcompilercompiler-vue1&&mkdircompilermount/src/compiler/index.js/***compiler*/exportdefaultfunctionmount(vm){if(!vm.$options.render){//如果没有提供渲染选项,编译生成渲染函数//获取模板lettemplate=''if(vm.$options.template){//模板existstemplate=vm.$options.template}elseif(vm.$options.el){//有挂载点template=document.querySelector(vm.$options.el).outerHTML//记录挂载点在例如,这个vm.$el=document.querySelector(vm.$options.el)将在._update中使用}//生成渲染函数constrender=compileToFunction(template)//在$options上挂载渲染函数vm.$options.render=render}}compileToFunction/src/compiler/compileToFunction.js/***解析模板字符串以获取AST语法树*从AST生成渲染函数语法树*@param{String}templatetemplatestring*@returnsrenderingFunction*/exportdefaultfunctioncompileToFunction(template){//解析模板并生成astconstast=parse(template)//生成ast以渲染functionconstrender=generate(ast)returnrender}parse/src/compiler/parse.js/***解析模板字符串并生成AST语法树*@param{*}模板模板字符串*@returns{AST}rootast语法树*/exportdefaultfunctionparse(template){//AST对象存储所有未配对的开始标签conststack=[]//最终的AST语法树letroot=nulllethtml=templatewhile(html.trim()){//过滤注释标签if(html.indexOf('')+3)continue}//匹配开始标签conststartIdx=html.indexOf('<')if(startIdx===0){if(html.indexOf('0){//表示开始标签之间有一段文字,在html中找下一个标签的起始位置constnextStartIdx=html.indexOf('<')//如果栈为空,说明这个text不属于任何元素,不处理直接丢掉if(stack.length){//到这里就说栈不为空,然后处理这段文字,放到top的腹部元素processChars(html.slice(0,nextStartIdx))}html=html.slice(nextStartIdx)}else{//表示开始标签不匹配,整个html是一段文字}}returnroot//parseStartTag函数声明//...//声明processElement函数}//processVModel函数声明//...//processVOn函数声明parseStartTag/src/compiler/parse.js/***解析开始标签*例如:...

*/functionparseStartTag(){//先找到开始标签的结束位置>constend=html.indexOf('>')//解析开始标签中的内容,标签名+属性,例如:divid="app"constcontent=html.slice(1,end)//截断html,从html字符串中删除上面解析的内容html=html.slice(end+1)//查找第一个空格位置constfirstSpaceIdx=content.indexOf('')//标签名称和属性字符串lettagName='',attrsStr=''if(firstSpaceIdx===-1){//如果没有空格,则content被认为是标签名称,例如

在这种情况下,content=h3tagName=content//没有属性attrsStr=''}else{tagName=content.slice(0,firstSpaceIdx)//其余的内容都是属性,例如id="app"xx=xxattrsStr=content.slice(firstSpaceIdx+1)}//获取属性数组,[id="app",xx=xx]constattrs=attrsStr?attrsStr.split(''):[]//进一步解析属性数组得到一个Map对象constattrMap=parseAttrs(attrs)//生成一个AST对象constelementAst=generateAST(tagName,attrMap)//如果根节点不存在,则表示当前节点是整个模板的第一个节点if(!root){root=elementAst}//将ast对象压入栈中。当遇到结束标签时,会弹出栈顶的ast对象。两者是一对。if(isUnaryTag(tagName)){processElement()}}parseEnd/src/compiler/parse.js/***处理结束标签,例如:...
*/functionparseEnd(){//从html字符串中切掉结束标记html=html.slice(html.indexOf('>')+1)//处理顶部元素processElement()}parseAttrs/src/compiler/parse.js/***解析属性数组得到一个由属性和值组成的Map对象*@param{*}attrs属性数组,[id="app",xx="xx"]*/functionparseAttrs(attrs){constattrMap={}for(leti=0,len=attrs.length;iitem.match(/^v-bind:(.*)/))){//处理v-bind指令,比如processVBind(curEle,RegExp.$1,rawAttr[`v-bind:${RegExp.$1}`])}elseif(propertyArr.find(item=>item.match(/^v-on:(.*)/))){//处理v-on命令,比如addprocessVOn(curEle,RegExp.$1,rawAttr[`v-on:${RegExp.$1}`])}//节点处理完成后,让它与父节点发生关系if(stackLen){stack[stackLen-1].children.push(curEle)curEle.parent=stack[stackLen-1]}}processVModel/src/compiler/parse.js/***对v-model指令进行处理,将处理结果直接放在curEle对象上*@param{*}curEle*/functionprocessVModel(curEle){const{tag,rawAttr,attr}=curEleconst{type,'v-model':vModelVal}=rawAttrif(tag==='input'){if(/text/.test(type)){//attr.vModel={tag,type:'text',value:vModelVal}}elseif(/checkbox/.test(type)){//attr.vModel={tag,type:'checkbox',value:vModelVal}}}elseif(tag==='textarea'){//attr.vModel={tag,value:vModelVal}}elseif(tag==='select'){//...attr.vModel={tag,value:vModelVal}}}processVBind/src/compiler/parse.js/***处理v-bind命令*@param{*}curEle当前正确处理的AST对象*@param{*}bindKeyv-bind:key中的key*@param{*}bindValv-bind:key=val中的val*/functionprocessVBind(curEle,bindKey,bindVal){curEle.attr.vBind={[bindKey]:bindVal}}processVOn/src/compiler/parse.js/***进程v-on指令*@param{*}curEle当前是处理后的AST对象*@param{*}vOnKeykeyinv-on:key*@param{*}vOnValv-on:key="val"inval*/functionprocessVOn(curEle,vOnKey,vOnVal){curEle.attr.vOn={[vOnKey]:vOnVal}}isUnaryTag/src/utils.js/***是否是自闭标签,一些内置的自闭标签,方便处理*/exportfunctionisUnaryTag(tagName){constunaryTag=['input']returnunaryTag.includes(tagName)}generate/src/compiler/generate.js/***从ast生成渲染函数*@param{*}astast语法树*@returns渲染function*/exportdefaultfunctiongenerate(ast){//渲染函数的字符串形式constrenderStr=genElement(ast)//通过newFunction把字符串形式的函数变成可执行函数,并用withto扩展渲染函数的范围链returnnewFunction(`with(this){return${renderStr}}`)}genElement/src/compiler/generate.js/***解析ast生成渲染函数*@param{*}ast语法树*@returns{string}渲染函数的字符串形式*/functiongenElement(ast){const{tag,rawAttr,attr}=ast//生成属性Map对象,静态属性+动态属性constattrs={...rawAttr,...attr}//处理子节点,得到所有子节点的渲染函数数组constchildren=genChildren(ast)//生成VNode的可执行方法return`_c('${tag}',${JSON.stringify(attrs)},[${children}])`}genChildren/src/compiler/generate.js/***处理ast节点的子节点,将子节点变成渲染函数*@param{*}ast节点的ast对象*@returns[childNodeRender1,....]*/functiongenChildren(ast){constret=[],{children}=ast//遍历所有子节点for(leti=0,len=children.length;i