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の差分を適用する関数はよくpatchなどと呼ばれます。
早速ですが、本題のpatchの深層に迫っていきましょう。

仮想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の実行

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

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

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

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

patchの処理はどのライフサイクルの後に実行されるのでしょうか?

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

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

ざっと見た感じこれらの関数がpatchに直接的に関与しています

  • 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実行処理はその_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の仕組みを共有できたらと思います。

Linuxユーザ切り替え時にSlackに通知する

こんにちは!奥村です!

2018年もアドウェイズエンジニアブログをよろしくお願いします。

Linuxのsuコマンドで特定のユーザに切り替わった際に、Slackへ通知するようにする設定をしてみたので
そちらの設定の記事となります。

手順

  • slackでIncoming WebHooksの設定をする
  • bashrcに何行かのコマンドを書く

これだけです

設定

SlackでIncoming WebHooksの設定をする

webブラウザのSlackにログインした状態で、

https://slack.com/services/new/incoming-webhook

こちらのURLにアクセスします。

f:id:AdwaysEngineerBlog:20180112032126p:plain

通知を飛ばすチャンネルを選択し、「add Incoming WebHooks integration」を押すと
incoming WebHooksのURLが発行されますので、それをどこかにメモっておいてください。

f:id:AdwaysEngineerBlog:20180112032651p:plain

設定すると、Slackのチャンネルのほうにも
「added an integration to this channel: incoming-webhook」と表示されているはずです。

詳細な使い方は

api.slack.com

こちらを参考にしてください。

bashrcに何行かのコマンドを書く

SSH等したときに読み込まれる

  • .bash_profile
  • .bashrc

がありますが 、
.bashrcはシェル起動時に読み込まれるもので、
.bash_profileは初回ログイン時に読み込まれるものだそうです。
(参考にさせていただきました。.bash_profile と .bashrc は何が違うの?使い分けを覚える - Corredor

ユーザの切り替え時に読み込まれるのは.bashrc なのでこちらに記述することにしました。 (「bashrc(Bash Run Command)とbash_profile(Bash Profile)」という違いもあります)

bashrc

bashrcに以下のコマンド群を追記すれば完成です。

# Swith user notification
TTY=`ps Tu | head -n 2 | tail -n 1 | awk '{print $7}'`
USER=`who | grep $TTY | awk '{ print $1 }'`
echo -n "Why do you need root?: "
read REASON < /dev/tty
DATA="{'channel': '#okumura-test', 'username': 'su-Notification', 'text': 'ROOT logged in\n user: $USER\n why: $REASON', 'icon_emoji': ':cop:'}"
curl -X POST https://hooks.slack.com/ここは事前に退避したincomming WebHookのURL -d "$DATA"
unset TTY USER DATA
  • TTY
    • どの端末かがセットされます。
  • USER
    • 指定されたTTYを誰が使っているのかがセットされます。
  • DATA
    • curlでポストする際に使う、JSON形式のテキストです。
  • read REASON < dev/tty
    • 端末への標準入力を変数REASONにセットします。

そして最後にWebHookのURLにPOSTしています。

さっそく試してみます。

試してみる

[vagrant@okumura-machine ~]$ su
パスワード:
Why do you need root?: 連続してメンテナンスをする
ok[root@okumura-machine vagrant]# 

パスワードとログインする理由を尋ねられ、質問に回答すると「OK」と返ってきます。 すると、

f:id:AdwaysEngineerBlog:20180112032753p:plain

のように警察アイコンのbotがユーザーと理由を通知してくれます。

これで、Linuxユーザ切り替え時にSlackに通知する設定は終わりです。

おまけとして
簡単に設定を反映できるように、ansibleも書きました。

- name: add su-Notification to bashrc
  blockinfile:
    dest: ~/.bashrc
    insertafter: EOF
    content: |
      # Swith user notification
      TTY=`ps Tu | head -n 2 | tail -n 1 | awk '{print $7}'`
      USER=`who | grep $TTY | awk '{ print $1 }'`
      echo -n "Why do you need root?: "
      read REASON < /dev/tty
      DATA="{'channel': '#okumura-test', 'username': 'su-Notification', 'text': 'ROOT logged in\n user: $USER\n  why: $REASON', 'icon_emoji': ':cop:'}"
      curl -X POST https://hooks.slack.com/services/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 -d "$DATA"
      unset TTY USER DATA

blockinfileモジュールで.bashrc に追記しているだけです。

まとめ

incomming WebHookを使うと、curlでslackへメッセージを送れることを知りました。
これで、手軽にシェルからslackへデータを送ることができますね。
シェル修行中の身ですが、これでまた世界が広がったような感じがします。

最近話題の「Meltdown」「Spectre」によってパスワードが抜き取られる可能性がでてきました。
だから、書いたというわけではありませんが、意図しないrootログインや無駄なログインはしっかりと管理したいものですね。

最後までご覧いただきありがとうございました。

新年明けましておめでとうございます。

f:id:AdwaysEngineerBlog:20171227100239j:plain

謹んで新年のお慶びを申し上げます。

昨年は大変お世話になりありがとうございました。
本年も昨年同様よろしくお願い申し上げます。
皆様のご健康とご多幸を心よりお祈り申し上げます。

平成30年元日

株式会社アドウェイズ
サービスデベロップメントグループ一同

AlexaでSlackにReminderを登録する

Adways Advent Calendar 2017 17日目の記事です。

http://blog.engineer.adways.net/entry/advent_calendar_2017


こんにちは、久保田です。

2度目の記事になりますが、またまたAlexaで遊んだ記事です。

今回は、Alexaを通して、SlackのReminderに予定を登録してみます。

設計

会話の流れはとりあえず以下のようにします。

ユ 「Alexa, スラックリマインダーで、明日の11時にMTGを設定して」
A 「明日の11時にMTGを設定しました。」  

まずはこの1パターンのみを考え、作ってみます。

設定

Alexaの設定をしていきます。

スキル情報

f:id:AdwaysEngineerBlog:20171225122116j:plain

対話モデル

f:id:AdwaysEngineerBlog:20171225122144p:plain

設定

f:id:AdwaysEngineerBlog:20171225122158j:plain

Lambda

プログラムはlambdaに作ります。

var https = require('https')

exports.handler = (event, context, callback) => {
    var slots = event.request.intent.slots; 
    var task = slots.Task.value;
    var date = slots.Date.value;
    var time = slots.Time.value;
    
    var resAlexa = {
        "version": "1.0",
        "response": {
            "outputSpeech": {
                "type": "PlainText",
                "text": date + time + 'に' +task+ 'を登録しました。'
            },
        }
    }
    
    var dn = new Date().getTime();
    var d  = (new Date(date + ' ' + time)).getTime();
    var unixTime = Math.ceil((d - dn) / 1000);
    https.get('https://slack.com/api/reminders.add?token='+process.env.TOKEN+'&text='+encodeURIComponent(task)+'&time='+unixTime+'&user='+process.env.USER+'&pretty=1', function(res){
        
        let body = '';
        res.setEncoding('utf8');

        res.on('data', (chunk) => {
            body += chunk;
        });

        res.on('end', (res) => {
            res = JSON.parse(body);
            console.log(res);
            callback(null, resAlexa);
            context.done(null, 'finish')
        });
    })
};

SlackのReminderのAPIの時間の扱い方が若干癖ありで微妙なのですが、一旦これで。

テスト

Alexaにはテストするためのシュミレーター画面があるので、それを使います。

f:id:AdwaysEngineerBlog:20171225122235p:plain

Slackの方をみてみましょう

f:id:AdwaysEngineerBlog:20171225122249p:plain

できているようですね。

まとめ

声経由でやって見ましたが、まだ日本語がうまく抽出できてないのか、僕の設定が間違っているのか、うまくTaskが取れない時もあったのですが、この流れで一応できました。
エラー処理などもやって、きちんと対話として成立させていきたいです。

ペアプロを導入しようと決意した話

Adways Advent Calendar 2017 16日目の記事です。

http://blog.engineer.adways.net/entry/advent_calendar_2017


社内サービスや社内システムを開発している組織のエンジニアリングマネージャーをさせていただいている山口です。

今回は、ペアプロを導入しようと決意した背景を共有させていただきます。

きっかけ


発端は、Joy, Inc.を読んだことです。
※本書の説明は省略させて頂きますが、非常に良い本ですのでオススメです!

この本を読んだ山口は、メンローイノベーションズのような組織を作りたいと思いました。

どうやって、作っていくのか。

ここでキーとなるのが、Joy, Inc. に出てくるペア作業です。

ペア作業はいままで見たなかでも最高のマネジメントツールだということだ。ペア作業により、従来あったたくさんの問題を解消できるようになる。ペア作業は学習システムを育む。また人間関係の構築、知識の塔の除去、新メンバーの立ち上げにも寄与し、生産性の問題を洗い出す役にも立つ。

よし、ペア作業やろう。

開発組織のペア作業といえば?ペアプロ。

ならば、ペアプロを導入だ!

どうせやるのであれば、抜本的に行こう!

よって、案件の開発は必ずペアプロで行うことにするぞ!

と決意したのです。

とはいえ


一般的にいいと言われているペアプロですが、パッションだけで導入するわけにもいかないので調査と検証を行いました。

まずは、ペアプロのメリットとデメリットを調べました。

ペアプロのメリット

調べてみると以下のようなメリットがあることがわかりました。

  • 属人化の軽減
  • レベル差の軽減、レベルの向上
    • レベルとは、技術レベル、PJや業務理解など
  • コミュニケーションの増加による、チーム感の向上
  • レビューコストの軽減
    • 常にレビューが行われている状態となるので、レビューを無くせる可能性も!
  • 新しいチャレンジのやりやすさ向上
  • メリハリのある開発

ペアプロのデメリット

中長期で見るとデメリットはない、もしくは無くせると思っています。 が、短期的にはありそうです。

働き方の変化が強制される

ソロでの開発では、その時の気分によってのんびり働いたり、キビキビ働いたりということが選べますが、
ペアでの開発では、一定の時間一定のペースで開発することが求められます。

また、弊社は出勤時間を個人の裁量で自由に選べる1という制度があるのですが、
ペアプロを導入すると、少なくともペアで出勤時間をある程度揃える必要があると考えています。

これらの問題は、乱暴な話かもしれませんが働き方に対する慣習に依るところが大きいと考えているので、
続けていくことでデメリットとして感じなくなるのではないかと思います。

生産性が低下する

これはよく言われていることでもありますし、直感的にも同意する方が多いのではないかと思います。

1つのタスクにかける人日だけを見たときには、中長期で見ても生産性は下がってしまうとは思います。

しかし、開発チーム全体として見たときの生産性を見ると、前述したメリットを享受することにより、
生産性は大きな問題にならないと考えています。

どうやって導入していくか


当たり前ですが、開発チームの外と内、それぞれに取り組みを理解して頂く必要があります。

開発チームの外に対して

正直な話、必勝の策はないと思っています。

メリットは実際にやってみても短期的には分かりづらいものとなるので、抵抗される可能性が高いです。

自分の場合は、各PJの責任者と開発部本部長が説得対象となります。

各PJの責任者

一部のPJの責任者には理解して頂くことが出来ました。

(残りはまだ取り組みを説明できていないです。)

PJの責任者も、開発チーム側に属人化の問題があることを問題だと考えており、
それが解消されるのであれば、やってもいいよ。という形でした。

ありがたいことです。

開発部の本部長

開発チームの生産性など自分と同じ責任を持つ立場だからこそ、納得していただくまで時間がかかるかなと思っていました。

しかし、既にペアプロのメリットなどを理解されていて、

「なぜ、今までやらなかったんでしょうね?」という発言が返ってくるぐらい歓迎されてしまいました。

いい上司を持ったなと思ったタイミングでした。

開発チームの中に対して

メンバー全員と個別にランチに行く時間を設けているので、殆どのメンバーにはそのタイミングで思いとメリット・デメリットを語りました。

みな、賛同してくれペアプロの取り組みも、自主的に進めてくれるなど積極的な行動をしてもらえました。

この行動はほんとに嬉しく、ペアプロを導入していきたいとより強く思うきっかけになりました。

少しだけ実践してみて


前述の通り、自主的にペアプロをやってみてくれたチームがありました。

そこから出てきたフィードバックとなります。

良かったこと

  • テストコードを書くノウハウが共有された
  • プロジェクトの既存機能の仕様などが共有された
  • コミュニケーションが活発に行われた
    • どのように実装すると品質があがるだろうか?
    • 納期と品質どこまでを追求するのか、妥協点をどこに置くか
    • 気軽に質疑応答が出来るようになった
    • 雑談が増え、新しいメンバーがより馴染んだ

課題

  • ペアプロの時間があまり取れなかった
    • 弊社の出勤時間を自由に選べる制度なども影響
  • 働き方に差があると、どちらかの働き方に寄ってしまう

まとめ

ペアプロの価値は、事前調査と実践で感じることが出来ました。

しかし、課題もありました。

今回の課題は、チームとしてどのような働き方をし、どのような結果を出すのか という点でチーム内の合意がないこと起因しそうです。

当たり前ではありますが、ペアプロを導入すればより良い組織になるというわけではなく、
組織の改善も同時に行い続ける必要があることがハッキリとわかったのです。

そして、自分の来年の目標が決まりました。

来年の目標

組織を改善しながらペアプロを導入するぞ!!!

参考記事

ペアプログラミングに関する調査報告

難易度は? 効果は? 実践して初めて分かった「ペアプログラミング」の実際


  1. 8時〜12時の間であればいつ出社してもOK。事前に出社時間を共有する必要もなし。