vue编译器如何生成渲染函数(vue,编程语言)

时间:2024-05-05 23:19:04 作者 : 石家庄SEO 分类 : 编程语言
  • TAG :

vue编译器如何生成渲染函数

深入源码

createCompiler() 方法 —— 入口

文件位置:/src/compiler/index.js

其中最主要的就是 generate(ast, options) 方法,它负责从 AST 语法树生成渲染函数.

/*在这之前做的所有的事情,只是为了构建平台特有的编译选项(options),比如web平台1、将html模版解析成ast2、对ast树进行静态标记3、将ast生成渲染函数-静态渲染函数放到code.staticRenderFns数组中-动态渲染函数code.render-在将来渲染时执行渲染函数能够得到vnode*/exportconstcreateCompiler=createCompilerCreator(functionbaseCompile(template:string,options:CompilerOptions):CompiledResult{/*将模版字符串解析为AST语法树每个节点的ast对象上都设置了元素的所有信息,如,标签信息、属性信息、插槽信息、父节点、子节点等*/constast=parse(template.trim(),options)/*优化,遍历AST,为每个节点做静态标记-标记每个节点是否为静态节点,保证在后续更新中跳过这些静态节点-标记出静态根节点,用于生成渲染函数阶段,生成静态根节点的渲染函数优化,遍历AST,为每个节点做静态标记*/if(options.optimize!==false){optimize(ast,options)}/*从AST语法树生成渲染函数如:code.render="_c('div',{attrs:{"id":"app"}},_l((arr),function(item){return_c('div',{key:item},[_v(_s(item))])}),0)"*/constcode=generate(ast,options)return{ast,render:code.render,staticRenderFns:code.staticRenderFns}})

generate() 方法

文件位置:src\compiler\codegen\index.js

其中在给 code 赋值时,主要的内容是通过 genElement(ast, state) 方法进行生成的.

/*从AST生成渲染函数:-render为字符串的代码-staticRenderFns为包含多个字符串的代码,形式为`with(this){returnxxx}`*/exportfunctiongenerate(ast:ASTElement|void,//ast对象options:CompilerOptions//编译选项):CodegenResult{/*实例化CodegenState对象,参数是编译选项,最终得到state,其中大部分属性和options一样*/conststate=newCodegenState(options)/*生成字符串格式的代码,比如:'_c(tag,data,children,normalizationType)'-data为节点上的属性组成JSON字符串,比如'{key:xx,ref:xx,...}'-children为所有子节点的字符串格式的代码组成的字符串数组,格式:`['_c(tag,data,children)',...],normalizationType`,-normalization是_c的第四个参数,表示节点的规范化类型(非重点,可跳过)注意:code并不一定就是_c,也有可能是其它的,比如整个组件都是静态的,则结果就为_m(0)*/constcode=ast?(ast.tag==='script'?'null':genElement(ast,state)):'_c("div")'return{render:`with(this){return${code}}`,staticRenderFns:state.staticRenderFns}}

genElement() 方法

文件位置:src\compiler\codegen\index.js

exportfunctiongenElement(el:ASTElement,state:CodegenState):string{if(el.parent){el.pre=el.pre||el.parent.pre}if(el.staticRoot&&!el.staticProcessed){/*处理静态根节点,生成节点的渲染函数1、将当前静态节点的渲染函数放到staticRenderFns数组中2、返回一个可执行函数_m(idx,trueor'')*/returngenStatic(el,state)}elseif(el.once&&!el.onceProcessed){/*处理带有v-once指令的节点,结果会有三种:1、当前节点存在v-if指令,得到一个三元表达式,`condition?render1:render2`2、当前节点是一个包含在v-for指令内部的静态节点,得到`_o(_c(tag,data,children),number,key)`3、当前节点就是一个单纯的v-once节点,得到`_m(idx,trueof'')`*/returngenOnce(el,state)}elseif(el.for&&!el.forProcessed){/*处理节点上的v-for指令,得到:`_l(exp,function(alias,iterator1,iterator2){return_c(tag,data,children)})`*/returngenFor(el,state)}elseif(el.if&&!el.ifProcessed){/*处理带有v-if指令的节点,最终得到一个三元表达式:`condition?render1:render2`*/returngenIf(el,state)}elseif(el.tag==='template'&&!el.slotTarget&&!state.pre){/*当前节点是template标签也不是插槽和带有v-pre指令的节点时走这里生成所有子节点的渲染函数,返回一个数组,格式如:`[_c(tag,data,children,normalizationType),...]`*/returngenChildren(el,state)||'void0'}elseif(el.tag==='slot'){/*生成插槽的渲染函数,得到:`_t(slotName,children,attrs,bind)`*/returngenSlot(el,state)}else{/*componentorelement处理动态组件和普通元素(自定义组件、原生标签、平台保留标签,如web平台中的每个html标签)*/letcodeif(el.component){/*处理动态组件,生成动态组件的渲染函数,得到`_c(compName,data,children)`*/code=genComponent(el.component,el,state)}else{//处理普通元素(自定义组件、原生标签)letdataif(!el.plain||(el.pre&&state.maybeComponent(el))){/*非普通元素或者带有v-pre指令的组件走这里,处理节点的所有属性,返回一个JSON字符串,比如:'{key:xx,ref:xx,...}'*/data=genData(el,state)}/*处理子节点,得到所有子节点字符串格式的代码组成的数组,格式:`['_c(tag,data,children)',...],normalizationType`其中的normalization表示节点的规范化类型(非重点,可跳过)*/constchildren=el.inlineTemplate?null:genChildren(el,state,true)/*得到最终的字符串格式的代码,格式:_c(tag,data,children,normalizationType)*/code=`_c('${el.tag}'${data?`,${data}`:''//data}${children?`,${children}`:''//children})`}/*如果提供了transformCode方法,则最终的code会经过各个模块(module)的该方法处理,不过框架没提供这个方法,不过即使处理了,最终的格式也是_c(tag,data,children)moduletransforms*/for(leti=0;i<state.transforms.length;i++){code=state.transforms[i](el,code)}//返回codereturncode}}

genChildren() 方法

文件位置:src\compiler\codegen\index.js

/*生成所有子节点的渲染函数,返回一个数组,格式如:`[_c(tag,data,children,normalizationType),...]`*/exportfunctiongenChildren(el:ASTElement,state:CodegenState,checkSkip?:boolean,altGenElement?:Function,altGenNode?:Function):string|void{//获取所有子节点constchildren=el.childrenif(children.length){//第一个子节点constel:any=children[0]//optimizesinglev-forif(children.length===1&&el.for&&el.tag!=='template'&&el.tag!=='slot'){/*优化处理:-条件:只有一个子节点&&子节点的上有v-for指令&&子节点的标签不为template或者slot-方式:直接调用genElement生成该节点的渲染函数,不需要走下面的循环然后调用genCode最后得到渲染函数*/constnormalizationType=checkSkip?state.maybeComponent(el)?`,1`:`,0`:``return`${(altGenElement||genElement)(el,state)}${normalizationType}`}//获取节点规范化类型,返回一个number:0、1、2(非重点,可跳过)constnormalizationType=checkSkip?getNormalizationType(children,state.maybeComponent):0//是一个函数,负责生成代码的一个函数constgen=altGenNode||genNode/*返回一个数组,其中每个元素都是一个子节点的渲染函数格式:['_c(tag,data,children,normalizationType)',...]*/return`[${children.map(c=>gen(c,state)).join(',')}]${normalizationType?`,${normalizationType}`:''}`}}

genNode() 方法

文件位置:src\compiler\codegen\index.js

functiongenNode(node:ASTNode,state:CodegenState):string{//处理普通元素节点if(node.type===1){returngenElement(node,state)}elseif(node.type===3&&node.isComment){//处理文本注释节点returngenComment(node)}else{//处理文本节点returngenText(node)}}

genComment() 方法

文件位置:src\compiler\codegen\index.js

//得到返回值,格式为:`_e(xxxx)`exportfunctiongenComment(comment:ASTText):string{return`_e(${JSON.stringify(comment.text)})`}

genText() 方法

文件位置:src\compiler\codegen\index.js

//得到返回值,格式为:`_v(xxxxx)`exportfunctiongenText(text:ASTText|ASTExpression):string{return`_v(${text.type===2?text.expression//noneedfor()becausealreadywrappedin_s():transformSpecialNewlines(JSON.stringify(text.text))})`}

genData() 方法

文件位置:src\compiler\codegen\index.js

/*处理节点上的众多属性,最后生成这些属性组成的JSON字符串,比如data={key:xx,ref:xx,...}*/exportfunctiongenData(el:ASTElement,state:CodegenState):string{//节点的属性组成的JSON字符串letdata='{'/*首先先处理指令,因为指令可能在生成其它属性之前改变这些属性执行指令编译方法,如web平台的v-text、v-html、v-model,然后在el对象上添加相应的属性,如v-text:el.textContent=_s(value,dir)v-html:el.innerHTML=_s(value,dir)当指令在运行时还有任务时,比如v-model,则返回directives:[{name,rawName,value,arg,modifiers},...}]*/constdirs=genDirectives(el,state)if(dirs)data+=dirs+','//key,data={key:xxx}if(el.key){data+=`key:${el.key},`}//ref,data={ref:xxx}if(el.ref){data+=`ref:${el.ref},`}//带有ref属性的节点在带有v-for指令的节点的内部,data={refInFor:true}if(el.refInFor){data+=`refInFor:true,`}//pre,v-pre指令,data={pre:true}if(el.pre){data+=`pre:true,`}//动态组件<componentis="xxx">,data={tag:'component'}if(el.component){data+=`tag:"${el.tag}",`}/*为节点执行模块(class、style)的genData方法,得到data={staticClass:xx,class:xx,staticStyle:xx,style:xx}moduledatagenerationfunctions*/for(leti=0;i<state.dataGenFns.length;i++){data+=state.dataGenFns[i](el)}/*其它属性,得到data={attrs:静态属性字符串}或者data={attrs:'_d(静态属性字符串,动态属性字符串)'}attributes*/if(el.attrs){data+=`attrs:${genProps(el.attrs)},`}//DOMprops,结果el.attrs相同if(el.props){data+=`domProps:${genProps(el.props)},`}/*自定义事件-data={`on${eventName}:handleCode`}或者-{`on_d(${eventName}:handleCode`,`${eventName},handleCode`)}eventhandlers*/if(el.events){data+=`${genHandlers(el.events,false)},`}/*带.native修饰符的事件,-data={`nativeOn${eventName}:handleCode`}或者-{`nativeOn_d(${eventName}:handleCode`,`${eventName},handleCode`)*/if(el.nativeEvents){data+=`${genHandlers(el.nativeEvents,true)},`}/*非作用域插槽,得到data={slot:slotName}slottargetonlyfornon-scopedslots*/if(el.slotTarget&&!el.slotScope){data+=`slot:${el.slotTarget},`}//scopedslots,作用域插槽,data={scopedSlots:'_u(xxx)'}if(el.scopedSlots){data+=`${genScopedSlots(el,el.scopedSlots,state)},`}/*处理v-model属性,得到data={model:{value,callback,expression}}componentv-model*/if(el.model){data+=`model:{value:${el.model.value},callback:${el.model.callback},expression:${el.model.expression}},`}/*inline-template,处理内联模版,得到:data={inlineTemplate:{render:function(){render函数},staticRenderFns:[function(){},...]}}*/if(el.inlineTemplate){constinlineTemplate=genInlineTemplate(el,state)if(inlineTemplate){data+=`${inlineTemplate},`}}//删掉JSON字符串最后的逗号,然后加上闭合括号}data=data.replace(/,$/,'')+'}'/*v-bind动态参数包装必须使用相同的v-bind对象应用动态绑定参数合并辅助对象,以便正确处理class/style/mustUseProp属性。*/if(el.dynamicAttrs){data=`_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`}//v-binddatawrapif(el.wrapData){data=el.wrapData(data)}//v-ondatawrapif(el.wrapListeners){data=el.wrapListeners(data)}returndata}

genDirectives() 方法

文件位置:src\compiler\codegen\index.js

/**运行指令的编译方法,如果指令存在运行时任务,则返回directives:[{name,rawName,value,arg,modifiers},...}]*/functiongenDirectives(el:ASTElement,state:CodegenState):string|void{//获取指令数组constdirs=el.directives//不存在指令,直接结束if(!dirs)return//指令的处理结果letres='directives:['//用于标记指令是否需要在运行时完成的任务,比如v-model的input事件lethasRuntime=falseleti,l,dir,needRuntime//遍历指令数组for(i=0,l=dirs.length;i<l;i++){dir=dirs[i]needRuntime=true//获取节点当前指令的处理方法,比如web平台的v-html、v-text、v-modelconstgen:DirectiveFunction=state.directives[dir.name]if(gen){//执行指令的编译方法,如果指令还需要运行时完成一部分任务,则返回true,比如v-modelneedRuntime=!!gen(el,dir,state.warn)}if(needRuntime){//表示该指令在运行时还有任务hasRuntime=true//res=directives:[{name,rawName,value,arg,modifiers},...]res+=`{name:"${dir.name}",rawName:"${dir.rawName}"${dir.value?`,value:(${dir.value}),expression:${JSON.stringify(dir.value)}`:''}${dir.arg?`,arg:${dir.isDynamicArg?dir.arg:`"${dir.arg}"`}`:''}${dir.modifiers?`,modifiers:${JSON.stringify(dir.modifiers)}`:''}},`}}//只有指令存在运行时任务时,才会返回resif(hasRuntime){returnres.slice(0,-1)+']'}}

genDirectives() 方法

文件位置:src\compiler\codegen\index.js

/*遍历属性数组props,得到所有属性组成的字符串如果不存在动态属性,则返回:'attrName,attrVal,...'如果存在动态属性,则返回:'_d(静态属性字符串,动态属性字符串)'*/functiongenProps(props:Array<ASTAttr>):string{//静态属性letstaticProps=``//动态属性letdynamicProps=``//遍历属性数组for(leti=0;i<props.length;i++){//属性constprop=props[i]//属性值constvalue=__WEEX__?generateValue(prop.value):transformSpecialNewlines(prop.value)if(prop.dynamic){//动态属性,`dAttrName,dAttrVal,...`dynamicProps+=`${prop.name},${value},`}else{//静态属性,'attrName:attrVal,...'staticProps+=`"${prop.name}":${value},`}}//闭合静态属性字符串,并去掉静态属性最后的','staticProps=`{${staticProps.slice(0,-1)}}`if(dynamicProps){//如果存在动态属性则返回:_d(静态属性字符串,动态属性字符串)return`_d(${staticProps},[${dynamicProps.slice(0,-1)}])`}else{//说明属性数组中不存在动态属性,直接返回静态属性字符串returnstaticProps}}

genHandlers() 方法

文件位置:src\compiler\codegen\events.js

/*生成自定义事件的代码动态:'nativeOn|on_d(staticHandlers,[dynamicHandlers])'静态:`nativeOn|on${staticHandlers}`*/exportfunctiongenHandlers(events:ASTElementHandlers,isNative:boolean):string{//原生为nativeOn,否则为onconstprefix=isNative?'nativeOn:':'on:'//静态letstaticHandlers=``//动态letdynamicHandlers=``/*遍历events数组events=[{name:{value:回调函数名,...}}]*/for(constnameinevents){consthandlerCode=genHandler(events[name])if(events[name]&&events[name].dynamic){//动态,dynamicHandles=`eventName,handleCode,...,`dynamicHandlers+=`${name},${handlerCode},`}else{//staticHandlers=`eventName:handleCode,...,`staticHandlers+=`"${name}":${handlerCode},`}}//闭合静态事件处理代码字符串,去除末尾的','staticHandlers=`{${staticHandlers.slice(0,-1)}}`if(dynamicHandlers){//动态,on_d(statickHandles,[dynamicHandlers])returnprefix+`_d(${staticHandlers},[${dynamicHandlers.slice(0,-1)}])`}else{//静态,`on${staticHandlers}`returnprefix+staticHandlers}}

genStatic() 方法

文件位置:src\compiler\codegen\index.js

/*生成静态节点的渲染函数1、将当前静态节点的渲染函数放到staticRenderFns数组中2、返回一个可执行函数_m(idx,trueor'')hoiststaticsub-treesout*/functiongenStatic(el:ASTElement,state:CodegenState):string{//标记当前静态节点已经被处理过了el.staticProcessed=true/*某些元素(模板)在v-pre节点中需要有不同的行为所有pre节点都是静态根,因此可将其用作包装状态更改并在退出pre节点时将其重置*/constoriginalPreState=state.preif(el.pre){state.pre=el.pre}/*将静态根节点的渲染函数push到staticRenderFns数组中,比如:[`with(this){return_c(tag,data,children)}`]*/state.staticRenderFns.push(`with(this){return${genElement(el,state)}}`)state.pre=originalPreState/*返回一个可执行函数:_m(idx,trueor'')idx=当前静态节点的渲染函数在staticRenderFns数组中下标*/return`_m(${state.staticRenderFns.length-1}${el.staticInFor?',true':''})`}

genOnce() 方法

文件位置:src\compiler\codegen\index.js

/*处理带有v-once指令的节点,结果会有三种:1、当前节点存在v-if指令,得到一个三元表达式,condition?render1:render22、当前节点是一个包含在v-for指令内部的静态节点,得到`_o(_c(tag,data,children),number,key)`3、当前节点就是一个单纯的v-once节点,得到`_m(idx,trueof'')`v-once*/functiongenOnce(el:ASTElement,state:CodegenState):string{//标记当前节点的v-once指令已经被处理过了el.onceProcessed=trueif(el.if&&!el.ifProcessed){/*如果含有v-if指令&&if指令没有被处理过则处理带有v-if指令的节点,最终得到一个三元表达式:condition?render1:render2*/returngenIf(el,state)}elseif(el.staticInFor){/*说明当前节点是被包裹在还有v-for指令节点内部的静态节点获取v-for指令的key*/letkey=''letparent=el.parentwhile(parent){if(parent.for){key=parent.keybreak}parent=parent.parent}//key不存在则给出提示,v-once节点只能用于带有key的v-for节点内部if(!key){process.env.NODE_ENV!=='production'&&state.warn(`v-oncecanonlybeusedinsidev-forthatiskeyed.`,el.rawAttrsMap['v-once'])returngenElement(el,state)}//生成`_o(_c(tag,data,children),number,key)`return`_o(${genElement(el,state)},${state.onceId++},${key})`}else{/*上面几种情况都不符合,说明就是一个简单的静态节点,和处理静态根节点时的操作一样,得到_m(idx,trueor'')*/returngenStatic(el,state)}}

genFor() 方法

文件位置:src\compiler\codegen\index.js

/*处理节点上v-for指令得到`_l(exp,function(alias,iterator1,iterator2){return_c(tag,data,children)})`*/exportfunctiongenFor(el:any,state:CodegenState,altGen?:Function,altHelper?:string):string{//v-for的迭代器,比如一个数组constexp=el.for//迭代时的别名constalias=el.alias//iterator为v-for="(item,idx)inobj"时会有,比如iterator1=idxconstiterator1=el.iterator1?`,${el.iterator1}`:''constiterator2=el.iterator2?`,${el.iterator2}`:''//提示,v-for指令在组件上时必须使用keyif(process.env.NODE_ENV!=='production'&&state.maybeComponent(el)&&el.tag!=='slot'&&el.tag!=='template'&&!el.key){state.warn(`<${el.tag}v-for="${alias}in${exp}">:componentlistsrenderedwith`+`v-forshouldhaveexplicitkeys.`+`Seehttps://vuejs.org/guide/list.html#keyformoreinfo.`,el.rawAttrsMap['v-for'],true/*tip*/)}//标记当前节点上的v-for指令已经被处理过了el.forProcessed=true//avoidrecursion//返回`_l(exp,function(alias,iterator1,iterator2){return_c(tag,data,children)})`return`${altHelper||'_l'}((${exp}),`+`function(${alias}${iterator1}${iterator2}){`+`return${(altGen||genElement)(el,state)}`+'})'}

genIf() 方法

文件位置:src\compiler\codegen\index.js

//处理带有v-if指令的节点,最终得到一个三元表达式,condition?render1:render2exportfunctiongenIf(el:any,state:CodegenState,altGen?:Function,altEmpty?:string):string{//标记当前节点的v-if指令已经被处理过了,避免无效的递归el.ifProcessed=true//avoidrecursion//得到三元表达式,condition?render1:render2returngenIfConditions(el.ifConditions.slice(),state,altGen,altEmpty)}functiongenIfConditions(conditions:ASTIfConditions,state:CodegenState,altGen?:Function,altEmpty?:string):string{//长度若为空,则直接返回一个空节点渲染函数if(!conditions.length){returnaltEmpty||'_e()'}//从conditions数组中拿出第一个条件对象{exp,block}constcondition=conditions.shift()//返回结果是一个三元表达式字符串,condition?渲染函数1:渲染函数2if(condition.exp){/*如果condition.exp条件成立,则得到一个三元表达式,如果条件不成立,则通过递归的方式找conditions数组中下一个元素,直到找到条件成立的元素,然后返回一个三元表达式*/return`(${condition.exp})?${genTernaryExp(condition.block)}:${genIfConditions(conditions,state,altGen,altEmpty)}`}else{return`${genTernaryExp(condition.block)}`}//v-ifwithv-onceshouldgeneratecodelike(a)?_m(0):_m(1)functiongenTernaryExp(el){returnaltGen?altGen(el,state):el.once?genOnce(el,state):genElement(el,state)}}

genSlot() 方法

文件位置:src\compiler\codegen\index.js

/*生成插槽的渲染函数,得到:_t(slotName,children,attrs,bind)*/functiongenSlot(el:ASTElement,state:CodegenState):string{//插槽名称constslotName=el.slotName||'"default"'//生成所有的子节点constchildren=genChildren(el,state)//结果字符串,_t(slotName,children,attrs,bind)letres=`_t(${slotName}${children?`,function(){return${children}}`:''}`constattrs=el.attrs||el.dynamicAttrs?genProps((el.attrs||[]).concat(el.dynamicAttrs||[]).map(attr=>({//slotpropsarecamelizedname:camelize(attr.name),value:attr.value,dynamic:attr.dynamic}))):nullconstbind=el.attrsMap['v-bind']if((attrs||bind)&&!children){res+=`,null`}if(attrs){res+=`,${attrs}`}if(bind){res+=`${attrs?'':',null'},${bind}`}returnres+')'}

genComponent() 方法

文件位置:src\compiler\codegen\index.js

/*生成动态组件的渲染函数,返回`_c(compName,data,children)`componentNameisel.component,takeitasargumenttoshunflow'spessimisticrefinement*/functiongenComponent(componentName:string,el:ASTElement,state:CodegenState):string{//所有的子节点constchildren=el.inlineTemplate?null:genChildren(el,state,true)//返回`_c(compName,data,children)`,compName是is属性的值return`_c(${componentName},${genData(el,state)}${children?`,${children}`:''})`}

总结

渲染函数的生成过程是什么?

编译器生成的渲染有两类:

  • render 函数,负责生成动态节点的 vnode

  • staticRenderFns 数组中的 静态渲染函数,负责生成静态节点的 vnode

渲染函数的生成过程,其实就是在遍历 AST 节点,通过递归的方式处理每个节点,最后生成格式如:_c(tag, attr, children, normalizationType)

  • tag 是标签名

  • attr 是属性对象

  • children 是子节点组成的数组,其中每个元素的格式都是 _c(tag, attr, children, normalizationTYpe) 的形式,

  • normalization 表示节点的规范化类型,是一个数字 0、1、2

静态节点是怎么处理的?

静态节点的处理分为两步:

  • 将生成静态节点 vnode 函数放到 staticRenderFns 数组中

  • 返回一个 _m(idx) 的可执行函数,即执行 staticRenderFns 数组中下标为 idx 的函数,生成静态节点的 vnode

v-once、v-if、v-for、组件 等都是怎么处理的?

  • 单纯的 v-once 节点处理方式 和 静态节点 一致

  • v-if 节点的处理结果是一个 三元表达式

  • v-for 节点的处理结果是可执行的 _l 函数,该函数负责生成 v-for 节点的 vnode

  • 组件的处理结果和普通元素一样,得到的是形如 _c(compName) 的可执行代码,生成组件的 vnode

 </div> <div class="zixun-tj-product adv-bottom"></div> </div> </div> <div class="prve-next-news">
本文:vue编译器如何生成渲染函数的详细内容,希望对您有所帮助,信息来源于网络。
上一篇:如何使用Python绘制时间序列图下一篇:

5 人围观 / 0 条评论 ↓快速评论↓

(必须)

(必须,保密)

阿狸1 阿狸2 阿狸3 阿狸4 阿狸5 阿狸6 阿狸7 阿狸8 阿狸9 阿狸10 阿狸11 阿狸12 阿狸13 阿狸14 阿狸15 阿狸16 阿狸17 阿狸18