從vue構造函數開始

1、前言

數據響應式和組件化系統如今是前端框架的標配了,vue固然也不例外。前端

以前已經聊過數據響應式的原理,這一期原本想對組件化系統展開探討。vue

組件包括根組件和子組件,每一個 Vue 實例,都是用new Vue(options)建立而來的,只是應用的根組件實例是用戶顯式建立的,而根組件實例裏的子組件是在渲染過程當中隱式建立的。node

因此問題是咱們所寫的以vue後綴結尾的文件是通過怎麼樣的流程到渲染到頁面上的dom結構?vue-cli

但這個問題太龐大,以至涉及到許多的前置知識點,本文從vue構造函數開始,來梳理一下其中的流程!緩存

爲何要了解這些前端框架

  • 數據驅動
  • 多端渲染
  • 分層設計vnode
  • 設計思想

2、vue構造函數

業務中不多會去處理Vue構造函數,在vue-cli初始化的項目中有main.js文件,通常會看到以下結構app

new Vue({
  el: '#app',
  i18n,
  template: '<App/>',
  components: { App }
})

記得以前在分享virtual-dom的時候提到,vue組件經過render方法獲取到vnode,以後再通過patch的處理,渲染到真實的dom。因此咱們的目標就是從vue構造函數開始,來梳理這個主流程框架

vue構造函數dom

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

Vue.prototype.init編輯器

Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  // a uid
  vm._uid = uid++
  // 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
    )
  }
  // 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')

    // 針對根組件
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

先不關注具體方法作了什麼大體流程包括

  • 合併組件的options
  • 初始化組件數據

    • 生命週期相關數據
    • 事件相關數據
    • 渲染相關數據
    • 調用beforeCreate鉤子
    • provide/inject相關數據
    • 狀態相關數據
    • 調用created鉤子

vm.$mount(vm.$options.el)

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

mountComponent函數

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el

  // 非生產環境下,對使用 Vue.js 的運行時版本進行警告
  callHook(vm, 'beforeMount')
  let updateComponent
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  // 建立watcher實例
  new Watcher(vm, updateComponent, noop, {
   before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
   }
  }, true /* isRenderWatcher */)
  return vm
}
  1. 調用beforeMount鉤子
  2. 建立渲染 Watcher,且 Watcher 實例會首次計算表達式,建立 VNode Tree,進而生成 DOM Tree

    1. 這裏回顧一下響應式依賴收集的過程
  3. 調用mounted鉤子
  4. 返回組件實例vm
  • vm._render 函數的做用是調用 vm.$options.render 函數並返回生成的虛擬節點(vnode)
  • vm._update 函數的做用是把 vm._render 函數生成的虛擬節點渲染成真正的 DOM

3、代理訪問

爲何經過vm.xxx能夠訪問到props和data數據?

經過Object.defineProperty在vm上新增長了一屬性,屬性訪問器描述符的get特性就是獲取vm._props[key](以props爲例)的值並返回,屬性的訪問器描述符的set特性就是設置vm._props[key]的值。

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
// 定義了get/set
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
// 代理訪問
proxy(vm, `_props`, key)

// initData 裏
proxy(vm, `_data`, key)

訪問this.a實際是訪問 this.data.a

4、計算屬性

4.1: 計算屬性和methods的例子

參考vue官網提供的例子

  • 計算屬性是基於它們的響應式依賴進行緩存的,只在相關響應式依賴發生改變時它們纔會從新求值
  • 相比之下,每當觸發從新渲染時,調用方法將總會再次執行函數

4.2: 代理訪問

在實例上訪問計算屬性實際是作了什麼

4.3: 初始化計算屬性

看一下initComputed方法

const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
  // 初始化在實例上掛載_computedWatchers
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      // 建立計算屬性 Watcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.

    // 注意此處:in 操做符將枚舉出原型上的全部屬性,包括繼承而來的計算屬性,所以針對組件特有的計算屬性與繼承而來的計算屬性,訪問方式不同
    // 一、組件實例特有的屬性:組件獨有的計算屬性將掛載在 vm 上
    // 二、組件繼承而來的屬性:組件繼承而來的計算屬性已掛載在 vm.constructor.prototype
    if (!(key in vm)) {
      // 處理組件實例獨有的計算屬性
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      // 計算屬性的 key 不能存在在 data 和 prop 裏
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}
  • 建立 vm._computedWatchers屬性
  • 根據computed的key建立watcher實例,稱爲計算屬性的觀察者
  • defineComputed(vm, key, userDef)
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  // 往 vm 上添加 computed 的訪問器屬性描述符對象
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
  • 肯定sharedPropertyDefinition.get是什麼
  • 添加加 computed 的訪問器屬性描述符對象

最後的訪問器屬性sharedPropertyDefinition大概是

sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: createComputedGetter(key),
  set: userDef.set // 或 noop
}

訪問計算屬性this.a實際觸發getter以下

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        // 如果有依賴發生過變化,則從新求值
        watcher.evaluate()
      }
      if (Dep.target) {
        // 將該計算屬性的全部依賴添加到當前 Dep.target 的依賴裏
        watcher.depend()
      }
      return watcher.value()
    }
  }
}

先來看一下watcher構造函數

class Watcher {
    constructor (
    vm: Component,
    expOrFn: string | Function,// 觸發get的方式
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean // 是不是渲染函數的觀察者
    )
  if (this.computed) {
    this.value = undefined
    // computed的觀察者
    this.dep = new Dep()
  } else {
  // 求值,何時收集依賴
    this.value = this.get()
  }
  
  // 收集依賴
  depend () {
  // Dep.target值是渲染函數的觀察者對象
    if (this.dep && Dep.target) {
      this.dep.depend()
    }
  }
  // 求值
  evaluate () {
    if (this.dirty) {
    // 關鍵地方
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }
}
  • 回顧一下響應式原理 Dep-watcher的觀察者模式
  • 在計算屬性的watcher裏收集了渲染函數的觀察者對象
  • 初始化求值的時候會觸發屬性的get,從而收集依賴也就是計算屬性的觀察者
  • 在計算屬性所依賴的數據變化時,就會觸發更新

4.4: 總結

到這裏咱們來回顧一下計算屬性相關的流程

  • 在vue實例上定義watchers屬性
  • 根據計算屬性的key,以及實際的get方法建立watcher實例
  • 實現代理訪問,定義訪問器屬性
  • 訪問計算屬性,第一次走到evaluate函數,從而觸發觸發渲染函數的get致使對應的watcher收集依賴

最後提供一個計算屬性實際的例子,來分析流程,(可是這裏貌似須要讀者熟悉dep,watcher的觀察者模式)

5、其它

本文思路從vue構造函數開始,在初始化流程中關注initstate方法,選擇其中的computed屬性展開介紹。

對computed屬性的初始化處理也是vue典型的初始化處理模式,其中多處可見的Object.defineProperty方法,實例化觀察者watcher對象,基於dep和watcher創建的觀察者模式。

在其它的數據初始化章節,在響應式處理流程都會遇到這些概念。

最後介紹一個數據流驅動的項目案例 H5編輯器案例

相關文章
相關標籤/搜索