I do not like to separate some functions into two parts as that leads to confusion. I have a hard enough time keeping lots of things straight.
I have managed to un-delay functions while keeping them whole. I have working examples in still developing code that are not simple/easy as they contain trace-log elements and a lot of device-specific parts.
The new function needs at least 2 time variables, start and wait.
At the start of the function the first check sees if wait is zero and if so, skips the time check.
If not zero then it checks ( millis - start < wait ) and if true, returns immediately, else it zeros wait.
Next stage is a state machine switch-case, the new function needs a state variable.
Each state is the old code up to the delay in the old code.
At that point, start and wait are set to millis() and the delay value then state is set to the next to run.
At the end state my functions set start to the time when the first state ran and wait to the desired wait for the whole function to run and then state to the first state.
I write the functions as inline and put them directly in loop().
Mine use enums for the states (much easier to read!) and have PROGMEM names that a trace-log (to serial) prints out showing time stamped execution points. They are also device(s) specific so I don’t just throw one in here. And they do work really well.
I had to go to such lengths as the site is in Hungary and I am in the US without most of the hardware to duplicate results so I added in the heavy-duty tracing just to keep my hair.
The base process is pretty simple. I had that working before I knew much about the hardware, a 1-wire string of sensors in one and a GSM routine now with many states (formerly 100ms to 5s delays) in another.
If you want real examples then send me a PM.