上篇文章是分析双向绑定在组件上的用法,这篇文章我们来看看在普通dom上的用法。
// app.vue
{{a}}---{{b}}
// render
var render = function render() {
var _vm = this,
_c = _vm._self._c
return _c("div", [
_vm._v("\n " + _vm._s(_vm.a) + "---" + _vm._s(_vm.b) + "\n "),
_c("input", {
directives: [
{ name: "model", rawName: "v-model", value: _vm.a, expression: "a" },
],
domProps: { value: _vm.a },
on: {
input: function ($event) {
if ($event.target.composing) return
_vm.a = $event.target.value
},
},
}),
])
}
可以看到v-model="b"被解析为三部分:第一部分是directives,我们知道这个是在自定义指令中用到过。v-model也是一个指令,初始值为a变量的值。第二部分是domProps,这个里面有个value,value的值会直接给dom。第三部分是on,这个是设置了一个oninput事件,并将当前的值赋给a变量。下面我们从源码具体来看看是如何使用这三部分的。
看过前几篇文章的估计都知道执行完render函数后生成了vnode,然后调用_update方法。_update方法会遍历每一个vnode去执行createElm方法,我们重点讲解这个方法。生成的vnode如下图所示:
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
...
var data = vnode.data;
var children = vnode.children;
var tag = vnode.tag;
if (isDef(tag)) {
...
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
setScope(vnode);
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
if (data && data.pre) {
creatingElmInVPre--;
}
}
else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
else {
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
我们重点看invokeCreateHooks方法:
function invokeCreateHooks(vnode, insertedVnodeQueue) {
for (var i_2 = 0; i_2 < cbs.create.length; ++i_2) {
cbs.create[i_2](emptyNode, vnode);
}
i = vnode.data.hook; // Reuse variable
if (isDef(i)) {
if (isDef(i.create))
i.create(emptyNode, vnode);
if (isDef(i.insert))
insertedVnodeQueue.push(vnode);
}
}
首先调用cbs.create[i_2](emptyNode, vnode)方法,该方法有8个函数。data上的directives会调用updateDirectives方法去处理,data上的domProps会调用updateDomProps去处理,data上的on会调用updateDomListeners去处理。
先看第一个触发的函数updateDomListeners:
function updateDOMListeners(oldVnode, vnode) {
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return;
}
var on = vnode.data.on || {};
var oldOn = oldVnode.data.on || {};
// vnode is empty when removing all listeners,
// and use old vnode dom element
target = vnode.elm || oldVnode.elm;
normalizeEvents(on);
updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context);
target = undefined;
}
可以看出先获取on对象,然后调用 normalizeEvents(on)方法,在本次未触发,然后执行updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)方法:
function updateListeners(on, oldOn, add, remove, createOnceHandler, vm) {
var name, cur, old, event;
for (name in on) {
cur = on[name];
old = oldOn[name];
event = normalizeEvent(name);
if (isUndef(cur)) {
warn$2("Invalid handler for event \"".concat(event.name, "\": got ") + String(cur), vm);
}
else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm);
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture);
}
add(event.name, cur, event.capture, event.passive, event.params);
}
...
}
...
}
先取出回调函数,最后执行add方法:
function add(name, handler, capture, passive) {
// async edge case #6566: inner click event triggers patch, event handler
// attached to outer element during patch, and triggered again. This
// happens because browsers fire microtask ticks between event propagation.
// the solution is simple: we save the timestamp when a handler is attached,
// and the handler would only fire if the event passed to it was fired
// AFTER it was attached.
if (useMicrotaskFix) {
var attachedTimestamp_1 = currentFlushTimestamp;
var original_1 = handler;
//@ts-expect-error
handler = original_1._wrapper = function (e) {
if (
// no bubbling, should always fire.
// this is just a safety net in case event.timeStamp is unreliable in
// certain weird environments...
e.target === e.currentTarget ||
// event is fired after handler attachment
e.timeStamp >= attachedTimestamp_1 ||
// bail for environments that have buggy event.timeStamp implementations
// #9462 iOS 9 bug: event.timeStamp is 0 after history.pushState
// #9681 QtWebEngine event.timeStamp is negative value
e.timeStamp <= 0 ||
// #9448 bail if event is fired in another document in a multi-page
// electron/nw.js app, since event.timeStamp will be using a different
// starting reference
e.target.ownerDocument !== document) {
return original_1.apply(this, arguments);
}
};
}
target.addEventListener(name, handler, supportsPassive ? { capture: capture, passive: passive } : capture);
}
这里主要是vue对bug的一些兼容性处理,最后调用了addEventListener去给当前dom绑定我们定义的事件。
再看第二个触发的函数updateDomProps:
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 || {};
// clone observed objects, as the user probably wants to mutate it
if (isDef(props.__ob__) || isTrue(props._v_attr_proxy)) {
props = vnode.data.domProps = extend({}, props);
}
...
for (key in props) {
cur = props[key];
...
if (key === 'value' && elm.tagName !== 'PROGRESS') {
// store value as _value as well since
// non-string values will be stringified
elm._value = cur;
// avoid resetting cursor position when value is the same
var strCur = isUndef(cur) ? '' : String(cur);
if (shouldUpdateValue(elm, strCur)) {
elm.value = strCur;
}
}
...
}
}
首先会取出我们的domProps,由于我们的key是value,所以当elm上的值和我们要给的值不一样的时候此时会执行elm.value = strCur重新给dom赋值。
第三个触发的就是updateDirectives方法:
function _update(oldVnode, vnode) {
var isCreate = oldVnode === emptyNode;
var isDestroy = vnode === emptyNode;
var oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context);
var newDirs = normalizeDirectives(vnode.data.directives, vnode.context);
var dirsWithInsert = [];
var dirsWithPostpatch = [];
var key, oldDir, dir;
for (key in newDirs) {
oldDir = oldDirs[key];
dir = newDirs[key];
if (!oldDir) {
// new directive, bind
callHook(dir, 'bind', vnode, oldVnode);
if (dir.def && dir.def.inserted) {
dirsWithInsert.push(dir);
}
}
else {
// existing directive, update
dir.oldValue = oldDir.value;
dir.oldArg = oldDir.arg;
callHook(dir, 'update', vnode, oldVnode);
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir);
}
}
}
if (dirsWithInsert.length) {
var callInsert = function () {
for (var i = 0; i < dirsWithInsert.length; i++) {
callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode);
}
};
if (isCreate) {
mergeVNodeHook(vnode, 'insert', callInsert);
}
else {
callInsert();
}
}
if (dirsWithPostpatch.length) {
mergeVNodeHook(vnode, 'postpatch', function () {
for (var i = 0; i < dirsWithPostpatch.length; i++) {
callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);
}
});
}
if (!isCreate) {
for (key in oldDirs) {
if (!newDirs[key]) {
// no longer present, unbind
callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
}
}
}
}
normalizeDirectives方法会给当前对象新增def,这是hook方法:
首先执行dirsWithInsert.push(dir),然后执行mergeVNodeHook(vnode, ‘insert’, callInsert)方法,该方法会把回调函数给invoker,执行完invoker就会删除回调函数,相当于只会执行一次。然后执行invokeCreateHooks的insertedVnodeQueue.push(vnode)保存当前vnode。那么啥时候执行insertedVnodeQueue的函数呢?这个会在最开始执行patch函数返回最终的elm之前会执行invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)方法去执行里面的函数。我们来看看为啥要执行insertd函数:
inserted: function (el, binding, vnode, oldVnode) {
...
else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
el._vModifiers = binding.modifiers;
if (!binding.modifiers.lazy) {
el.addEventListener('compositionstart', onCompositionStart);
el.addEventListener('compositionend', onCompositionEnd);
// Safari < 10.2 & UIWebView doesn't fire compositionend when
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
el.addEventListener('change', onCompositionEnd);
/* istanbul ignore if */
if (isIE9) {
el.vmodel = true;
}
}
}
}
在执行input的insertd方法时会启用几个监听函数,其实这个监听就是在输入中文的时候不会还没输完就触发回调,而是输完拼音以后再触发。
使用v-model会将当前的input设置oninput监听事件将最新的值赋给当前绑定的变量,而且会使用domProps给当前dom设置初始值。最后触发insertd的钩子设置composition事件,这里之所以用insertd钩子是因为监听事件只需要设置一次。