The main problem here is grouping the commands correctly. Subshells are a secondary issue.
x|y will redirect the output of x to the input of y
Yes, but x | y; z isn't going to redirect the output of x to both y and z.
In df . | read a; read a b; echo "$a", the pipeline only connects df . and read a, the other commands have no connection to that pipeline. You have to group the reads together to create a compound command: df . | { read a; read a b; } or df . | (read a; read a b) for the pipeline to be connected to both of them. This also applies to other constructs which group commands: so if one side of a pipe is while ...; do ...; done, if ... ; then ...! fi, for ...; do ...; done, etc., the entire compound command is connected to the pipe.
However, now comes the subshell issue: commands in a pipeline are usually† run in a subshell, so setting a variable in them doesn't affect the parent shell. So the echo command has to be in the same subshell as the reads. So: df . | { read a; read a b; echo "$a"; }.
Now whether you use ( ... ) or { ...; } makes no particular difference here since the commands in a pipeline are usually† run in subshells anyway.
† Usually. However, there are some cases where the last command in a pipeline might be run in the current shell, such as with the lastpipe option in bash, or by default in ksh or zsh. In this case, using parentheses (...) forces a subshell where braces {...;} wouldn't. See this answer for more details.
a=$(findmnt --noheadings --output SOURCE $(stat --printf=%m .)), avoiding any parsing of command output.