Vue.js 升級踩坑小記

黃軼發表於2017-11-27

本文並不是什麼高深的技術文章,只是記錄我最近遇到一個因為 Vue 升級導致我的一個專案踩坑以及我解決問題的過程。文章雖長但不水,寫下來的目的是想和大家分享一下我遇到問題時候一個思考的方法和態度。

背景:去年我在慕課網推出了一門 Vue.js 的入門實戰課程——Vue.js 高仿餓了麼外賣 App ,這門課程收到了非常不錯的反響,於是今年又在慕課網上繼續推出了 Vue.js 的高階進階實戰課程——Vue.js 音樂 App,同樣反饋不錯。每天晚上下班回家,我會去問答區看一下學生們的問題,發現近期有不少同學反饋了同樣的問題,iOS 微信裡點選不能播放歌曲了,PC 可以。通常遇到這種問題我會讓學生先去訪問我的專案的線上地址,看看我的程式碼會不會有問題,得到的結論是我的線上程式碼沒問題,但他們自己寫就不行,並且說已經完全和我的程式碼做了對比,這就讓我覺得十分詭異。沒過多久,有些學生就想出了一個辦法,在全域性 document 繫結一個同步的 click 事件,在 click 事件的回撥函式中同步觸發一次 audio 的 play 方法,似乎解決了問題,也得到了一些同學的採納,但是我看到以後的第一反應是不能用這種太 hack 的方式去解決問題,必須找到問題的本質,於是乎我開始了一段很有意思的找問題的過程。

定位問題

先看現象:同學們寫的程式碼在 iOS 微信瀏覽器下不能播放,PC 是可以的;我線上的程式碼是都可以。瞭解現象後我開始排查問題:

  • 同學們的程式碼寫的有問題?
    雖然會有這種可能性,但從 2 個維度被我否決了:1. 同學們也都對比過我的原始碼的,而且出問題的同學也不是個別現象;2. 如果是程式碼問題,那麼大多可能性是 PC 和移動端都不能播放。

  • 找不同?
    這個問題是最新才出現的,同學們開始學習編寫課程程式碼都也是通過 vue-cli 腳手架先初始化程式碼。接著我大概看了一下新版的腳手架初始化的程式碼,果然是大不同,webpack 升級到 3+,配置發生了很大的變化。不過依據我的經驗,構建工具的升級是不會影響業務程式碼的,一定還有別的原因。

  • Vue.js 升級?
    除了 webpack 配置的不同,最新腳手架初始化的程式碼用的 Vue.js 版本是 2.5+,而我線上程式碼的 Vue.js 版本是 2.3+,難道是 Vue.js 導致的問題嗎?帶著這個疑問我去翻閱了 Vue.js 的 release log,發現 Vue.js 大大小小版本釋出了十幾次。如果每個都仔細檢視也會很耗時,於是我採用了一個經典的 2 分法的思路去定位,我先把 Vue.js 升級到 2.4.0,發現竟然安裝不了(這是 Vue.js 剛升到 2.4 npm 釋出的 bug),於是又升級到 2.4.1,然後拿我的手機試了一下,還是可以播放的。接著我把 Vue.js 升級到 2.5.0,手機一試果然不能播放了,(擦。。)我心裡默唸一句,總算找到問題所在了。

問題的本質

以上定位到問題大概花了我半小時時間,但是我並沒有找到問題的根本原因,於是我翻閱了 Vue.js 2.5 的 release log,由於很長就不列了。Vue.js 每次升級主要分成 2 大類,Features & Improvements 和 Bug Fixes。我從上往下依次掃了一遍,把一些關於它核心的改動都點進去看了一下程式碼的修改,最終鎖定了這一條:

use MessageChannel for nextTick 6e41679, closes #6566 #6690

接著我點進去看了一下改動,我滴天,改動很大呀,nextTick 的核心實現變了,MutationObserver 不見了,改成了 MessageChannel 的實現。等等,有些同學看到這裡,可能會懵,這都是些啥呀。不急,我先簡單解釋一下 Vue 的 nextTick。

nextTick

介紹 Vue 的 nextTick 之前,我先簡單介紹一下 JS 的執行機制:JS 執行是單執行緒的,它是基於事件迴圈的。對於事件迴圈的理解,阮老師有一篇文章寫的很清楚,大致分為以下幾個步驟:

(1)所有同步任務都在主執行緒上執行,形成一個執行棧(execution context stack)。

(2)主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。

(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。

(4)主執行緒不斷重複上面的第三步。

主執行緒的執行過程就是一個 tick,而所有的非同步結果都是通過 “任務佇列” 來排程被排程。 訊息佇列中存放的是一個個的任務(task)。 規範中規定 task 分為兩大類,分別是 macro task 和 micro task,並且每個 macro task 結束後,都要清空所有的 micro task。

關於 macro task 和 micro task 的概念,這裡不會細講,簡單通過一段程式碼演示他們的執行順序:

for (macroTask of macroTaskQueue) {
    // 1. Handle current MACRO-TASK
    handleMacroTask();

    // 2. Handle all MICRO-TASK
    for (microTask of microTaskQueue) {
        handleMicroTask(microTask);
    }
}複製程式碼

在瀏覽器環境中,常見的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate;常見的 micro task 有 MutationObsever 和 Promise.then。對於它們更多的瞭解,感興趣的同學可以看這篇文章

回到 Vue 的 nextTick,nextTick 顧名思義,就是下一個 tick,Vue 內部實現了 nextTick,並把它作為一個全域性 API 暴露出來,它支援傳入一個回撥函式,保證回撥函式的執行時機是在下一個 tick。官網文件介紹了 Vue.nextTick 的使用場景:

Usage: Defer the callback to be executed after the next DOM update cycle. Use it immediately after you’ve changed some data to wait for the DOM update.
使用:在下次 DOM 更新迴圈結束之後執行延遲迴調,在修改資料之後立即使用這個方法,獲取更新後的 DOM。

在 Vue.js 裡是資料驅動檢視變化,由於 JS 執行是單執行緒的,在一個 tick 的過程中,它可能會多次修改資料,但 Vue.js 並不會傻到每修改一次資料就去驅動一次檢視變化,它會把這些資料的修改全部 push 到一個佇列裡,然後內部呼叫 一次 nextTick 去更新檢視,所以資料到 DOM 檢視的變化是需要在下一個 tick 才能完成。

接下來,我們來看一下 Vue 的 nextTick 的實現,在 Vue.js 2.5+ 的版本,抽出來一個單獨的 next-tick.js 檔案去實現它。

/* @flow */
/* globals MessageChannel */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

// Here we have async deferring wrappers using both micro and macro tasks.
// In < 2.4 we used micro tasks everywhere, but there are some scenarios where
// micro tasks have too high a priority and fires in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using macro tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use micro task by default, but expose a way to force macro task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false

// Determine (macro) Task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// Determine MicroTask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
    // in problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc
}

/**
 * Wrap a function so that if any code inside triggers state change,
 * the changes are queued using a Task instead of a MicroTask.
 */
export function withMacroTask (fn: Function): Function {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true
    const res = fn.apply(null, arguments)
    useMacroTask = false
    return res
  })
}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}複製程式碼

我們在有之前的知識背景,再理解 nextTick 的實現就不難了,這裡有一段很關鍵的註釋:在 Vue 2.4 之前的版本,nextTick 幾乎都是基於 micro task 實現的,但由於 micro task 的執行優先順序非常高,在某些場景下它甚至要比事件冒泡還要快,就會導致一些詭異的問題,如 issue #4521#6690#6566;但是如果全部都改成 macro task,對一些有重繪和動畫的場景也會有效能影響,如 issue #6813。所以最終 nextTick 採取的策略是預設走 micro task,對於一些 DOM 互動事件,如 v-on 繫結的事件回撥函式的處理,會強制走 macro task。

這個強制是怎麼做的呢,原來在 Vue.js 在繫結 DOM 事件的時候,預設會給回撥的 handler 函式呼叫 withMacroTask 方法做一層包裝,它保證整個回撥函式執行過程中,遇到資料狀態的改變,這些改變都會被推到 macro task 中。

對於 macro task 的執行,Vue.js 優先檢測是否支援原生 setImmediate,這是一個高版本 IE 和 Edge 才支援的特性,不支援的話再去檢測是否支援原生的 MessageChannel,如果也不支援的話就會降級為 setTimeout 0

nextTick 對 audio 播放的影響

回到我們的問題,iOS 微信瀏覽器不能播放歌曲和 nextTick 有什麼關係呢?先來看一下我們的歌曲播放這個功能的實現方法。

我們的程式碼會有一個播放器元件 player.vue,在這個元件中我們會持有一個 html5 的 audio 標籤。由於可呼叫播放的地方很多,比如在歌曲列表元件、榜單元件、搜尋結果元件等等,因此我們用 vuex 對播放相關的資料進行管理。我們把正在播放的列表 playlist 和當前播放索引 currentIndex 用 state 維護,當前播放的歌曲 currentSong 通過它們計算而來:

// state.js
const state = {
  playlist: [],
  currentIndex:0
}
// getters.js
export const currentSong = (state) => {
  return state.playlist[state.currentIndex] || {}
}複製程式碼

然後我們在 player.vue 元件裡 watch currentSong 的變化去播放歌曲:

// player.vue
watch : {
   currentSong(newSong,oldSong) {
      if (!newSong.id || !newSong.url || newSong.id === oldSong.id) {
          return
       }
       this.$refs.audio.src = newSong.url
       this.$refs.audio.play()
   }
}複製程式碼

這樣我們就可以在任何元件中提交對 playlistcurrentIndex 的修改來達到播放不同歌曲的目的。那麼這麼寫和 nextTick 有什麼關係呢?

因為在 Vue.js 中,watcher 的回撥函式執行預設是非同步的,當我們提交對 playlist 或者 currenIndex 的修改,都會觸發 currentSong 的變化,但是由於是非同步,並不會立刻執行 watcher 的回撥函式,而會在 nextTick 後執行。所以當我們點選歌曲列表中的歌曲後,在 click 的事件回撥函式中會提交對 playlistcurrentIndex 的修改, 經過一系列同步的邏輯執行,最終是在 nextTick 後才會執行 wathcer 的回撥,也就是呼叫 audio 的 play。

所以本質上,就是使用者點選到 audio 的 play 並不是在一個 tick 中完成,並且前面提到 Vue.js 中對 v-on 繫結事件執行的 nextTick 過程會強制使用 macro task。那麼到底是不是由於 nextTick 影響了 audio 在 iOS 微信瀏覽器中的播放呢,
我們就來把化繁為簡,寫一個簡單 demo 來驗證這個問題,用的 Vue.js 版本是 2.5+ 的。

<template>
    <div id="app">
        <audio ref="audio"></audio>
        <button @click="changeUrl">click me</button>
    </div>
</template>

<script>
    const musicList = [
    'http://ws.stream.qqmusic.qq.com/108756223.m4a?fromtag=46',
    'http://ws.stream.qqmusic.qq.com/101787871.m4a?fromtag=46',
    'http://ws.stream.qqmusic.qq.com/718475.m4a?fromtag=46'
  ]

  export default {
    name: 'app',
    data() {
      return {
        index: 0,
        url: ''
      }
    },
    methods: {
      changeUrl() {
        this.index = (this.index + 1) % musicList.length
        this.url = musicList[this.index]
      }
    },
    watch: {
      url(newUrl) {
        this.$refs.audio.src = newUrl
        this.$refs.audio.play()
      }
    }
  }
</script>複製程式碼

這段程式碼的邏輯非常簡單,我們會新增一個 watcher 監聽 url 變化,當點選按鈕的時候,會呼叫 changeUrl 方法,修改 url,然後 watcher 的回撥函式執行,並呼叫 audio 的 play 方法。這段程式碼在 PC 瀏覽器是可以正常播放歌曲的,但是在 iOS 微信瀏覽器裡卻不能播放,這就證實了我們之前的猜想——在使用者點選事件的回撥函式到 audio 的播放如果經歷了 nextTick 在 iOS 微信瀏覽器下不能播放。

macro task 的鍋?

有些同學可能會認為,當使用者點選了按鈕到播放的過程在 iOS 微信瀏覽器或者是 iOS safari 瀏覽器應該需要在同一個 tick 才能執行,果真需要這樣嗎?我們把上述程式碼做一個簡單的修改:

changeUrl() {
  this.index = (this.index + 1) % musicList.length
  this.url = musicList[this.index]

  setTimeout(()=>{
    this.$refs.audio.src = this.url
    this.$refs.audio.play()
  }, 0)
}複製程式碼

我們現在不利用 Vue.js 的 nextTick 了,直接來模擬 nextTick 的過程,發現使用 setTimeout 0 是可以在 iOS 微信瀏覽器器、包括 iOS safari 下播放的,然而實際上我們只要在 1000ms 內的延時時間播放都是可以的,但是超過 1000ms,比如 setTimeout 1001 又不能播放了,感興趣的同學可以試試,這個現象的理論依據我還沒找到,如果知道理論的同學也非常歡迎留言告訴我。

所以通過上述的實驗,我們發現並不一定要在同一個 tick 執行播放,那麼為啥 Vue.js 的 nextTick 是不可以的呢?回到 nextTick 的 macro task 的實現,它優先 setImmediate、然後 MessageChannel,最後才是 setTimeout 0。我們知道,除了高版本 IE 和 Edge,setImmediate 是沒有原生支援的,除非一些工具對它進行了重新改寫。而 MessageChannel 的瀏覽器支援程度還是非常高的,那麼我把這段 demo 的非同步過程改成用 MessageChannel 實現。

changeUrl() {
  this.index = (this.index + 1) % musicList.length
  this.url = musicList[this.index]

  let channel = new MessageChannel()
  let port = channel.port2
  channel.port1.onmessage = () => {
    this.$refs.audio.src = this.url
    this.$refs.audio.play()
  }
  port.postMessage(1)
}複製程式碼

這段程式碼在 PC 瀏覽器是可以播放的,而在 iOS 微信瀏覽器又不能播放了,除錯後發現 this.$refs.audio.play() 的邏輯也是可以執行到的,但是歌曲並不能播放,應該是瀏覽器對 audio 播放在使用 MessageChannel 做非同步的一種限制。

前面提到實現 macro task 還有一種方法是利用 postMessage,它的瀏覽器支援程度也很好,我們來把 demo 改成利用它來實現。

changeUrl() {
  this.index = (this.index + 1) % musicList.length
  this.url = musicList[this.index]

  addEventListener('message', () => {
    this.$refs.audio.src = this.url
    this.$refs.audio.play()
  }, false);
  postMessage(1, '*')
}複製程式碼

這段程式碼在 PC 瀏覽器和 iOS 微信瀏覽器以及 iOS safari 都可以播放的,說明並不是 macro task 的鍋,而是 MessageChannel 的鍋。其實 macro task 還有很多實現方式,感興趣的同學可以看看 core-js 中對於 macro task 的幾種實現方式

如何解決?

現在我們定位到問題的本質是因為 Vue.js 的 nextTick 中優先使用了 MessageChannel,它會影響 iOS 微信瀏覽器的播放,那麼我們如何用最小成本來解決這個問題呢?

Vue.js 的版本降級

如果是真實執行在生產環境中的專案,毫無疑問這肯定是優先解決問題的首選,因為確實也是因為 Vue.js 的升級才造成這個 bug 的。在我們的實際專案中,我們都是鎖死某個 Vue.js 的版本的,除非我們想使用某個 Vue.js 新版的 feature 或者是當前版本遇到了一個嚴重 bug 而新版已經修復的情況,我們才會考慮升級 Vue.js,並且每次升級都需要經過完整的功能測試。

為何把 Vue.js 降級到 2.4+ 就沒問題呢,因為 Vue.js 2.5 之前的 nextTick 都是優先使用 microtask 的,那麼 audio 播放的時機實際上還是在當前 tick,所以當然不會有問題。

說到版本問題,其實這也是 Vue.js 的一點瑕疵吧,升版本的時候有時候改動過於激進了,比如這次關於 nextTick 的升級,它其實是 Vue.js 一個非常核心的功能,但是它只有單元測試,並沒有大量的功能測試 case 覆蓋,也只能通過社群幫助反饋問題做改進了。

同步的 watcher

Vue.js 的 watcher 預設是非同步的,當然它也提供了同步的 watcher,這樣 watcher 的回撥函式執行就不需要經歷了 nextTick,這樣確實可以修復這個 bug,但又會引起別的問題。因為我們的音樂播放器有一個 feature 是可以在播放的過程中切換播放模式,我們支援順序播放、隨機播放、單曲迴圈三種播放模式,當我們從順序播放切到隨機播放模式的時候,實際上是對播放列表 playlist 做了修改,同時也修改了 currentIndex,這樣可以保證我們在切換模式的時候並不會修改當前歌曲。那麼問題來了,由於 currentSong 是由 playlistcurrentIndex 計算而來的,對它們任何一個修改,都會觸發 currentSong 的變化,由於我們現在改成同步的 watcher,那麼 currentSong 的回撥會執行 2 次,這樣第一次的修改導致計算出來的歌曲就變成了另外一首了,這個顯然也不是我們期望的。所以同步 watcher 也是不可行的。

其它方式

其實還有很多方式都能“修復”這個問題,比如我們不通過 watcher,改成每次點選通過 event bus 去通知;比如仍然使用同步 watcher,但 currentSong 不通過計算,直接用 state 保留;比如每次點選事件不通過 v-on 繫結,我們直接在 mounted 的鉤子函式裡利用原生的 addEventListener 去繫結 click 事件。

當然,上述幾個方式都是可行的,但是我並不推薦這麼去改,因為這樣對業務程式碼的改動實在太大了,如果我們本身的寫法如果是合理的,卻要強行改成這些方式,就好像是:我知道了框架的某一個坑,我用一些奇技淫巧繞過了這些坑,這樣做也是不合理的。

框架產生的意義是什麼:制定一種友好的開發規範,提升開發效率,讓開發人員更加專注業務邏輯的開發。所以優秀的框架不應該限制開發人員對於一些場景下功能的實現方式,僅僅是因為這種實現方式雖然本身合理但可能會觸發框架某個坑。

臨時的 hack 方法

由於不想動業務程式碼,所以我就想了一些比較 hack 的辦法,因為是 MessageChannel 的鍋,所以我就在 Vue.js 的初始化前,引入了一段 hack.js

// hack for global nextTick
function noop() {
}

window.MessageChannel = noop
window.setImmediate = noop複製程式碼

這樣的話 Vue.js 在初始化 nextTick 的時候,發現全域性的 setImmediateMessageChannel 被改寫了,就自動降級為 setTimeout 0 的實現,這樣就可以規避掉我們的問題了。當然,這種 hack 方式算是沒有辦法的辦法了,我並不推薦。

給 Vue.js 提 issue

所以這種情況最合理的就是給 Vue.js 提 issue,我確實也是這麼做了,去 Github 上提了一個 issue,第一次給 Vue.js 提 issue,發現 Vue 官方這塊做的還是蠻人性化的,直接給一個提 issue 的連結,通過填寫一些表單來描述這個 issue,並且推薦了一個很好的復現問題的工具 CodeSandbox 。這個 issue 當天就收到了尤大的回覆,他表示 Vue.js 的 nextTick 確實會造成這個問題,但是我應該在同一個 tick 完成歌曲的播放,而不應該使用 watcher,接著就 close 了 issue。因為我提 issue 為了更直觀的演示核心問題,用的就是上面提到的非常簡單的 demo,所以在這種場景下,他說的也沒問題,確實沒有必要使用 watcher,於是我趕緊又回覆了 issue,說明了一下我的真實使用場景,並表明希望從 Vue.js 核心去修復這個問題。可惜的是,尤大目前也並沒有再回復這個 issue。

總結

通過記錄我這一次發現問題——定位問題——解決問題的過程,我想給同學帶來的思考不僅僅是這個問題本身,還有我們遇到問題後的一些態度。發現問題並不難,很多人在寫程式碼中都會發現問題,那麼發現問題後你的第一反應是嘗試自己解決,還是去求助,我相信前者肯定更好。那麼在解決之前需要定位問題,這裡我要提到一個詞,叫“面向巧合程式設計”,很多人遇到問題後會不斷嘗試這種辦法,很可能某個辦法就從表象上“解決”了這個問題,卻不知道為什麼,這種解決問題的方式是很不靠譜的,你可能並沒有根本上解決問題,又可能解決了這個問題卻又引發另一個問題。所以定位問題的本質就非常關鍵了,其實這是一個能力,一個好的工程師不僅會寫程式碼,也要會查問題,能快速定位到問題的本質,是一個優秀的工程師的必要條件,這一點不容易,需要平時不斷地的積累。在定位到問題的本質後,就要解決問題了,一道題往往有多解,但每種解法是否合理,這也是一個需要思考的過程,多和一些比你厲害的人交流,多積攢一些這方面的經驗,這也是一個積累的過程。如果以後你再遇到問題,也用這樣的態度去面對的問題,那麼你也會很快的成長。

很多同學學習我的音樂課程後,會問:“黃老師,你什麼時候再出新視訊呀?”,其實我想說這門課程你真的學完了嗎?因為它的定位是一門 Vue.js 的進階課程,不僅僅是因為課程的專案本身比較複雜,而且專案中很多知識點都可以做延伸的學習,另外專案難免會有一些小 bug 和一些由於介面改動引發的功能不可用的情況,遇到這些問題除了給我提 issue,嘗試自己去解決然後給我提 pull request 的方式是不是對自己的提升更大呢?所以這門課程還是值得多去挖掘的,如果真正榨乾了這門課的價值再來問我也不遲,當然我也會給你們帶來更多幹貨的課程。

最後也來小小安利我的這門 Vue.js 進階課程吧(慕課網地址),感興趣的同學可以點進去看看課程介紹。課程的專案是託管在我的 Github 私服的,並不開源,所以外面的一切和這個課程相關的程式碼都是盜版的。這個原始碼我是一直維護的,包括最近 Vue.js 的腳手架的升級,以及依賴方介面的一些改造造成的功能不可用問題,都已經得到了解決。簡單地截幾張截圖:

vue-music-issue
vue-music-issue

這一張是對 issue 的處理,我們在課程推出來後解決了幾十個 issue,如果有同學在學習過程中遇到問題建議去翻閱 issue 尋找答案。有一些版本的升級的 issue 我不會關,為了讓同學們可以更方便的找到。

vue-music-contribute
vue-music-contribute

這一張是程式碼提交記錄,可以看到除了我還是有一些很不錯的同學在一起維護這個專案,這其中有一個同學學習非常主動,自驅力很強,常與我探討技術問題,最近他也加入了滴滴,在我們部門做了很多的產出。

更直觀的感受這個專案,可以掃描下方的二維碼,體驗一下接近原生 App 的感覺:

二維碼
二維碼

我們有一個官方的課程交流群,如果購買了這門課程,歡迎與其它同學一起交流學習,也可以加我的 qq 和微信,交流技術問問題都可以,不過我一般白天很忙,晚上才有時間。

當然,想關注我的一些動態,也歡迎 follow 我的 Github

希望同學們一起來支援正版,抵制盜版,我會為大家帶來更多優質的課程以及其它的一些形式的技術方向的分享。

本文參考的一些值得延伸學習的文章:

JavaScript 執行機制詳解:再談Event Loop

Tasks, microtasks, queues and schedules

相關文章