……
指令这个讲起来还有点复杂,先把html弄上来:
<body> <div id='app'> <div v-if="vIfIter" v-bind:style="styleObject"> <input v-show="vShowIter" v-model='vModel' /> <span v-once>{{msg}}</span> <div v-html="html"></div> </div> <div class='on'>empty Node</div> </div> </body> <script src='./vue.js'></script> <script> var app = new Vue({ el: '#app', data: { vIfIter: true, vShowIter: true, vModel: 1, styleObject: { color: 'red' }, msg: 'Hello World', html: '<span>v-html</span>' }, }); </script>
上一节是解析完了input标签的2个属性,并将其打包进了directives属性中返回。
接着会继续跑genData函数,如下:
function genData(el) { var data = '{'; // 从这出来 var dirs = genDirectives(el); if (dirs) { data += dirs + ','; } // code... // DOM props if (el.props) { data += "domProps:{" + (genProps(el.props)) + "},"; } // event handlers if (el.events) { data += (genHandlers(el.events, false, warn$3)) + ","; } // code... data = data.replace(/,$/, '') + '}'; // v-bind data wrap if (el.wrapData) { data = el.wrapData(data); } return data }
由于在处理v-model的时候给el添加了props与events属性,所以之后会跑进两个处理函数。
首先看看genProps:
// props => {name:value,value:(vModel)} function genProps(props) { var res = ''; for (var i = 0; i < props.length; i++) { var prop = props[i]; res += """ + (prop.name) + "":" + (transformSpecialNewlines(prop.value)) + ","; } return res.slice(0, -1) } // 替换新换行符 function transformSpecialNewlines(text) { return text .replace(/u2028/g, '\u2028') .replace(/u2029/g, '\u2029') }
比较简单,直接看返回的字符串:
接下来是处理添加的events:
// event handlers if (el.events) { data += (genHandlers(el.events, false, warn$3)) + ","; } function genHandlers(events, native, warn) { var res = native ? 'nativeOn:{' : 'on:{'; for (var name in events) { var handler = events[name]; // click.right => contextmenu // 右键并不能触发click事件 建议使用H5新属性 然而这个属性兼容性捉急 res += """ + name + "":" + (genHandler(name, handler)) + ","; } return res.slice(0, -1) + '}'; } // name => inpout // handler => {modifiers => null,value:'if(...)...'} function genHandler(name, handler) { if (!handler) { return 'function(){}' } if (Array.isArray(handler)) { return ("[" + (handler.map(function(handler) { return genHandler(name, handler); }).join(',')) + "]") } // simplePathRE => /^s*[A-Za-z_$][w$]*(?:.[A-Za-z_$][w$]*|['.*?']|[".*?"]|[d+]|[[A-Za-z_$][w$]*])*s*$/; // 这是匹配的啥玩意啊! var isMethodPath = simplePathRE.test(handler.value); // fnExpRE => /^s*([w$_]+|([^)]*?))s*=>|^functions*(/; // 匹配箭头函数或function var isFunctionExpression = fnExpRE.test(handler.value); // 匹配完 两个都是false // 包装成函数形式返回 if (!handler.modifiers) { return isMethodPath || isFunctionExpression ? handler.value : ("function($event){" + (handler.value) + "}") // inline statement } // 处理事件后缀 else { // code... } }
事件处理方面也很明了,首先判断是原生事件还是自定义,然后根据表达式的形式进行处理返回,本例会被包装为一个普通的函数返回,如图:
字符串代表DOM上有一个input事件,值为"on:..."
至此,input标签AST转换完毕,返回的code字符串如图:
"_c('input', {directives:[{name:"show",rawName:"v-show",value:(vShowIter),expression:"vShowIter"},{name:"model",rawName:"v-model",value:(vModel),expression:"vModel"}], domProps:{"value":(vModel)}, on:{"input":function($event){if($event.target.composing)return;vModel=$event.target.value}}})"
包含tag名、内置指令v-model/v-show、props、events。
下面处理span标签,包含一个v-once:
<span v-once>{{msg}}</span>
这个el有个once属性,会进入genOnce函数:
function genOnce(el) { el.onceProcessed = true; // 优先处理v-if if (el.if && !el.ifProcessed) { return genIf(el) } else if (el.staticInFor) { // 同时出现v-once与v-for } else { return genStatic(el) } } // hoist static sub-trees out function genStatic(el) { el.staticProcessed = true; // 该节点属于静态dom staticRenderFns.push(("with(this){return " + (genElement(el)) + "}")); return ("_m(" + (staticRenderFns.length - 1) + (el.staticInFor ? ',true' : '') + ")") } function genElement(el) { if (el.staticRoot && !el.staticProcessed) { // once/for/if/templte/slot } else { // component or element var code; if (el.component) { code = genComponent(el.component, el); } else { var data = el.plain ? undefined : genData(el); var children = el.inlineTemplate ? null : genChildren(el, true); code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")"; } // module transforms => return code } }
由于v-once是一次性渲染,所以会被归类于静态渲染,render函数会被弹入staticRenderFns数组。
这里还有一个值得注意的是,由于span节点只有v-once,并没有其余的属性,没有必要进入genData进行属性切割,所以早在parseHTML阶段,该el的plain属性被置为true,跳过genData阶段。
// parseHTML => processRawAttrs function processRawAttrs(el) { var l = el.attrsList.length; if (l) { // code... } else if (!el.pre) { el.plain = true; } }
呃……
没有属性处理会genChildren,子节点是一个表达式,会返回一个如图字符串:,最开始跑源码的时候见过_v、_s,不用的是该表达式被弹入了staticRenderFns数组。
至此,span标签解析完毕。
下面开始解析下一个div标签:
<div v-html="html"></div>
该标签包含一个内置指令,所以会进入genDirectives分支,并调用对应的gen函数处理v-html:
function html(el, dir) { // dir.value => html if (dir.value) { addProp(el, 'innerHTML', ("_s(" + (dir.value) + ")")); } }
代码很简单,将v-html对应的值用_s包裹起来,添加到props属性中,如图:
这里需要注意的是,该函数并未返回任何值,因为v-html不需要弄进directives属性,所以默认返回的是undefined,直接会跳过后面条件判断返回。
因为设置了props属性,所以后面的genProps也要跑,过程不看了直接看结果:
至此,v-html的标签解析完了,返回一个render函数:
下面解析根元素的下一个子元素,静态div:
<div class='on'>empty Node</div>
调用流程是这样的:generate => genChildren => genNode => genElement => genData && genChildren
节点包含一个静态的类以及字符串子节点,所以会调用最后的genData与genChildren返回一个render字符串,流程比较简单,所以直接放结果:
到此为止,所有的AST都被转化为render函数,可以看一下结构:
_c('div' /*<div id='app'>*/ , { attrs: { "id": "app" } }, [(vIfIter) /*v-if条件*/ ? // 条件为真渲染下面的DOM _c('div' /*<div v-if="vIfIter" v-bind:style="styleObject">*/ , { style: (styleObject) }, [_c('input' /*<input v-show="vShowIter" v-model='vModel' />*/ , { directives: [{ name: "show", rawName: "v-show", value: (vShowIter), expression: "vShowIter" }, { name: "model", rawName: "v-model", value: (vModel), expression: "vModel" }], domProps: { "value": (vModel) }, on: { "input": function($event) { if ($event.target.composing) return; vModel = $event.target.value } } }), _v(" ") /*这些是回车换行符*/ , _m(0) /*<span v-once>{{msg}}</span>*/ , _v(" "), _c('div' /*<div v-html="html"></div>*/ , { domProps: { "innerHTML": _s(html) } }) ]) : // 否则渲染一个空的div...(错了) _e() /*comment*/ , _v(" "), _c('div' /*<div class='on'>empty Node</div>*/ , { staticClass: "on" }, [_v("empty Node")]) ])
这样看起来结构就非常清晰了,render函数针对v-if是使用一个三元表示式处理的,该函数的整体框架结构为_c(tagName,attrs,childrens),其中childrens可能嵌套多个_c。
其中有个小点需要注意的是里面并没有v-once的那个span标签,取而代之的是一个_m(0),这是因为span标签是静态节点,所以被弹入staticRenderFns数组并作为第一个元素,这里将来会将其取出来并渲染。
这个patch下次再讲,最近被项目搞的脑袋疼。