Vue源碼(上篇)


某課網有個488人名幣的源碼解讀視頻看不起,只能搜很多得資料慢慢理解,看源碼能知道大佬的功能模塊是怎么分塊寫的,怎么復用的,已經vue是怎么實現的

資料來自
vue源碼
喜歡唱歌的小獅子
web喵喵喵
Vue.js源碼全方位深入解析
恰恰虎的博客
learnVue
最后四集視頻

總文件目錄

  • scripts:包含構建相關的腳本和配置文件。作者聲明一般開發不需要關注此目錄
  • dist:構建出的不同分發版本,只有發布新版本時才會跟新,開發分支的新特性不會反映在此
  • packages:包含服務端渲染和模板編譯器兩種不同的NPM包,是提供給不同使用場景使用的
  • test:包含所有測試代碼
  • flow:這是一個類型檢查工具,可以加入類型的限制,提高代碼質量,導致源碼代碼變成了這樣
function sum(a: number, b:number) {
  return a + b;
}
  • src:Vue的源碼,使用ES6和Flow類型注釋編寫的
  • types:使用TypeScript定義的類型聲明,並且包含了測試文件,不太明白為什么要同時使用兩種靜態類型檢查語言

src文件夾

  • Compiler 編譯器
    • parser:解析器的作用是將模板轉換成元素AST對象。
    • optimizer:優化器負責檢測靜態抽象樹的渲染進行優化。
    • codegen:代碼生成器直接從AST對象生成代碼字符串。
  • Core 核心
    • Observer:觀察者系統,實現監測數據變化的功能。
    • Vdom:Vue虛擬節點樹,實現虛擬節點的創建和刷新功能。
    • instance:Vue類,包含構造函數和原型方法的創建。
    • Global-API:全局API。
    • Components:通用抽象組件。
    • util:輔助函數。
  • Platforms 平台,不同平台的區別代碼
  • Server 服務器渲染,ssr
  • Sfc 單文件組件文件編譯,這一文件夾目前只包含了一個叫parser.js的文件,用來將單文件組件解析為SFC描述對象,輸出給編譯器繼而執行模板編譯。
  • Shared 共享常量和函數,是一個放工具的文件夾

Global-api

  • util:雖然暴露了一些輔助方法,但官方並不將它們列入公共API中,不鼓勵外部使用。
  • set:設置響應式對象的響應式屬性,強制觸發視圖更新,在數組更新中非常實用,不適用於根數據屬性。
  • delete:刪除響應式屬性強制觸發視圖更新, 使用情境較少。
  • nextTick:結束此輪循環后執行回調,常用於需要等待DOM更新或加載完成后執行的功能。
  • use:安裝插件,自帶規避重復安裝。
  • mixin:常用於混入插件功能,不推薦在應用代碼中使用。
  • extend:創建基於Vue的子類並擴展初始內容。
  • directive:注冊全局指令。
  • component:注冊全局組件。
  • filter:注冊全局過濾器。

Vue的核心

  • 創建Vue的構造函數,src/core/instance/index.js
function Vue (options) {
  // 安全性判斷,如果不是生產環境且不是Vue的實例,在控制台輸出警告
  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)
}

// 下面這些Mixin都是往Vue函數的原型對象里添加方法
// 掛載初始化方法
initMixin(Vue)
// 掛載狀態處理相關方法
stateMixin(Vue)
// 掛載事件響應相關方法
eventsMixin(Vue)
// 掛載生命周期相關方法
lifecycleMixin(Vue)
// 掛載視圖渲染方法
renderMixin(Vue)

// 這里就是暴露給Global-api去添加全局的方法
export default Vue
  • initMixin,添加_init方法,里面有后續的所有重要的
export function initMixin (Vue: Class<Component>) {
  // 在Vue類的原型上掛載_init()方法
  // 接收類型為原始對象的options形參,此參數為非必選參數
  Vue.prototype._init = function (options?: Object) {
    // 將實例對象賦值給vm變量
    // 這里會再次進行Component類型檢查確保vm接收到的是Vue類的實例
    const vm: Component = this

    // 合並options對象
    if (options && options._isComponent) {
      // 內部組件的options初始化
      initInternalComponent(vm, options)
    } else {
      // 否則執行合並options函數,並賦值給vm的公共屬性
      // 在這里的合並函數主要是解決與繼承自父類的配置對象的合並
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // 暴露實例對象
    vm._self = vm
    // 初始化實例的生命周期相關屬性
    initLifecycle(vm)
    // 初始化事件相關屬性和監聽功能
    initEvents(vm)
    // 初始化渲染相關屬性和功能
    initRender(vm)
    // 調用生命周期鈎子函數beforeCreate
    callHook(vm, 'beforeCreate')
    // 初始化父組件注入屬性
    initInjections(vm) // resolve injections before data/props
    // 初始化狀態相關屬性和功能
    initState(vm)
    // 初始化子組件屬性提供器
    initProvide(vm) // resolve provide after data/props
    // 調用生命周期鈎子函數created
    callHook(vm, 'created')
    // 執行DOM元素掛載函數
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

生命周期相關

  • lifecycleMixin
// 導出lifecycleMixin函數,接收形參Vue,
// 使用Flow進行靜態類型檢查指定為Component類
export function lifecycleMixin (Vue: Class<Component>) {
  // 為Vue原型對象掛載_update私有方法
  // 接收vnode虛擬節點類型參數和一個可選的布爾值hydrating
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { ... }

  // 為Vue實例掛載$forceUpdate方法,實現強制更新
  Vue.prototype.$forceUpdate = function () { ... }

  // 為Vue實例掛載$destroy方法
  Vue.prototype.$destroy = function () { ... }
}
  • initLifecycle,主要是在生命周期開始之前設置一些相關的屬性的初始值,組件也有自己的生命周期
// 導出initLifecycle函數,接受一個Component類型的vm參數
export function initLifecycle (vm: Component) {
  // 獲取實例的$options屬性,賦值為options變量
  const options = vm.$options

  let parent = options.parent
  // 判斷是否存在且非抽象
  if (parent && !options.abstract) {
    // 遍歷尋找最外層的非抽象父級
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    // 將實例添加到最外層非抽象父級的子組件中
    parent.$children.push(vm)
  }

  // 初始化實例的公共屬性
  // 設置父級屬性,如果之前的代碼未找到父級,則vm.$parent為undefined
  vm.$parent = parent
  // 設置根屬性,沒有父級則為實例對象自身
  vm.$root = parent ? parent.$root : vm

  // 初始化$children和$refs屬性
  // vm.$children是子組件的數組集合
  // vm.$refs是指定引用名稱的組件對象集合
  vm.$children = []
  vm.$refs = {}

  // 生命周期相關的私有屬性
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

調用生命周期的方法callHook

export function callHook (vm: Component, hook: string) {
  //記錄當前watch的實例
  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`)
      }
    }
  }
  //觸發@hook:定義的鈎子方法
  if (vm._hasHookEvent) {
    // 發布訂閱調用模式,$emit來自initEvent
    vm.$emit('hook:' + hook)
  }
  //釋放當前的watch實例
  popTarget()
}

函數相關

  • eventsMixin,添加幾個用於訂閱發布的方法
export function eventsMixin (Vue: Class<Component>) {

  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    // 定義實例變量
    const vm: Component = this
    // 如果傳入的event參數是數組,遍歷event數組,為所有事件注冊fn監聽函數
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        this.$on(event[i], fn)
      }
    }
    // 返回實例本身
    return vm
  }

  // 為Vue原型對象掛載$once方法
  // 參數event只接受字符串,fn是監聽函數
  Vue.prototype.$once = function (event: string, fn: Function): Component { ... }
  
  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component { ... }
  // 為Vue原型對象掛載$emit方法,只接受單一event
  Vue.prototype.$emit = function (event: string): Component { ... }
}
  • initEvents
// 定義並導出initEvents函數,接受Component類型的vm參數
export function initEvents (vm: Component) {
  //初始化vm._events對象
  // 存儲父組件綁定當前子組件的事件,保存到vm._events中
  vm._events = Object.create(null);
  //是否存在hook:鈎子事件
  vm._hasHookEvent = false;
  // init parent attached events
  var listeners = vm.$options._parentListeners;
  if (listeners) {
    updateComponentListeners(vm, listeners);
  }
}

渲染相關

  • renderMixin
export function renderMixin (Vue: Class<Component>) {

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

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

    vm.$vnode = _parentVnode
    // render 就是 render表達式,
    // vnode 就是 虛擬dom
    let vnode = render.call(vm._renderProxy, vm.$createElement)
    return vnode
  }
}
  • initRender,就是定義幾個屬性而已
export function initRender (vm: Component) {
  // 初始化實例的根虛擬節點
  vm._vnode = null
  // 定義實例的靜態樹節點
  vm._staticTrees = null
  // 獲取配置對象
  const options = vm.$options
  // 設置父占位符節點
  const parentVnode = vm.$vnode = options._parentVnode
  // renderContext存儲父節點有無聲明上下文
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(vm.$options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject

  /*將createElement函數綁定到該實例上,該vm存在閉包中,不可修改,vm實例則固定。這樣我們就可以得到正確的上下文渲染*/
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  /*常規方法被用於公共版本,被用來作為用戶界面的渲染方法*/
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  
  // $attrs & $listeners這個屬性進行數據監聽
  const parentData = parentVnode && parentVnode.data
  defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
  defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}

參數處理

  • stateMixin
export function stateMixin (Vue: Class<Component>) {

  const dataDef = {}
  dataDef.get = function () { return this._data }
  const propsDef = {}
  propsDef.get = function () { return this._props }

  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)

  Vue.prototype.$set = set
  Vue.prototype.$delete = del

  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) {
      cb.call(vm, watcher.value)
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}
  • initState,這里才真正跟MVVM連接起來
export function initState (vm: Component) {
  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)
  }
}

// 這里展示下initData方法,其他也差不多
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}

  // 判斷props對象跟data對象跟methods對象里有沒有名字一樣的key值
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`
      )
    } else if (!isReserved(key)) {
      // 代理data的值,當訪問this.message時,實際上訪問的是this[_data][message]
      proxy(vm, `_data`, key)
    }
  }
  // 對內容進行監聽
  observe(data, true)
}

上面出現的幾個重要的方法

  • proxy,用於代理參數,當訪問this.message時,實際上訪問的是this[_data][message]
  • defineReactive,最核心的對象監聽方法
  • observe和Observe,這兩個都是在內部調用了defineReactive

總結所有的init

image.png

當參數處理完畢,data,prop等數據已經被監聽,Dep數組也已經創建好,但是是空的,這時可以看到上面的init的生命周期才寫到created,我們知道在生命周期mounted前是沒有dom元素的,所以在$mount階段才開始生成虛擬dom,$mount方法在哪里定義的呢,查看下一篇筆記


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM