Vue源碼學習1——Vue構造函數

Vue源碼學習1——Vue構造函數

這是我第一次正式閱讀大型框架源碼,剛開始的時候徹底不知道該如何入手。Vue源碼clone下來以後這麼多文件夾,Vue的這麼多方法和概念都在哪,徹底沒有頭緒。如今也只是很粗略的瞭解一下,我的認爲這篇只是能作到你們閱讀Vue的參考導航,能夠較快的找到須要看的文件或方法。不少細節依然沒有理解到位,可是能夠慢慢來,先分享一波~vue

源碼文件目錄結構

- benchmarks 暫時不知道是什麼node

- dist 存放打包後的文件夾webpack

- examples 示例,這個地方能夠本身寫一些簡單例子,而後經過調試看整個代碼運行的過程來了解源碼是怎麼寫的web

- flow 靜態類型檢查,好比 (n:number)即n須要是number類型算法

- packages 查資料說是vue還能夠分別生成其餘的npm包npm

- scripts 打包相關的配置文件夾json

- src 咱們研究的主要文件夾,下面會詳細再說明api

- test 測試文件夾數組

- types 暫時不知道是什麼緩存

/src

接下來重點說src這個文件夾,這裏面須要重點看core這個文件夾,這裏面纔是咱們真正須要研究的地方以下圖:

- components 組件,如今裏面只有KeepAlive一個

- global-api 全局api,能夠給Vue添加全局方法,好比裏面咱們常使用的Vue.use()

-instance 核心文件夾,裏面是實例相關的一些方法,例如初始化實例、實例事件綁定、渲染、狀態、生命週期等

- observe 雙向數據綁定相關文件(暫時不太清楚)

- util 工具方法,看到裏面有props、nextTick之類的方法(暫時不太清楚)

- vdom 虛擬dom

大致文件結構說了一下,可是不少還不是很清晰。對於我這樣的小白來講,個人建議是能夠從 npm run dev 開始一步步開始看,採起的方法是「倒序」

package.json

打開這個文件找到'dev'命令,以下

"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",

'rollup' 是Vue使用的打包工具,從上面能夠看出執行這個命令是到 'scripts/config.js' 那就打開這個文件

scripts/config.js

if (process.env.TARGET) {
  module.exports = genConfig(process.env.TARGET)
} else {
  exports.getBuild = genConfig
  exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}

genConfig 方法是設置一些配置,和webpack裏的設置差很少,而後找到 web-full-dev

'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  }

能夠看到入口文件是'web/entry-runtime-with-compiler.js'

src/platforms/web/entry-runtime-with-compiler.js

在這個文件裏能夠看到

import Vue from './runtime/index'

該方法中有一個$mount須要注意,這個就是渲染的入口,接下來會說這個方法

src/platforms/web/runtime/index.js

`import Vue from 'core/index'`

src/core/index.js

import Vue from './instance/index'

src/core/instance/index.js

終於進入到最重要的方法,Vue的構造方法以下

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue(options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue
  • initMixin :對於各類vue實例各類屬性進行初始化
  • stateMixin :Vue原型上綁定state相關的方法和屬性,data、props等
  • eventsMixin :Vue原型上綁定事件相關方法
  • lifecycleMixin :Vue原型上綁定生命週期相關方法,好比_update、$forceUpdate、$destroy
  • renderMixin : Vue原型上綁定和渲染相關的方法

src/core/instance/init.js

  • 第一點:initMixin 是Vue的一些初始化實例的方法,在尚未構造一個對象前是不會進入到這個方法內部,當經過new出一個對象後纔會進入,緣由以下:
Vue.prototype._init = function (options?: Object) {

這裏有一個options?:object的校驗,剛開始即只是引入<script src="../../dist/vue.js"></script>這個文件,當var vm = new Vue()以後才進入_init方法內部。

  • 第二點:_init方法中對於$options設置:
vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )

mergeOptions這個方法是合併option,第一個參數是往$options塞入下面的參數

第二個參數就是咱們本身設置的option,好比data、el;第三個參數以下:

  • 第三點:_init方法中其餘初始化方法
initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

接下來將會一個個初始化方法說明,初次以外_init方法還有一些變量的初始化,好比_uid、_isVue、_name、_renderProxy的初始化

  • 第四點:最後在_init方法中須要注意
if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }

調用$mount掛載根元素,這個方法就是以前提到的

src/core/instance/state.js

  • 第一點: stateMixin是對於Vue原型對象(Vue.prototype)加上$data、$props、$delete、$watch、$set屬性。而且經過Object.defineProperty對$data、$props屬性進行set和get

  • 第二點:initState方法是在init.js中調用,即實例化以後才調用的,是個實例對象添加屬性。
export function initState(vm: Component) {
// 首先在vm上初始化一個_watchers數組,緩存這個vm上的全部watcher
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

對於實例對象進行相關屬性的初始化,另外data、props由於須要雙向綁定,在initData、initProps中都有一個proxy方法對這兩個屬性進行set和get的設置

src/core/instance/events.js

  • 第一點: eventsMixin是對於Vue原型對象(Vue.prototype)綁定一些事件方法,好比$on、$once、$off、$emit

  • 第二點: initEvents是對於實例對象初始化事件
export function initEvents(vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events 初始化父級相關事件
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

建立_events一個空對象以後用來存放事件,_hasHookEvent是一個優化標記(能夠暫時不理會),而後初始化父級事件。根據是否有父級監聽事件,若是有則更新父級事件

src/core/instance/lifecycle.js

  • 第一點: lifecycleMixin是對Vue原型對象(Vue.prototype)綁定_update、$forceUpdate、$destroy三個生命週期方法。_update方法中經過調用__patch__方法更新虛擬dom;
if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }

$forceUpdate強制從新渲染實例自己和插入插槽內容的子組件;$destroy銷燬一個實例,清理它與其它實例的鏈接,解綁它的所有指令及事件監聽器,觸發 beforeDestroy 和 destroyed 的鉤子

  • 第二點: initLifecycle是在_init方法中調用,是實例生命週期的初始化,其中會包括不少變量
export function initLifecycle(vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent 建立第一個非抽象父組件,抽象組件:它自身不會渲染一個 DOM 元素,也不會出如今父組件鏈中,例如<keep-alive>
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null // watcher對象
  vm._inactive = null // 和keep-alive中組件狀態有關係
  vm._directInactive = false // 和keep-alive中組件狀態有關係
  vm._isMounted = false //當前實例是否被掛載
  vm._isDestroyed = false // 當前實例是否被銷燬
  vm._isBeingDestroyed = false // 當前實例是否正在被銷燬或者沒銷燬徹底
}
  • 第三點: callHook是在_init方法中調用,這個方法是直接調用鉤子,調用形式以下
    callHook(vm, 'beforeCreate')
    callHook(vm, 'created')

src/core/instance/render.js

  • 第一點: renderMixin方法主要是給Vue原型對象綁定$nextTick、_render兩個方法,其中_render方法代碼以下:
Vue.prototype._render = function (): VNode {
    ……
    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      if (process.env.NODE_ENV !== 'production') {
        if (vm.$options.renderError) {
          try {
            vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
          } catch (e) {
            handleError(e, vm, `renderError`)
            vnode = vm._vnode
          }
        } else {
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    }
    ……
    return vnode
  }

在這個方法中主要是try……catch這裏建立了vnode。 vnode = render.call(vm._renderProxy, vm.$createElement) 建立一個vnode而且返回,若是失敗則返回一個空的vnode vnode = createEmptyVNode()

  • 第二點: initRender是在_init方法中調用,進行實例渲染屬性的綁定而且對一些屬性的監聽
export function initRender(vm: Component) {
  ……
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  ……
}

這裏着重關注一下createElement方法,傳入vnode以及dom的屬性建立真正dom節點。

src/core/instance/inject.js

在_init方法中調用了initProvide、initInjections兩個方法,這兩個方法在實際應用中不是不少,查看Vue API說provide 和 inject 主要爲高階插件/組件庫提供用例。並不推薦直接用於應用程序代碼中。因此這裏不作說明,有須要的能夠到這個文件查看相關方法

vue的渲染過程

  • 第一步: _init方法中
if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }

渲染入口,調用$mount方法開始

  • 第二步:entry-runtime-with-compiler.js中的$mount方法,代碼以下
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  ……
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') { // 圖一
          template = idToTemplate(template)
            ……
        }
      } else if (template.nodeType) {  // 圖一 
        template = template.innerHTML
      } else { // 圖二
         ……
         return this
      }

    } else if (el) { // 圖三
      template = getOuterHTML(el)
    }

    if (template) {
      ……

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

       ……
    }
  }
  return mount.call(this, el, hydrating)
}

能夠從上面大體看出結構,template是能夠從el傳入,也能夠是options中的template以及render方法三種方式傳入,對應Vue官網以下:

其中能夠看到,經過el或者template的方式都須要調用compileToFunctions將字符串轉換成方法,而render是不須要,這裏能夠看出render的性能應該會好一些,可是el和template咱們使用較易理解。可是不論是哪種最後都是生成render方法,而後再綁定到實例對象上。另外方法中的mount是從runtime/index.js中建立的。

  • 第三步: 接下來就進入runtime/index.js看到mount方法調用mountComponent,而後找到這個方法是在lifecycle.js
export function mountComponent(
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  ……
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
        ……
      const vnode = vm._render()
        ……
      vm._update(vnode, hydrating)
        ……
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  new Watcher(vm, updateComponent, noop, {
        ……
     callHook(vm, 'beforeUpdate')
        ……
  }, true /* isRenderWatcher */)
        ……
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

而後調用前一步調用的_render方法是在render.js中的_render方法中try……catch地方調用了第二步中生成的render方法。經過_render方法生成vnode,傳入_update方法

  • 第四步:最後在前面也提到在_update方法中有一個patch對比更新真實dom,這裏是涉及到diff算法進行對比新舊VNode對象進行更新,暫時不太瞭解,下圖是我網上找到能夠比較形象解釋diff、patch的做用

以上就是一個大致渲染的過程。

總結

本文只是作了Vue構造函數總體的一個流程展現,哪些參數是在哪一個文件中掛載上去的以及vue渲染的一個簡單流程。但其實每一個環節均可以拓展出不少知識,好比響應式的數據綁定、虛擬DOM、diff算法、patch、生命週期等等,這些能夠在以後再一個個點進行了解。下圖是沒有參數的vue實例的參數

相關文章
相關標籤/搜索