Vue源码后记-其余内置指令(3)
其实吧,写这些后记我才真正了解到vue源码的精髓,之前的跑源码跟闹着玩一样。
go!
之前将AST转换成了render函数,跳出来后,由于仍是字符串,所以调用了makeFunction将其转换成了真正的函数:
function compileToFunctions(template, options, vm) { // code... // compile var compiled = compile(template, options); // code... // 转换render res.render = makeFunction(compiled.render, fnGenErrors); var l = compiled.staticRenderFns.length; // 转换staticRenderFns res.staticRenderFns = new Array(l); for (var i = 0; i < l; i++) { res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i], fnGenErrors); } // code... return (functionCompileCache[key] = res) } function makeFunction(code, errors) { try { return new Function(code) } catch (err) { // error... } }
这个没啥讲的
将render转换成VNode其实也没什么讲的,重点看一下之前没见过的函数,
_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函数包含_c、_v、_m、_e、_s5个函数,其中_c、_v、_s之前都讲过,这里看一下_m、_e是什么。
_m
直接看源码:
Vue.prototype._m = renderStatic; function renderStatic(index, isInFor) { var tree = this._staticTrees[index]; // 如果该静态节点已经被渲染且不在v-for中 // 复用该节点 if (tree && !isInFor) { return Array.isArray(tree) ? cloneVNodes(tree) : cloneVNode(tree) } // otherwise, render a fresh tree. tree = this._staticTrees[index] = this.$options.staticRenderFns[index].call(this._renderProxy); markStatic(tree, ("__static__" + index), false); return tree }
可以看到,对于静态节点,vue做了一层缓存,尽量复用现成的虚拟DOM,但是目前是初次渲染,所以会创建一个新的。
这里有两步。
第一步:this.$options.staticRenderFns[index].call(this._renderProxy)
即取出staticRenderFns对应索引的函数并执行,将其缓存到_staticTrees上。
之前在生成render函数时,将v-once的节点当成静态节点处理,弹入了该数组,函数如下:
(function() { with(this) { return _c('span', [_v(_s(msg))]) } })
这里_s将msg字符串化,_v生成一个文本VNode,_c生成一个带有tag的VNode,children为之前的VNode。
第二步:markStatic(tree, ("__static__" + index), false)
给VNode做标记。
// tree => VNode // key => __static__0 // isonce => false function markStatic(tree, key, isOnce) { if (Array.isArray(tree)) { for (var i = 0; i < tree.length; i++) { if (tree[i] && typeof tree[i] !== 'string') { // key => __static__0_0... markStaticNode(tree[i], (key + "_" + i), isOnce); } } } else { markStaticNode(tree, key, isOnce); } } function markStaticNode(node, key, isOnce) { node.isStatic = true; node.key = key; node.isOnce = isOnce; }
比较简单,直接看结果了:
_e
这个其实我在注释里写了,就是一个空的div,瞄一眼源码,发现我错了:
Vue.prototype._e = createEmptyVNode; var createEmptyVNode = function() { var node = new VNode(); node.text = ''; node.isComment = true; return node };
生成一个空的VNode,将其标记为注释,内容为空。
//剩下的太简单,我不想讲啦!撤了,最近心情不好,兼容360,这客户我真是日了狗了。
还没完,讲讲patch阶段那些directives、on、domProps是如何渲染的吧!
input
<input v-show="vShowIter" v-model='vModel' />
VNode中的data属性细节可以看图,这里看一下domProps、on是如何渲染的。
首先on是事件相关,刚发现chrome调试一个特别好用的东西,可以看函数流向!
图中patch是渲染DOM的入口函数,createElm生成DOM节点,createChildren递归处理子节点,invokeCreateHooks则负责处理节点的属性,updateDOMListeners很显然是处理事件绑定,看一下源码:
function updateDOMListeners(oldVnode, vnode) { // 新旧VNode至少有一个存在on属性 if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) { return } // 保存属性 var on = vnode.data.on || {}; var oldOn = oldVnode.data.on || {}; target$1 = vnode.elm; // 特殊情况处理 normalizeEvents(on); updateListeners(on, oldOn, add$1, remove$2, vnode.context); }
除去判断,这里会先对特殊情况下的on做特殊处理,然后再进行事件绑定,可以看下处理的代码:
function normalizeEvents(on) { var event; /* istanbul ignore if */ if (isDef(on[RANGE_TOKEN])) { // IE input[type=range] only supports `change` event event = isIE ? 'change' : 'input'; on[event] = [].concat(on[RANGE_TOKEN], on[event] || []); delete on[RANGE_TOKEN]; } if (isDef(on[CHECKBOX_RADIO_TOKEN])) { // Chrome fires microtasks in between click/change, leads to #4521 event = isChrome ? 'click' : 'change'; on[event] = [].concat(on[CHECKBOX_RADIO_TOKEN], on[event] || []); delete on[CHECKBOX_RADIO_TOKEN]; } }
可以看到,特殊情况有两种:
第一种是IE下的type=range,这个H5属性只支持IE10+,并且在IE中只有change事件。
第二种是Chrome下的radio、checkbox,事件类型会被置换为click。
接下来是事件的绑定函数:
// on/oldOn => 新旧VNode事件 // add/remove$$1 => 事件的绑定与解绑函数 function updateListeners(on, oldOn, add, remove$$1, vm) { var name, cur, old, event; for (name in on) { cur = on[name]; old = oldOn[name]; // str => obj event = normalizeEvent(name); if (isUndef(cur)) { // error } // 添加事件 else if (isUndef(old)) { if (isUndef(cur.fns)) { cur = on[name] = createFnInvoker(cur); } add(event.name, cur, event.once, event.capture, event.passive); } // 事件替换 else if (cur !== old) { old.fns = cur; on[name] = old; } } // 旧VNode事件解绑 for (name in oldOn) { if (isUndef(on[name])) { event = normalizeEvent(name); remove$$1(event.name, oldOn[name], event.capture); } } }
在遍历所有事件类型字符串的时候,由于可能会有特殊标记,所以会对其进行解析转换为一个对象:
var normalizeEvent = cached(function(name) { var passive = name.charAt(0) === '&'; name = passive ? name.slice(1) : name; var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first name = once$$1 ? name.slice(1) : name; var capture = name.charAt(0) === '!'; name = capture ? name.slice(1) : name; return { name: name, once: once$$1, capture: capture, passive: passive } });
意思简单明了,这里就不解释了。
接下来会将事件处理函数作为属性挂载到一个invoker函数上:
// 将函数或函数数组作为属性挂到函数上 可以调用执行 function createFnInvoker(fns) { function invoker() { var arguments$1 = arguments; var fns = invoker.fns; if (Array.isArray(fns)) { for (var i = 0; i < fns.length; i++) { fns[i].apply(null, arguments$1); } } else { // return handler return value for single handlers return fns.apply(null, arguments) } } invoker.fns = fns; return invoker }
这样做的原因可能是方便执行事件,有时候一个DOM会有多个相同事件,此时事件会是一个数组,通过这样处理后,无论是单一函数还是函数数组都可以通过直接调用invoker来执行。
下面就是最后一个步骤,事件绑定:
// event => input // handler => invoker // 剩余三个为之前normalizeEvent的属性 function add$1(event, handler, once$$1, capture, passive) { // 一次性执行事件 if (once$$1) { var oldHandler = handler; var _target = target$1; handler = function(ev) { // 单参数 or 多参数 var res = arguments.length === 1 ? oldHandler(ev) : oldHandler.apply(null, arguments); // 执行完立马解绑事件 if (res !== null) { remove$2(event, handler, capture, _target); } }; } target$1.addEventListener( event, handler, supportsPassive ? { capture: capture, passive: passive } : capture ); }
对于一次性执行事件,这里的处理和jQuery源码里还是蛮像的,不过要简洁多了,这个很简单,没啥讲的。
下面处理domProps,起初我以为这个属性是专门处理组件间传值那个props的,后来发现这属性有点瞎:
function updateDOMProps(oldVnode, vnode) { if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) { return } var key, cur; var elm = vnode.elm; var oldProps = oldVnode.data.domProps || {}; var props = vnode.data.domProps || {}; // __ob__属性代表动态变化的值 if (isDef(props.__ob__)) { props = vnode.data.domProps = extend({}, props); } // 新VNode缺失属性置为空 for (key in oldProps) { if (isUndef(props[key])) { elm[key] = ''; } } for (key in props) { cur = props[key]; // 这两种情况特殊处理 if (key === 'textContent' || key === 'innerHTML') {
} if (key === 'value') { // 先保存值 之后所有值会被转为字符串 elm._value = cur; // avoid resetting cursor position when value is the same var strCur = isUndef(cur) ? '' : String(cur); if (shouldUpdateValue(elm, vnode, strCur)) { elm.value = strCur; } } else { elm[key] = cur; } } } function shouldUpdateValue(elm, vnode, checkVal) { return (!elm.composing && ( vnode.tag === 'option' || // document.activeElement !== elm && elm.value !== checkVal isDirty(elm, checkVal) || // 处理trim,number isInputChanged(elm, checkVal) )); }
其中包括两种情况,textContent、innerHTML、value以及其他,此处props的值为value,会判断当前DOM的值是否一致,然后进行修正。
当props为textContent或innerHTML时,需要将所有子节点清空,然后将对应的属性修改为对应的值。
至此,基本上必要的点已经完事了~啊。。。。88