denoland/deno コミットリーディング 12/05

ファシリテータ: @ggtmtmgg セクリタリ: @ggtmtmgg

https://github.com/denoland/deno

0bec0fa594486876ff63d02e98ab454515e9c0d8

Title: Remove leaks from snapshot_creator
タイトル: snapshot_creatorからリークの可能性を取り除く
CommitDate: Mon Dec 3 19:34:01 2018 -0800

# LSAN build. We are in the process of getting a completely clean LSAN build,
# but it will take some work. So for now we just run a subset of the tests.

LSANを完全にcleanにビルドしたいけど、ちょっと大変らしい。
LSANが何かよくわからない。
LSANゾーン(Logical SANゾーン): Fabric間でお互いのFabricをマージせずにIntegrated Routingというルーティングによってデバイス間をゾーンする機能

# We want to detect leaks during the build process as well as when executing
# the tests. So set the ASAN_OPTIONS env var before build.py is run.
export ASAN_OPTIONS=detect_leaks=1

テストを実行するときにビルドプロセスの脆弱性を特定したい。だからASAN_OPTIONSをビルド前に指定する。

-  DENO_BUILD_MODE=release ./tools/build.py -j2
+  ./tools/build.py -C target/release -j2

DENO_BUILD_MODEの脱却が見られる。DENO_BUILD_MODEがどこかに残ってれば書き換えてプルリク送れそう。

-  std::string snapshot_str(reinterpret_cast<char*>(snapshot.data_ptr),
-                           snapshot.data_len);

   std::ofstream file_(snapshot_out_bin, std::ios::binary);
-  file_ << snapshot_str;
+  file_.write(reinterpret_cast<char*>(snapshot.data_ptr), snapshot.data_len);
   file_.close();
+
+  delete[] snapshot.data_ptr;
+  deno_delete(d);
+

あまりどういう処理なのかわからないけど、コネクションを適切にクローズして、snapshotを適切に削除できてそう。

9e839b7e2325e55147cc7e49ed9576cc000eaf63

Title: Avoid memory leak (#1265)
CommitDate: Mon Dec 3 19:07:34 2018 -0800

-    // TODO(zero-copy) Use Buf::leak(buf) to leak the heap allocated buf. And
-    // don't do the memcpy in ImportBuf() (in libdeno/binding.cc)
+    // deno_respond will memcpy the buf into V8's heap,
+    // so borrowing a reference here is sufficient.
     unsafe {
       libdeno::deno_respond(
         self.libdeno_isolate,
         self.as_void_ptr(),
         req_id,
-        buf.into(),
+        buf.as_ref().into(),
       )
     }
   }

deno_respondはV8のヒープ領域にメモリーコピーをする、なので参照を借りてこればここでは充分。 (何いっているかわからない。)

f6c841a6cd85b8e8db3c6cb52fa41811535a72ca

Title: Turn off kPromiseResolvedAfterResolved warning
CommitDate: Mon Dec 3 16:30:08 2018 -0800

+    case "ResolveAfterResolved":
+      // Should not warn. See #1272
+      break;

nodeではresolveされたPromiseをPromise.raceしようとしても特に警告は出ないらしい。 なのでdenoでも無視しようという考え。

参考

アドベントペアプロ #2 react-native

2018/12/04のアドベントペアプロの議事録

ファシリテータ: @ggtmtmgg セクリタリ: @ggtmtmgg 参加者: @saitoxu レポジトリ: https://github.com/facebook/react-native Issue: https://github.com/facebook/react-native/issues/20410

Yogaメインルーチンの整理

// STEP 1: CALCULATE VALUES FOR REMAINDER OF ALGORITHM
// STEP 2: DETERMINE AVAILABLE SIZE IN MAIN AND CROSS DIRECTIONS
// STEP 3: DETERMINE FLEX BASIS FOR EACH ITEM
// STEP 4: COLLECT FLEX ITEMS INTO FLEX LINES
// STEP 5: RESOLVING FLEXIBLE LENGTHS ON MAIN AXIS
// STEP 6: MAIN-AXIS JUSTIFICATION & CROSS-AXIS SIZE DETERMINATION
// STEP 7: CROSS-AXIS ALIGNMENT
// STEP 8: MULTI-LINE CONTENT ALIGNMENT
// STEP 9: COMPUTING FINAL DIMENSIONS
// STEP 10: SIZING AND POSITIONING ABSOLUTE CHILDREN

// STEP 11: SETTING TRAILING POSITIONS FOR CHILDREN

用語の共通認識を取る

MAIN DIRECTIONS: flexboxのdirection
MAIN-AXIS: direction水平軸
CROSS-AXIS: direction直行軸
FLEX BASIS: direction方向の要素の初期の長さ
FLEXIBLE LENGTHS: direction方向の要素の長さ
JUSTIFICATION: 最適化(?)
COMPUTING FINAL DIMENSIONS: 最終的なサイズを確定する
TRAILING POSITIONS: (多分)要素の後ろ側のスペースの大きさ(自信度20%)
devidation: 違い・偏差

Yoga.cppのYGNodelayoutImpl関数のSTEP1の手前の前処理

概要: Margin, Boarder, Padding, コメントアウトで普通のアルゴリズムとの差異・引数・詳細が述べられている。

Layout Modeごとに挙動が変わる。
YGMesuerModeUndefined: max content
YGMeasuerModeExactly: fill available
YGMeasuerModeAtMost: fit content
RowDirection: 横の向き
ColumnDirection: 縦の向き

max-content: The intrinsic preferred width.
resolution:
RTL: Right To Left, アラビア語とかで使う概念
L2738: 親のnodeのdirectionをset(inherit or ltr or rtl)
L2746-2784は上下左右のマージン,ボーダー,パディングを設定 ※RNのバージョンによってやや行番号がずれているので後で最新化する BoundAxis:
Measure: 長さを図る関数
YGNodeWithMeasureFuncSetMeasuredDimensions: MesureFuncを適用して計算し直したDimensionsをnodeにセットする
node->setLayoutMesuredDimension: MesuredDimensionをセットする
node->getMeasure(): MeasureFuncを返す関数。RCTShadowViewMeasure, RCTBaseTextInputShadowViewMeasure, RCTTextShadowViewMeasureという実態だったりする。 MeasureFuncをもつShadowViewは意外と少なそう。TextとTextInput以外にMeasureFuncがSetされるシチュエーションは少なさそう。 performLayout: specifies whether the caller is interested in just the dimentions of the node or it requires the entire node and its subtree to be layed out(with final positions) perform:

// setLayoutPadding(値, 位置): paddingをsetする
// YGUnwrapFloatOptional(Optionalな変数): Optional型の変数から値を取り出す
// node->getTrailingPadding(flexColumnDirection, ownerWidth): 末尾のpadding値を計算する
// YGEdgeBottom: enum
  node->setLayoutPadding(
      YGUnwrapFloatOptional(
          node->getTrailingPadding(flexColumnDirection, ownerWidth)),
      YGEdgeBottom);

cppのtypedefで関数を定義するときの書き方 typedef 返り値の型 (*関数名)(引数の型);

typedef YGSize (*YGMeasureFunc)(
    YGNodeRef node,
    float width,
    YGMeasureMode widthMode,
    float height,
    YGMeasureMode heightMode);

そしてLayoutを計算する準備が整った。

メモ

CSSのmax content, fill available, fit contentの違いが分からない
cppのexplicit: コンストラクタにつける。いまいちよくわからない。
引数の暗黙的型変換を防ぐためのもののよう http://starpentagon.net/life/2017/01/explicit_constructor RCTShadowViewMeasureとかの中でYGMeasureModeごとに場合分けしてたりしてなかったりしてて、結局呼び出し側でも場合分けしてるのがむだな感じする。

参考

アドベントペアプロ #1 denoland/deno

react-native-oss.hatenablog.com

ファシリテータ: @ggtmtmgg セクリタリ: @ggtmtmgg 参加者: @Haga, @binaryta レポジトリ: denoland/deno

denoはnodeの作者がnodeの欠点を補う形で作っているTypeScriptエンジン

Denoのビルドに関する手順

  1. サブモジュールごとclone $ git clone --recurse-submodules https://github.com/denoland/deno.git (chromiumのdepotツール群も内部利用している)

  2. rustの処理系をインストール $ brew install rust

  3. pythonは2.x系を使うのでない場合はinstall $ pyenv install 2.7.x

  4. denoのセットアップ $ ./tools/setup.py

  5. denoのビルド $ ./tools/build.py

  6. ビルドしたdenoを動かす $ ./target/debug/deno tests/002_hello.ts

プルリクエストまでの流れ

  1. テストの実行 $ ./tools/test.py
  2. コードフォーマットの実行 $ ./tools/format.py
  3. リンターの実行 $ ./tools/lint.py

立ち止まって分岐を考える

  • TODOを上から見ていく
  • Issueを上から見ていく
  • ソースコードを上から見ていく
  • ドキュメントを上から見ていく
  • commitを上から見ていく⬅

11月は83個のcommitがマージされている。 commitを見ていく

feat: Support for bigints in console

bigintがconsole.logで使えるようになる

Cargo and GN should build into same directory.

CargoはRustのビルドシステム GNはv8のなんか GNはtarget/debugに出力してる、Cargoはtarget/debug/build/deno-26d2b5325de0f0cf/outとかに出力してる。 これを両方target/debugにする Rustのコードでletを二行続けて使ってるのが特徴的

let out_dir = env::var_os("OUT_DIR").unwrap();
let out_dir = env::current_dir().unwrap().join(out_dir);

Upgrade Prettier to support BigInt syntax in TS

新しいprettierがBigIntに対応しててその新しいPrettierを実行する

Upgrade Rust crates

extern/cratesはRust版importのやつっぽい Cargo.tomlがRust版package.jsonっぽい

test rust version before rustup

rustupはUNIXライクシステムで動作するバージョン管理のやつ nodeでいうnodenv multirustの新しい版。 multirustもRustバイナリをインストールできるバージョン管理システム

Add Process.output

stdoutを最終文字までUint8Arrayにして返す

Add deno.readAll()

deno.readAll()はReaderを渡すとPromise<Uint8Array>を返す。

Fix flaky REPL test

REPLの反応速度のテスト??

clippy fixes

clippyはRustのlinter

Upgrade To TypeScript 3.2

TypeScriptを3.2に上げる

Make //build a git submodule

build_extra/toolchain/win/BUILD.gnを消してサブモジュール化している

立ち止まって分岐を考える

  • TODOを上から見ていく⬅
  • Issueを上から見ていく
  • ソースコードを上から見ていく
  • ドキュメントを上から見ていく x commitを上から見ていく

git grep

$ g grep TODO -- js/
js/blob.ts:  // TODO(qti3e) Implement convertLineEndingsToNative.
js/blob_test.ts:// TODO(qti3e) Test the stored data in a Blob after implementing FileReader API.
js/buffer.ts:      throw Error("ErrTooLarge"); // TODO DenoError(TooLarge)
js/buffer_test.ts:    // TODO buf.writeByte()
js/buffer_test.ts:    // TODO buf.readByte()
js/compiler.ts:  // TODO ideally this are not static and can be influenced by command line
js/compiler.ts:      // TODO: all this does is push the problem downstream, and TypeScript
js/dom_types.ts:  // TODO
js/dom_types.ts:  // TODO
js/fetch.ts:  readonly locked: boolean = false; // TODO
js/fetch.ts:  statusText = "FIXME"; // TODO
js/fetch.ts:  readonly type = "basic"; // TODO
js/fetch.ts:  redirected = false; // TODO
js/fetch_test.ts:// TODO(ry) The following tests work but are flaky. There's a race condition
js/file_info.ts:   * for this file/directory. TODO Match behavior with Go on windows for mode.
js/files.ts:// TODO This is just a placeholder - not final API.
js/net.ts:// TODO support other types:
js/net.ts:// TODO Support finding network from Addr, see https://golang.org/pkg/net/#Addr
js/net.ts: * TODO: `tcp4` (IPv4-only), `tcp6` (IPv6-only), `udp`, `udp4` (IPv4-only),
js/net_test.ts:  // TODO Currently ReadResult does not properly transmit EOF in the same call.
js/net_test.ts:/* TODO Fix broken test.
js/net_test.ts:/* TODO Fix broken test.
js/net_test.ts:/* TODO Fix broken test.
js/net_test.ts:/* TODO Fix broken test.
js/process.ts:// TODO Maybe extend VSCode's 'CommandOptions'?
js/process.ts:  signal?: number; // TODO: Make this a string, e.g. 'SIGTERM'.
js/read_link_test.ts:  // TODO Add test for Windows once symlink is implemented for Windows.
js/read_link_test.ts:  // TODO Add test for Windows once symlink is implemented for Windows.
js/remove_test.ts:  // TODO(ry) Is Other really the error we should get here? What would Go do?
js/stat_test.ts:// TODO Add tests for modified, accessed, and created fields once there is a way
js/symlink.ts:  // TODO Use type for Windows.
js/testing/testing.ts:    // TODO Do this on the same line as test name is printed.
js/testing/util.ts:// TODO(ry) Use unknown here for parameters types.
js/testing/util.ts:// TODO(ry) Use unknown here for parameters types.
js/timers.ts:// TODO(piscisaureus): fix that ^.
js/timers.ts:  // TODO: use a monotonic clock.
js/timers_test.ts:      // TODO: clearInterval(id) here alone seems not working
js/v8_source_maps.ts:      // TODO Support source map URLs relative to the source URL

js/net_test.tsコメントアウトされていたテストをコメントインしてみる

テストは通るっぽい コンパイルエラーが出ている

masterの状態で./tools/unit_tests.py target/debug/deno を実行してみたらエラーがでた

test result: ok. 37 passed; 0 failed; 0 ignored; 0 measured; 182 filtered out

running 8 tests
test getTravisDataSuccess_permW0N1E0R0
HttpOther: an error occurred trying to connect: Connection refused (os error 61)
    at maybeError (deno/js/errors.ts:38:12)
    at handleAsyncMsgFromRust (deno/js/dispatch.ts:27:17)

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 211 filtered out

Error: There were 1 test failures.
    at setTimeout (file:///Users/binaryta/git/deno/js/testing/testing.ts:110:13)
    at fire (deno/js/timers.ts:131:3)

-- 思考ログはここまで --

メモ

Rustの疑問

Q: どうやって実行する
A: プレイグラウンド https://play.rust-lang.org/

Q: assert_eq!(x.unwrap(), "air");の!ってなに?
A: !がついてるのはマクロ

Q: unwrap()ってなに?
A: Some(v) から vを取り出す

Q: Some("air")のSomeってなに?
A: なんかOptionと一緒に使うやつ

Q: let x: Option<&str> = None;のOption<&str>ってなに?
A: Optional型になる

Q: Panicってなに?
A: Rustの例外

Q: BUILD.gnってなに?
A:

やること

  • .gitignoreにVSCodeMacOSの記述を追記してPRを送る ← @binaryta
  • js/net_test.tsコメントアウトされていたテストをコメントインしてみる ← @Haga
  • masterの状態で./tools/unit_tests.py target/debug/deno を実行してみたらエラーが出た件issueを立てる ← @binaryta
  • Rustの疑問を解消しておく ← @ggtmtmgg

2018年12月 アドベントペアプロドキュメント

12/3から12/23までの三週間の平日JS Ninjaでアドベントペアプロを行います!

JS Ninja ペアプロ - Google カレンダー
JS Ninja OSSペアプロ Advent Calendar 2018 - Qiita

こんだけOSSペアプロすればいいことがいっぱいあるのでがんばろー!というモチベーションです。

確実に遂行するためにファシリテータとセクリタリという役割を定義します。 全日程のファシリテータとセクリタリはデフォルトで @ggtmtmgg です。参加できない日は他の人を任命していきます。

ファシリテータさん

その日程が確実に遂行されることに責任を持つ。

当日やること

開催の連絡とペアプロの進行

当日の前日の23:59までにやること

Googleカレンダー上で次の項目を編集する。

  • タイムボックスの設定
  • セクリタリの任命
  • 参加者の募集
  • タイトルの設定

セクリタリさん

該当日の議事録をJS Ninja OSSペアプロ Advent Calendar 2018 - Qiitaに投稿する! 簡単な思考ログになろうとも必ず投稿すること! 自分のブログに投稿してくれてもおけ! JS Ninja's blogの投稿はこちらから → ログイン - はてな

React NativeのNativeModuleの登録プロセスを追ってみた

参加者

@binaryta
@__syumai

前回の振り返り

  • JavaScript側でsetNativePropsが実行され、どのようにNative側に渡される値が作られるかを追い掛けた
  • 最終的に、TextInputのpropsを更新した時に、enqueueNativeCallと言うメソッドで積まれている moduleIdmethodIdparams (updatePayload) の値を特定できた
{
  moduleID: 41,
  methodID: 6,
  params: { // updatePayload
    fontSize: 40,
  },
}

ここからわかること 

  • JavaScript側で、Native側のmoduleIdとmethodIdを知っている
    • これどこに持ってる?
  • TextInputと言うのは一つのNativeModule?
  • TextInputと言うNativeModuleが登録された時に割り当てられたIDがmoduleIdなのでは?
  • 登録を行うと、JavaScriptにも、各moduleのIDが渡されている?

今回やっていくこと

  • オーバー・ザ・ブリッジ本の、NativeModulesの章を読み進めて、moduleId、methodIdがどう使われているのか突き止める

NativeModule

公式ドキュメントより

https://facebook.github.io/react-native/docs/native-modules-ios#ios-calendar-module-example

上記リンクに、iOSのCalendar APIにアクセスするNativeModuleを作成するための方法が書かれている

NativeModuleを作る方法ざっくりまとめ

  • Objective-Cのclassで、 RCTBridgeModule protocolを実装する
  • 下記の2つのマクロを使う
    • RCT_EXPORT_MODULE()
    • RCT_EXPORT_METHOD()

RCT_EXPORT_MODULE()

  • moduleの名前を吐き出して、JavaScript側から見えるようにする
  • RCT_EXPORT_MODULE(HogeManager) のように呼ぶと、HogeManagerと言う名前でexportされる
  • 名前を明示的に指定しなければ、class名がそのまま使われる
  • 接頭辞として RCT がclass名についていれば、省略した状態でexportされる (RCTHogeManager => HogeManager)

RCT_EXPORT_METHOD()

RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
{
  RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
}
  • methodを吐き出して、JavaScript側から呼べるようにする
  • 上記の例では、 addEvent と言うmethodがexportされ、引数としてnameとlocationの2つを受け取る設定になる
  • 実行されるmethodの実体は、 {} の中に書かれ、これをJavaScript側から呼ぶことが出来るようになる
  • 引数として指定できる型は、基本的に、JSONと相互に変換出来る型のみ
  • それ以外は、 RCTConvert helperを使って変換する

作り方を見てわかったこと

  • NativeModuleを登録する時に、moduleとmethodをそれぞれexportしていることはわかった
  • TextInputも、同様な方法でNativeModuleとして登録されている?(わからない)

TextInputの継承ツリーを追う

方針

  • この中のどこかでTextInputがRCT_EXPORT_MODULEされている?
  • であれば、TextInputのmoduleIdとmethodIdが取れるようになり、どこが具体的に呼ばれるのか特定できそう!
  • TextInputはclearisFocusedという2つのメソッドをもつので、ネイティブモジュールからexportされているのでは?(以下参照)
    https://facebook.github.io/react-native/docs/textinput#methods

見つかったもの

  • RCTSinglelineTextInputViewManager : RCTBaseTextInputViewManager
    • RCT_EXPORT_MODULE している
      • JSから直接呼べる?
    • viewメソッド
  • RCTSinglelineTextInputView : RCTBaseTextInputView
    • RCT_EXPORT_MODULE していない
  • RCTBaseTextInputView : RCTView
    • RCT_EXPORT_MODULE していない

わかったこと

  • 全然exportしてない
  • clearisFocused は、NativeModuleと一切関係なく、普通にJSだけで実装されていた

次回やりたい事

  • ネイティブモジュールのインスタンスmoduleDataById という配列に登録している、と言うことがオーバー・ザ・ブリッジ本に書かれていたので、ここの中身を直接見に行ってみる

React NativeでsetNativePropsした結果がどうNativeに反映されるのか JS編

React Native OSSもくもく会では、現在、こちらのIssueに取り組んでいます。
(メンバー: @ggtmtmgg, @takahi5, @__syumai, @binaryta)
https://github.com/facebook/react-native/issues/20971
内容としては、 setNativeProps を実行してTextInputの文字サイズを変更しようとした時に、予想通りの挙動をしてくれないという物でした。

setNativePropsとは

通常、ReactでComponentのpropsを更新したい場合は、該当のComponentに対して値を渡しているstateをsetStateで更新します。 setStateを実行すると、VDOMのツリー全体で、前回setStateした時点と差分があるかどうかの比較が走り、もし差分があれば、その部分のDOMツリーを再構成してrenderし直します。

一方、React Nativeを使う上で、stateは更新したくないが、Viewのプロパティだけを更新したい場合があります。 例えば、アニメーションするComponentを実装したいと考えた時、Viewのプロパティを更新するのにsetStateを使うと、Reactのrenderが連続的に走ってしまい、スムーズな動作が実現できなくなってしまいます。 setNativePropsを呼ぶと、Reactのstateを更新することなく、NativeのViewのプロパティのみを直接書き換えることが出来ます。

setNativePropsの実行例

<TextInput
  ref={this.inputRef}
  style={{
    fontSize: 20,
  }}
/>

<TouchableOpacity
  onPress={() => 
    this.inputRef.current.setNativeProps({
      style: {
        fontSize: 40,
      },
    });
  }
>
  <Text>Press me to change TextInput fontSize!</Text>
</TouchableOpacity>

今回問題となっている挙動

I'm trying to implement a dynamic fontSize on React Native based on the content size, but I hit a React Native limitation / bug.

You can change the fontSize multiple times before typing anything in the TextInput, but once you type something you cannot change it anymore. See it in action in the link below.

ざっくり翻訳

React Nativeで、コンテンツの大きさに応じて、動的にfontSizeを変更するものを実装しようとしています。しかし、React Nativeの制限、もしくはバグに引っかかってしまいました。

TextInputに文字を入力していない状態では、何度もfontSizeを変更することが出来ました。しかし、文字を入力した状態では一切変更出来なくなります。

Demo

本来予想される動作

setNativePropsする度、意図通りにTextInputのサイズが変わって欲しかった

setNativePropsの実装

setNativePropsがNativeのViewに反映されるまでの流れは、大きく2つに分けることが出来ます。

  1. JS側で変更するPropsを処理して、変更依頼をenqueueする。
  2. Native側で変更依頼をdequeueしてビューに反映させる。

今回はJS側の変更依頼をenqueueする処理に問題がないかを調べていきます。

JS側の実装

setNativePropsのJSの実装の実体は主に以下になります。

react-native/Libraries/Renderer/oss/ReactNativeRenderer-dev.js#L16165

setNativeProps: function(nativeProps) {
  ...
  var updatePayload = create(nativeProps, viewConfig.validAttributes);

  if (updatePayload != null) {
    UIManager.updateView(
      maybeInstance._nativeTag,
      viewConfig.uiViewClassName,
      updatePayload
    );
  }
}

この関数を以下の2つの部分に分けて実装を見ていきます。

  1. updatePayloadを作成する
  2. UIManager.updateViewにupdatePayloadを渡して変更依頼をenqueueする

1. updatePayloadを作成する

updatePayloadはcreateという関数で作られています。

var updatePayload = create(nativeProps, viewConfig.validAttributes);

そしてそのcreateではaddPropertiesを実行しています。

function create(props, validAttributes) {
  return addProperties(
    null, // updatePayload
    props,
    validAttributes
  );
}

そしてそのaddPropertiesでは引数にemptyObjectを渡した状態でdiffPropertiesを実行しています。

function addProperties(updatePayload, props, validAttributes) {
  // TODO: Fast path
  return diffProperties(updatePayload, emptyObject, props, validAttributes);
}

diffPropertiesはupdatePayloadを返す役割を持っており、実体はこの関数です。
だいぶ簡略化したのですがちょっと長いのでソースコード内に説明を記述していきます。

function diffProperties(updatePayload, prevProps, nextProps, validAttributes) {
  ...
  /* nextPropsに対してループ処理をします。
   * 今回のシチュエーションでは以下のオブジェクトが渡ってきます。
   * {
   *   style: { fontSize: 40 }
   * }
   * */
  for (var propKey in nextProps) {
    /* validAttributesは以下のようなオブジェクトです。
     * 
     * {
     *   ...
     *   autoCorrect: true
     *   backfaceVisibility: true
     *   backgroundColor: {diff: null, process: ƒ}
     *   blurOnSubmit: true
     *   ...
     * }
     * 
     * diffは値の変更を判定する関数です。
     * processは値をNative側に渡す前に処理する関数です。
     * backgroundColorは実数値に変換されます。
     */

    // もしattributeAttributesが値を持たないpropKeyであれば無視します
    attributeConfig = validAttributes[propKey];
    if (!attributeConfig) {
      continue; // not a valid native prop
    }

    ...

    if (prevProp === nextProp) {
      // もし値が変わってなくても無視します
      continue; // nothing changed
    }


    if (typeof attributeConfig !== "object") {
      /*
      * もしattributeConfigがただのtrueであれば
      * defaultDifferで差分の有無を確認し
      * 差分があればupdatePayloadに追加します。
      */
      if (defaultDiffer(prevProp, nextProp)) {
        (updatePayload || (updatePayload = {}))[propKey] = nextProp;
      }
    } else if (
      typeof attributeConfig.diff === "function" ||
      typeof attributeConfig.process === "function"
    ) {
      /*
       * diffがfunctionならdiffを利用して差分を確認する。
       * processがfunctionならprocessを利用して値を処理する。
       */
      var shouldUpdate =
        prevProp === undefined ||
        (typeof attributeConfig.diff === "function"
          ? attributeConfig.diff(prevProp, nextProp)
          : defaultDiffer(prevProp, nextProp));
      if (shouldUpdate) {
        var _nextValue =
          typeof attributeConfig.process === "function"
            ? attributeConfig.process(nextProp)
            : nextProp;
        (updatePayload || (updatePayload = {}))[propKey] = _nextValue;
      }
    } else {
      ...
      /*
       * もしattributeConfigがObjectじゃなければ、再帰的にdiffPropertiesを呼び出す。
       * 例えばpropKeyが "style" のような場合にここを通ります。
       */
      updatePayload = diffNestedProperty(
        updatePayload,
        prevProp,
        nextProp,
        attributeConfig
      );
      ...
    }
  }
  ...
  return updatePayload;
}

これで晴れてupdatePayloadを返せました。
ちなみに、setNativeProps({ style: { fontSize: 40 } });を実際に実行してみるとupdatePayloadは以下の値になりました。

{
 fontSize: 40
}

要求通りにupdatePayloadが作られていたことがわかりました。

2. UIManager.updateViewにupdatePayloadを渡す

if (updatePayload != null) {

  UIManager.updateView(
    maybeInstance._nativeTag,
    viewConfig.uiViewClassName,
    updatePayload
  );
}

UIManager.updateViewは内部的にenqueueNativeCallを呼んでいます。
enqueueNativeCallは(かなり簡略化すると)以下のような関数です。

enqueueNativeCall(
    moduleID: number,
    methodID: number,
    params: any[],
    onFail: ?Function,
    onSucc: ?Function,
  ) {
  ...
  this._queue[MODULE_IDS].push(moduleID);
  this._queue[METHOD_IDS].push(methodID);
  ...
  this._queue[PARAMS].push(params);
  ...
}

enqueueNativeCallはキューにmoduleId, methodId, paramsを積んでいて、
UIManager.updateViewは、enqueueNativeCallで下記の値をキューに積んでいました。

{
  moduleID: 41,
  methodID: 6,
  params: updatePayload
}

まとめ

setNativePropsの実装を追い掛けた結果、渡したい値がちゃんと作られて、enqueueNativeCallでキューに積まれていることが確認出来ました。
JavaScript側の実装には問題が無さそうと言うことがわかったので、次は、Native側の実装(今回は、iOS)を追い掛けてみようと思います。

【ReactNative】FlastListがスクロールできなくなるissueに取り組んだログ

@ggtmtmggです。

FlatListの小要素にmargintTop: '5%'とかを指定するとスクロールできなくなる問題に、 @saitoxuさんと取り組んだ学びのメモです。

前回までのメモはこちら https://gist.github.com/ggtmtmgg/9aaea0c6ddb1819623e3ef83e44e3b51

今回は主にObjective-Cのコードを読んでました。Objective−Cに関して初心者同然なのでそれに関する学びが多かったです。

Issueの再定義

元々は「FlatListの小要素にmargintTop: '5%'とかを指定するとスクロールできなくなる問題」だったのですが、問題の根源を追っていった結果、「ScrollViewcontentHeightを取るタイミングで小要素のheightを正しく取れていない問題」に帰着しました。

これがどのようにしては導き出されたかは割愛します。

思考のフロー

前回までの思考で、RCTScrollContentShadowViewの中で、ScrollViewのContentのLayout(要するにHeight)の計算をしているという認識を持っています。

このLayoutの計算を正しくすればIssueは解決するというのがこの瞬間の一番の仮説です。

今気になっていることを整理

  • RCTScrollContentShadowViewの実装はどうなっているのか
  • ScrollViewConetntHeightは本当に間違っているのか
    • ScrollViewContentHeightが間違っていることによってスクロールできなくなるという仮説が本当に正しいのか再調査したい
  • ScrollViewConetntHeightはいつ確定するのか
    • ContentHeightが間違っているのであれば実際に計算して値が確定する瞬間を見たい
  • ScrollViewConetntHeightはどういった計算式で確定するのか
    • ContentHeightが間違っているのであれば実際にどんな計算をしているのか知りたい

RCTScrollContentShadowViewの実装を覗いていく

https://github.com/facebook/react-native/tree/master/React/Views/ScrollView/RCTScrollContentShadowView.m

"shadow"は"follow and observe (someone) closely and secretly"という意味で使っているっぽい。にしてもShadowViewの比喩はもっと腹落ちできそう。

ざっくりRCTScrollContentShadowViewは、ScrollViewのContent部分の裏側で計算とかしてくれるViewという認識。

RCTScrollContentShadowViewRCTShadowViewを継承している。

@implementation RCTScrollContentShadowView

- (void)layoutWithMetrics:(RCTLayoutMetrics)layoutMetrics
            layoutContext:(RCTLayoutContext)layoutContext
{
  ...
  [super layoutWithMetrics:layoutMetrics layoutContext:layoutContext];
}

@end

[super layoutWithMetrics:layoutMetrics layoutContext:layoutContext]でScrollContentのLayoutoの計算をしてそう。

今でこそ簡単に読めるけどこのコードは[インスタンス名 メソッド名:第一引数 引数名:名前付き引数]という意味でインスタンスメソッドを実行している。

superRCTShadowViewなのそちらを見に行く

RCTShadowViewの実装を読み解く

https://github.com/facebook/react-native/tree/master/React/Views/RCTShadowView.m

  • RCTShadowView+Layout.mってファイル見かけたけどなんだろう。
  • UIEdgeInsetsってクラスについて
    • UIKitの構造体。insetというレイアウトの概念があるらしい。
  • yogaConfigはシングルトンらしい ref: ObCのシングルトンの話
  + (YGConfigRef)yogaConfig
  {
    static YGConfigRef yogaConfig;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
      yogaConfig = YGConfigNew();
      YGConfigSetPointScaleFactor(yogaConfig, RCTScreenScale());
      YGConfigSetUseLegacyStretchBehaviour(yogaConfig, true);
    });
    return yogaConfig;
  }
  • Yoga.cppのL3957に必要のなさそうな分岐を発見
  // We store points for Pixel as we will use it for rounding
if (pixelsInPoint == 0.0f) {
  // Zero is used to skip rounding
  config->pointScaleFactor = 0.0f;
} else {
  config->pointScaleFactor = pixelsInPoint;
}

↓ 以下の一行だけで良さそう

config->pointScaleFactor = pixelsInPoint;
typedef NS_ENUM(unsigned int, meta_prop_t) {
  META_PROP_LEFT,
  META_PROP_TOP,
  META_PROP_RIGHT,
  META_PROP_BOTTOM,
  META_PROP_START,
  META_PROP_END,
  META_PROP_HORIZONTAL,
  META_PROP_VERTICAL,
  META_PROP_ALL,
  META_PROP_COUNT,
};
  • RCTShadowView.m L200 (__bridge void *)selfはどういう記法だろう

  • RCTComponent.hにプルリクのチャンスを発見。RCT系インスタンスがReactRootViewであるかどうかの判定がイマイチみたい。

// TODO: this is kinda dumb - let's come up with a
// better way of identifying root React views please!
static inline BOOL RCTIsReactRootView(NSNumber *reactTag)
{
  return reactTag.integerValue % 10 == 1;
}
  • objcにはid型がある
Objective-Cでは「id」型という型が定義されています。このid型は汎用的な型なのでどんな型でもセットすることができます。(JavaでいうObject型のようなものです。)
(id型は内部的には対象オブジェクトへのポインタを持っています。)

このid型も参照型の1つですが、id型に限り宣言時に「*」(アスタリスク)は不要です。

今回よくわからなくて調べたこと

  • @synthesizeってアノテーションはなんだろう
    • @propertyを使うと、コンパイル時に勝手にゲッターとセッターを定義してくれる
    • @synthesizeは、ヘッダーファイルで@propertyとして定義したプロパティをどのメンバ変数に適用するかを示すのに使う
    • 要するに@propertyではゲッターセッター名、@synthesizeでは実際のメンバ変数を指定する
    • ちなみに、synthesizeは合成するという意味
  • deallocってメソッドはなんだろう
    • NSObjectのインスタンスメソッド
    • TipsとしてObjective-Cの全てのオブジェクトはNSObjectを継承している
    • インスタンスが破棄される時に呼ばれると予想される。自信はない。
    • deallocはオブジェクトの参照の開放のために使う
    • C系のメモリの管理をもう少し理解してからまたしらべます。
  • ARCってなんだろう
    • Automatic Reference Countingの略
    • NSObjectのretain/release/autoreleaseをいい感じにしてくれる
  • 無名カテゴリってなんだろう

    • 定義済みのクラスをコンパイル時に拡張するクラス拡張というものがある
    • 無名カテゴリはそれによく似ているが、動的にクラス定義を書き換える
    • カテゴリはインスタンス変数を操作することが出来ない
    • カテゴリと無名カテゴリの違いと関係はよく分かっていない
  • 関数ではなくマクロをつかっている理由はなんだろう

  • 結果よくわからなかったです
#define RCT_SET_YGVALUE(ygvalue, setter, ...)    \
switch (ygvalue.unit) {                          \
  case YGUnitAuto:                               \
  case YGUnitUndefined:                          \
    setter(__VA_ARGS__, YGUndefined);            \
    break;                                       \
  case YGUnitPoint:                              \
    setter(__VA_ARGS__, ygvalue.value);          \
    break;                                       \
  case YGUnitPercent:                            \
    setter##Percent(__VA_ARGS__, ygvalue.value); \
    break;                                       \
}
  • + (YGConfigRef)yogaConfig;+の意味はなんだろう
  • +で定義されるのはクラスメソッド
  • -で定義されるのはインスタンスメソッド