You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
/** * 编译器的入口 * 运行时的 Vue.js 包就没有这部分的代码,通过 打包器 结合 vue-loader + vue-compiler-utils 进行预编译,将模版编译成 render 函数 * * 就做了一件事情,得到组件的渲染函数,将其设置到 this.$options 上 */constmount=Vue.prototype.$mountVue.prototype.$mount=function(el?: string|Element,hydrating?: boolean): Component{// 挂载点el=el&&query(el)// 挂载点不能是 body 或者 html/* istanbul ignore if */if(el===document.body||el===document.documentElement){process.env.NODE_ENV!=='production'&&warn(`Do not mount Vue to <html> or <body> - mount to normal elements instead.`)returnthis}// 配置项constoptions=this.$options// resolve template/el and convert to render function/** * 如果用户提供了 render 配置项,则直接跳过编译阶段,否则进入编译阶段 * 解析 template 和 el,并转换为 render 函数 * 优先级:render > template > el */if(!options.render){lettemplate=options.templateif(template){// 处理 template 选项if(typeoftemplate==='string'){if(template.charAt(0)==='#'){// { template: '#app' },template 是一个 id 选择器,则获取该元素的 innerHtml 作为模版template=idToTemplate(template)/* istanbul ignore if */if(process.env.NODE_ENV!=='production'&&!template){warn(`Template element not found or is empty: ${options.template}`,this)}}}elseif(template.nodeType){// template 是一个正常的元素,获取其 innerHtml 作为模版template=template.innerHTML}else{if(process.env.NODE_ENV!=='production'){warn('invalid template option:'+template,this)}returnthis}}elseif(el){// 设置了 el 选项,获取 el 选择器的 outerHtml 作为模版template=getOuterHTML(el)}if(template){// 模版就绪,进入编译阶段/* istanbul ignore if */if(process.env.NODE_ENV!=='production'&&config.performance&&mark){mark('compile')}// 编译模版,得到 动态渲染函数和静态渲染函数const{ render, staticRenderFns }=compileToFunctions(template,{// 在非生产环境下,编译时记录标签属性在模版字符串中开始和结束的位置索引outputSourceRange: process.env.NODE_ENV!=='production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,// 界定符,默认 {{}}delimiters: options.delimiters,// 是否保留注释comments: options.comments},this)// 将两个渲染函数放到 this.$options 上options.render=renderoptions.staticRenderFns=staticRenderFns/* istanbul ignore if */if(process.env.NODE_ENV!=='production'&&config.performance&&mark){mark('compile end')measure(`vue ${this._name} compile`,'compile','compile end')}}}// 执行挂载returnmount.call(this,el,hydrating)}
compileToFunctions
/src/compiler/to-function.js
/** * 1、执行编译函数,得到编译结果 -> compiled * 2、处理编译期间产生的 error 和 tip,分别输出到控制台 * 3、将编译得到的字符串代码通过 new Function(codeStr) 转换成可执行的函数 * 4、缓存编译结果 * @param { string } template 字符串模版 * @param { CompilerOptions } options 编译选项 * @param { Component } vm 组件实例 * @return { render, staticRenderFns } */returnfunctioncompileToFunctions(template: string,options?: CompilerOptions,vm?: Component): CompiledFunctionResult{// 传递进来的编译选项options=extend({},options)// 日志constwarn=options.warn||baseWarndeleteoptions.warn/* istanbul ignore if */if(process.env.NODE_ENV!=='production'){// 检测可能的 CSP 限制try{newFunction('return 1')}catch(e){if(e.toString().match(/unsafe-eval|CSP/)){// 看起来你在一个 CSP 不安全的环境中使用完整版的 Vue.js,模版编译器不能工作在这样的环境中。// 考虑放宽策略限制或者预编译你的 template 为 render 函数warn('It seems you are using the standalone build of Vue.js in an '+'environment with Content Security Policy that prohibits unsafe-eval. '+'The template compiler cannot work in this environment. Consider '+'relaxing the policy to allow unsafe-eval or pre-compiling your '+'templates into render functions.')}}}// 如果有缓存,则跳过编译,直接从缓存中获取上次编译的结果constkey=options.delimiters
? String(options.delimiters)+template
: templateif(cache[key]){returncache[key]}// 执行编译函数,得到编译结果constcompiled=compile(template,options)// 检查编译期间产生的 error 和 tip,分别输出到控制台if(process.env.NODE_ENV!=='production'){if(compiled.errors&&compiled.errors.length){if(options.outputSourceRange){compiled.errors.forEach(e=>{warn(`Error compiling template:\n\n${e.msg}\n\n`+generateCodeFrame(template,e.start,e.end),vm)})}else{warn(`Error compiling template:\n\n${template}\n\n`+compiled.errors.map(e=>`- ${e}`).join('\n')+'\n',vm)}}if(compiled.tips&&compiled.tips.length){if(options.outputSourceRange){compiled.tips.forEach(e=>tip(e.msg,vm))}else{compiled.tips.forEach(msg=>tip(msg,vm))}}}// 转换编译得到的字符串代码为函数,通过 new Function(code) 实现// turn code into functionsconstres={}constfnGenErrors=[]res.render=createFunction(compiled.render,fnGenErrors)res.staticRenderFns=compiled.staticRenderFns.map(code=>{returncreateFunction(code,fnGenErrors)})// 处理上面代码转换过程中出现的错误,这一步一般不会报错,除非编译器本身出错了/* istanbul ignore if */if(process.env.NODE_ENV!=='production'){if((!compiled.errors||!compiled.errors.length)&&fnGenErrors.length){warn(`Failed to generate render function:\n\n`+fnGenErrors.map(({ err, code })=>`${err.toString()} in\n\n${code}\n`).join('\n'),vm)}}// 缓存编译结果return(cache[key]=res)}
/** * 解析结束标签,比如:</div> * 最主要的事就是: * 1、处理 stack 数组,从 stack 数组中找到当前结束标签对应的开始标签,然后调用 options.end 方法 * 2、处理完结束标签之后调整 stack 数组,保证在正常情况下 stack 数组中的最后一个元素就是下一个结束标签对应的开始标签 * 3、处理一些异常情况,比如 stack 数组最后一个元素不是当前结束标签对应的开始标签,还有就是 * br 和 p 标签单独处理 * @param {*} tagName 标签名,比如 div * @param {*} start 结束标签的开始索引 * @param {*} end 结束标签的结束索引 */functionparseEndTag(tagName,start,end){letpos,lowerCasedTagNameif(start==null)start=indexif(end==null)end=index// 倒序遍历 stack 数组,找到第一个和当前结束标签相同的标签,该标签就是结束标签对应的开始标签的描述对象// 理论上,不出异常,stack 数组中的最后一个元素就是当前结束标签的开始标签的描述对象// Find the closest opened tag of the same typeif(tagName){lowerCasedTagName=tagName.toLowerCase()for(pos=stack.length-1;pos>=0;pos--){if(stack[pos].lowerCasedTag===lowerCasedTagName){break}}}else{// If no tag name is provided, clean shoppos=0}// 如果在 stack 中一直没有找到相同的标签名,则 pos 就会 < 0,进行后面的 else 分支if(pos>=0){// 这个 for 循环负责关闭 stack 数组中索引 >= pos 的所有标签// 为什么要用一个循环,上面说到正常情况下 stack 数组的最后一个元素就是我们要找的开始标签,// 但是有些异常情况,就是有些元素没有给提供结束标签,比如:// stack = ['span', 'div', 'span', 'h1'],当前处理的结束标签 tagName = div// 匹配到 div,pos = 1,那索引为 2 和 3 的两个标签(span、h1)说明就没提供结束标签// 这个 for 循环就负责关闭 div、span 和 h1 这三个标签,// 并在开发环境为 span 和 h1 这两个标签给出 ”未匹配到结束标签的提示”// Close all the open elements, up the stackfor(leti=stack.length-1;i>=pos;i--){if(process.env.NODE_ENV!=='production'&&(i>pos||!tagName)&&options.warn){options.warn(`tag <${stack[i].tag}> has no matching end tag.`,{start: stack[i].start,end: stack[i].end})}if(options.end){// 走到这里,说明上面的异常情况都处理完了,调用 options.end 处理正常的结束标签options.end(stack[i].tag,start,end)}}// 将刚才处理的那些标签从数组中移除,保证数组的最后一个元素就是下一个结束标签对应的开始标签// Remove the open elements from the stackstack.length=pos// lastTag 记录 stack 数组中未处理的最后一个开始标签lastTag=pos&&stack[pos-1].tag}elseif(lowerCasedTagName==='br'){// 当前处理的标签为 <br /> 标签if(options.start){options.start(tagName,[],true,start,end)}}elseif(lowerCasedTagName==='p'){// p 标签if(options.start){// 处理 <p> 标签options.start(tagName,[],false,start,end)}if(options.end){// 处理 </p> 标签options.end(tagName,start,end)}}}
parseHtmlOptions
src/compiler/parser/index.js
定义如何处理开始标签、结束标签、文本节点和注释节点。
start
/** * 主要做了以下 6 件事情: * 1、创建 AST 对象 * 2、处理存在 v-model 指令的 input 标签,分别处理 input 为 checkbox、radio、其它的情况 * 3、处理标签上的众多指令,比如 v-pre、v-for、v-if、v-once * 4、如果根节点 root 不存在则设置当前元素为根节点 * 5、如果当前元素为非自闭合标签则将自己 push 到 stack 数组,并记录 currentParent,在接下来处理子元素时用来告诉子元素自己的父节点是谁 * 6、如果当前元素为自闭合标签,则表示该标签要处理结束了,让自己和父元素产生关系,以及设置自己的子元素 * @param {*} tag 标签名 * @param {*} attrs [{ name: attrName, value: attrVal, start, end }, ...] 形式的属性数组 * @param {*} unary 自闭合标签 * @param {*} start 标签在 html 字符串中的开始索引 * @param {*} end 标签在 html 字符串中的结束索引 */functionstart(tag,attrs,unary,start,end){// 检查命名空间,如果存在,则继承父命名空间// check namespace.// inherit parent ns if there is oneconstns=(currentParent&¤tParent.ns)||platformGetTagNamespace(tag)// handle IE svg bug/* istanbul ignore if */if(isIE&&ns==='svg'){attrs=guardIESVGBug(attrs)}// 创建当前标签的 AST 对象letelement: ASTElement=createASTElement(tag,attrs,currentParent)// 设置命名空间if(ns){element.ns=ns}// 这段在非生产环境下会走,在 ast 对象上添加 一些 属性,比如 start、endif(process.env.NODE_ENV!=='production'){if(options.outputSourceRange){element.start=startelement.end=end// 将属性数组解析成 { attrName: { name: attrName, value: attrVal, start, end }, ... } 形式的对象element.rawAttrsMap=element.attrsList.reduce((cumulated,attr)=>{cumulated[attr.name]=attrreturncumulated},{})}// 验证属性是否有效,比如属性名不能包含: spaces, quotes, <, >, / or =.attrs.forEach(attr=>{if(invalidAttributeRE.test(attr.name)){warn(`Invalid dynamic argument expression: attribute names cannot contain `+`spaces, quotes, <, >, / or =.`,{start: attr.start+attr.name.indexOf(`[`),end: attr.start+attr.name.length})}})}// 非服务端渲染的情况下,模版中不应该出现 style、script 标签if(isForbiddenTag(element)&&!isServerRendering()){element.forbidden=trueprocess.env.NODE_ENV!=='production'&&warn('Templates should only be responsible for mapping the state to the '+'UI. Avoid placing tags with side-effects in your templates, such as '+`<${tag}>`+', as they will not be parsed.',{start: element.start})}/** * 为 element 对象分别执行 class、style、model 模块中的 preTransforms 方法 * 不过 web 平台只有 model 模块有 preTransforms 方法 * 用来处理存在 v-model 的 input 标签,但没处理 v-model 属性 * 分别处理了 input 为 checkbox、radio 和 其它的情况 * input 具体是哪种情况由 el.ifConditions 中的条件来判断 * <input v-mode="test" :type="checkbox or radio or other(比如 text)" /> */// apply pre-transformsfor(leti=0;i<preTransforms.length;i++){element=preTransforms[i](element,options)||element}if(!inVPre){// 表示 element 是否存在 v-pre 指令,存在则设置 element.pre = trueprocessPre(element)if(element.pre){// 存在 v-pre 指令,则设置 inVPre 为 trueinVPre=true}}// 如果 pre 标签,则设置 inPre 为 trueif(platformIsPreTag(element.tag)){inPre=true}if(inVPre){// 说明标签上存在 v-pre 指令,这样的节点只会渲染一次,将节点上的属性都设置到 el.attrs 数组对象中,作为静态属性,数据更新时不会渲染这部分内容// 设置 el.attrs 数组对象,每个元素都是一个属性对象 { name: attrName, value: attrVal, start, end }processRawAttrs(element)}elseif(!element.processed){// structural directives// 处理 v-for 属性,得到 element.for = 可迭代对象 element.alias = 别名processFor(element)/** * 处理 v-if、v-else-if、v-else * 得到 element.if = "exp",element.elseif = exp, element.else = true * v-if 属性会额外在 element.ifConditions 数组中添加 { exp, block } 对象 */processIf(element)// 处理 v-once 指令,得到 element.once = true processOnce(element)}// 如果 root 不存在,则表示当前处理的元素为第一个元素,即组件的 根 元素if(!root){root=elementif(process.env.NODE_ENV!=='production'){// 检查根元素,对根元素有一些限制,比如:不能使用 slot 和 template 作为根元素,也不能在有状态组件的根元素上使用 v-for 指令checkRootConstraints(root)}}if(!unary){// 非自闭合标签,通过 currentParent 记录当前元素,下一个元素在处理的时候,就知道自己的父元素是谁currentParent=element// 然后将 element push 到 stack 数组,将来处理到当前元素的闭合标签时再拿出来// 将当前标签的 ast 对象 push 到 stack 数组中,这里需要注意,在调用 options.start 方法// 之前也发生过一次 push 操作,那个 push 进来的是当前标签的一个基本配置信息stack.push(element)}else{/** * 说明当前元素为自闭合标签,主要做了 3 件事: * 1、如果元素没有被处理过,即 el.processed 为 false,则调用 processElement 方法处理节点上的众多属性 * 2、让自己和父元素产生关系,将自己放到父元素的 children 数组中,并设置自己的 parent 属性为 currentParent * 3、设置自己的子元素,将自己所有非插槽的子元素放到自己的 children 数组中 */closeElement(element)}}
end
/** * 处理结束标签 * @param {*} tag 结束标签的名称 * @param {*} start 结束标签的开始索引 * @param {*} end 结束标签的结束索引 */functionend(tag,start,end){// 结束标签对应的开始标签的 ast 对象constelement=stack[stack.length-1]// pop stackstack.length-=1// 这块儿有点不太理解,因为上一个元素有可能是当前元素的兄弟节点currentParent=stack[stack.length-1]if(process.env.NODE_ENV!=='production'&&options.outputSourceRange){element.end=end}/** * 主要做了 3 件事: * 1、如果元素没有被处理过,即 el.processed 为 false,则调用 processElement 方法处理节点上的众多属性 * 2、让自己和父元素产生关系,将自己放到父元素的 children 数组中,并设置自己的 parent 属性为 currentParent * 3、设置自己的子元素,将自己所有非插槽的子元素放到自己的 children 数组中 */closeElement(element)}
chars
/** * 处理文本,基于文本生成 ast 对象,然后将该 ast 放到它的父元素的肚子里,即 currentParent.children 数组中 */functionchars(text: string,start: number,end: number){// 异常处理,currentParent 不存在说明这段文本没有父元素if(!currentParent){if(process.env.NODE_ENV!=='production'){if(text===template){// 文本不能作为组件的根元素warnOnce('Component template requires a root element, rather than just text.',{ start })}elseif((text=text.trim())){// 放在根元素之外的文本会被忽略warnOnce(`text "${text}" outside root element will be ignored.`,{ start })}}return}// IE textarea placeholder bug/* istanbul ignore if */if(isIE&¤tParent.tag==='textarea'&¤tParent.attrsMap.placeholder===text){return}// 当前父元素的所有孩子节点constchildren=currentParent.children// 对 text 进行一系列的处理,比如删除空白字符,或者存在 whitespaceOptions 选项,则 text 直接置为空或者空格if(inPre||text.trim()){// 文本在 pre 标签内 或者 text.trim() 不为空text=isTextTag(currentParent) ? text : decodeHTMLCached(text)}elseif(!children.length){// 说明文本不在 pre 标签内而且 text.trim() 为空,而且当前父元素也没有孩子节点,// 则将 text 置为空// remove the whitespace-only node right after an opening tagtext=''}elseif(whitespaceOption){// 压缩处理if(whitespaceOption==='condense'){// in condense mode, remove the whitespace node if it contains// line break, otherwise condense to a single spacetext=lineBreakRE.test(text) ? '' : ' '}else{text=' '}}else{text=preserveWhitespace ? ' ' : ''}// 如果经过处理后 text 还存在if(text){if(!inPre&&whitespaceOption==='condense'){// 不在 pre 节点中,并且配置选项中存在压缩选项,则将多个连续空格压缩为单个// condense consecutive whitespaces into single spacetext=text.replace(whitespaceRE,' ')}letres// 基于 text 生成 AST 对象letchild: ?ASTNodeif(!inVPre&&text!==' '&&(res=parseText(text,delimiters))){// 文本中存在表达式(即有界定符)child={type: 2,// 表达式expression: res.expression,tokens: res.tokens,// 文本
text
}}elseif(text!==' '||!children.length||children[children.length-1].text!==' '){// 纯文本节点child={type: 3,
text
}}// child 存在,则将 child 放到父元素的肚子里,即 currentParent.children 数组中if(child){if(process.env.NODE_ENV!=='production'&&options.outputSourceRange){child.start=startchild.end=end}children.push(child)}}},
comment
/** * 处理注释节点 */functioncomment(text: string,start,end){// adding anything as a sibling to the root node is forbidden// comments should still be allowed, but ignored// 禁止将任何内容作为 root 的节点的同级进行添加,注释应该被允许,但是会被忽略// 如果 currentParent 不存在,说明注释和 root 为同级,忽略if(currentParent){// 注释节点的 astconstchild: ASTText={// 节点类型type: 3,// 注释内容
text,// 是否为注释isComment: true}if(process.env.NODE_ENV!=='production'&&options.outputSourceRange){// 记录节点的开始索引和结束索引child.start=startchild.end=end}// 将当前注释节点放到父元素的 children 属性中currentParent.children.push(child)}}
/** * 获取 el 对象上执行属性 name 的值 */exportfunctiongetBindingAttr(el: ASTElement,name: string,getStatic?: boolean): ?string{// 获取指定属性的值constdynamicValue=getAndRemoveAttr(el,':'+name)||getAndRemoveAttr(el,'v-bind:'+name)if(dynamicValue!=null){returnparseFilters(dynamicValue)}elseif(getStatic!==false){conststaticValue=getAndRemoveAttr(el,name)if(staticValue!=null){returnJSON.stringify(staticValue)}}}
getAndRemoveAttr
/src/compiler/helpers.js
/** * 从 el.attrsList 中删除指定的属性 name * 如果 removeFromMap 为 true,则同样删除 el.attrsMap 对象中的该属性, * 比如 v-if、v-else-if、v-else 等属性就会被移除, * 不过一般不会删除该对象上的属性,因为从 ast 生成 代码 期间还需要使用该对象 * 返回指定属性的值 */// note: this only removes the attr from the Array (attrsList) so that it// doesn't get processed by processAttrs.// By default it does NOT remove it from the map (attrsMap) because the map is// needed during codegen.exportfunctiongetAndRemoveAttr(el: ASTElement,name: string,removeFromMap?: boolean): ?string{letval// 将执行属性 name 从 el.attrsList 中移除if((val=el.attrsMap[name])!=null){constlist=el.attrsListfor(leti=0,l=list.length;i<l;i++){if(list[i].name===name){list.splice(i,1)break}}}// 如果 removeFromMap 为 true,则从 el.attrsMap 中移除指定的属性 name// 不过一般不会移除 el.attsMap 中的数据,因为从 ast 生成 代码 期间还需要使用该对象if(removeFromMap){deleteel.attrsMap[name]}// 返回执行属性的值returnval}
// 在 el.attrsMap 和 el.attrsList 中添加指定属性 name// add a raw attr (use this in preTransforms)exportfunctionaddRawAttr(el: ASTElement,name: string,value: any,range?: Range){el.attrsMap[name]=valueel.attrsList.push(rangeSetItem({ name, value },range))}
/** * 处理元素上的 key 属性,设置 el.key = val * @param {*} el */functionprocessKey(el){// 拿到 key 的属性值constexp=getBindingAttr(el,'key')if(exp){// 关于 key 使用上的异常处理if(process.env.NODE_ENV!=='production'){// template 标签不允许设置 keyif(el.tag==='template'){warn(`<template> cannot be keyed. Place the key on real elements instead.`,getRawBindingAttr(el,'key'))}// 不要在 <transition=group> 的子元素上使用 v-for 的 index 作为 key,这和没用 key 没什么区别if(el.for){constiterator=el.iterator2||el.iterator1constparent=el.parentif(iterator&&iterator===exp&&parent&&parent.tag==='transition-group'){warn(`Do not use v-for index as key on <transition-group> children, `+`this is the same as not using keys.`,getRawBindingAttr(el,'key'),true/* tip */)}}}// 设置 el.key = expel.key=exp}}
/** * 处理作为插槽传递给组件的内容,得到: * slotTarget => 插槽名 * slotTargetDynamic => 是否为动态插槽 * slotScope => 作用域插槽的值 * 直接在 <comp> 标签上使用 v-slot 语法时,将上述属性放到 el.scopedSlots 对象上,其它情况直接放到 el 对象上 * handle content being passed to a component as slot, * e.g. <template slot="xxx">, <div slot-scope="xxx"> */functionprocessSlotContent(el){letslotScopeif(el.tag==='template'){// template 标签上使用 scope 属性的提示// scope 已经弃用,并在 2.5 之后使用 slot-scope 代替// slot-scope 即可以用在 template 标签也可以用在普通标签上slotScope=getAndRemoveAttr(el,'scope')/* istanbul ignore if */if(process.env.NODE_ENV!=='production'&&slotScope){warn(`the "scope" attribute for scoped slots have been deprecated and `+`replaced by "slot-scope" since 2.5. The new "slot-scope" attribute `+`can also be used on plain elements in addition to <template> to `+`denote scoped slots.`,el.rawAttrsMap['scope'],true)}// el.slotScope = valel.slotScope=slotScope||getAndRemoveAttr(el,'slot-scope')}elseif((slotScope=getAndRemoveAttr(el,'slot-scope'))){/* istanbul ignore if */if(process.env.NODE_ENV!=='production'&&el.attrsMap['v-for']){// 元素不能同时使用 slot-scope 和 v-for,v-for 具有更高的优先级// 应该用 template 标签作为容器,将 slot-scope 放到 template 标签上 warn(`Ambiguous combined usage of slot-scope and v-for on <${el.tag}> `+`(v-for takes higher priority). Use a wrapper <template> for the `+`scoped slot to make it clearer.`,el.rawAttrsMap['slot-scope'],true)}el.slotScope=valel.slotScope=slotScope}// 获取 slot 属性的值// slot="xxx",老旧的具名插槽的写法constslotTarget=getBindingAttr(el,'slot')if(slotTarget){// el.slotTarget = 插槽名(具名插槽)el.slotTarget=slotTarget==='""' ? '"default"' : slotTarget// 动态插槽名el.slotTargetDynamic=!!(el.attrsMap[':slot']||el.attrsMap['v-bind:slot'])// preserve slot as an attribute for native shadow DOM compat// only for non-scoped slots.if(el.tag!=='template'&&!el.slotScope){addAttr(el,'slot',slotTarget,getRawBindingAttr(el,'slot'))}}// 2.6 v-slot syntaxif(process.env.NEW_SLOT_SYNTAX){if(el.tag==='template'){// v-slot 在 tempalte 标签上,得到 v-slot 的值// v-slot on <template>constslotBinding=getAndRemoveAttrByRegex(el,slotRE)if(slotBinding){// 异常提示if(process.env.NODE_ENV!=='production'){if(el.slotTarget||el.slotScope){// 不同插槽语法禁止混合使用warn(`Unexpected mixed usage of different slot syntaxes.`,el)}if(el.parent&&!maybeComponent(el.parent)){// <template v-slot> 只能出现在组件的根位置,比如:// <comp>// <template v-slot>xx</template>// </comp>// 而不能是// <comp>// <div>// <template v-slot>xxx</template>// </div>// </comp>warn(`<template v-slot> can only appear at the root level inside `+`the receiving component`,el)}}// 得到插槽名称const{ name, dynamic }=getSlotName(slotBinding)// 插槽名el.slotTarget=name// 是否为动态插槽el.slotTargetDynamic=dynamic// 作用域插槽的值el.slotScope=slotBinding.value||emptySlotScopeToken// force it into a scoped slot for perf}}else{// 处理组件上的 v-slot,<comp v-slot:header />// slotBinding = { name: "v-slot:header", value: "", start, end}// v-slot on component, denotes default slotconstslotBinding=getAndRemoveAttrByRegex(el,slotRE)if(slotBinding){// 异常提示if(process.env.NODE_ENV!=='production'){// el 不是组件的话,提示,v-slot 只能出现在组件上或 template 标签上if(!maybeComponent(el)){warn(`v-slot can only be used on components or <template>.`,slotBinding)}// 语法混用if(el.slotScope||el.slotTarget){warn(`Unexpected mixed usage of different slot syntaxes.`,el)}// 为了避免作用域歧义,当存在其他命名槽时,默认槽也应该使用<template>语法if(el.scopedSlots){warn(`To avoid scope ambiguity, the default slot should also use `+`<template> syntax when there are other named slots.`,slotBinding)}}// 将组件的孩子添加到它的默认插槽内// add the component's children to its default slotconstslots=el.scopedSlots||(el.scopedSlots={})// 获取插槽名称以及是否为动态插槽const{ name, dynamic }=getSlotName(slotBinding)// 创建一个 template 标签的 ast 对象,用于容纳插槽内容,父级是 elconstslotContainer=slots[name]=createASTElement('template',[],el)// 插槽名slotContainer.slotTarget=name// 是否为动态插槽slotContainer.slotTargetDynamic=dynamic// 所有的孩子,将每一个孩子的 parent 属性都设置为 slotContainerslotContainer.children=el.children.filter((c: any)=>{if(!c.slotScope){// 给插槽内元素设置 parent 属性为 slotContainer,也就是 template 元素c.parent=slotContainerreturntrue}})slotContainer.slotScope=slotBinding.value||emptySlotScopeToken// remove children as they are returned from scopedSlots nowel.children=[]// mark el non-plain so data gets generatedel.plain=false}}}}
// handle <slot/> outlets,处理自闭合 slot 标签// 得到插槽名称,el.slotNamefunctionprocessSlotOutlet(el){if(el.tag==='slot'){// 得到插槽名称el.slotName=getBindingAttr(el,'name')// 提示信息,不要在 slot 标签上使用 key 属性if(process.env.NODE_ENV!=='production'&&el.key){warn(`\`key\` does not work on <slot> because slots are abstract outlets `+`and can possibly expand into multiple elements. `+`Use the key on a wrapping element instead.`,getRawBindingAttr(el,'key'))}}}
/** * 处理元素上的 class 属性 * 静态的 class 属性值赋值给 el.staticClass 属性 * 动态的 class 属性值赋值给 el.classBinding 属性 */functiontransformNode(el: ASTElement,options: CompilerOptions){// 日志constwarn=options.warn||baseWarn// 获取元素上静态 class 属性的值 xx,<div class="xx"></div>conststaticClass=getAndRemoveAttr(el,'class')if(process.env.NODE_ENV!=='production'&&staticClass){constres=parseText(staticClass,options.delimiters)// 提示,同 style 的提示一样,不能使用 <div class="{{ val}}"></div>,请用// <div :class="val"></div> 代替if(res){warn(`class="${staticClass}": `+'Interpolation inside attributes has been removed. '+'Use v-bind or the colon shorthand instead. For example, '+'instead of <div class="{{ val }}">, use <div :class="val">.',el.rawAttrsMap['class'])}}// 静态 class 属性值赋值给 el.staticClassif(staticClass){el.staticClass=JSON.stringify(staticClass)}// 获取动态绑定的 class 属性值,并赋值给 el.classBindingconstclassBinding=getBindingAttr(el,'class',false/* getStatic */)if(classBinding){el.classBinding=classBinding}}
transformNode
/src/platforms/web/compiler/modules/style.js
/** * 从 el 上解析出静态的 style 属性和动态绑定的 style 属性,分别赋值给: * el.staticStyle 和 el.styleBinding * @param {*} el * @param {*} options */functiontransformNode(el: ASTElement,options: CompilerOptions){// 日志constwarn=options.warn||baseWarn// <div style="xx"></div>// 获取 style 属性conststaticStyle=getAndRemoveAttr(el,'style')if(staticStyle){// 提示,如果从 xx 中解析到了界定符,说明是一个动态的 style,// 比如 <div style="{{ val }}"></div>则给出提示:// 动态的 style 请使用 <div :style="val"></div>/* istanbul ignore if */if(process.env.NODE_ENV!=='production'){constres=parseText(staticStyle,options.delimiters)if(res){warn(`style="${staticStyle}": `+'Interpolation inside attributes has been removed. '+'Use v-bind or the colon shorthand instead. For example, '+'instead of <div style="{{ val }}">, use <div :style="val">.',el.rawAttrsMap['style'])}}// 将静态的 style 样式赋值给 el.staticStyleel.staticStyle=JSON.stringify(parseStyleText(staticStyle))}// 获取动态绑定的 style 属性,比如 <div :style="{{ val }}"></div>conststyleBinding=getBindingAttr(el,'style',false/* getStatic */)if(styleBinding){// 赋值给 el.styleBindingel.styleBinding=styleBinding}}
Vue 源码解读(8)—— 编译器 之 解析(上)
当学习成为了习惯,知识也就变成了常识。 感谢各位的 关注、点赞、收藏和评论。
新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn
文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。
封面
特殊说明
由于文章篇幅限制,所以将 Vue 源码解读(8)—— 编译器 之 解析 拆成了上下两篇,所以在阅读本篇文章时请同时打开 Vue 源码解读(8)—— 编译器 之 解析(下)一起阅读。
前言
Vue 源码解读(4)—— 异步更新 最后说到刷新 watcher 队列,执行每个
watcher.run
方法,由watcher.run
调用watcher.get
,从而执行watcher.getter
方法,进入实际的更新阶段。这个流程如果不熟悉,建议大家再去读一下这篇文章。当更新一个渲染 watcher 时,执行的是
updateComponent
方法:可以看到每次更新前都需要先执行一下
vm._render()
方法,vm._render 就是大家经常听到的 render 函数,由两种方式得到:用户自己提供,在编写组件时,用 render 选项代替模版
由编译器编译组件模版生成 render 选项
今天我们就来深入研究编译器,看看它是怎么将我们平时编写的类 html 模版编译成 render 函数的。
编译器的核心由三部分组成:
解析,将类 html 模版转换为 AST 对象
优化,也叫静态标记,遍历 AST 对象,标记每个节点是否为静态节点,以及标记出静态根节点
生成渲染函数,将 AST 对象生成渲染函数
由于编译器这块儿的代码量太大,所以,将这部分知识拆成三部分来讲,第一部分就是:解析。
目标
深入理解 Vue 编译器的解析过程,理解如何将类 html 模版字符串转换成 AST 对象。
源码解读
接下来我们去源码中找答案。
入口 - $mount
编译器的入口位置在
/src/platforms/web/entry-runtime-with-compiler.js
,有两种方式找到这个入口断点调试,Vue 源码解读(2)—— Vue 初始化过程 中讲到,初始化的最后一步是执行
$mount
进行挂载,在全量的 Vue 包中这一步就会进入编译阶段。通过 rollup 的配置文件一步步的去找
compileToFunctions
compile
baseOptions
baseCompile
parse
注意:由于这部分的代码量太大,于是将代码在结构上做了一些调整,方便大家阅读和理解。
parseHTML
advance
parseStartTag
handleStartTag
parseEndTag
parseHtmlOptions
定义如何处理开始标签、结束标签、文本节点和注释节点。
start
end
chars
comment
createASTElement
preTransformNode
getBindingAttr
getAndRemoveAttr
processFor
addRawAttr
processElement
processKey
processRef
processSlotContent
getSlotName
processSlotOutlet
processComponent
transformNode
transformNode
链接
感谢各位的:关注、点赞、收藏和评论,我们下期见。
当学习成为了习惯,知识也就变成了常识。 感谢各位的 关注、 点赞、收藏和评论。
新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn
文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。
The text was updated successfully, but these errors were encountered: