vue 快速入門 系列 —— Vue 例項的初始化過程

彭加李發表於2022-01-30

其他章節請看:

vue 快速入門 系列

Vue 例項的初始化過程

書接上文,每次呼叫 new Vue() 都會執行 Vue.prototype._init() 方法。倘若你看過 jQuery 的原始碼,你會發現每次呼叫 jQuery() 也會執行一個初始化的方法(即 jQuery.fn.init())。兩者在執行初始化方法後都會返回一個例項vue 例項jQuery 例項),而且在初始化過程中,都會做許多事情。本篇就和大家一起來看一下 vue 例項的初始化過程。

Tip:本篇亦叫 Vue.prototype._init() 的原始碼解讀。解讀順序和原始碼的順序保持一致。

function Vue (options) {
  ...
  this._init(options)
}
// 核心程式碼
Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // 合併引數
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // 初始化生命週期、初始化事件...
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    // 觸發生命鉤子:beforeCreate 
    callHook(vm, 'beforeCreate')
    // resolve injections before data/props
    // 我們可以使用的順序:inject -> data/props -> provide
    initInjections(vm) 
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    if (vm.$options.el) {
      // 掛載
      vm.$mount(vm.$options.el)
    }
  }

initLifecycle(初始化生命週期)

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

  // locate first non-abstract parent
  // 定位第一個非抽象父節點
  let parent = options.parent
  // 有 parent,並且自己不是抽象的,則找到最近一級的非抽象 parent,並將自己放入其 $children 陣列中
  if (parent && !options.abstract) {
    // 如果 parent 是抽象的(abstract),則繼續往上級找 parent,直到 parent 不是抽象的為止
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    // 將 vm 放入 parent 中
    parent.$children.push(vm)
  }

  // 初始化例項 property:vm.$parent、vm.$root、$children、vm.$refs
  vm.$parent = parent                   // 父例項,如果當前例項有的話
  vm.$root = parent ? parent.$root : vm // 當前元件樹的根 Vue 例項
  vm.$children = []                    // 當前例項的直接子元件
  vm.$refs = {}                        // 一個物件,持有註冊過 ref attribute 的所有 DOM 元素和元件例項。

  // 以下劃線(_)開頭的應該是私有例項屬性
  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

initLifecycle() 方法做了一下幾件事:

  • 找到最近一級非抽象 parent,並將自己放入其 $children 陣列中
  • 初始化例項 property:vm.$parent、vm.$root、$children、vm.$refs
  • 給 vm 初始化一些以下劃線(_)開頭的私有屬性

這個方法所做的事太簡單了!和我的猜測不一致。

最初我認為初始化生命週期(initLifecycle()),應該和官網的生命週期圖相關。現在在來看一下這張圖,發現 _init() 中的程式碼僅僅對應這張圖的前一半而已。

graph TD a("new Vue()") --> b("init Events 和 Lifecycle") b --> |beforeCreate| c("init injects 和 reactivity") c --> |created| d("編譯模板") d --> |beforeMount| e("create vm.$el and replace 'el' with it") e --> |mounted| f("...")

initEvents 初始化事件

export function initEvents (vm: Component) {
  // 建立一個沒有原型的物件,賦值給 _events
  vm._events = Object.create(null)
  // 是否有鉤子事件
  vm._hasHookEvent = false
  // init parent attached events
  // 初始化父元件附加的事件
  const listeners = vm.$options._parentListeners
  if (listeners) {
    // 更新元件監聽器
    updateComponentListeners(vm, listeners)
  }
}

initEvents() 好像沒幹啥事。

但一個方法總得乾點事,所以如果實在要說這個函式哪裡做了點事,應該就和 _parentListeners 有關。

export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm
  // 更新監聽器
  // 第一個引數是父元件的監聽器,第二個是父元件監聽器的老版本
  // 之後就是 add、remove,很簡單,即註冊事件和刪除事件
  updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
  target = undefined
}

initEvents() 做的事情就是,更新父元件給子元件註冊的事件。這裡有兩個關鍵詞:父子元件、註冊。

TipupdateListeners() 就兩個邏輯:

  • 遍歷新的 listeners。如果老的版本上沒有定義,說明是新增。裡面用 on;新老版本不一致,以新版本為準。
  • 遍歷舊的 oldListeners。新的版本沒有定義,說明刪除了。裡面用 remove
export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  createOnceHandler: Function,
  vm: Component
) {
  let name, def, cur, old, event
  // 遍歷新的監聽器
  for (name in on) {
    def = cur = on[name]
    old = oldOn[name]
    event = normalizeEvent(name)
    ...

    if (isUndef(cur)) {
     ...
    // 老的版本上沒有定義,說明是新增。裡面用 on
    } else if (isUndef(old)) {
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur, vm)
      }
      if (isTrue(event.once)) {
        cur = on[name] = createOnceHandler(event.name, cur, event.capture)
      }
      add(event.name, cur, event.capture, event.passive, event.params)
    // 新老版本不一致,以新版本為準
    } else if (cur !== old) {
      old.fns = cur
      on[name] = old
    }
  }
  // 遍歷舊的監聽器
  for (name in oldOn) {
    // 新的版本沒有定義,說明刪除了。裡面用 remove
    if (isUndef(on[name])) {
      event = normalizeEvent(name)
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

父子元件

我們通過一個實驗來了解一下父元件和其中的子元件的建立過程。

定義父子兩個元件,並都有4個生命週期鉤子函式 beforeCreatecreatedbeforeMountmounted

// WelComeButton.vue - 子元件
<template>
  <div>
    <button v-on:click="$emit('welcome')">Click me to be welcomed</button>
  </div>
</template>
<script>
export default {
  beforeCreate () {
    console.log('beforeCreate')
  },
  created () {
    console.log('created')
  },
  beforeMount () {
    console.log('beforeMount')
  },
  mounted () {
    console.log('mounted')
  }
}
</script>
// About.vue - 父元件
<template>
  <div class="about">
    <welcome-button></welcome-button>
  </div>
</template>

<script>
import WelcomeButton from './WelComeButton.vue'
export default {
  components: { WelcomeButton },
  beforeCreate () {
    console.log('parent beforeCreate')
  },
  created () {
    console.log('parent created')
  },
  beforeMount () {
    console.log('parent beforeMount')
  },
  mounted () {
    console.log('parent mounted')
  }
}
</script>
// 瀏覽器輸出

parent beforeCreate
parent created
parent beforeMount
beforeCreate
created
beforeMount
mounted
parent mounted

父子元件的建立過程如下:

  1. 父元素的 beforeCreate。會初始化事件和生命週期
  2. 父元素的 created。初始化 inject、data/props、provide
  3. 父元素的 beforeMount。編譯模板為渲染函式
  4. 子元素的 beforeCreate
  5. 子元素的 created
  6. 子元素的 beforeMount
  7. 子元素的 mounted
  8. 父元素的 mounted。建立 vm.$el(Vue 例項使用的根 DOM 元素),並掛載檢視

於是我們知道包含子元件的元件,它的落地(更新到真實 dom)過程:

  1. 將父元件編譯成渲染函式
  2. 建立子元件,並掛載到父元件中
  3. 父元件被掛載

註冊

提到註冊事件,我們會想到 v-on

v-on 用在普通元素上時,監聽原生 DOM 事件。用在自定義元素元件上時,監聽子元件觸發的自定義事件。

// v-on 用於普通元素
<button v-on:click="greet">Greet</button>
// v-on 用於自定義元素元件
<div id='app'>
  <!-- 父元件給子元件註冊了事件 chang-count,事件的回撥方法是 changCount -->
  <button-counter v-on:chang-count='changCount'></button-counter>
</div>

我們通常使用模板,並在其上註冊事件,模板會編譯生成渲染函式,接著就到了虛擬 DOM,每次執行渲染函式都會生成一份新的 vNode,新的 vNode 和舊的 vNode 對比,查詢出需要更新的dom 節點,最後就更新 dom。這個過程會建立一些元素,此時才會去判斷到底是元件還是原生的元素(或平臺標籤)。

為什麼得在建立元素的時候才去判斷到底是元件還是原生的元素?

筆者猜測:在前面做這個判斷從技術上是可以做到的,因為這個邏輯(元件 or 原生的元素)判斷不復雜;所以另一種可能就是將這個邏輯放在更新 dom 的時候更加合理。如果是普通元素,直接建立,如果是元件,則建立元件,如果包含子元件,則先建立(或例項化)子元件,並會傳遞一些引數,其中就包含通過 v-on 註冊的事件。

initRender 初始化渲染

export function initRender (vm: Component) {
  // 重置子樹的根
  vm._vnode = null 
  // 重置 _staticTrees。
  // once 只渲染元素和元件一次。隨後的重新渲染,元素/元件及其所有的子節點將被視為靜態內容並跳過。這可以用於優化更新效能。
  vm._staticTrees = null // v-once cached trees
  // 用於當前 Vue 例項的初始化選項
  const options = vm.$options
  // 父樹中的佔位符節點
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  // 渲染上下文,即父節點的上下文
  const renderContext = parentVnode && parentVnode.context
  // vm.$slots,用來訪問被插槽分發的內容
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  // vm.$scopedSlots,用來訪問作用域插槽
  vm.$scopedSlots = emptyObject

  // 定義 vm._c。建立元素型別的 vNode
  // 渲染函式中會使用這個方法。
  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)

  // 定義 vm.$attrs、vm.$listeners
  // vm.$attrs,包含了父作用域中不作為 prop 被識別 (且獲取) 的 attribute 繫結
  // vm.$listeners,包含了父作用域中的 (不含 .native 修飾器的) v-on 事件監聽器
  const parentData = parentVnode && parentVnode.data

  if (process.env.NODE_ENV !== 'production') {
    ...
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}

此函式的功能有些零散,但至少我們知道該方法定義了 6 個例項屬性:vm.$slotsvm.$scopedSlotsvm._cvm.$createElementvm.$attrsvm.$listeners

vm._c

此函式將會建立 vnode。這個我們可以通關原始碼來驗證:

vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  ...
  return _createElement(context, tag, data, children, normalizationType)
}

真正起作用的是 _createElement

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data: any).__ob__)) {
    ...
    return createEmptyVNode()
  }
  ...
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  ...
  // 定義 vnode
  let vnode, ns
  if (typeof tag === 'string') {
    ...
  } else {
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

重點看一下返回值(return),都是 vnode

模板一文中,我們知道模板編譯成渲染函式,執行渲染函式就會生成一份 vNode。

callHook

callHook(vm, 'beforeCreate') 會觸發 beforeCreated 對應的回撥。請看原始碼:

// 將鉤子拿出來,觸發
export function callHook(vm: Component, hook: string) {
  // #7573 呼叫生命週期鉤子時禁用 dep 收集
  pushTarget()
  // 是一個陣列。比如可以通過 Vue.mixin 注入一個 created,這樣就能有兩個 created。
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  // 同一個 hook 有多個回撥
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      // 此方法真正呼叫回撥,裡面包含一些錯誤處理
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  // 觸發私有鉤子
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}

callHook() 真正呼叫鉤子的方法是 invokeWithErrorHandling()。請看原始碼:

// handler 指回撥
export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    // res,指回撥的結果
    res = args ? handler.apply(context, args) : handler.call(context) // {1}
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}

其中 回撥的結果(行{1})放在 try...catch 中,如果報錯,則會進入處理錯誤邏輯,即 handleError()。看父元件、父父元件...(一直往上找),如果沒能捕獲錯誤,則進入全域性錯誤處理(globalHandleError)。請看原始碼:

export function handleError (err: Error, vm: any, info: string) {
  pushTarget()
  try {
    if (vm) {
      let cur = vm
      // 依次找父元件,父父元件...,如果定義了錯誤捕獲(errorCaptured),並能捕獲錯誤,則退出函式
      // 否則進入全域性錯誤處理
      while ((cur = cur.$parent)) {
        const hooks = cur.$options.errorCaptured
        if (hooks) {
          // 依次迭代錯誤捕獲
          for (let i = 0; i < hooks.length; i++) {
            try {
              // 如果錯誤捕獲返回 false,則視為已捕獲,結束函式
              const capture = hooks[i].call(cur, err, vm, info) === false
              if (capture) return
            } catch (e) {
              globalHandleError(e, cur, 'errorCaptured hook')
            }
          }
        }
      }
    }
    // 全域性處理錯誤
    globalHandleError(err, vm, info)
  } finally {
    popTarget()
  }
}

initInjections

inject 就是給子孫元件注入屬性或方法。就像這樣:

// 父級元件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 子元件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}

執行 initInjections() 方法,首先獲取 vm 中注入的 inject(包含注入的 key 和對應的屬性或方法),然後將 inject 繫結到 vm 上,期間會關閉響應。

// 初始化注入
export function initInjections(vm: Component) {
  // 拿到注入的key以及對應的屬性或方法。資料結構是 [{key: provideProperyOrFunction},...]
  const result = resolveInject(vm.$options.inject, vm)
  // 若有注入
  if (result) {
    // 官網:provide 和 inject 繫結並不是可響應的。
    // 關閉響應
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        ...
      } else {
        // 訪問 key 時,其實就會訪問 result[key],即呼叫注入的函式
        defineReactive(vm, key, result[key])
      }
    })
    toggleObserving(true)
  }
}

TipresolveInject() 會返回一個包含物件的陣列,裡面是 inject 屬性以及對應的值:

export function resolveInject(inject: any, vm: Component): ?Object {
  if (inject) {
    const result = Object.create(null)
    const keys = hasSymbol
      ? Reflect.ownKeys(inject)
      : Object.keys(inject)
    // 依次遍歷 inject 的每個 key,從當前 vm 開始找 provide,若沒有則依次往上一級找
    // 如果找到,則註冊到一個空物件(result)中
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      // #6574 in case the inject object is observed...
      if (key === '__ob__') continue
      const provideKey = inject[key].from
      let source = vm
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          // 找到inject對應的 provide,存入 result 物件中
          // _provided 在下文的 initProvide 中被初始化
          result[key] = source._provided[provideKey]
          break
        }
        // 找上一級
        source = source.$parent
      }
      // source 為假值,說明一直找到頂部,都找到
      if (!source) {
        ...
      }
    }
    return result
  }
}

initState 初始化狀態

初始化狀態,即初始化 props、methods、data、computed 和 watch。

export function initState(vm: Component) {
  vm._watchers = []
  // 用於當前 Vue 例項的初始化選項
  const opts = vm.$options
  // 初始化 props
  if (opts.props) initProps(vm, opts.props)
  // 初始化  methods
  if (opts.methods) initMethods(vm, opts.methods)
  // 初始化 data
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  // computed
  if (opts.computed) initComputed(vm, opts.computed)
  // watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initProps

定義元件時,我們可以通過 props 定義父元件傳來的屬性。就像這樣:

// 接收父元件傳來的 title 屬性
Vue.component('blog-post', {
  props: ['title'],
  template: '<h3>{{ title }}</h3>'
})

initProps() 會將 prop 和對應的屬性或方法加入 vm._props 中,並將 prop 代理到 vm._props。如果訪問 props,例如 vm.titlexx,其實訪問的是 vm._props.titlexx。請看原始碼:

function initProps(vm: Component, propsOptions: Object) {
  // propsData,建立例項時傳遞 props
  const propsData = vm.$options.propsData || {}
  // 下面會將 prop 和對應的屬性或方法繫結到此物件中
  const props = vm._props = {}
  const keys = vm.$options._propKeys = []
  ...
  // propsOptions 是 vm.$options.props
  for (const key in propsOptions) {
    keys.push(key)
    // value 是 prop 對應的屬性或方法
    const value = validateProp(key, propsOptions, propsData, vm)
    // Tip:直接看生成環境的邏輯即可
    if (process.env.NODE_ENV !== 'production') {
      ...
    } else {
      // defineReactive 將資料轉為響應式。給 props 新增 key 和對應的 value。
      defineReactive(props, key, value)
    }
    // 如果 key 不在 vm 中,則將 key 代理到 _props
    // 就是說,如果訪問props,例如 vm.titlexx,其實訪問的是 vm._props.titlexx
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

initMethods

initMethods() 會將我們定義的方法放到 vm 中。開發環境下會檢查方法名,比如不能和 prop 中重複,不能和現有 Vue 例項方法衝突。

function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      ...
      if (props && hasOwn(props, key)) {
        // `方法 "${key}" 已經被定義為一個 prop。`,
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      if ((key in vm) && isReserved(key)) {
        // `方法 "${key}" 與現有的 Vue 例項方法衝突。 ` +
        // `避免定義以_或$開頭的元件方法。`
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    // bind(methods[key], vm),將 methods[key] 方法繫結到 vm 中
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

initData

initData() 首先會取得 data,並放入 vm._data 中。依次將 data 中的 key 代理到 vm._data 中,期間會檢查 key 是否與 methods 或 props 中 key 相同。如果訪問 data,例如訪問 vm.age,其實訪問的是 vm._data.age。請看原始碼:

function initData(vm: Component) {
  // 取得資料 vm.$options.data
  let data = vm.$options.data
  // 如果資料是函式,則呼叫 getData(即 data.call(vm, vm))返回資料,每個例項都有一份
  // 如果data不是函式,則每個例項公用這份 data
  // vm._data 指向 data,後面會做一個代理。訪問 vm.age,其實訪問的是 vm._data.age
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // data 如果不是一個物件,開發環境則發出警告:資料函式應該返回一個物件
  if (!isPlainObject(data)) {
   ...
  }
  // proxy data on instance
  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]
    // key 不能和 methods 中相同
    if (process.env.NODE_ENV !== 'production') {
      ...
    }
    // key 不能和 props 中相同
    if (props && hasOwn(props, key)) {
     ...
    // 沒有被預定,則將 key 代理到 _data
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

initComputed

computed 用法如下:

computed: {
  aDouble: vm => vm.a * 2
}

initComputed() 會依次迭代我們定義的 computed,給每一個 key 都會建立一個 Watcher,並給 Watcher 傳入 key 對應的回撥方法,最後在 vm 上定義計算屬性(defineComputed(vm, key, userDef))。請看原始碼:

function initComputed(vm: Component, computed: Object) {
  // 建立一個空物件給 vm._computedWatchers,是計算屬性的 watcher
  const watchers = vm._computedWatchers = Object.create(null)
  // 是否是服務端渲染
  const isSSR = isServerRendering()

  // 迭代 computed
  for (const key in computed) {
    // 取得 key 對應的方法
    const userDef = computed[key]
    // computed 還支援 get、set
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    ...

    // 非服務端渲染
    if (!isSSR) {
      // create internal watcher for the computed property.
      // 為計算屬性建立內部觀察者。訪問 vm['計算屬性'] 時會使用
      watchers[key] = new Watcher(
        vm,
        // 取值(watcher.value)時會用到
        getter || noop,        // {1}
        noop,
        computedWatcherOptions
      )
    }
    // 定義計算屬性
    if (!(key in vm)) {
      // userDef,即 key 對應的回撥
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      // 計算屬性不能在 data、props和methods中
      ...
    }
  }
}

如果你想知道 defineComputed(vm, key, userDef) 做了什麼?請繼續看。

defineComputed() 的核心功能在最後一句:

// 定義計算屬性
export function defineComputed(
  target: any,
  key: string,
  userDef: Object | Function
) {
  // 不是服務端渲染,則需要快取
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop

  } else {
     // 計算屬性可以有 get、set
     ...
  }
  ...
  // target 是 vm
  // 訪問 vm[key] 就會訪問 sharedPropertyDefinition
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

我們這裡不是服務端渲染,所以進入 createComputedGetter()

function createComputedGetter(key) {
  return function computedGetter() {
    // 取得在 initComputed() 中定義的 watcher
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 計算屬性是有快取的(官網:計算屬性是基於它們的響應式依賴進行快取的)
      // 髒的(比如說計算屬性依賴的某個資料值變了,就是髒的),則重新求值
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      // 取得 watcher 的值。會訪問 initComputed() 方法中的 getter(行{1})
      return watcher.value
    }
  }
}

defineComputed(vm, key, userDef) 做什麼事情,它的名字其實已經告訴我們了(即定義計算屬性)。比如訪問一個計算屬性,會取得對應計算屬性的 Watcher,然會從 watcher 中取得對應的值。其中 watcher 的 dirty 與快取有關。

Tip:有關 Water 的介紹可以看 偵測資料的變化

initWatch

用法如下:

watch: {
    a: function (val, oldVal) {
      console.log('new: %s, old: %s', val, oldVal)
    }
}

initWatch() 會依次迭代我們傳入的 watch,並通過 createWatcher 建立 Watcher。請看原始碼:

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

createWatcher() 的本質是 vm.$watch()

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  // hander 是函式
  // vm.$watch() 方法賦予我們監聽例項上資料變化的能力
  return vm.$watch(expOrFn, handler, options)
}

initProvide

provide 就是提供給子孫元件注入屬性或方法。就像這樣:

// 父級元件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

initProvide 與上文的 initInjections 對應。

initProvide() 主要就是將使用者傳入的 provide 儲存到 vm._provided,後續給 inject 使用。請看原始碼:

export function initProvide(vm: Component) {
  const provide = vm.$options.provide
  // 存起來,供子孫元件使用
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

vm.$mount

_init() 的末尾就是掛載(vm.$mount()):

if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }

擴充套件

props、data、methods、computed 的 key 為什麼不能相同

因為這些 key 最後都繫結在 vm 上,所以不能相同。請看原始碼:

// props
proxy(vm, `_props`, key)

// data
proxy(vm, `_data`, key)

// methods
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)

// compued
defineComputed(vm, key, userDef)

data 中可以使用 props嗎

initState() 中有如下程式碼:

// 初始化 props
if (opts.props) initProps(vm, opts.props)
...
// 初始化 data
if (opts.data) {
  initData(vm)
} else {
  observe(vm._data = {}, true /* asRootData */)
}

由於 props 先初始化,所以在 data 中可以使用 props。請看示例:

// 父元件
<welcome-button name="peng"></welcome-button>

// WelcomeButton.vue
<template>
  <div>
    name={{ name }} <br />
    myName={{ myName }}
  </div>
</template>
<script>
export default {
  props: ['name'],
  data () {
    return {
      myName: this.name + 'jiali'
    }
  }
}
</script>

瀏覽器輸出:

name=peng
myName=pengjiali

Tip:props 中使用 data 卻是不可以的,因為 data 初始化在 props 後面。

computed 和 watch 誰先執行

請問下面這段程式碼,控制檯輸出什麼:

<template>
  <div>
    <!-- 讀取三個屬性 -->
    {{ doubleAge }} {{ age }} {{ name }}
  </div>
</template>
<script>
export default {
  data () {
    return {
      age: 18,
      name: 'peng'
    }
  },
  computed: {
    doubleAge: function (vm) {
      const result = this.age * 2
      console.log('computed')
      return result
    }
  },
  watch: {
    age: {
      handler: function (val, oldVal) {
        console.log('watch age')
      },
      immediate: !true
    },
    name: {
      handler: function (val, oldVal) {
        console.log('watch name')
      },
      // 立即執行
      immediate: true
    }
  },
  created () {
    setTimeout(() => this.age++, 5000)
  }
}
</script>
watch name
computed
// 過5秒
watch age
computed

雖然在 initState() 中先初始化 computed,再初始化 watch,但在這個例子中,卻是先執行 watch,後執行 computed。

// computed
if (opts.computed) initComputed(vm, opts.computed)
// watch
if (opts.watch && opts.watch !== nativeWatch) {
  initWatch(vm, opts.watch)
}

其他章節請看:

vue 快速入門 系列

相關文章