はじめに
前回でgenerateが終わり、compileまでが終わりました。
今回はmountComponentでどのように実際のdomが出来上がっていくかを見ていきたいと思います。
mount.callから
まずmount.call(this, el, hydrating)のthisはvmで、elはdiv#vue_exampleです。
vm
$attrs: (...)
$listeners: (...)
message: (...)
$data: (...)
$props: (...)
_uid: 0
_isVue: true
$options: {components: {…}, directives: {…}, filters: {…}, el: "#vue_example", _base: ƒ, …}
_renderProxy: Proxy {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
_self: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
$parent: undefined
$root: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
$children: []
$refs: {}
_watcher: Watcher {vm: Vue, deep: false, user: false, lazy: false, sync: false, …}
_vnode: VNode {tag: "div", data: {…}, children: Array(1), text: undefined, elm: div#vue_example, …}
_staticTrees: null
$vnode: undefined
$slots: {}
$scopedSlots: {}
_c: ƒ (a, b, c, d)
$createElement: ƒ (a, b, c, d)
_watchers: (4) [Watcher, Watcher, Watcher, Watcher]
_data: {__ob__: Observer}
$el: div#vue_example
このなかで1-1で作った_data,_renderProxyや、1-3で作ったoptionsのrenderが重要な役割を果たしていきます。
mount.callからmountComponentへ
function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el;
callHook(vm, 'beforeMount');
var updateComponent;
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
hydrating = false;
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm
}
vm.$elを作り、updateConmonentFunctionを作って、new Watcherに行く
watcher絡みは詳しくは後でやります。
new Watcher
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
this.cb = cb;
this.id = ++uid$2; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
warn(
"Failed watching path: \"" + expOrFn + "\" " +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
);
}
}
this.value = this.lazy
? undefined
: this.get();
};
やっていることはまずnew Watch.vmに引数のvmをセットし、vm._watcherにnew Watchをセットして相互に参照できるようにして、vm,wacthersにnew Watchをpush
this.expressionに
"function () {
vm._update(vm._render(), hydrating);
}"
と関数の文字列を入れて、
this.getterには上の関数そのものを入れる
最後のthis.valueにはthis.get()の返り値が入る
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
this.getter.callはfunction () {
vm._update(vm._render(), hydrating);
}で、callなのでthisはvmとなる。
まずはvm._renderへ
Vue.prototype._render = function () {
var vm = this;
var ref = vm.$options;
var render = ref.render;
var _parentVnode = ref._parentVnode;
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
);
}
vm.$vnode = _parentVnode;
// render self
var vnode;
try {
currentRenderingInstance = vm;
vnode = render.call(vm._renderProxy, vm.$createElement);
} catch (e) {
handleError(e, vm, "render");
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e);
} catch (e) {
handleError(e, vm, "renderError");
vnode = vm._vnode;
}
} else {
vnode = vm._vnode;
}
} finally {
currentRenderingInstance = null;
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0];
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
);
}
vnode = createEmptyVNode();
}
// set parent
vnode.parent = _parentVnode;
return vnode
};
}
vm._renderでthisはvmで、renderにはcompileで作ったrender関数
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"vue_example"}},[_v("\n "+_s(message)+"\n")])}
})
が入る
そして vnode = render.call(vm._renderProxy, vm.$createElement);
が実行されるとき↑の無名関数のthisはvm._renderProxyとなる、これは1-1のnew Proxyで作ったもので、hasHandlerが登録されていた。
だが、このwith(this)とは何でしょうか?
例えばobj.a=5、obj.b=7として、
with(obj){
console.log(a)//5
console.log(b)//7
}
with(c=9){
console.log(c)//9
}
console.log(c)//undefined
with(){}で{}内の変数はwith()で宣言していれば使えるようになる、といったものです。
参考
http://blog.tojiru.net/article/197591734.html
さて、withの使い方はわかったので、今回のケースではwith(this=vm._renderProxy)でした。
そもそもvm._renderProxyとはvmをProxyでwrapしたもので、? in vm._renderProxyとなった時にhasHandlerが起動するのでした。
問題の関数を簡略化すると
with(vm._renderProxy){
return _c()
}
となります。
ここのどこに ? in vm._renderProxyがあるのでしょうか?
例として
const handler1 = {
has (target, key) {
console.log(target,key);
if(key in target){
return true;
}else{
return false;
}
}
};
const monster1 = {
_secret: 'easily scared',
eyeCount: 4
};
const proxy1 = new Proxy(monster1, handler1);
console.log('eyeCount' in proxy1);//true
console.log("aaa" in proxy1);//false
with(proxy1){
console.log(eyeCount);
}
//withの結果
Object { _secret: "easily scared", eyeCount: 4 } "console"
Object { _secret: "easily scared", eyeCount: 4 } "eyeCount"
4
上記の結果からwithを使うと変数を "console" in proxy1みたいに探しているみたいです。
なのでhasが処理に割り込むということですね。
なので今回は_c in vm._renderProxyとなります。
今回はhasのチェックは_cとか_vとか全部通るので素直に_cとかが何をしているかだけ追っていけば大丈夫です。
では戻ってrender関数を見ていきましょう。
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"vue_example"}},[_v("\n "+_s(message)+"\n")])}
})
ここのmessageを取得するときにvm.messageはproxyでvm._data.messageとなることを思い出し、さらにvm._data.messageはObservableという仕組みになっていました。
なのでvm._data.messageの値を取得するときはObject.definePropertyで付けたgetが起動します。
messageの値を取得した後s(message)へ,_sはvm._protoにあるto_String関数のことです。
_vはcreateTextVNode関数で、
new VNode
ここでcontext=vm、tag=div、data=attrs: {id: "vue_example"}、children=_vで作ったVNode
function createElement (
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
) {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children;
children = data;
data = undefined;
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE;
}
return _createElement(context, tag, data, children, normalizationType)
}
function _createElement (
context,
tag,
data,
children,
normalizationType
) {
var vnode, ns;
if (typeof tag === 'string') {
var Ctor;
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
if (config.isReservedTag(tag)) {
// platform built-in elements
if (isDef(data) && isDef(data.nativeOn)) {
warn(
("The .native modifier for v-on is only valid on components but it was used on <" + tag + ">."),
context
);
}
//今回はここ!
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
);
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children);
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) { applyNS(vnode, ns); }
if (isDef(data)) { registerDeepBindings(data); }
return vnode
} else {
return createEmptyVNode()
}
}
_createElementではchildrenのvNodeとattrとtagを使ってvNodeをつくる
作られたvNodeはcontextにvm、dataにattrなど必要な情報が入っている
tag: "div"
data: {attrs: {…}}
children: [VNode]
context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
この作られたvNodeがvm._renderの返り値で、watcherのgetterから呼び出したvm._updateへ
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
vm.updateでは初回と二度目以降で_patchの引数が違う、ここでvNodeのdiffをやるんですが、その詳細はdiffアルゴリズム編で
今回は初回の動きを見ていきます。
初回はpatchでoldVnodeがdiv#vue_exampleでvnodeがさっきつくったやつ
function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isRealElement) {
oldVnode = emptyNodeAt(oldVnode);
// replacing existing element
var oldElm = oldVnode.elm;
var parentElm = nodeOps.parentNode(oldElm);
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
);
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
var ancestor = vnode.parent;
var patchable = isPatchable(vnode);
while (ancestor) {
for (var i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor);
}
ancestor.elm = vnode.elm;
if (patchable) {
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
cbs.create[i$1](emptyNode, ancestor);
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
var insert = ancestor.data.hook.insert;
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (var i$2 = 1; i$2 < insert.fns.length; i$2++) {
insert.fns[i$2]();
}
}
} else {
registerRef(ancestor);
}
ancestor = ancestor.parent;
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm
}
}
function emptyNodeAt (elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
emptyNodeAtでoldVNodeにtag: "div"、elm:div#vue_exampleのVNodeを入れる
nodeOpsは
createElement: ƒ createElement$1(tagName, vnode)
createElementNS: ƒ createElementNS(namespace, tagName)
createTextNode: ƒ createTextNode(text)
createComment: ƒ createComment(text)
insertBefore: ƒ insertBefore(parentNode, newNode, referenceNode)
removeChild: ƒ removeChild(node, child)
appendChild: ƒ appendChild(node, child)
parentNode: ƒ parentNode(node)
nextSibling: ƒ nextSibling(node)
tagName: ƒ tagName(node)
setTextContent: ƒ setTextContent(node, text)
setStyleScope: ƒ setStyleScope(node, scopeId)
となっていて、document.createElementなどのwrapper?
function parentNode (node) {
return node.parentNode
}
function nextSibling (node) {
return node.nextSibling
}
createElmへ
ここのvnode.elm=createElement()で実際のDOMが作られる(childrenはまだ)
setScopeではよくvueを使ってるとdata-v-509622e6みたいになっていたりすると思うんですけど、ここでsetされます。今回はcss使っていないので飛ばします。
createElm続き
ここのcreateChildrenでvnode.elmにchildrenをset
createChildren→createElmで、今回はcreateTextNode
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
var data = vnode.data;
var children = vnode.children;
var tag = vnode.tag;
if (isDef(tag)) {
{
} else if (isTrue(vnode.isComment)) {
} else {
//今回はここ!insertでparentElm(div)に↵ Hello Vue.js!↵を入れる
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
function createTextNode (text) {
return document.createTextNode(text)
}
createElmに戻ってinvokeCreateHooks→updateAttrs→setAttrs→baseSetAttr→el.setAttributeでvNode.elmにattrsを追加
invokeCreateHooksのcsb.createは
create: Array(8)
0: ƒ updateAttrs(oldVnode, vnode)
1: ƒ updateClass(oldVnode, vnode)
2: ƒ updateDOMListeners(oldVnode, vnode)
3: ƒ updateDOMProps(oldVnode, vnode)
4: ƒ updateStyle(oldVnode, vnode)
5: ƒ _enter(_, vnode)
6: ƒ create(_, vnode)
7: ƒ updateDirectives(oldVnode, vnode)
となっていて、今回はattrsだけだけど、実際はinvokeCreateHooksでstyleとかdirectiveなどもvNode.elmに追加する,
_enterはtransition関連,createはref
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
cbs.create[i$1](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); }
}
}
createElmに戻ってparentElmに今まで作ってきたvnode.elmをinsertします。
ここでparentElmはparentElm = nodeOps.parentNode(oldElm)で、oldElmはdiv#vue_exampleのことでした。
なのでparentElmはbodyのことでした。
ここでinsertBeforeをするとbodyにはもともとのdiv#vue_example({{message}})と今作ったvNode(Hello Vue.js!)が一時的に共存することとなります。
ここまででcreateElmがおわりpatchに戻ります。ここからいらなくなってもともとのdiv#vue_exampleを取り除いていきます.
function patch (oldVnode, vnode, hydrating, removeOnly) {
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm
}
}
removeVNodes→removeAndInVokeRemoveHook→removeNode
removeNodeでbodyからdiv#vue_exampleを取り除きます。
removeVNodesに戻って、invokeDestroyHookではrefやdirectiveを取り除いています。今回は関係ないので飛ばし
Vue.updateに戻ってvm.\$el=vnode.elm=新しく作ったdiv#vue_exampleとなります。
vue.$el._Vue = vmとして、$elからVueInstanceが取り出せるように
これで長々としていたWatcherのthis.value=this.get()が終わります。get()の返り値はundefinedです。
ここまでWatcherでやってきたことをまとめると、
まずwatcherとvmをwatcher.vm=vm、vm._watcher=wactherで相互参照できるようにしました。
そこからwatcher.get()でvnode.elmを作って、vm.\$elを元々古いdiv#vue_exampleを指していたのを新しく作ったdiv#vue_exampleにしました。
watcherとvmは相互参照できるので、watcher側からでもきちんと新しいvm.\$elとなります。
ここまでで全体の流れは終わりです。
次回以降はwatcherやdepを見ていきたいと思います。