Vue源码后记-其余内置指令(1)
把其余的内置指令也搞完吧,来一个全家桶。
案例如下:
<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>
基本上内置指令都有,由于v-on涉及事件,也就是methods,这个后面再说,这里暂时只处理指令。另外添加了一个纯净的节点,可以跑一下ref和optimize。
跳过前面所有无聊的流程,直接进入parseHTML,切割方面也没什么看头,最外层div切割完,会进入v-if那个标签,即:
<div v-if="vIfIter" v-bind:style="styleObject">
正常切割后,如图所示:
attrs存放着该标签的2个属性,分别为v-if与v-bind:style,简单的切割后,会调用handleStart进一步处理,其中就包含一系列process函数:
function start(tag, attrs, unary) { // code... if (inVPre) { processRawAttrs(element); } else { processFor(element); processIf(element); processOnce(element); processKey(element); element.plain = !element.key && !attrs.length; processRef(element); processSlot(element); processComponent(element); for (var i$1 = 0; i$1 < transforms.length; i$1++) { transforms[i$1](element, options); } processAttrs(element); } // code... }
这里对for、if、once等内置指令进行2次处理,for之前专门分析过一节,所以不管,首先看看if:
// el为之前的切割对象 function processIf(el) { // 将v-if从attrsList中移除 因为会影响render函数的生成 var exp = getAndRemoveAttr(el, 'v-if'); if (exp) { // el.if => vIfIter el.if = exp; addIfCondition(el, { exp: exp, block: el }); } // 处理else与else-if else { // code... } } // 保存节点display状态 function addIfCondition(el, condition) { if (!el.ifConditions) { el.ifConditions = []; } el.ifConditions.push(condition); }
案例只有v-if,else和else-if有兴趣自己去玩吧,处理完后得到这么一个对象:,与v-for类似,有一个属性专门保存对象的值,另外有个codition保存状态。
接下来处理v-bind:style属性,处理函数在下面的transforms数组中,一个负责class,一个负责style,看一个就行了。
function transformNode$1(el, options) { var warn = options.warn || baseWarn; // 获取静态style属性并添加在staticStyle属性上 var staticStyle = getAndRemoveAttr(el, 'style'); if (staticStyle) { // warning... el.staticStyle = JSON.stringify(parseStyleText(staticStyle)); } // 获取动态绑定的style var styleBinding = getBindingAttr(el, 'style', false /* getStatic */ ); if (styleBinding) { el.styleBinding = styleBinding; } } // 该函数专门用来处理v-bind绑定的属性 // name => style // getStatic => false function getBindingAttr(el, name, getStatic) { // 处理缩写: var dynamicValue = getAndRemoveAttr(el, ':' + name) || getAndRemoveAttr(el, 'v-bind:' + name); // dynamicValue => styleObject if (dynamicValue != null) { return parseFilters(dynamicValue) } else if (getStatic !== false) { var staticValue = getAndRemoveAttr(el, name); if (staticValue != null) { return JSON.stringify(staticValue) } } }
这里分别对静态的style动态(v-bind:style)的style分别进行处理,静态的直接抽取出来JSON化保存到一个属性。
动态的根据简写或全名来进行获取,获取到对应的值,这里是styleObject,然后调用parseFilters进行过滤。
这个函数当初抄源码那个恶心哦。。。
这里放一下这个函数:
// 处理过滤器 function parseFilters(exp) { var inSingle = false; var inDouble = false; var inTemplateString = false; var inRegex = false; var curly = 0; var square = 0; var paren = 0; var lastFilterIndex = 0; var c, prev, i, expression, filters; // 遍历字符串 for (i = 0; i < exp.length; i++) { // prev => 上一个字符 // c => 当前字符 prev = c; c = exp.charCodeAt(i); if (inSingle) { if (c === 0x27 && prev !== 0x5C) { inSingle = false; } } else if (inDouble) { if (c === 0x22 && prev !== 0x5C) { inDouble = false; } } else if (inTemplateString) { if (c === 0x60 && prev !== 0x5C) { inTemplateString = false; } } else if (inRegex) { if (c === 0x2f && prev !== 0x5C) { inRegex = false; } } // 单独出现|符号 且大中小括号分别配对 else if ( c === 0x7C && // | exp.charCodeAt(i + 1) !== 0x7C && exp.charCodeAt(i - 1) !== 0x7C && !curly && !square && !paren ) { if (expression === undefined) { // 截取expresion为|符号前面的字符串 lastFilterIndex = i + 1; expression = exp.slice(0, i).trim(); } else { pushFilter(); } } else { switch (c) { case 0x22: inDouble = true; break // " case 0x27: inSingle = true; break // ' case 0x60: inTemplateString = true; break // ` case 0x28: paren++; break // ( case 0x29: paren--; break // ) case 0x5B: square++; break // [ case 0x5D: square--; break // ] case 0x7B: curly++; break // { case 0x7D: curly--; break // } } // 正则表达式 if (c === 0x2f) { // / var j = i - 1; var p = (void 0); // find first non-whitespace prev char for (; j >= 0; j--) { p = exp.charAt(j); if (p !== ' ') { break } } if (!p || !validDivisionCharRE.test(p)) { inRegex = true; } } } } if (expression === undefined) { expression = exp.slice(0, i).trim(); } else if (lastFilterIndex !== 0) { // 这里截取过滤函数 pushFilter(); } function pushFilter() { (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim()); lastFilterIndex = i + 1; } if (filters) { for (i = 0; i < filters.length; i++) { expression = wrapFilter(expression, filters[i]); } } return expression } function wrapFilter(exp, filter) { var i = filter.indexOf('('); if (i < 0) { // _f: resolveFilter return ("_f(\"" + filter + "\")(" + exp + ")") } else { var name = filter.slice(0, i); var args = filter.slice(i + 1); return ("_f(\"" + name + "\")(" + exp + "," + args) } }
这个函数很长很长,主要是格式化绑定的值。由官网的实例可知,模板语法支持形式诸如{{message | filter}}或者v-bind:id='str | filter',甚至支持正则语法。而这个函数就是处理这种复杂值的。
而筛选器涉及到options的参数filter,不在本篇的内置指令讨论之内,所以暂时跳过。这里会直接返回传进去的字符串,即:,然后顺便作为属性绑定到vm对象上。
切割完v-if的div标签,接下来是:
<input v-show="vShowIter" v-model='vModel' />
该标签属于自闭合标签。依照惯例,依次用正则分割出两个attr,象征性放个图:
接下来也会进入handleStartTag函数,处理分割出的各种属性。
在处理v-show与v-model时,并没有专门的process函数,这些内置指令被统一用一个processAttrs处理,这里看看是如何被处理的:
// 处理其余的v-指令 function processAttrs(el) { var list = el.attrsList; var i, l, name, rawName, value, modifiers, isProp; for (i = 0, l = list.length; i < l; i++) { name = rawName = list[i].name; value = list[i].value; // dirRE => /^v-|^@|^:/ // 专业匹配v- @ :三剑客 if (dirRE.test(name)) { el.hasBindings = true; // 匹配一些后缀 诸如事件的.self/.prevent等 modifiers = parseModifiers(name); // 截取到后缀后去掉 if (modifiers) { name = name.replace(modifierRE, ''); } // bindRE => /^:|^v-bind:/ // 处理v-bind绑定的属性 if (bindRE.test(name)) { // v-bind // code... } // onRE => /^@|^v-on:/ else if (onRE.test(name)) { // 绑定事件处理器 name = name.replace(onRE, ''); addHandler(el, name, value, modifiers, false, warn$2); } // 普通指令 else { // 截取v-后面的字符串 name = name.replace(dirRE, ''); // argRE => /:(.*)$/ var argMatch = name.match(argRE); var arg = argMatch && argMatch[1]; if (arg) { name = name.slice(0, -(arg.length + 1)); } addDirective(el, name, rawName, value, arg, modifiers); // v-for的别名跟v-model重复 if ("development" !== 'production' && name === 'model') { checkForAliasModel(el, value); } } } else { // 处理普通的属性绑定 // warning:<div id="{{ val }}"> => use <div :id="val"> addAttr(el, name, JSON.stringify(value)); } } } // name => show、model function parseModifiers(name) { // modifierRE => /\.[^.]+/g var match = name.match(modifierRE); if (match) { var ret = {}; match.forEach(function(m) { ret[m.slice(1)] = true; }); return ret } } // el => function addDirective(el, name, rawName, value, arg, modifiers) { (el.directives || (el.directives = [])).push({ name: name, rawName: rawName, value: value, arg: arg, modifiers: modifiers }); }
这里分别处理三种情况:v-、:、@,分别是内置指令、属性绑定、事件绑定,分别调用不同的方法处理并添加对应的属性到vm上。
内置指令处理完会生成一个directives的数组属性绑定到vm上,并将切割后的属性对象作为数组元素,如图:
进行下一个tag切割,即:
<span v-once>{{msg}}</span>
这里的内置属性为v-once,有一个processOnce函数处理这个指令:
function processOnce(el) { var once$$1 = getAndRemoveAttr(el, 'v-once'); if (once$$1 != null) { el.once = true; } }
太简单,注释都懒得写了。
处理{{msg}}的过程就不写了,在跑源码的时候用的就是这个形式。
下一个tag:
<div v-html="html"></div>
这个指令没有特殊函数处理,被丢到了processAttrs函数,然后通过addDirective添加到directives数组中,如图:
至此,所有的内置指令相关的标签都解析完了,还剩一个纯净的DOM节点:
<div class='on'>empty Node</div>
正常切割完节点后开始解析属性,此处的class并没有用v-bind进行绑定,所以在调用transformNode方法处理class属性时,会被认定为static属性,如下:
function transformNode(el, options) { var warn = options.warn || baseWarn; // 获取静态的class => on var staticClass = getAndRemoveAttr(el, 'class'); // warning... if (staticClass) { // JSON化后作为属性添加到对象上 el.staticClass = JSON.stringify(staticClass); } var classBinding = getBindingAttr(el, 'class', false /* getStatic */ ); if (classBinding) { el.classBinding = classBinding; } }
弄完,大概是个这样子:
AST转化完后会进入optimize阶段,可以稍微讲下这个地方,首先会对所有节点进行标记:
function optimize(root, options) { if (!root) { return } isStaticKey = genStaticKeysCached(options.staticKeys || ''); isPlatformReservedTag = options.isReservedTag || no; // 标记是否静态节点 markStatic$1(root); // 标记根节点 markStaticRoots(root, false); } function markStatic$1(node) { // 标记当前节点是否为静态节点 node.static = isStatic(node); if (node.type === 1) { if (!isPlatformReservedTag(node.tag) && node.tag !== 'slot' && node.attrsMap['inline-template'] == null ) { return } for (var i = 0, l = node.children.length; i < l; i++) { // 遍历子节点递归判断 var child = node.children[i]; markStatic$1(child); // 如果子节点是非静态 那么父节点也是非静态 if (!child.static) { node.static = false; } } } } function isStatic(node) { // {{...}}大括号表达式 if (node.type === 2) { // expression return false } // 纯文本节点 if (node.type === 3) { // text return true } // 子节点没有hasBindings、v-for、v-if、非slot/component标签、组件、静态属性判断 return !!(node.pre || (!node.hasBindings && // no dynamic bindings !node.if && !node.for && // not v-if or v-for or v-else !isBuiltInTag(node.tag) && // not a built-in isPlatformReservedTag(node.tag) && // not a component !isDirectChildOfTemplateFor(node) && Object.keys(node).every(isStaticKey) )) }
这里一层一层递归进行标记,所有的AST对象会添加一个static属性,只有最后那个纯净节点的static标记为true(其实有3个子节点,多出来的是排版形成的回车换行符):
当一个节点被标记为静态节点,之后的虚拟DOM在通过diff算法比较差异时会跳过该节点以提升效率,这就是AST的优化。
静态节点标记完后,还有最后一步,调用markStaticRoots函数进行二次优化,并会对v-if做特殊处理:
function markStaticRoots(node, isInFor) { if (node.type === 1) { if (node.static || node.once) { node.staticInFor = isInFor; } // For a node to qualify as a static root, it should have children that // are not just static text. Otherwise the cost of hoisting out will // outweigh the benefits and it's better off to just always render it fresh. if (node.static && node.children.length && !( node.children.length === 1 && node.children[0].type === 3 )) { node.staticRoot = true; return } else { node.staticRoot = false; } if (node.children) { for (var i = 0, l = node.children.length; i < l; i++) { markStaticRoots(node.children[i], isInFor || !!node.for); } } // v-if if (node.ifConditions) { walkThroughConditionsBlocks(node.ifConditions, isInFor); } } }
这里的优化直接看那段注释就可以了,大概意思是:如果一个节点的子节点只有一个表达式,那就没有必要当做非静态节点。
看一下v-if的处理函数:
function walkThroughConditionsBlocks(conditionBlocks, isInFor) { for (var i = 1, len = conditionBlocks.length; i < len; i++) { markStaticRoots(conditionBlocks[i].block, isInFor); } }
看毛,直接跳出来了,这里的ifCondition只有一个值,所以跳过了。
优化完,会进行generate,把AST转化为函数:
function generate(ast, options) { // save previous staticRenderFns so generate calls can be nested // code... staticRenderFns = prevStaticRenderFns; onceCount = prevOnceCount; return { render: ("with(this){return " + code + "}"), staticRenderFns: currentStaticRenderFns } } // 处理静态、v-once、v-for、v-if、template/slot function genElement(el) { if (el.staticRoot && !el.staticProcessed) { return genStatic(el) } else if (el.once && !el.onceProcessed) { return genOnce(el) } else if (el.for && !el.forProcessed) { return genFor(el) } else if (el.if && !el.ifProcessed) { return genIf(el) } else if (el.tag === 'template' && !el.slotTarget) { return genChildren(el) || 'void 0' } else if (el.tag === 'slot') { return genSlot(el) } else { // component or element // code... return code } }
案例中的v-if、v-once在这里都会被特殊处理,首先看一下v-if:
function genIf(el) { el.ifProcessed = true; // avoid recursion return genIfConditions(el.ifConditions.slice()) } function genIfConditions(conditions) { if (!conditions.length) { return '_e()' } // 取出v-if对应的表达式 => vIfIter var condition = conditions.shift(); if (condition.exp) { return ("(" + (condition.exp) + ")?" + (genTernaryExp(condition.block)) + ":" + (genIfConditions(conditions))) } else { return ("" + (genTernaryExp(condition.block))) } // v-if with v-once should generate code like (a)?_m(0):_m(1) // 处理v-once与v-if同时出现的情况 function genTernaryExp(el) { return el.once ? genOnce(el) : genElement(el) } }
函数首先会将ifCondition标记为true,防止递归处理子节点时候又跳到这个函数,接下来会判断该节点是否同时有v-once,这里没有,调用genElement处理其余属性。
在genData中,会对class与style进行处理,其中也包括v-bind绑定的属性:
function genData$2(el) { var data = ''; // 静态style if (el.staticStyle) { data += "staticStyle:" + (el.staticStyle) + ","; } // v-bind:style => styleObject if (el.styleBinding) { data += "style:(" + (el.styleBinding) + "),"; } return data }
除去v-if,其余处理被包装为一个字符串,如图:
这里对v-if的render函数包装会暂时停下来,优先递归处理子节点,简单看一下函数:
function genChildren(el, checkSkip) { var children = el.children; if (children.length) { var el$1 = children[0]; // 单纯的v-for子节点 if (children.length === 1 && el$1.for && el$1.tag !== 'template' && el$1.tag !== 'slot') { return genElement(el$1) } // 对每一个子节点做判断 var normalizationType = checkSkip ? getNormalizationType(children) : 0; return ("[" + (children.map(genNode).join(',')) + "]" + (normalizationType ? ("," + normalizationType) : '')) } }
子节点这里做了一点优化,如果只是单纯的v-for,就不做类型区别判断,直接生成render函数,就像之前解析v-for的案例一样,所以上一篇是没有跑这个的。
这一次不太一样,有3个子节点,加上两个空白换行,共有5个。
跳过v-for的判断,会进入子节点类型分类:
// determine the normalization needed for the children array. // 0: no normalization needed // 1: simple normalization needed (possible 1-level deep nested array) // 2: full normalization needed function getNormalizationType(children) { var res = 0; for (var i = 0; i < children.length; i++) { var el = children[i]; if (el.type !== 1) { continue } // el.for !== undefined || el.tag === 'template' || el.tag === 'slot' if (needsNormalization(el) || (el.ifConditions && el.ifConditions.some(function(c) { return needsNormalization(c.block); }))) { res = 2; break } // !isPlatformReservedTag$1 => isHTMLTag(tag) || isSVG(tag) => 非内置标签 if (maybeComponent(el) || (el.ifConditions && el.ifConditions.some(function(c) { return maybeComponent(c.block); }))) { res = 1; } } return res }
函数的作用可以直接看注释,不懂也没关系,这里简单解释一下,该函数将子节点分为三类:
1、默认普通子节点
2、包含v-for属性或者是template/slot的模板标签
3、非内置标签,即自定义组件
三类子节点分别对应res的0、1、2。
这里都是普通子节点,res返回0,跳出来后返回的字符串后面就不会拼接一个类型,直接拼接空字符。
("[" + (children.map(genNode).join(',')) + "]" + (normalizationType ? ("," + normalizationType) : ''))
这里接下来会调用genNode进行子节点处理:
function genNode(node) { if (node.type === 1) { return genElement(node) } else { return genText(node) } }
该函数对不同类型的子节点做不同处理,首先是input标签,进入genElement函数,之前有看过该函数,这里有一点不一样的是会进入genDirectives,处理内置指令v-show、v-model:
function genDirectives(el) { var dirs = el.directives; if (!dirs) { return } var res = 'directives:['; var hasRuntime = false; var i, l, dir, needRuntime; for (i = 0, l = dirs.length; i < l; i++) { dir = dirs[i]; needRuntime = true; // html/model/text || bind/cloak var gen = platformDirectives$1[dir.name] || baseDirectives[dir.name]; if (gen) { // compile-time directive that manipulates AST. // returns true if it also needs a runtime counterpart. needRuntime = !!gen(el, dir, warn$3); } if (needRuntime) { hasRuntime = true; // 很长很长的拼接 res += "{name:\"" + (dir.name) + "\",rawName:\"" + (dir.rawName) + "\"" + (dir.value ? (",value:(" + (dir.value) + "),expression:" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (",arg:\"" + (dir.arg) + "\"") : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},"; } } if (hasRuntime) { return res.slice(0, -1) + ']' } }
函数意思很简单,取出对应的内置指令对象,判断是否存在gen函数,然后进行长拼接,第一个v-show不存在gen函数,所以直接拼接,结果如图:
这里针对第一个指令进行了拼接,接下来还有一个v-model,这个指令存在对应的gen函数,所以流程会多一步:
function model(el, dir, _warn) { warn$1 = _warn; var value = dir.value; var modifiers = dir.modifiers; var tag = el.tag; var type = el.attrsMap.type; { // 有傻逼会用:type绑定input的类型吗??? // 另外type=file是无法用v-model监听的 } // 针对不同的input类型做处理 if (tag === 'select') { genSelect(el, value, modifiers); } else if (tag === 'input' && type === 'checkbox') { genCheckboxModel(el, value, modifiers); } else if (tag === 'input' && type === 'radio') { genRadioModel(el, value, modifiers); } else if (tag === 'input' || tag === 'textarea') { genDefaultModel(el, value, modifiers); } else if (!config.isReservedTag(tag)) { genComponentModel(el, value, modifiers); // component v-model doesn't need extra runtime return false } else { // 不支持v-model的标签 } // ensure runtime directive metadata return true }
这里首先做了错误预判,type属性的无法动态绑定的,file上传的类型v-mode也不起作用,然后针对不同类型的input标签,包括select/checkbox/radio/text/textarea做处理。
由于只是个没有type的<input/>,所以默认为text并进入genDefaultModel分支:
// value => vModel function genDefaultModel(el, value, modifiers) { var type = el.attrsMap.type; var ref = modifiers || {}; var lazy = ref.lazy; var number = ref.number; var trim = ref.trim; var needCompositionGuard = !lazy && type !== 'range'; var event = lazy ? 'change' : type === 'range' ? RANGE_TOKEN : 'input'; // 获取对应input标签的值 // 包含处理后缀为trim、number var valueExpression = '$event.target.value'; if (trim) { valueExpression = "$event.target.value.trim()"; } if (number) { valueExpression = "_n(" + valueExpression + ")"; } var code = genAssignmentCode(value, valueExpression); if (needCompositionGuard) { code = "if($event.target.composing)return;" + code; } addProp(el, 'value', ("(" + value + ")")); addHandler(el, event, code, null, true); if (trim || number || type === 'number') { addHandler(el, 'blur', '$forceUpdate()'); } }
这里生成了获取对应input值得表达式,正常为$event.target.value,$event代表原生的事件,如果有trim/number后缀,会自动调用去空白与数字化函数。
接着调用genAssignmentCode,这里涉及一个idx属性,没搞懂具体是什么情况会出现:
// value => vModel function genAssignmentCode(value, assignment) { // {exp:vModel,idx:null} var modelRs = parseModel(value); if (modelRs.idx === null) { return (value + "=" + assignment) } else { // code... } } function parseModel(val) { str = val; len = str.length; index$1 = expressionPos = expressionEndPos = 0; if (val.indexOf('[') < 0 || val.lastIndexOf(']') < len - 1) { return { exp: val, idx: null } } // code... }
针对本例中简单的属性设置,会直接返回一个对象。
此时code为一个字符串表示式:,很简单就是获取目标节点的值。
下面有一个关于composing的判断,这个属性在MDN是这样解释的:
简单来讲,这是一个只读属性,发生在compositionstart事件之后,compositionend事件之前,这两个事件类似于keyup、keydown,该属性指的是IDE输入过程中,如图:
这里ddd是一个编辑中的状态,触发了composing事件,此时v-model是不响应的,所以可以看到拼接的字符串如下所示:
即:如果文字在编辑中,那么直接返回。
接下来会调用addProp函数:
addProp(el, 'value', ("(" + value + ")"));
一句代码函数,看了就懂是干嘛用的:
// el => dom // name => value // value => (vModel) function addProp(el, name, value) { (el.props || (el.props = [])).push({ name: name, value: value }); }
判断是否有props属性,并添加一个对象到属性中,值得注意的是,这里的值用一个括号包装起来了,所以是(vModel)。
下面是给dom绑定一个事件:
// event => input // code => if(...)... addHandler(el, event, code, null, true); function addHandler(el, name, value, modifiers, important, warn) { // warn prevent and passive modifier // code... // 检测事件是否带有capture/once/paasive后缀 // code... var events; // 没有修饰符生成一个空对象 if (modifiers && modifiers.native) { delete modifiers.native; events = el.nativeEvents || (el.nativeEvents = {}); } else { events = el.events || (el.events = {}); } // 生成一个事件对象 var newHandler = { value: value, modifiers: modifiers }; var handlers = events[name]; /* istanbul ignore if */ if (Array.isArray(handlers)) { important ? handlers.unshift(newHandler) : handlers.push(newHandler); } else if (handlers) { events[name] = important ? [newHandler, handlers] : [handlers, newHandler]; } else { events[name] = newHandler; } }
这里会在当前dom节点的ast上添加一个events属性,值为之前生成的表达式字符串:
处理完这个,最后会对trim、number、type=number做特殊处理,每次响应进行强制更新,格式化输入值。
至此,v-model指令处理完毕,回到genDirectives拼接到了v-show字符串的后面:
只要有属性,hasRuntime就会被置为true,因为拼接的字符串最后是逗号,在return的时候需要去除这个逗号并添加一个中括号完整表达式。
先这样吧,下次搞。