其实吧,写这些后记我才真正了解到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