Vue原始碼解析:Vue例項

AmberdeBF發表於2018-05-04

Vue原始碼解析:Vue例項(一)

Vue原始碼解析:Vue例項

為什麼要寫這個


用了很久的JS,也用了很久的Vue.js,從最初的Angular到React到現在在用的Vue。學習路徑基本上是:

  • 別人告訴我,這個地方你要用ng-module,那麼我就用ng-module,至於ng-modul的功能是什麼,我不知道
  • 帶我的大佬不厭其煩了,教授了我查閱API的方法(找到官網,一般都有),自從開始閱讀API以後,我會的方法越來越多,心情非常激動的使用一個又一個新功能
  • 開始去思考每一個框架的實現細節原理

所以就有現在我想要去研究Vue的原始碼,研究的方法是跟著Vue官網的教程,一步步的找到教程中功能的實現程式碼分析實現的程式碼細節,並且會詳細解釋程式碼中涉及的JS(ES6)知識。即使是前端新人也可以輕鬆閱讀

你能得到什麼


你可以得到以下知識:

  • Vue.js 原始碼知識
  • ES5、ES6基礎知識

面對物件


  • 前端新人
  • 不想花大量時間閱讀原始碼但是想快速知道Vue.js實現細節的人
  • 我自己

話不多說,下面就開始我的第一節筆記,對應官網教程中的Vue例項

Vue例項

Vue例項包含

  • 建立一個Vue例項

      var vm = new Vue({
        // 選項 options
      })
    複製程式碼
  • 資料與方法

      // 該物件被加入到一個 Vue 例項中
      var vm = new Vue({
          data: data
      })
    複製程式碼
  • 例項生命週期鉤子

      new Vue({
      data: {
          a: 1
      },
      created: function () {
          // `this` 指向 vm 例項
          console.log('a is: ' + this.a)
      }
      })
      // => "a is: 1"
    複製程式碼
  • 生命週期圖示

Vue原始碼解析:Vue例項

建立一個Vue例項

每個Vue應用都是通過Vue函式建立一個新的Vue例項開始:

var vm = new Vue({
  // 選項
 })
複製程式碼

我們從Github下載到Vue.js原始碼後解壓開啟,探索new Vue建立了一個什麼東西

開啟下載的vue-dev,找到vue-dev/src/core/index.js

// vue-dev/src/core/index.js

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

initGlobalAPI(Vue)

Object.defineProperty(Vue.prototype, '$isServer', {
    get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
    get () {
        /* istanbul ignore next */
        return this.$vnode && this.$vnode.ssrContext
    }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
    value: FunctionalRenderContext
})

Vue.version = '__VERSION__'

export default Vue
複製程式碼

Vue是從這裡定義的,在vue-dev/src/core/index.js的頭部找到

import Vue from './instance/index'
複製程式碼

開啟vue-dev/src/core/instance/index.js

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)
}



export default Vue
複製程式碼

Vue例項在這裡得到了各種初始化(init),在這裡申明瞭一個構造器(Constructor)Vue,在構造器裡呼叫了_init方法,並向_init方法中傳入了options

 this._init(options)
複製程式碼

顯然_init不是Function原型鏈中的方法,必定是在某處得到定義。緊接著後面看到一系列的Mixin函式呼叫

   initMixin(Vue)
   stateMixin(Vue)
   eventsMixin(Vue)
   lifecycleMixin(Vue)
   renderMixin(Vue)
複製程式碼

顯然是這一堆Mixin方法賦予了Vue例項一個_init方法(之後會有單獨的一篇筆記講述Mixin是怎樣的一種設計思維,相關知識會從原型鏈講起)

顧名思義,根據函式名字猜測_init是來自於initMixin方法,根據

import { initMixin } from './init'
複製程式碼

找到vue-dev/src/core/instance/init.js(由於實在是太長了全貼上過來不方便閱讀,故根據需要貼上相應的節選,如果想要全覽的小夥伴可以去下載原始碼來看完整的)

在vue-dev/src/core/instance/init.js中我們搜尋_init,找到下面這個方法

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    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')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
複製程式碼

其中

export function initMixin (Vue: Class<Component>) {}
複製程式碼

裡的

Vue: Class<Component>
複製程式碼

來自於flow語法,一個不錯的靜態型別檢測器

這個initMixin方法裡只幹了一件事,就是給Vue.prototype._init賦值,即在所有Vue例項的原型鏈中新增了_init方法。這個_init方法又做了些什麼呢?

  • 它給Vue例項新增了很多的屬性,比如$options

  • 它給vm初始化了代理

      initProxy(vm)
    複製程式碼
  • 它給vm初始化了很多

      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')
    複製程式碼
  • 它甚至偷偷的喚起了鉤子函式

      callHook(vm, 'beforeCreate')
      callHook(vm, 'created')
    複製程式碼

例項生命週期鉤子 & 生命週期圖示

所謂的喚起鉤子函式callHook是做什麼的呢?我們找到

import { initLifecycle, callHook } from './lifecycle'
複製程式碼

開啟這個檔案lifecycle.js

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook]
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}
複製程式碼

可以看到,callHook函式的作用是,呼叫option裡使用者設定的生命週期函式。例如

new Vue({
  data: {
    a: 1
  },
  created: function () {
    // `this` 指向 vm 例項
    console.log('a is: ' + this.a)
  }
})
// => "a is: 1"
複製程式碼

new Vue() 到 beforeCreate 到 created


它在'beforeCreate'和'created'之間幹了什麼呢?

 callHook(vm, 'beforeCreate')
 initInjections(vm) // resolve injections before data/props
 initState(vm)
 initProvide(vm) // resolve provide after data/props
 callHook(vm, 'created')
複製程式碼

對應生命週期圖示來看程式碼

Vue原始碼解析:Vue例項

    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')
複製程式碼

在new Vue()之後,呼叫了_init(),在_init()內,呼叫了

    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
複製程式碼

這點正好對應官網生命週期圖示中new Vue()與生命週期鉤子'beforeCreate'之間的Init Events & Lifecycle,也就是說我們在option中設定的鉤子函式,會在這個生命週期節點得到呼叫,是因為這個callHook(vm, 'beforeCreate')(vue-dev/src/core/instance/init.js),而在這個時間節點之前完成Init Events & Lifecycle的正是

    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
複製程式碼

除了官方提到的Events和Lifecycle的Init之外,還在這個生命週期節點完成了Render的Init

之後是Init injections & reactivity,對應的函式呼叫是

initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
複製程式碼

這段函式呼叫之後_init()還沒有結束,後面有

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}
複製程式碼

對應生命週期示意圖中的

Vue原始碼解析:Vue例項

依據options中是否包含el來決定是否mount(掛載)這個el

毫無疑問,$mount函式必定是完成下一步的關鍵,在src資料夾中搜尋$mount的定義,在/vue-dev/src/platforms/web/runtime/index.js中找到了

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
複製程式碼

Vue.$mount函式內包含兩個重要的函式

  • query()
  • mountComponent()

其中

// src/platforms/web/util/index.js

export function query (el: string | Element): Element {
  if (typeof el === 'string') {
    const selected = document.querySelector(el)
    if (!selected) {
      process.env.NODE_ENV !== 'production' && warn(
        'Cannot find element: ' + el
      )
      return document.createElement('div')
    }
    return selected
  } else {
    return el
  }
}
複製程式碼

可以看到,query()是對document.querySelector()的一個包裝,作用是依據new Vue(options)optionsel設定的元素選擇器進行DOM內元素的選取,並設定了相應的容錯、報錯方法

created 到 beforeMount 到 mounted


// src/core/instance/lifecycle.js

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
複製程式碼

當options裡不存在render函式的時候,會執行createEmptyVNode,新增到vm.$options.render,之後執行生命週期鉤子函式callHook(vm, 'beforeMount'),即對應的生命週期為

Vue原始碼解析:Vue例項

 if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
複製程式碼

如果render function存在,則直接呼叫beforeMount生命週期鉤子函式,如果不存在,則通過createEmptyVNodeCompile template into render function Or compile el's outerHTML as template。

下一步就是看createEmptyVNode是如何做到compile something into render function的。

// src/core/vdom/vnode.js

export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}
複製程式碼

createEmptyVNode通過new VNode()返回了VNode例項

VNode是一個很長的class,這裡只放VNode的constructor作參考

constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }
複製程式碼

事實上VNode是Vue虛擬的DOM節點,最後這個虛擬DOM節點被掛載到vm.$options.render,到這裡

 vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')
複製程式碼

喚起生命週期鉤子函式beforeMount,正式進入beforeMount to mounted的階段

Vue原始碼解析:Vue例項
現在要做的是Create vm.$el and replace "el" with it

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
複製程式碼

倒著來找callHook(vm, 'mounted')的觸發,在這之前,做了這麼幾件事

  • 定義updateComponent,後被Watcher使用
  • 呼叫建構函式Watcher產生新例項
  • 判斷vm.$vnode 是否為null,如果是,則callHook(vm, 'mounted')

src/core/observer/watcher.js裡可以找到Watcher的定義,這裡展示它的constructor

    constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.computed = !!options.computed
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.computed // for computed watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } else {
      this.value = this.get()
    }
  }
複製程式碼

(累死我了,要休息一哈,第一次寫,對於部分細節是否要深入把握不好。深入的話太深了一個知識點要講好多好多好多,可能一天都說不完。講太淺了又覺得啥幹活都沒有,不好把握各位諒解。要是有錯誤的望各位提出來,我也算是拋磚引玉了)

相關文章