vue 原始碼詳解(一):原型物件和全域性 `API`的設計

ifIhaveWings發表於2021-08-11

vue 原始碼詳解(一):原型物件和全域性 API的設計

1. 從 new Vue() 開始

我們在實際的專案中使用 Vue 的時候 , 一般都是在 main.js 中通過 new Vue({el : '#app , ...options}) 生成根元件進行使用的, 相關的配置都通過 options 傳入。 Vue 的原型物件會幫我們初始化好很多屬性和方法, 我們可以通過 this.property 直接呼叫即可; 而 Vue 這個類也通過類的靜態方法初始化了一些全域性的 api, 我們可以通過類名直接呼叫, 比如 Vue.component()Vue 的原型物件和全域性 API 是通過混入的方式融入 Vue 中的。

如下面程式碼所示, import Vue from './instance/index' 引入 Vue 的建構函式,在使用者呼叫之前, Vue 先做了一些初始化工作, 具體做了哪些工作看 vue/src/core/instance/index.js(點選跳轉) 中的程式碼(下邊第二段):

  1. function Vue (options) { ... } 定義了 Vue 建構函式, 我們呼叫 new Vue 時,只會執行一句程式碼, 即 this._init(options);
  2. 定義完建構函式,依次呼叫 initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) , 從而將 Vue 的初始化函式、狀態初始化函式、事件初始化函式、生命週期初始化函式、渲染函式混入到 Vue 的原型物件。這才使得每個元件都有了便捷的功能。初始化函式具體都做了什麼工作, 且看後續的分析。

vue/src/core/index.js :

import Vue from './instance/index' // 1. 引入 Vue 建構函式
import { initGlobalAPI } from './global-api/index' // 2. 引入初始化全域性 API 的依賴
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

initGlobalAPI(Vue) // 3. 初始化全域性 API

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/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) // 1. Vue 例項初始化
}

initMixin(Vue) // 2
stateMixin(Vue) // 3
eventsMixin(Vue) // 4
lifecycleMixin(Vue) // 5
renderMixin(Vue) // 6

export default Vue

註釋 1 處, new Vue() 時, 只執行了一個初始化工作 this._init(options) ; 值得注意的是, 在定義完成建構函式後,此時尚未有 new Vue 的呼叫, 即在例項建立之前, 會執行註釋 2 3 4 5 6 處的初始化工作, 讓後初始化全域性 API ,至此準備工作已經就緒, 通過呼叫 new Vue 生成 Vue 例項時,會呼叫 this._init(options) 。接下來,探索一下 Vue 生成例項前, 依次做了哪些工作。

1.1 initMixin (vue\src\core\instance\init.js)

let uid = 0

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this // 1. vm 即 this, 即 Vue 的例項物件
    // a uid
    vm._uid = uid++ // 每個 Vue 例項物件都可以看成一個元件, 每個元件有一個 _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 
    // 合併引數, options 是我們呼叫 `new Vue({ el : 'app'chuand, ...args})` 時傳入的引數
    // 合併完成後將合併結果掛載到當前 `Vue` 例項
    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( // 合併完成後將合併結果掛載到當前 `Vue` 例項
        // 這個函式會檢查當前 Vue 例項的否早函式和其父類、祖先類上的 options 選項, 並能監聽是否發生了變化, 將 祖先類、父類和當前 Vue 例項的 options 合併到一起
        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) // 1. 初始化宣告週期
    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)
    }
  }
}

1.1.1 initLifecycle

上邊程式碼給每個例項標記一個唯一的 _uid 屬性, 然後標記是否為 Vue 例項, 將使用者傳入的引數和 Vue 自有引數合併後,掛載到 Vue$options 屬性 。

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  // 這個註釋已經很明瞭了, 就是查詢當前 vue 例項的第一個非抽象父元件
  // 找到後會將當前的元件合併到父元件的 `$children` 陣列裡
  // 從而建立了元件的父子關係
  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
  vm._inactive = null // 這倆先忽略, 俺也不知道幹嘛的
  vm._directInactive = false // 這倆先忽略, 俺也不知道幹嘛的
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

如上, 初始化宣告週期的時候, 會簡歷當前元件與其他元件的父子關係, 如果找到父元件, 會將 $root 指標指向父元件,找不到的話, 指向當前 Vue 例項。接下來 vm.$children = [] 初始化子元件列表, vm.$refs = {} 初始化引用列表, vm._watcher = null 初始化觀察者列表, 此時還沒有觀察者,無法檢測資料變化, vm._isMounted = false 標記當前元件尚未掛載到 DOM, vm._isDestroyed = false 標記當前元件並不是一個被銷燬的例項,這與垃圾回收有關係的, vm._isBeingDestroyed = false 標記當前元件是否正在銷燬工作。

至此, 宣告週期的初始化已經完成了。

1.1.2 initEvents

vue/src/core/instance/events.js :

1.2 stateMixin : 狀態初始化

vue/src/core/instance/state.js :

export function stateMixin (Vue: Class<Component>) {
  // flow somehow has problems with directly declared definition object
  // when using Object.defineProperty, so we have to procedurally build up
  // the object here.
  const dataDef = {}
  dataDef.get = function () { return this._data }
  const propsDef = {}
  propsDef.get = function () { return this._props }
  if (process.env.NODE_ENV !== 'production') {
    dataDef.set = function () {
      warn(
        'Avoid replacing instance root $data. ' +
        'Use nested data properties instead.',
        this
      )
    }
    propsDef.set = function () {
      warn(`$props is readonly.`, this)
    }
  }
  Object.defineProperty(Vue.prototype, '$data', dataDef) // 1
  Object.defineProperty(Vue.prototype, '$props', propsDef) // 2

  Vue.prototype.$set = set // 3
  Vue.prototype.$delete = del // 4

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}

上邊程式碼對例項的狀態做了初始化。 在註釋 1 2 兩個地方分別給 Vue 原型物件增加了 $data $props 兩個屬性, 這兩個屬性的值分別是當前 vm_data _props 屬性, 並且設定這兩個屬性是不可以修改的。

註釋 3 4 處為 vm 新增了 setdelete 方法, setdelete 是幹嘛的就不用介紹了吧, Vue 物件本身也有 Vue.setVue.delete 這兩個方法, 都是來源於下邊 set 這個函式, 他的作用體現在下邊程式碼註釋的 1 2 處:

引數 target 為物件或者陣列, target 有一個 __ob__ 屬性, 這個屬性的來源是在 Observer 這個類中的建構函式,其中有一句是 def(value, '__ob__', this) , value 是待觀測的物件, 也就是我們寫程式碼時傳入的 data中的屬性, 然後我們傳入的 data 其實都被代理到 __ob__ 這個屬性上了,以後我們操作 data 中的資料或者訪問 data 中的資料都會被代理到 __ob__ 這個屬性。

之後又在原型物件掛載了 $watcher 方法, 該方法的返回值是一個銷燬 watcher 的方法。 至於 watcher 是個啥, 以及 watcher 的作用,後邊再談。

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val) // 1
  ob.dep.notify() // 2
  return val
}

vue\src\core\util\lang.js :

/**
 * Define a property.
 */
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

1.3 事件初始化

其實就是在 Vue 原型物件上掛載了一些方法 ($on $once $off $emit) , 基於釋出訂閱模式,實現了一個事件響應系統, 與 nodejs 中的 eventEmitter 是極其相似的。這就是我們常用的事件匯流排機制的來源。

簡單解析一下下面的程式碼 :

$on 是事件的訂閱, 通過他的引數 (event: string | Array<string>, fn: Function) 可知, 可以一次訂閱多個事件,他們共享一個處理函式, 然後將所有的處理函式以鍵值對的形式({eventName : handler[]})儲存在 vm._events 物件中,等待事件釋出。一旦事件釋出, 就會根據事件型別( eventName )去事件處理函式列表(handler[])中,讀取處理函式並執行。

$emit 是事件的釋出, 生產環境中對事件名稱(也就是型別),進行了大小寫轉換, 不用區分事件名稱的大小寫了, 當然我們編碼不能這樣粗狂的去寫哈。 然後 cbs 是根據事件名稱讀取的處理函式的列表, const args = toArray(arguments, 1) 是處理事件的引數, 函式 toArray$emit 函式的引數除掉第一個以後, 最終傳入了我們的訂閱函式中。 即
vm.$emit('render', 'a',124) 程式碼最終呼叫結果是 vm._events['render'] 列表中所有的函式都以 ('a', 123) 為引數執行一次。

$off 是將事件的訂閱函式從訂閱列表中刪除, 它提供了兩個引數 (event?: string | Array<string>, fn?: Function), 兩個引數都是可選的, 並且不能只穿第二引數。 如果實參列表為空, 則當前 vm 上訂閱的所有事件和事件的處理函式都將被刪除;如果第二引數為空, 則當前 vmvm._events[event] 中所有的處理函式將被清空; 如果第二個引數 fn 不為空, 則只將 vm._events[event] 事件處理列表中的 fn 函式刪除。

$once 表示事件處理只執行一次, 多次釋出事件,也只會執行一次處理函式。這個函式有點小技巧。先建立一個 on 函式, 然後把事件處理函式 fn 掛載到這個函式物件上, 函式也是物件,可以有自己的屬性,這個沒有疑問吧。 on 函式中只有兩句程式碼 vm.$off(event, on), 讓 vm 解除 on 函式的訂閱, 這就可以保證以後不會再執行 on 函式了; 下一句fn.apply(vm, arguments) 呼叫 fn , 這保證了 fn 被執行了一次。 哈哈哈, 666.

事件的初始化就這樣講完了。

export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }

  Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    function on () {
      vm.$off(event, on)
      fn.apply(vm, arguments)
    }
    on.fn = fn
    vm.$on(event, on)
    return vm
  }

  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this
    // all
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // array of events
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn)
      }
      return vm
    }
    // specific event
    const cbs = vm._events[event]
    if (!cbs) {
      return vm
    }
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    // specific handler
    let cb
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
    return vm
  }

  Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    if (process.env.NODE_ENV !== 'production') {
      const lowerCaseEvent = event.toLowerCase()
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          `Event "${lowerCaseEvent}" is emitted in component ` +
          `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
          `Note that HTML attributes are case-insensitive and you cannot use ` +
          `v-on to listen to camelCase events when using in-DOM templates. ` +
          `You should probably use "${hyphenate(event)}" instead of "${event}".`
        )
      }
    }
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      const info = `event handler for "${event}"`
      for (let i = 0, l = cbs.length; i < l; i++) {
        invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return vm
  }
}

1.4 lifecycleMixin 生命週期初始化

程式碼如下, 在 Vue 的原型物件上增加了三個方法 _update $forceUpdate $destroy, 依次來看下都做了什麼事吧。

vm._update 通過 __patch__ 函式把虛擬節點 vnode 編譯成真實 DOM. 並且, 元件的更新也是在這裡完成虛擬節點到真實 DOM 的轉換。並且父元件更新後, 子元件也會更新。

vm.$forceUpdate 若果當前元件上有觀察者, 則直接更細元件。

vm.$destroy 銷燬元件, 如果當前元件正在走銷燬的流程,則直接返回, 等待繼續銷燬。 否則, 會觸發 beforeDestroy 這個宣告週期, 並將當前元件標記為正在銷燬的狀態。 然後將當前元件從父元件中刪除, 然後銷燬所有的 watcher, 銷燬 vm._data__ob__ , 標記元件狀態為 已銷燬,重新生成真實 DOM , 觸發 destroyed 生命週期方法, 移除當前元件訂閱的事件和事件的處理函式, 將當前元件對父元件的引用清空。

vue/src/core/instance/lifecycle.js

export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

  Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
      vm._watcher.update()
    }
  }

  Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    // remove self from parent
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // teardown watchers
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // call the last hook...
    vm._isDestroyed = true
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    callHook(vm, 'destroyed')
    // turn off all instance listeners.
    vm.$off()
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}

1.5 renderMixin 渲染函式初始化

也是向 Vue 的原型物件掛載一些方法。

installRenderHelpers(Vue.prototype) 向 vm 增加了模板的解析編譯所需要的一些方法;

$nextTick 即我們在寫程式碼時常用的 this.$nextTick() , 它返回一個 Promise 例項 p, 我們可以在 pthen 函式中訪問到更新到 DOM 元素的資料, 也可以向 this.nextTick 傳遞一個回撥函式 ff 也可以訪問更新到 DOM 元素的資料。

_render 方法生成虛擬節點。詳見後邊的程式碼。

vue/src/core/instance/render.js

export function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }

    // 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 {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production' && 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
      }
    } finally {
      currentRenderingInstance = null
    }
    // if the returned array contains only a single node, allow it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }
}

2. Vue 全域性 API

Vue 中全域性 API 一共有一下 12 個。全域性 API 是通過建構函式 Vue 直接呼叫的, 有一些方法在例項上也做了同步, 可以通過例項物件去呼叫。 比如常用的 Vue.nextTick , 可以通過 this.$nextTick 進行呼叫。下面就依次分析一下每個全域性 API 的使用和實現思路吧。

  • Vue.extend
  • Vue.nextTick
  • Vue.set
  • Vue.delete
  • Vue.directive
  • Vue.filter
  • Vue.component
  • Vue.use
  • Vue.mixin
  • Vue.compile
  • Vue.observable
  • Vue.version

src/core/global-api/index.js

2.1 Vue.extend

相關文章