Vue.jsの仮想DOMと差分レンダリングの仕組み③

こんにちは、成田です。
今回が連載ラストとなりますので、是非お付き合いください。

blog.engineer.adways.net

blog.engineer.adways.net
②回目の投稿では初回レンダリング時に走るpatchの処理の流れを追いました。

今回は状態変更が行われた際に差分レンダリングをするためのdiffアルゴリズムのあたりを追って行くことにします。
なので今回は図1の赤枠で囲った差分更新のフローを読み進めていくことなります。
図1では単に「差分更新」と抽象的に表現していますが、「差分更新」の内部は別途見ていきます。
図1は前回も掲載していますが、差分レンダリングを行うこのpatch関数のフローチャートがよくわからない場合は前回の記事を是非参照してください。

f:id:AdwaysEngineerBlog:20180216174157j:plain
図1 patch関数のフローチャート

差分の算出(diff)

まずpatch関数の全体のソースを再掲します。

https://github.com/vuejs/vue/blob/v2.5.13/src/core/vdom/patch.js#L665

return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }

  let isInitialPatch = false
  const insertedVnodeQueue = []

  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue, parentElm, refElm)
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patch existing root node
      patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
    } else {
      if (isRealElement) {
        // mounting to a real element
        // check if this is server-rendered content and if we can perform
        // a successful hydration.
        if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
          oldVnode.removeAttribute(SSR_ATTR)
          hydrating = true
        }
        if (isTrue(hydrating)) {
          if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
            invokeInsertHook(vnode, insertedVnodeQueue, true)
            return oldVnode
          } else if (process.env.NODE_ENV !== 'production') {
            warn(
              'The client-side rendered virtual DOM tree is not matching ' +
              'server-rendered content. This is likely caused by incorrect ' +
              'HTML markup, for example nesting block-level elements inside ' +
              '<p>, or missing <tbody>. Bailing hydration and performing ' +
              'full client-side render.'
            )
          }
        }
        // either not server-rendered, or hydration failed.
        // create an empty node and replace it
        oldVnode = emptyNodeAt(oldVnode)
      }

      // replacing existing element
      const oldElm = oldVnode.elm
      const 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)) {
        let ancestor = vnode.parent
        const patchable = isPatchable(vnode)
        while (ancestor) {
          for (let i = 0; i < cbs.destroy.length; ++i) {
            cbs.destroy[i](ancestor)
          }
          ancestor.elm = vnode.elm
          if (patchable) {
            for (let i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, ancestor)
            }
            // #6513
            // invoke insert hooks that may have been merged by create hooks.
            // e.g. for directives that uses the "inserted" hook.
            const insert = ancestor.data.hook.insert
            if (insert.merged) {
              // start at index 1 to avoid re-invoking component mounted hook
              for (let i = 1; i < insert.fns.length; i++) {
                insert.fns[i]()
              }
            }
          } else {
            registerRef(ancestor)
          }
          ancestor = ancestor.parent
        }
      }

      // destroy old node
      if (isDef(parentElm)) {
        removeVnodes(parentElm, [oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    }
  }

図1の赤枠「差分更新」に該当する関数がpatch関数内で実行しているpatchVnode関数になります。
patchVnode関数は複雑なため、少しつづ見ていきましょう。

patchVnode

https://github.com/vuejs/vue/blob/v2.5.13/src/core/vdom/patch.js#L481

  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    if (oldVnode === vnode) {
      return
    }

    const elm = vnode.elm = oldVnode.elm

    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }

    // reuse element for static trees.
    // note we only do this if the vnode is cloned -
    // if the new node is not cloned it means the render functions have been
    // reset by the hot-reload-api and we need to do a proper re-render.
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

patchVnodeのフローは大まかに以下のような流れとなります

  • oldVnodeとvnodeが同じ場合は関数を抜ける
  • vnodeがtextノードではない場合
    • oldVnodeとvnodeの両方に子Nodeがあり、両者の子Nodeが同じでない場合は、updateChildrenを実行
    • vnodeだけに子Nodeがある場合は、これらの子Nodeを作成
    • oldVnodeだけに子Nodeがある場合は、それらのNodeを削除
  • oldVnode.textとvnode.textに違いがある場合は、textの内容だけ変える

末端のVNodeとtext VNode以外はvnodeがtextノードではない場合の流れをたどるので、ほとんどのVNodeではupdateChildrenが実行されます。
このpatchVnode関数のみではまだ実DOMへの変更は適用されていません。
それでは次にupdateChildren関数の内部について見ていきましょう。

updateChildren

https://github.com/vuejs/vue/blob/v2.5.13/src/core/vdom/patch.js#L384

 function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm

  // removeOnly is a special flag used only by <transition-group>
  // to ensure removed elements stay in correct relative positions
  // during leaving transitions
  const canMove = !removeOnly

  if (process.env.NODE_ENV !== 'production') {
    checkDuplicateKeys(newCh)
  }   

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
      canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      if (isUndef(idxInOld)) { // New element
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
      } else {
        vnodeToMove = oldCh[idxInOld]
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          // same key but different element. treat as new element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        }   
      }   
      newStartVnode = newCh[++newStartIdx]
    }   
  }   
  if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  }   
}
  • oldVnodeとvnodeのfirstChild(子VNodeの先頭)、lastChild(子VNodeの最後)をそれぞれ取得し、oldStartVnode、oldEndVnode、newStartVnode、newEndVnodeに割り当てる
  • oldStartVnodeとnewStartVnodeが同じノードである場合は、patchVnodeが呼び出され、oldStartVnodeとnewStartVnodeの両方が次の子として設定される

今回のサンプル実装の場合は上記のような流れとなり、 patchVnode -> updateChildren -> patchVnode -> updateChildren -> .....
というような再帰処理が走りることで差分が適用されます。

大雑把ですが、以上が再レンダリング時の差分の適用の流れになります。
( 正直diffアルゴリズムの流れがまだあまり掴めていないので、後日追記するかもしれません )

まとめ

3回に渡って仮想DOMの仕組みと差分レンダリングの仕組みを追っていきました。
記事中には誤りがあるかもしれないので、もし発見した場合は指摘を頂けると幸いです。
最後までご覧いただきありがとうございました。

Scalaでテスト実行前に任意の処理を実行する

こんにちは!まっちゃんです。

昔の話になりますが、
別プロジェクトの開発途中に、ローカル環境のデータベースだと思ってテストコードを実施したところ、
実は共通で参照しているテスト環境のデータベースに接続してしまい、
残念ながらテストデータがすべて消えてしまうという事が発生しました。
(本番環境だと非常によろしくないですね)

そのプロジェクトでは使用するデータベース全てのテストコード専用のものを用意し、
ローカル環境とテスト環境を完全に区切る対策を取りましたが、
自分のプロジェクトでは sbt を用いて対策していることを思い出したのでそれについて書かせていただきます。

上記、対策の手段として、次の事を行います。

  • テスト実行時、環境変数からDBの接続情報を取得する
  • DBの接続情報がローカルでない場合はテストを実施させない

今回使用しているプロジェクトはこちらの記事に上がっているものと、ほぼ同等のものです。

blog.engineer.adways.net

もしくはテンプレートをダウンロードを行い、各バージョンを修正して実行してみてください。

$ sbt new scala/scala-seed.g8

$ cd ${your_project}

build.sbt のバージョンを修正

import Dependencies._

lazy val root = (project in file(".")).
  settings(
    inThisBuild(List(
      organization := "com.example",
      scalaVersion := "2.11.8",
      version      := "0.1.0-SNAPSHOT"
    )),
    name := "Hello",
    libraryDependencies += scalaTest % Test
  )

project/build.properties のバージョン修正

sbt.version=0.13.13

テスト用の独自タスクを設定する

テスト実施は基本的にsbtを通して実行します。
テストに関するタスクを実装していきます。

build.sbt

lazy val preCheckTestTask = TaskKey[Unit]("preCheckTest", "preCheckTest")

preCheckTest という独自タスクを設定しました。

preCheckTest 独自タスクに処理を追加する

先ほど設定したタスクに処理を書いていきます。

build.sbt

preCheckTestTask := {
  // TODO 独自タスクの処理
}

ここに対策の手段である  

  • テスト実行時、環境変数からDBの接続情報を取得する
  • DBの接続情報がローカルでない場合はテストを実施させない

の処理を記述します。

build.sbt

preCheckTestTask := {
    if(sys.env.getOrElse("DB_HOST", default = "localhost") != "localhost") {
      sys.error("テスト実行不可")
    }
}

テスト実行前に、独自タスクの処理を実行させる

テストを実行するコマンドは複数あるので、
それぞれのコマンドごとに設定をします。

build.sbt

(test in Test) <<= (test in Test) dependsOn preCheckTestTask,
(testOnly in Test) <<= (testOnly in Test) dependsOn preCheckTestTask,
(testQuick in Test) <<= (testQuick in Test) dependsOn preCheckTestTask
build.sbt:17: warning: `<<=` operator is deprecated. Use `key := { x.value }` or `key ~= (old => { newValue })`.
See http://www.scala-sbt.org/0.13/docs/Migrating-from-sbt-012x.html
    (test in Test) <<= (test in Test) dependsOn preCheckTestTask /*,
                   ^

<<= は非推奨になった模様です。

sbt Reference Manual — Migrating from sbt 0.12.x

実行

実行するとこのような形になります。

$ sbt test

こちらは問題なくテストが実行されます。

$ DB_HOST=192.168.1.1 sbt test

...

[error] (*:preCheckTest) テスト実行不可

こちらはlocalhostではないのでテスト実行不可とエラーが出力されます。

テンプレートをダウンロードして確認する場合、
最終的な build.sbt は下記になります。

build.sbt

import Dependencies._

lazy val root = (project in file(".")).
  settings(
    inThisBuild(List(
      organization := "com.example",
      scalaVersion := "2.11.8",
      version      := "0.1.0-SNAPSHOT"
    )),
    name := "Hello",
    libraryDependencies += scalaTest % Test,
    preCheckTestTask := {
      if(sys.env.getOrElse("DB_HOST", default = "localhost") != "localhost") {
        sys.error("テスト実行不可")
      }
    },
    (test in Test) <<= (test in Test) dependsOn preCheckTestTask,
    (testOnly in Test) <<= (testOnly in Test) dependsOn preCheckTestTask,
    (testQuick in Test) <<= (testQuick in Test) dependsOn preCheckTestTask
  )

lazy val preCheckTestTask = TaskKey[Unit]("preCheckTest", "preCheckTest")

まとめ

今回は sbt の独自タスクを用いて対応できました。
上記の手段で、対策はできましたが完全ではありません。
設定ファイルの情報は参照していない、バージョンによる非推奨の部分があるなど、
不完全なものを対応していく場合、その点を考慮に入れながら開発・対策していかないといけません。

また冒頭でも書きましたが、別プロジェクトでは異なる対策を行っているので、
どの方法が適しているのかも考えないといけませんね。

Vue.jsの仮想DOMと差分レンダリングの仕組み②

こんにちは、成田です。

blog.engineer.adways.net
前回に引き続き、今回もVue.jsの仮想DOMと差分レンダリングの仕組みを探っていきたいと思います。
間違いがあるかもしれませんので間違いがあったら指摘を頂けたら幸いです。

前回は差分レンダリングの実行タイミングを説明したと思います。
オプション系ライフサイクル中で差分レンダリング(patch)が実行されるタイミングは、
初回レンダリング時はbeforeMount
再レンダリング時はbeforeUpdate
ということ、そしてその実行タイミング周辺のソースを読んでいきました。

今回は差分レンダリング(patch)の仕組みに迫ります。

差分レンダリング(patch)の仕組み

前回のサンプル実装とその実DOMツリーを示した図を思い出して頂きたいので再掲します。
図に関しては前回は改行や空白によってできるText Nodeの存在を無視してしまっていたので修正しています。

サンプル実装のソース

<body>
  <h2>Vue.js debug</h2>
  <div id="main">
    <section>
      <span class='counter'>Parent {{count}}</span>
      <button @click='increment()'>Increment</button>
    </section>
    <section>
      <my-component></my-component>
    </section>
  </div>
</body>

<script>
Vue.component("my-component", {
  data: function() {
    return { count: 0 }
  },
  methods: {
    increment: function() {
      this.count++;
    }
  },
  template: `
  <div>
    <span class='counter'>Child {{count}}</span>
    <button @click='increment()'>Increment</button>
  </div>`
});

new Vue({
  el: "#main",
  data: { count: 0 },
  methods: {
    increment: function() {
      this.count++;
    }
  }
});
</script>

f:id:AdwaysEngineerBlog:20180202062438p:plain
図1. サンプル実装が生成する実DOMツリー

今回は初回レンダリング時に適用される差分レンダリング(patch)のみに焦点を当て説明します。
Vue.jsでは差分レンダリングは実際に内部で呼ばれる関数であるpatchのことを指しますので、以後差分レンダリング(diff/patchの両方)をpatchと呼ぶことにします。

前回示した上図(図1)はVue.jsが内部でいろいろと処理して最終的に構築する実DOMツリーです。
この実DOMツリーを構築するまでにnew Vue(...)直後に何が行われどのように仮想DOMツリーを形成しているのか。
というのが今回のテーマです。
そして以下のpatchの処理が実装されたファイルを元に説明します。
https://github.com/vuejs/vue/blob/dev/src/core/vdom/patch.js

仮想DOMツリーの構造

仮想DOMツリー生成ロジックにはVNodeがどんなプロパティを持っているのか知る必要があります。
そのためまずVNodeが保持するプロパティについて以下に記します。
VNodeの実装ファイルは↓を参照してください。
https://github.com/vuejs/vue/blob/dev/src/core/vdom/vnode.js

VNodeクラス

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  fnScopeId: ?string; // functioanl scope id support

  // ...................
}

英語で注釈が振ってあるので何となく理解しやすいですね。
内部実装でよく出てくるインスタンス変数だけいくつかピックアップしました。

変数名 役割
tag タグ名
data 属性値 (classやid等)
children そのVNode直下の子VNodeの配列を保持している
text テキストノードの場合だけ使用される. 文字列を保持する
elm 実Node(実際の要素)を保持する.
context そのVNodeが所属するコンポーネントのインスタンス
key リストレンダリング時に使用されるVNodeを識別するためのユニークな識別子
parent 親となるコンポーネントVNodeを保持する|

VNodeツリーは最終的にコンポーネントが内部に保持することになります。
サンプル実装でいうところのRootComponent( new Vue(...)したやつ )とMyComponentは内部にVNodeツリーを保持しています。
今回のサンプル実装のようにネストされたコンポーネントの場合、親側は子コンポーネント内のVNodeツリーまでは保持せず子コンポーネントをVNodeとして認識します。
そして子は子のスコープ内でさらにVNodeツリーを保持しています。
console.logで確認してみます。

RootComponentのmountedにconsole.logを仕込む

new Vue({
  // ........

  mounted: function () {
    console.log(this)
  }
});

f:id:AdwaysEngineerBlog:20180202062531p:plain

図2. console.logによるRootComponentのインスタンスを出力

図2に示したようにネストされたコンポーネントはその親となるコンポーネントの$childrenプロパティに子コンポーネントとして保持されます。
そしてVNodeツリーは_vnodeプロパティが保持しています。

実際に図2のことをわかりやすく図示すると以下の図3のようになります。

f:id:AdwaysEngineerBlog:20180202062649p:plain

図3. VNodeツリー

Vue.jsの仮想DOMはコンポーネント単位で生成され、VNodeの集合となり構築されます。
図3で示したvue-component-1-my-componentというtagを保持するVNode(MyComponentのVNode)は内部に下半分のオレンジ色の枠の仮想DOMツリーを保持しています。

初回レンダリング時におけるpatch

初回レンダリングは更新時に比べてそこまでロジックが難しくありません。
更新時はコンポーネント内でどこが変更されたのか(diff)を算出をするためです。

patchの実装を覗いてみます。
( 参照先: https://github.com/vuejs/vue/blob/dev/src/core/vdom/patch.js )

return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }

  let isInitialPatch = false
  const insertedVnodeQueue = []

  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue, parentElm, refElm)
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patch existing root node
      patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
    } else {
      if (isRealElement) {
        // mounting to a real element
        // check if this is server-rendered content and if we can perform
        // a successful hydration.
        if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
          oldVnode.removeAttribute(SSR_ATTR)
          hydrating = true
        }
        if (isTrue(hydrating)) {
          if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
            invokeInsertHook(vnode, insertedVnodeQueue, true)
            return oldVnode
          } else if (process.env.NODE_ENV !== 'production') {
            warn(
              'The client-side rendered virtual DOM tree is not matching ' +
              'server-rendered content. This is likely caused by incorrect ' +
              'HTML markup, for example nesting block-level elements inside ' +
              '<p>, or missing <tbody>. Bailing hydration and performing ' +
              'full client-side render.'
            )
          }
        }
        // either not server-rendered, or hydration failed.
        // create an empty node and replace it
        oldVnode = emptyNodeAt(oldVnode)
      }

      // replacing existing element
      const oldElm = oldVnode.elm
      const 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)) {
        let ancestor = vnode.parent
        const patchable = isPatchable(vnode)
        while (ancestor) {
          for (let i = 0; i < cbs.destroy.length; ++i) {
            cbs.destroy[i](ancestor)
          }
          ancestor.elm = vnode.elm
          if (patchable) {
            for (let i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, ancestor)
            }
            // #6513
            // invoke insert hooks that may have been merged by create hooks.
            // e.g. for directives that uses the "inserted" hook.
            const insert = ancestor.data.hook.insert
            if (insert.merged) {
              // start at index 1 to avoid re-invoking component mounted hook
              for (let i = 1; i < insert.fns.length; i++) {
                insert.fns[i]()
              }
            }
          } else {
            registerRef(ancestor)
          }
          ancestor = ancestor.parent
        }
      }

      // destroy old node
      if (isDef(parentElm)) {
        removeVnodes(parentElm, [oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    }
  }

  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

引数のoldVnode, vnodeにそれぞれ変更前, 変更後のVNodeが入ってきますが、初回実行は前の状態がないのでoldVnodeにはVNodeではないもの (undefined実要素(実Node)) が入ってきます。
oldVnodeが存在しないため単に未定義かどうか、実要素が入って来ているかどうかを判断し、VNodeを生成します。
上記のpatch実装を表すフローチャートが図4になります。
いくつか処理を省いて簡単に書いています。

f:id:AdwaysEngineerBlog:20180202062747p:plain

図4. patch関数のフローチャート

まずRootComponentにおけるpatchフローに関してです。
oldVnode にはnew Vueによるインスタンス生成時にelオプションで指定したdiv#mainの実要素(実Node)が入ってきます。
その実要素はいったんVNodeに変換され、子のVNodeツリーを全て再帰的に生成していきます。

f:id:AdwaysEngineerBlog:20180202062830p:plain

図5. RootComponentに対するpatch関数実行時のフローチャート

子Component (今回の場合MyComponent) に相当するVNodeが生成されるタイミングでは再帰処理は一旦終わりますが、patchはコンポーネント単位で実行されるため次はMyComponentがpatchにエントリーしてきます。
そしてMyComponentの場合はnew Vueによるインスタンス生成ではないためoldVnodeの値は実要素ではなくundefinedとなり以下の赤枠の部分に分岐していきます。

f:id:AdwaysEngineerBlog:20180202062907p:plain

図6. MyComponentに対するpatch関数実行時のフローチャート

以降はRootComponentの時と同様にcreateElm関数により再帰的にVNodeを生成していき、VNodeツリー(仮想DOM)が構築完了となります。

まとめ

今回はpatchの内部で何が行われているのかをフローチャート等で図示しながら探っていきました。
初回レンダリングは結構単純な実装でした!
再レンダリング時にはdiffアルゴリズムなどが関与してくるので1年目新米エンジニアの僕には読み解けるか不安があります。
ということでまた続きを更新しますのでよろしくお願いします!

次回は再レンダリング時のpatchの仕組みについて探っていきます。

AWS Database Migration Serviceを使ってRDSに移行してみた話

はじめに

こんにちは、インフラDiv.の矢吹です。
PS4のモンハンやるか、やらないか迷っている今日この頃です。

今回の話

本番のEC2上で動いているWordPressのMySQLを、
RDSに持って行く過程でせっかくならAWS DMSも試してみようということで触ってみました。

当初考えていたメリット

  • レプリケーションインスタンスがEC2のMySQL更新を読み込むので移行を事実上ダウンタイムなしで出来る
  • コマンドとか一切打たなくて良いんじゃないか説

事前準備

  • テスト環境用のVPC作成
    • サブネットは二つ紐付け必要
    • DNS 解決:はい
    • DNS ホスト名:はい
  • 今回はテスト環境での実施のため、本番で動いているEC2からAMIを作成する
  • AMIよりweb,dbが一緒に入っているEC2を作成する(対象元サーバ)
  • 移行先のRDSを作成する(対象先サーバ)
    • EC2-MySQLのバージョンと出来る限り合わせる
    • EC2とは違うVPCで作成してみた(VPCが違う環境の移行が可能か試したかったため)

対象元サーバ情報

  • インスタンスタイプ
    • c4.xlarge
  • OS
    • Amazon Linux AMI release 2016.03
  • MySQLバージョン
    • mysql Ver 14.14 Distrib 5.5.56, for Linux (x86_64) using readline 5.1

対象先インスタンス情報(RDS)

DB詳細

  • DBエンジンのバージョン:mysql 5.5.57
  • インスタンスタイプ:t2.medium
  • マルチAZ配置:有効
  • ストレージタイプ:汎用(SSD)
  • ストレージ割り当て:50GB
  • DBインスタンス識別子:wordpress-yabuki
  • マスターユーザの名前:root
  • マスターパスワード:適当

データベースの設定

  • データベースの名前:wordpress (wordpress-dbじゃ作成出来なかった)
  • データベースのポート:3306
  • タグをスナップショットへコピー:いいえ
  • IAMのDB認証を有効にする:指定なし
  • 暗号を有効化:いいえ

バックアップ

  • 7日

モニタリング

  • 拡張モニタリリングを有効にする:有効

メンテナンス

  • マイナーバージョン自動アップグレード:はい

移行前の設定

  • 外部からデータを取得する権限をEC2上のMySQLに付与する
    GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
    確認
    show grants for 'root'@'%';

  • RDS側ユーザに特権権限を付与する
    GRANT ALL PRIVILEGES ON `%`.* TO 'wordpress'@'%';
    確認
    show grants;

移行方法

AWS DMS(Data Migration Service)を利用してMySQLデータを移行
参考サイト: https://dev.classmethod.jp/cloud/aws/lets-try-aws-database-migration-service-dms/

移行手順

  1. [DMS]レプリケーションインスタンス作成
    f:id:AdwaysEngineerBlog:20180124192503p:plain

画像:レプリケーションインスタンス作成画像
f:id:AdwaysEngineerBlog:20180124192553p:plain
※VPCには二つ以上のサブネットが紐づいていないと実行出来ないらしい

  1. [DMS]ソースエンドポイント作成
    1. EC2のMySQLを設定
  2. [DMS]ターゲットエンドポイント作成
    1. RDSのMySQLを設定

画像:ソースエンドポイント,ターゲットエンドポイント作成画像 f:id:AdwaysEngineerBlog:20180124192727p:plain

※レプリケーションインスタンスに若干時間取られる。
この段階で「テストの実行」をして接続テストを行っておいた方が良い。

  1. [DMS]タスクの作成(予定)
    • ターゲットテーブル作成モード
      • 何もしない
    • レプリケーションにLOB列を含める
      • 完全LOBモード
    • LOBチャンクサイズ
      • 64
    • 検証の有効化
      • 有効
    • ロギングの有効化
      • 有効

画像:タスクの作成
f:id:AdwaysEngineerBlog:20180124192909p:plain

移行後の設定

  • wordpress-dbのテーブルにprimary keyが設定されておらず、auto_increment属性もないため設定する
    参考サイト: https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Source.MySQL.html#CHAP_Source.MySQL.Limitations
    PRIMARY KEY設定を行う
    ALTER TABLE `wp_posts` ADD PRIMARY KEY (`ID`);
    ALTER TABLE `wp_posts` MODIFY `ID` BIGINT(20) unsigned NOT NULL AUTO_INCREMENT;
    確認
    Show columns from wp_****;
    ※ データtrancateしないとだめな場合があるかもです。
  • MySQLの切り替えを行う(wp-config.php) /var/www/html/wp-config.php
    define('DB_NAME', 'wordpress-db');
    define('DB_HOST', 'localhost');
    ↓(RDSのパスに変更)
    define('DB_NAME', 'wordpress');
    define('DB_HOST', 'wordpress.cs**********.ap-northeast-1.rds.amazonaws.com');

考察・感想

  • メリット(良いなと思った点)
    • レプリケーションインスタンスによって常時mysqlを読み込んでくれるのは便利
      • wordpressの更新を気にせず出来る
    • 接続テストが出来るので繋がっているかどうか分かりやすい
  • デメリット(うーんって感じな点)
    • primary key,auto_increment属性等のテーブル情報が移行されないのは辛い
    • 手順が多いかも?(今回の作業ならmysqldumpの方が圧倒的に簡単でしかも早い)

mysqlの更新を止めれない場合はmysqldumpてテーブルを先に作って、AWS DMS使えばいいのかなって思います。
ではではーまたー。

Vue.jsの仮想DOMと差分レンダリングの仕組み①

こんにちは、成田です。

何回かに分けて記事を公開していこうかなと思っております。

前提

Vue.js version : 2.5.13

対象読者

  • JSフレームワークの知見がある
  • 仮想DOMの仕組みを知りたい

はじめに

なぜVue.jsか?という質問が飛んできそうなので一応述べさせて頂きます。

そもそもですが僕がコミットしているチームではVue.jsを使っています。
なぜVue.jsを選択したかというと、流行りのフロントエンドフレームワークだということももちろんありましたが、Vue.jsのhtmlの記述がReactのようなJSX記法ではなく限りなくhtmlに近いDSLを持っていることが一番大きいです。
それに加え、既存のプロジェクトに適用しやすいようなメリットが多かったのも選択した理由になりますが、その辺に関しては以下のVue.js公式ページを参照してみてください。
https://jp.vuejs.org/v2/guide/comparison.html

とまぁ結論としては僕が使っているフレームワークの内部実装はどうなってるのか?
という疑問と興味を持ったためです。

Vue.jsの仮想DOMと差分レンダリングの仕組み

仮想DOMの差分を適用する処理はよくdiff/patchなどと呼ばれます。
特にVue.jsではその処理を patch という関数で実装しています。↓
https://github.com/vuejs/vue/blob/dev/src/core/vdom/patch.js

早速ですが、本題の仮想DOMと差分レンダリングの深層に迫っていきましょう。

仮想DOMがどのように生成され、
生成された仮想DOMの変更差分はどのように適用されるのかというところに焦点を当てて説明しますが、
ざっと3つに分けて以下のような順序で処理を追っていきます。
で、今回はライフサイクルと差分レンダリング(patch)の実行を探っていきます。

  1. ライフサイクルと差分レンダリング(patch)の実行
  2. 差分レンダリング(patch)の仕組み
  3. 再レンダリング

サンプルとして簡易的な実装を用意しました。
このサンプル実装を元に説明します。

<body>
  <h2>Vue.js debug</h2>
  <div id="main">
    <section>
      <span class='counter'>Parent {{count}}</span>
      <button @click='increment()'>Increment</button>
    </section>
    <section>
      <my-component></my-component>
    </section>
  </div>
</body>

<script>
Vue.component("my-component", {
  data: function() {
    return { count: 0 }
  },
  methods: {
    increment: function() {
      this.count++;
    }
  },
  template: `
  <div>
    <span class='counter'>Child {{count}}</span>
    <button @click='increment()'>Increment</button>
  </div>`
});

new Vue({
  el: "#main",
  data: { count: 0 },
  methods: {
    increment: function() {
      this.count++;
    }
  }
});
</script>

このソースがブラウザに実際にレンダリングされた場合、下図のような実DOMツリーを生成します。
もちろん仮想DOMから実DOMが生成され、その内容が実DOMツリーとなって生成されています。
実DOMはhtmlのNodeの集合でありツリー構造です。 そして次回以降の記事で解説しますが、仮想DOMももちろん仮想Node(VNode)で形成されるツリー構造です。

DOMとは何か
https://developer.mozilla.org/ja/docs/DOM/DOM_Reference/Introduction

f:id:AdwaysEngineerBlog:20180119173118p:plain

仮想DOMは最終的にjavascriptにより実DOMを生成します。
なので当然ですが実DOM生成の内部実装には、javascriptを知る人なら誰もがわかる以下のように記述が存在します。

document.createElement(tag)

↓こちらの内部実装を参照してみるとすぐわかります。
https://github.com/vuejs/vue/blob/dev/src/platforms/web/runtime/node-ops.js

仮想DOMについて長々と述べると趣旨がずれそうなので、早速DOMがレンダリングされる際のフローを追っていきます。

1. ライフサイクルと差分レンダリング(patch)の実行

先ほど述べた通り、Vue.jsでは diff / patch の処理を以下のファイルのpatch関数内に実装しています。
https://github.com/vuejs/vue/blob/dev/src/core/vdom/patch.js

ReactなどのJSフレームワーク同様、Vue.jsにもライフサイクル関数がいくつか提供されています。

  • beforeCreate
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeDestroy
  • destroyed

公式ページに描かれている図がわかりやすいです。

引用: https://jp.vuejs.org/v2/guide/instance.html

差分レンダリングはどのライフサイクルの後に実行されるのでしょうか?

このファイルに記述されています。
https://github.com/vuejs/vue/blob/dev/src/core/instance/lifecycle.js

結論からいうと、初回レンダリング時はbeforeMountのあとに実行されます。
再レンダリング時の実行タイミングはまた違う流れになるのでまた後で見ていきます。

ざっと見た感じこれらの関数が差分レンダリング処理に直接的に関与しています

  • lifecycleMixin
  • mountComponent
  • callHook

lifecycleMixin

/* src/core/instance/lifecycle.js */
export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
      // no need for the ref nodes after initial patch
      // this prevents keeping a detached DOM tree in memory (#5851)
      vm.$options._parentElm = vm.$options._refElm = null
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }

    /*
     ... 略 ...
     */
  }

  Vue.prototype.$forceUpdate = function () {
    /* ... 略 ... */
  }

  Vue.prototype.$destroy = function () {
    /* ... 略 ... */
  }
}

llifecycleMixin関数はmounted以降のライフサイクルメソッド群をVueのプロトタイプチェーンに定義するために以下のAPIの初期化処理と、状態更新の処理を内包した関数(_update関数)の初期化を担う関数です。

_update $forceUpdate $destroy

_update関数以外はインスタンスメソッドとしてのライフサイクルなので実際にVue.jsをimportしたら使用できるこれらのAPIになります。

mounted後( 仮想DOMが実DOMになった後 )のライフサイクルは実行タイミングが不定なため別のデータフローが走ります。

そのためmounted前のこれら↓ライフサイクルは一回のみ実行されます。

  • beforeCreate
  • created
  • beforeMount
  • mounted

状態更新を行う関数(_update関数)をVueのプロトタイプチェーンに定義されています。
実際に差分レンダリング(patch関数)を実行する関数もプロトタイプチェーンでpatch関数という名で定義されていて、patch実行処理はその_update関数の内部から呼び出されます。
以下の部分です。

  /* 初回レンダリングの時 */
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(
      vm.$el, vnode, hydrating, false /* removeOnly */,
      vm.$options._parentElm,
      vm.$options._refElm
    )
    // no need for the ref nodes after initial patch
    // this prevents keeping a detached DOM tree in memory (#5851)
    vm.$options._parentElm = vm.$options._refElm = null

  /* 再レンダリング */
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }

mountComponent

上述したlifecycleMixin関数内部の_update関数ですが、実際にこの関数内から呼ばれます。

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  /*
    ... 略 ...
   */
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      /*
        ... 略 ...
       */
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      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, null, 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
}

このmountComponent関数は初回レンダリング時にのみ作用します。

再レンダリング時はこのWatcherというのがよしなに監視して、_update関数を間接的に呼ぶことで状態更新を行います。

new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)



この関数(mountComponent関数)が呼び出されるタイミングと
前述した_update関数が走るタイミングを実際にconsoleで確認していきます。

mountComponent関数の開始時とreturn前にconsole.group
_update関数の開始時にconsole.logを仕込んでみました。

export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    console.log(vnode);
    /* ....
    ... 略 ...
    */
  }
}
/* ....
... 略 ...
*/
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  console.group('mountComponent');
  /* ....
  ... vm._update()が実行される ...
  */
  console.groupEnd('mountComponent');
  return vm
}

こちらが初回レンダリング時
f:id:AdwaysEngineerBlog:20180119173153p:plain



こちらは再レンダリング時 (counterがインクリメントされた後) f:id:AdwaysEngineerBlog:20180119173206p:plain

この関数は初回レンダリング時にのみ作用します. 再レンダリング時はこのWatcherがよしなに監視して、_update関数を間接的に呼ぶことで状態更新を行います。

前述したことが何となく確認できましたね。

初回レンダリング時にmountComponentが2回呼ばれるのは今回のサンプル実装で子コンポーネント(MyComponent)を作ってるからですね。

再レンダリング時にはmountComponent関数が呼ばれずに_update関数が実行されています。

callHook

export function callHook (vm: Component, hook: string) {
  const handlers = vm.$options[hook]
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
}

オプション系ライフサイクルメソッドを呼び出す関数です。
前述のmountComponent関数内やinitLifecycle関数内でもcallHookが使用されています。

Vue.jsにはオプション系のライフサイクルインスタンスメソッドとしてのライフサイクルの 2つが存在しますが、 このcallHook関数で呼び出されるライフサイクルはオプション系です。

まとめ

今回は差分レンダリング(patch)の適用タイミングを追っていきました。
次回は差分レンダリング(patch)の仕組みを共有できたらと思います。