首頁>技術>

前言

Vue 原始碼淺析,分三塊大內容:初始化、資料動態響應、模板渲染。

這系列算不上逐行解析,示例的程式碼可能只佔原始碼一小部分,但相信根據二八法則,搞清這些內容或許可以撐起 80% 對原始碼的理解程度。

我藉此機會,在玩 Vue3.0 之前開始最後一段 Vue2 的學習收尾,同時把這些學習總結分享給各位。

離網上那些 Vue 深入淺析的文章還有很多差距,如有不對之處,請各位指正。

從 Vue 建構函式開始

我們寫 Vue 程式碼時,都是通過新建 Vue 例項開始:

var app = new Vue({  el: '#app',  data: {    message: 'Hello Vue!'  }});

那麼就先找到 Vue 的函式定義:

// source-code\\vue\\src\\core\\instance\\index.jsfunction Vue (options) {  //...  this._init(options)}initMixin(Vue)//...

這個物件的引用 this._init 就是之後通過 initMixin 方法中宣告好的 Vue.prototype._init 原型方法。

// source-code\\vue\\src\\core\\instance\\init.jsexport function initMixin (Vue: Class<Component>) {  Vue.prototype._init = function (options?: Object) {    //...  }}

那麼我們接下來的一切都是以此 _init 為起點展開。

處理 options

跳過一些目前不涉及的邏輯,我們看下物件引用 vm.$options 到底是怎麼取得的:

Vue.prototype._init = function (options?: Object) {  const vm: Component = this  //...  vm.$options = mergeOptions(    resolveConstructorOptions(vm.constructor),    options || {},    vm  )}

內部通過呼叫 mergeOptions 方法得到最終的 vm.$options。

接下來細看 mergeOptions 方法:

// source-code\\vue\\src\\core\\\\util\\options.jsexport function mergeOptions (  parent: Object,  child: Object,  vm?: Component): Object {  //...  normalizeProps(child, vm)  normalizeInject(child, vm)  normalizeDirectives(child)  if (!child._base) {    if (child.extends) {      parent = mergeOptions(parent, child.extends, vm)    }    if (child.mixins) {      for (let i = 0, l = child.mixins.length; i < l; i++) {        parent = mergeOptions(parent, child.mixins[i], vm)      }    }  }  const options = {}  let key  for (key in parent) {    mergeField(key)  }  for (key in child) {    if (!hasOwn(parent, key)) {      mergeField(key)    }  }  function mergeField (key) {    const strat = strats[key] || defaultStrat    options[key] = strat(parent[key], child[key], vm, key)  }  

標準化部分屬性 options

涉及 vue 中的:props、inject、directives:

normalizeProps(child, vm)normalizeInject(child, vm)normalizeDirectives(child)

為什麼需要標準化呢?

就是為了給我們提供多種編寫程式碼的方式,最後按照規定的資料結構標準化。

下面分別貼出對應上述三者的相關程式碼,相信很容易知道它們在做什麼:

function normalizeProps (options: Object, vm: ?Component) {  const props = options.props  if (!props) return  const res = {}  let i, val, name  if (Array.isArray(props)) {    i = props.length    while (i--) {      val = props[i]      if (typeof val === 'string') {        name = camelize(val)        res[name] = { type: null }      }     }  } else if (isPlainObject(props)) {    for (const key in props) {      val = props[key]      name = camelize(key)      res[name] = isPlainObject(val)        ? val        : { type: val }    }  } }

比如,props: ['name', 'nick-name'] 會被轉成如下形式:

同樣,inject 和 directives 也會對使用者的簡寫方式做標準化處理,這裡不做過多描述。

比如:為 inject 中的欄位屬性新增 from 欄位;為 directives 中的函式定義,初始化 bind 和 update 方法。

遞迴合併 mergeOptions

會根據當前例項的 extends、mixins 和 parent 中的屬性做合併操作:

if (!child._base) {  if (child.extends) {    parent = mergeOptions(parent, child.extends, vm)  }  if (child.mixins) {    for (let i = 0, l = child.mixins.length; i < l; i++) {      parent = mergeOptions(parent, child.mixins[i], vm)    }  }}

當然此處的 mergeOptions 還是遞迴呼叫本方法。所以此方法不是重點,核心在後面的方法:mergeFields。

合併欄位 mergeField

邏輯非常明顯,遍歷 parent 上的屬性 key,然後根據某種策略,將該屬性 key 掛到 options 物件上;之後,遍歷 child (本 Vue 物件)屬性 key,只要不是 parent 的屬性,也一併加到 options 上:

const options = {}let keyfor (key in parent) {  mergeField(key)}for (key in child) {  if (!hasOwn(parent, key)) {    mergeField(key)  }}function mergeField (key) {  const strat = strats[key] || defaultStrat  options[key] = strat(parent[key], child[key], vm, key)}

策略 strat

上面提到了某種策略,其實就是特定寫了幾種父子合併取值優先順序的判斷。

顯示最基本的 defaultStrat 預設策略:

const defaultStrat = function (parentVal: any, childVal: any): any {  return childVal === undefined    ? parentVal    : childVal}

child 屬性不存在,則直接使用 parent 屬性。

剩下就是根據特殊屬性,來定義的策略:

strats.el , strats.propsData //defaultStratstrats.data //mergeDataOrFnstrats[hook] //concatstrats[ASSET_TYPES+'s'] (components,directives,filters) //extendstrats.watch //concatstrats.props,strats.methods,strats.inject,strats.computed //extendstrats.provide //mergeDataOrFn

涉及很多我們常用的 vue 屬性,但拋去一些特殊的策略,某些策略還是有共性的,比如都呼叫了:mergeDataOrFn。

能看到 mergeDataOrFn 中核心程式碼,主要根據 call 來執行對應的屬性值 function:

export function mergeDataOrFn (  parentVal: any,  childVal: any,  vm?: Component): ?Function {  if (!vm) {    //...  } else {    return function mergedInstanceDataFn () {      // instance merge      const instanceData = typeof childVal === 'function'        ? childVal.call(vm, vm)        : childVal      const defaultData = typeof parentVal === 'function'        ? parentVal.call(vm, vm)        : parentVal      if (instanceData) {        return mergeData(instanceData, defaultData) //(child,parent)      } else {        return defaultData      }    }  }}

最後將結果,扔給 mergeData 方法:

function mergeData (to: Object, from: ?Object): Object {  if (!from) return to  let key, toVal, fromVal  const keys = hasSymbol    ? Reflect.ownKeys(from)    : Object.keys(from)  for (let i = 0; i < keys.length; i++) {    key = keys[i]    // in case the object is already observed...    if (key === '__ob__') continue    toVal = to[key]    fromVal = from[key]    if (!hasOwn(to, key)) { //特別說明 hasOwn 是根據 hasOwnProperty 做的;忽略 prototype 屬性      set(to, key, fromVal)    } else if (      toVal !== fromVal &&      isPlainObject(toVal) &&      isPlainObject(fromVal)    ) {      mergeData(toVal, fromVal)    }  }  return to}

如果 child 中沒有 key 屬性,則將 parent 屬性賦值給它。

當如果 child or parent 是一個物件時,則會繼續遞迴 mergeData ,直至全部處理完。

特別說明 hasOwn 方法是根據 hasOwnProperty 做的,會忽略 prototype 屬性,所以在 set 方法中會有特別的處理:

export function set (target: Array<any> | Object, key: any, val: any): 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 (!ob) {    target[key] = val    return val  }  defineReactive(ob.value, key, val)  ob.dep.notify()  return val}

這是生命週期對應的策略,會遍歷所有的生命週期方法,並把父子的週期方法做 concat 操作:

function mergeHook (  parentVal: ?Array<Function>,  childVal: ?Function | ?Array<Function>): ?Array<Function> {  const res = childVal    ? parentVal      ? parentVal.concat(childVal)      : Array.isArray(childVal)        ? childVal        : [childVal]    : parentVal  return res    ? dedupeHooks(res)    : res}

對於 ASSET_TYPES 型別以及 props、methods、inject、computed 策略,則會做 extend 繼承操作。

export function extend (to: Object, _from: ?Object): Object {  for (const key in _from) {    to[key] = _from[key]  }  return to}

最後 watch 會稍微複雜寫,直接看程式碼:

strats.watch = function (  parentVal: ?Object,  childVal: ?Object,  vm?: Component,  key: string): ?Object {  //...  const ret = {}  extend(ret, parentVal)  for (const key in childVal) {    let parent = ret[key]    const child = childVal[key]    if (parent && !Array.isArray(parent)) {      parent = [parent]    }    ret[key] = parent      ? parent.concat(child)      : Array.isArray(child) ? child : [child]  }  return ret}

同時保留 parent、child 的 watch 屬性,畢竟他們都要工作。通過 concat 和生命週期處理方式一樣,都儲存起來。

proxy 代理

目前我們這裡不是 Vue 3.0 ,還未涉及新增加的 proxy 新特性,但在目前的版本中,已經有相關 proxy 的功能,不過對於非 production 環境沒做強制要求,在開發環境中也只是做些 warn 之類的功能。

if (process.env.NODE_ENV !== 'production') {  initProxy(vm)} else {  vm._renderProxy = vm}

阮一峰老師的 es6 文章已經對 proxy 做了很細緻的說明,所以這裡不再對 has、get 之類的功能做補充,只是貼出相關程式碼:

const hasHandler = {  has (target, key) {    const has = key in target    const isAllowed = allowedGlobals(key) ||          (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))    if (!has && !isAllowed) {      if (key in target.$data) warnReservedPrefix(target, key)      else warnNonPresent(target, key)    }    return has || !isAllowed  }}const getHandler = {  get (target, key) {    if (typeof key === 'string' && !(key in target)) {      if (key in target.$data) warnReservedPrefix(target, key)      else warnNonPresent(target, key)    }    return target[key]  }}initProxy = function initProxy (vm) {  if (hasProxy) {    // determine which proxy handler to use    const options = vm.$options    const handlers = options.render && options.render._withStripped    ? getHandler    : hasHandler    vm._renderProxy = new Proxy(vm, handlers)  } else {    vm._renderProxy = vm  }}

注意,如果瀏覽器不支援 proxy 特性,最後將執行到:vm._renderProxy = vm

一些初始化工作

接下來對生命週期、事件、渲染函式做些初始化工作,不是太重要,這裡簡單示意下:

initLifecycle(vm)initEvents(vm)initRender(vm)
呼叫生命週期方法 callHook

之後,我們將見到第一次呼叫生命週期 beforeCreate 方法;當然初始化資料響應狀態的流程後,還會呼叫 created 方法。

//...callHook(vm, 'beforeCreate')initInjections(vm) // resolve injections before data/propsinitState(vm)initProvide(vm) // resolve provide after data/propscallHook(vm, 'created')//...

我們看下 callHook 怎麼工作:

export function callHook (vm: Component, hook: string) {  // #7573 disable dep collection when invoking lifecycle hooks  pushTarget()  const handlers = vm.$options[hook]  const info = `${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()}
export function invokeWithErrorHandling (  handler: Function,  context: any,  args: null | any[],  vm: any,  info: string) {  let res  try {    res = args ? handler.apply(context, args) : handler.call(context)    //...  } catch (e) {    handleError(e, vm, info)  }  return res}

能看到 call 對應的生命週期名字後,就會通過 invokeWithErrorHandling 方法內的來執行對應的生命週期方法,並且通過 try/catch 來捕獲出現的錯誤。

不過重要的是,還是要知道不同生命週期方法在整個 Vue 執行過程中的切入點(這裡先貼出此處兩個方法):

解析依賴和注入

正如官網所述:

provide 和 inject 主要在開發高階外掛/元件庫時使用。並不推薦用於普通應用程式程式碼中。

可能我們平時的開發程式碼很少用到,現在看下初始化 init 中,他們是如何被初始化的。

宣告 provide

雖然 inject 先於 provide 初始化,但必須現有 provide 這個蛋(父類提供),看下做了什麼:

export function initProvide (vm: Component) {  const provide = vm.$options.provide  if (provide) {    vm._provided = typeof provide === 'function'      ? provide.call(vm)      : provide  }}

定義了 vm._provided 屬性,它將在後面交給對應注入的 inject 屬性 key 運作。

注入 injection

先看下入口方法 initInjections:

export function initInjections (vm: Component) {  const result = resolveInject(vm.$options.inject, vm)  //...}
export function resolveInject (inject: any, vm: Component): ?Object {  if (inject) {    // inject is :any because flow is not smart enough to figure out cached    const result = Object.create(null)    const keys = hasSymbol      ? Reflect.ownKeys(inject)      : Object.keys(inject)    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)) {          result[key] = source._provided[provideKey]          break        }        source = source.$parent      }      if (!source) {        if ('default' in inject[key]) {          const provideDefault = inject[key].default          result[key] = typeof provideDefault === 'function'            ? provideDefault.call(vm)            : provideDefault        } else if (process.env.NODE_ENV !== 'production') {          warn(`Injection "${key}" not found`, vm)        }      }    }    return result  }}

遍歷 inject 物件上的內容(在 options 合併時,已經做了引數的標準化。比如,具備了 from 引數),把物件上的屬性名 key,在 vm._provided 物件上尋找,父類是否有提供過依賴。

如果找到,變將賦值給當前 inject 屬性 key 對應的 value,當然沒有找到,則會執行 inject 定義的 default:

result[key] = source._provided[provideKey]

不過還沒完,因為我們知道 inject 注入的依賴,可以在 vue 中不同的地方使用(比如,官網示例提到的生命週期方法,和 data 屬性),並且賦予了資料響應能力,就是執行了如下方法(具體分析後續章節展開):

export function initInjections (vm: Component) {  //...  defineReactive(vm, key, result[key])  //...}
初始化狀態 state

備註下,initState 方法先於 initProvide,這裡文章排版,放在此處說明:

initInjections(vm) // resolve injections before data/propsinitState(vm)initProvide(vm) // resolve provide after data/props

initState 方法內涉及了 vue 中,我們常用的屬性:prop、methods、data、computed、watch,這些都是具備資料動態響應的能力,所以解釋起來會比較複雜,下篇繼續:

// source-code\\vue\\src\\core\\instance\\state.jsexport 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)  }}

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • Android lowmemorykiller分析