Vue源碼閱讀(一):Vue構造函數與初始化過程

--本文采自本人公衆號【猴哥別瞎說】vue

接着前文的準備。咱們知道:在 Vue2.6.10 的源碼結構中,入口文件是在 src/platforms/web/entry-runtime-with-compiler.js。那麼咱們就具體來看看裏面的代碼吧。node

在初次看源碼的時候,有兩個值得注意的點:web

  1. 不要摳細節,把握總體的方向,以囫圇吞棗的方式看便可。
  2. 制定閱讀的目標,有重點地去看

瀏覽器

咱們先來看看咱們這次的目標:Vue的構造函數到底在哪裏?具體的初始化過程又是怎樣的?bash

追蹤Vue構造函數

咱們先來看 entry-runtime-with-compiler.js 文件的核心部分:併發

//將$mount函數進行拓展,對用戶輸入的$options的template/el/render進行解析、處理
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  //處理用戶自定義選項,可知執行順序是 render > template > el
  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      //字符串模板
      if (typeof template === 'string') {
        //選擇器
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        //傳進來的是dom
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      //若是隻設置el,那麼就會直接獲取其中的元素
      //值得注意的是,會將元素自己覆蓋
      template = getOuterHTML(el)
    }
    if (template) {
      //獲取render函數
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  //執行原先的掛載函數
  return mount.call(this, el, hydrating)
}
複製代碼

看代碼知道,這個文件僅僅是對已有的 mount 函數作了加強:將用戶輸入的自定義模板選項,進行處理併發揮render函數。dom

從這裏能夠得出的結論是:ide

1.若是參數中同時存在el、render、template變量,其優先級順序爲:render > template > el。函數

2.編譯發生的時間段(compileToFunctions,將模板轉化爲render函數)在整個初始化過程當中很是早的,在執行具體mount函數以前。oop

這裏並非真正的 Vue 構造函數所在。咱們接着看 ./runtime/index.js 文件:

// install platform patch function
//定義補丁函數,這是將虛擬DOM轉換爲真實DOM的操做,很是重要
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
//定義掛載函數
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  //這纔是真正的掛載函數
  return mountComponent(this, el, hydrating)
}
複製代碼

結果發現,該文件僅僅是定義了原型鏈上的__patch__函數以及$mount函數。而詳細的掛載過程還不在這裏。具體細節咱們先不看。

繼續找:core/index.js

//初始化全局API
initGlobalAPI(Vue)
複製代碼

結果依然仍是失望的。不由感慨:這個俄羅斯套娃也太多層了吧。在core/index.js內部,也僅僅只是初始化了全局API而已。這個並非今天咱們關注的重點,仍是繼續找:src/core/instance/index.js :

function Vue (options) {
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
複製代碼

在這裏,終於找到了Vue的構造函數!!!與此同時,咱們也發現:該文件執行了許多 Mixin 操做。這些詳細的部分咱們先不理會,咱們先來看看構造函數中的 this._init() 到底作了什麼事情呢?

能夠經過瀏覽器調試的方式,也能夠經過全局搜索prototype._init的方式,找到_init()的所在文件:src/core/instance/init.js。

//初始化順序:生命週期->事件監聽->渲染->beforeCreate->注入->state初始化->provide->created
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm) // 初始化 props/data/watch/methods, 此處會是研究數據響應化的重點
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    //若是存在el元素,則會自動執行$mount,這也是必需要理解的
    //也就是說,在寫法上若是有el元素,能夠省略$mount
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
複製代碼

從這裏能夠得出的結論是:

1.在_init()函數中,咱們看到vue實例初始化時候的執行順序:生命週期->事件監聽->渲染->beforeCreate->注入數據inject->組件狀態初始化->提供數據provide->created。

2.若是存在el元素,則會自動執行掛載。

若是咱們想要了解數據響應化的細節,那就應該詳細去看initState()函數。它會是咱們下節課的重點。

在這個時候,咱們找到了主要的文件脈絡。至於具體的初始化過程,咱們還須要深刻去看$mount過程當中作了什麼。因而咱們回到真正的掛載函數mountComponent()函數裏面去看看,其中發生的掛載細節:

//這纔是真正的mount函數
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  ...
  callHook(vm, 'beforeMount')
  //核心代碼邏輯
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    ...
  } else {
    updateComponent = () => {
      //更新Component,主要作了兩個事情:render(生成vdom)、update(轉換vdom爲dom)
      vm._update(vm._render(), hydrating)
    }
  }

  // 在此處定義Watcher(一個Vue實例對應的是一個Watcher),而且與updateComponent關聯起來
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
  
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
複製代碼

在上面的代碼中,咱們看到,在vue實例掛載的過程當中,會新建一個 Watcher。這個 Watcher 的做用是相似於一個觀察者,它若是收到數據發生了變化的消息,那麼就會執行 updateComponent 函數。而這個 updateComponent 函數,主要作了兩個事情:render(生成 vdom)、update(轉換 vdom 爲 dom)。

綜上,梳理爲如下的流程圖:

屏幕快照 2019-11-22 下午10.29.06.png-58.9kB

那麼,Watcher 的工做原理是怎樣的,它是如何在整個數據響應化的過程當中發揮做用的?下篇文章將會給你答案!

相關文章
相關標籤/搜索