LoginSignup
1
0

More than 3 years have passed since last update.

Vue Internals①astを中心に 1-4mountComponent

Posted at

はじめに

前回で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が起動します。

obdefinemess.png
dep絡みはまた後でやります。

messageの値を取得した後s(message)へ,_sはvm._protoにあるto_String関数のことです。
_s=toString.png
_vはcreateTextVNode関数で、
_v=createTextVNode.png
new VNode
new VNode.png

無名関数に戻って次は_c=createElement
_c=createElement.png

ここで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アルゴリズム編で

今回は初回の動きを見ていきます。
vm._update.png
初回は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へ
createElmでdocument.createElement.png
ここのvnode.elm=createElement()で実際のDOMが作られる(childrenはまだ)

createElement$1.png

setScopeではよくvueを使ってるとdata-v-509622e6みたいになっていたりすると思うんですけど、ここでsetされます。今回はcss使っていないので飛ばします。

createElm続き
createElm2.png
ここの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)
  }

insert11.png

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); }
      }
    }

updateAttr.png
setAttr.png
baseSetAttr.png

createElmに戻ってparentElmに今まで作ってきたvnode.elmをinsertします。
ここでparentElmはparentElm = nodeOps.parentNode(oldElm)で、oldElmはdiv#vue_exampleのことでした。
なのでparentElmはbodyのことでした。
insertBody.png
ここでinsertBeforeをするとbodyにはもともとのdiv#vue_example({{message}})と今作ったvNode(Hello Vue.js!)が一時的に共存することとなります。

dualBody.png

ここまでで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
removeVNodess.png

removeNode1111.png
removeNodeでbodyからdiv#vue_exampleを取り除きます。
afterRemoveNode.png
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を見ていきたいと思います。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0