To add to Mark Adelsberger's answer and address your comment here:
I used to take commit as a snapshot but not diff content. So I expect the commit command just takes a snapshot of the index and stores it.
This is correct.  However, when you use git commit --only, the way Git achieves this is complicated.  (It's also not well documented.)
I normally talk about "the" index / staging-area / cache.  Git does have one particular distinguished index, "the" index, although it is actually per-work-tree: if you run git worktree add, you not only get a new work-tree, but also a new index (and new HEAD, and other work-tree-specific refs such as those for git bisect).  But Git is capable of working with additional temporary index files, and this is what git commit --only and git commit --include do.
Let's look at your setup again:
$ mkdir t
$ cd t
$ git init
Initialized empty Git repository in /mnt/c/test/git-test/t/.git/
$ touch a b
$ git add .
At this point, "the" index (the main one in .git/index) contains two files.  Here they are:
$ git ls-files --stage
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       a
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       b
Now, however, you run git commit a -m a, creating the initial commit (a root commit, with no parents).  This command—git commit --only a, more or less—works by:
- creating a new temporary index, .git/indexdigits;
- initializing that index from the current commit;1
- running the equivalent of GIT_INDEX_FILE=.git/indexdigits add a;
- running the equivalent of cp .git/index .git/index.moredigitsto create a second temporary index;
- running the equivalent of GIT_INDEX_FILE=.git/index.moredigits add a;2
- building a commit from the first temporary index, in the way git commitnormally builds a commit from the main index;3 and
- finishing off the commit by renaming the second temporary index to .git/index, so that it becomes the primary index.
What this does is:
- Create and use a temporary index for the commit that contains the HEADcommit plus the--onlyfiles.  The main index is undisturbed in case the new commit fails (though in your case it succeeds).
- Create and set up a second temporary index to be used if the commit succeeds.
- Attempt the commit using the first temporary index.
If the commit succeeds, the first temporary index is discarded and the second temporary index becomes the main index (via a rename operation so that it's all atomic).  If the commit fails, both temporary index files are removed.
This means that after a successful git commit --only, the main index is updated as if you had run git add on the --only files.  After a failed one—the commit can fail due to pre-commit hooks, or you erasing the commit message, for instance—all is as if you had never run git commit --only at all.
(In your case, since you didn't modify the file a before running git commit --only a, you can't tell some of these cases apart.)
When you went on to run git commit --only b, these steps repeated but with file b instead of file a.
1There is no current commit, as you haven't created any yet, so this is treated as a special case: Git creates this as an empty index.
2This git add winds up having no effect, since the file named a is still empty.  Had you modified the file named a in your work-tree at this point, though, it would have updated the second temporary index.
3Since Git is not using the file .git/index to build this new commit, any pre-commit hook that assumes that the index is named .git/index will do the wrong thing.  Note that with added work-trees, the main index for that added work-tree has a different name as well (.git/worktrees/<name>/index, if I remember correctly offhand).