Author of this article: Mr.Luo, front end manager of Baychat, author blog MRLuo. Life.

From the beginning of this year (2017), our team began to introduce “vue.js” to develop mobile products. In the testing process of a project, the testing girl told us a strange bug: in a page that played music, there was a place to display the current playing position of the music synchronously; After the music starts playing, the content of this place changes constantly, but when the page scrolls, the content doesn’t change, and it looks like something is blocked.

The problem was only found in our iOS client, but not in wechat and Safari, leading us to suspect that it was due to some code on the client side. But after careful investigation, the problem is not so simple.

In the iOS WebView

There are two kinds of WebViews in iOS: UIWebView and WKWebView.

WKWebView is available from iOS 8. In addition to providing better performance and less memory footprint, it also improves some of the bad experiences in UIWebView, such as the Scroll event triggering. In UIWebView, the Scroll event will only fire after scrolling has completely stopped; In WKWebView, it is constantly triggered during scrolling.

However, WKWebView is not downward compatible with UIWebView, and the replacement cost is not small, so there are still quite a number of apps still using UIWebView, such as our Beitchat APP and Sina Weibo.

Even so, it was a simple matter for us to ask our iOS team to temporarily open the problem page with a WKWebView. The measured result is: there is no blocking problem in WKWebView.

Demo

To better reproduce this problem, we made a demo page with the following key code:

<template>
    <div>
        <audio ref="player" :src="audioURL" @timeupdate="updateTime" controls></audio>
        <div class="current-time">{{ time }}</div>
    </div>
</template>

<script>
export default {
    data() {
        return {
            audioURL: require('./music.mp3'),
            time: ' '
        };
    },
    methods: {
        updateTime() {
            this.time = this.$refs.player.currentTime;
            document.title = this.time; }}};</script>
Copy the code

Page function is very simple, when playing music, through the timeupdate event to update the value of the data field “time”, so that the current playing position is constantly updated to the interface. It also updates the value of “time” to the page title to check that the assignment of “time” was successful.

Open this page with Sina Weibo APP, and the running effect is as follows:

As you can see, after scrolling the page is finished, the number in the page is no longer updated, but the title continues to change. This shows that the timeUpdate event is constantly triggered, and the value of the “time” field is constantly updated, but the process of updating the data to the interface (refreshing the DOM) is blocked.

What’s blocked is…

As it happens, we found another phenomenon in the bugged product page: after the blocking problem, the function to call the client in the page was blocked. This led us to suspect that it was the client’s pot, but it turned out not to be. We encapsulated the client function calls as promises, and during debugging, we found that the Promise instance could not enter either the THEN or catch process.

We started to suspect that a Promise was blocking, so we added two buttons “Button1” and “Button2” to the demo:

<template>
    <div>
        <audio ref="player" :src="audioURL" @timeupdate="updateTime" controls></audio>
        <div class="current-time">{{ time }}</div>
        <input type="button" value="Button1" @click="click1" />
        <input type="button" value="Button2" @click="click2" />
    </div>
</template>

<script>
export default {
    data() {
        return {
            audioURL: require('./music.mp3'),
            time: ' '
        };
    },
    methods: {
        click1() { alert('click1'); },
        click2() {
            Promise.resolve().then((a)= > {
                alert('click2');
            });
        },
        updateTime() {
            this.time = this.$refs.player.currentTime;
            document.title = this.time; }}};</script>
Copy the code

As expected, after clicking on play music and scrolling, clicking on “Button1” brought up “Click 1”, but clicking on “Button2” got no response. This proves that it was indeed Promise that was blocked.

The culprit turned out to be…

I went to a search engine to find the answer, but found the source code for vue.js. To open the file locally, you do have this code:

As you can see from the comments here, the “vue.js” development team also knew about the blocking problem with Promise under UIWebView and fixed it, but why was it still a problem in the demo page?

It is important to minimize the amount of code and dependencies needed to reproduce the problem. So, I initialized a new project with “vue-cli” and put the demo page into it. At this time, I open the page with Sina Weibo for the same operation, and there is no blocking problem.

Then, install “SASS”, “postCSS-px2REM”, “Vuex” and “babel-Polyfill” in sequence, and re-open the Demo page after each installation. In the end, the problem recurred once the Babel-polyfill was filled.

babel-polyfill

Both Safari and WebView on iOS 8 and above already support promises, but babel-polyfill overcomes native promises with its own Promise! A look at the code for the “Corejs” on which “babel-Polyfill” depends shows that it checks Promise’s features more rigorously:

Since iOS promises don’t fully support these features, “Corejs” overlays the native Promise with its own Promise. Furthermore, it appears that the “vue.js” fix for the blocking problem is not valid for the “corejs” Promise.

The solution

There are three solutions:

  1. Do not install “babel-polyfill”, but this will cause “Vuex” to fail in older browsers.
  2. Change UIWebView to WKWebView, but that’s not something you can do anytime soon.
  3. After loading “babel-polyfill,” reset the browser Promise back to the native Promise.

Considering that those additional features are rarely needed in real development, option 3 is a better interim solution.

Start by tweaking the way “babel-Polyfill” is introduced to transfer its code files to the server by other means. Then modify the project entry file, the “index.html” in the root directory:

<script>
var _Promise;
// check whether iOS9+ (iOS9+ only supports Symbol)
var useNativePromise = typeof Promise= = ='function' &&    
    /^(iPhone|iPad|iPod)/.test(navigator.platform) &&
   typeof Symbol= = ='function';
if (useNativePromise) { _Promise = Promise; }
</script>
<script src="/ / s2.imgbeiliao.com/assets/js/lib/babel-polyfill/6.23.0/polyfill.min.js"></script>
<script>
if (_Promise) { Promise = _Promise; }
</script>
Copy the code

The code above goes like this: when iOS>=9 is checked, the original Promise is saved, and when babel-polyfill is finished, the saved Promise is overwritten back. What about iOS<9? The test girl had a hard time finding an iOS 8 iPhone to test and found that there was no blocking problem, so iOS<9 can be ignored.

Now that “babel-polyfill” has been introduced via the script tag, we can remove the dependency on it:

npm uninstall babel-polyfill --save
Copy the code

And then modify the “. / build/webpack base. Conf. Js “, remove “Babel – polyfill packaging entry:

entry: {
    // app: ['babel-polyfill', './src/main.js']
    app: ['./src/main.js']}Copy the code

This temporary solution is not elegant, let the client as soon as possible to WKWebView is the way to go.

Afterword.

Apple recently released iOS 11. In the iOS 11 WebView, promises are already complete and can be checked by the “corejs” feature, so there is no more blocking.