Vue 2.5 數(shù)據(jù)綁定實(shí)現(xiàn)邏輯(三)initState

Vue 實(shí)例在建立的時(shí)候會(huì)運(yùn)行一系列的初始化操作仗扬,而在這些初始化操作里面,和數(shù)據(jù)綁定關(guān)聯(lián)最大的是 initState蕾额。這個(gè)里面要說(shuō)的也是比較多早芭,有可能這次的文章里面寫(xiě)不全,先寫(xiě)這看吧诅蝶。

首先看 initState

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

這里面主要是對(duì) props, methods, data, computed 和 watch 進(jìn)行初始化(如果還不知道這幾個(gè)屬性都是什么退个,建議先去看一下官方文檔并且寫(xiě)幾個(gè)小例子)。這些屬性都是要在 Dom 渲染時(shí)獲取的调炬,自然也大都需要進(jìn)行數(shù)據(jù)綁定语盈。

initProps

function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  observerState.shouldConvert = isRoot
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      ......
    } else {
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  observerState.shouldConvert = true
}

省略的地方是開(kāi)發(fā)環(huán)境中為了方便調(diào)試寫(xiě)的代碼,Vue 源碼中有相當(dāng)多的地方是這樣寫(xiě)的缰泡。

整體邏輯就是:

  1. 把所有 prop 的 key 另存在 options 的 _propKeys 中黎烈。
  2. 對(duì)于每一個(gè) prop,將其 key 添加到 _propKeys 中匀谣,獲取其 value照棋,并執(zhí)行 defineReactive 函數(shù)。(不了解的可以看上一節(jié))
  3. 對(duì)于每一個(gè) prop, 調(diào)用 proxy 函數(shù)在 Vue 對(duì)象上建立一個(gè)該值的引用武翎。

在獲取 prop 的 value 的時(shí)候調(diào)用了 validateProp 進(jìn)行驗(yàn)證并取驗(yàn)證后的返回值烈炭。

export function validateProp (
  key: string,
  propOptions: Object,
  propsData: Object,
  vm?: Component
): any {
  const prop = propOptions[key]
  const absent = !hasOwn(propsData, key)
  let value = propsData[key]
  // handle boolean props
  if (isType(Boolean, prop.type)) {
    if (absent && !hasOwn(prop, 'default')) {
      value = false
    } else if (!isType(String, prop.type) && (value === '' || value === hyphenate(key))) {
      value = true
    }
  }
  // check default value
  if (value === undefined) {
    value = getPropDefaultValue(vm, prop, key)
    // since the default value is a fresh copy,
    // make sure to observe it.
    const prevShouldConvert = observerState.shouldConvert
    observerState.shouldConvert = true
    observe(value)
    observerState.shouldConvert = prevShouldConvert
  }
  if (
    process.env.NODE_ENV !== 'production' &&
    // skip validation for weex recycle-list child component props
    !(__WEEX__ && isObject(value) && ('@binding' in value))
  ) {
    assertProp(prop, key, value, vm, absent)
  }
  return value
}

注意,prop 驗(yàn)證只有在開(kāi)發(fā)環(huán)境中才會(huì)進(jìn)行宝恶,并且并不會(huì)影響渲染符隙,只會(huì)發(fā)出警告。

這里的工作主要是在 prop 沒(méi)有傳值時(shí)獲取 prop 的默認(rèn)值(默認(rèn)值是自己設(shè)置的)垫毙,并對(duì)該值執(zhí)行 observe霹疫。對(duì)于布爾類型,如果沒(méi)有默認(rèn)值則認(rèn)為默認(rèn)值是 false综芥。

如果是開(kāi)發(fā)環(huán)境丽蝎,則會(huì)進(jìn)行類型驗(yàn)證,這個(gè)驗(yàn)證是典型的根據(jù)構(gòu)造函數(shù)名進(jìn)行類型驗(yàn)證的膀藐,這個(gè)函數(shù)名獲取到以后會(huì)進(jìn)行字符串的比對(duì)屠阻,最近也正想自己寫(xiě)一個(gè)比較完善的類型驗(yàn)證組件,所以在這篇文章里就不詳述了额各,免得跑題国觉。

這里多次對(duì) observerState.shouldConvert 進(jìn)行賦值,這個(gè)值的 true or false 直接決定了 Observer 是否會(huì)建立虾啦。

至于這個(gè) propsData 是什么時(shí)候取得的呢麻诀,當(dāng)然是在模板編譯的時(shí)候取得的痕寓。關(guān)于 prop 還有很多需要說(shuō)的,有可能還要另外寫(xiě)一篇文章來(lái)說(shuō)明蝇闭。

initMethod

對(duì) method 的初始化相對(duì)其他來(lái)說(shuō)還是比較簡(jiǎn)單的

function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (methods[key] == null) {
        warn(
          `Method "${key}" has an undefined value in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
  }
}

主要是在開(kāi)發(fā)環(huán)境中檢測(cè):

  1. 方法名是否為空
  2. 方法名是否和一個(gè) prop 沖突
  3. 方法名是否和已有的 Vue 實(shí)例方法沖突

另外會(huì)用 bind 將該方法的作用域綁定到 Vue 實(shí)例對(duì)象上呻率,且創(chuàng)建一個(gè)在 Vue 實(shí)例對(duì)象上的引用(這點(diǎn)很重要)

export function bind (fn: Function, ctx: Object): Function {
  function boundFn (a) {
    const l: number = arguments.length
    return l
      ? l > 1
        ? fn.apply(ctx, arguments)
        : fn.call(ctx, a)
      : fn.call(ctx)
  }
  // record original fn length
  boundFn._length = fn.length
  return boundFn
}

這個(gè) bind 是用apply 和 call 重寫(xiě)的 bind,據(jù)說(shuō)是會(huì)比原生的 bind 要快丁眼,但是實(shí)在才學(xué)尚淺,不明白為什么昭殉。

initData

如果對(duì)上篇文章說(shuō)到的內(nèi)容比較熟悉的話苞七,這里應(yīng)該就沒(méi)什么難度了。

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // 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]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    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.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

首先會(huì)獲取 data挪丢,如果 data 是函數(shù)的話蹂风,則調(diào)用 getData 獲取函數(shù)的返回值。
這里面還是在檢測(cè)一些重名的問(wèn)題乾蓬,就不想細(xì)說(shuō)了惠啄。

這里最重要的是對(duì) data 運(yùn)行 observe 函數(shù)建立起 Observer 和鉤子函數(shù)

initComputed

這里就比較麻煩了,由于計(jì)算屬性并不是值任内,而是函數(shù)撵渡,并且返回值還會(huì)和一些值有關(guān),同時(shí)還要涉及到緩存的問(wèn)題死嗦,就需要一些特殊的方法進(jìn)行處理了趋距,為了避免文章太長(zhǎng),就放在下一篇說(shuō)越除。

initWatch

說(shuō)到這里就一定要補(bǔ)充一下之前沒(méi)有說(shuō)到的關(guān)于 Watcher 的問(wèn)題了节腐,先看代碼,一步步往下說(shuō)摘盆。

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

首先是對(duì)于每一個(gè) watch 屬性運(yùn)行 createWatcher(想想也應(yīng)該知道是建立一個(gè) Watcher 對(duì)象)

function createWatcher (
  vm: Component,
  keyOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(keyOrFn, handler, options)
}

這里主要進(jìn)行了兩步預(yù)處理翼雀,代碼上很好理解,主要做一些解釋:

第一步孩擂,可以理解為用戶設(shè)置的 watch 有可能是一個(gè) options 對(duì)象狼渊,如果是這樣的話則取 options 中的 handler 作為回調(diào)函數(shù)。(并且將options 傳入下一步的 vm.$watch)

第二步类垦,watch 有可能是之前定義過(guò)的 method囤锉,則獲取該方法為 handler。

下面就要看 $watch 方法了护锤,這個(gè)方法是在 stateMixin 中定義的

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

這里的邏輯是官地,如果 cb(就是前面的 handler)是對(duì)象的話則再運(yùn)行一遍 createWatcher 進(jìn)行處理,然后建立一個(gè) Watcher 對(duì)象進(jìn)行監(jiān)聽(tīng)烙懦,如果 options 中的 immediate 為 true 則立即執(zhí)行該回調(diào)函數(shù)驱入,最后返回一個(gè)函數(shù)用來(lái)停止監(jiān)聽(tīng)。

接下來(lái)就要看看這個(gè)回調(diào)函數(shù)是什么時(shí)候運(yùn)行的了

run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

再次看到 Watcher 的 run 方法,這里面判斷 user 如果為 true 則運(yùn)行 cb 函數(shù)亏较,這個(gè)函數(shù)就是之前傳入的 handler 回調(diào)函數(shù)莺褒, user 則在 vm.$watch 中賦值為true,其他地方建立的 Watcher 則基本都為 false雪情,其他的幾個(gè)如 lasy 等參數(shù)也是通過(guò) options 傳入的遵岩,這里就不詳細(xì)說(shuō)了,具體可以自己看一下代碼或者官方API文檔巡通。

結(jié)語(yǔ)

到這一步為止(先不算計(jì)算屬性的初始化)尘执,數(shù)據(jù)綁定的邏輯基本分析完了,這篇文章看完以后重點(diǎn)還是要看看 Watcher 對(duì)象的設(shè)計(jì)宴凉,可以說(shuō)這個(gè)監(jiān)視器設(shè)計(jì)的相當(dāng)巧妙誊锭,廢話不多說(shuō)了,希望大家有什么見(jiàn)解或者分析有誤的可以提出來(lái)弥锄。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末丧靡,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子籽暇,更是在濱河造成了極大的恐慌温治,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件戒悠,死亡現(xiàn)場(chǎng)離奇詭異罐盔,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)救崔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)惶看,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人六孵,你說(shuō)我怎么就攤上這事纬黎。” “怎么了劫窒?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵本今,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我主巍,道長(zhǎng)冠息,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任孕索,我火速辦了婚禮逛艰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘搞旭。我一直安慰自己散怖,他們只是感情好菇绵,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著镇眷,像睡著了一般咬最。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上欠动,一...
    開(kāi)封第一講書(shū)人閱讀 51,541評(píng)論 1 305
  • 那天永乌,我揣著相機(jī)與錄音,去河邊找鬼具伍。 笑死翅雏,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的沿猜。 我是一名探鬼主播枚荣,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼碗脊,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼啼肩!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起衙伶,我...
    開(kāi)封第一講書(shū)人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤祈坠,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后矢劲,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體赦拘,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年芬沉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了躺同。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡丸逸,死狀恐怖蹋艺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情黄刚,我是刑警寧澤捎谨,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站憔维,受9級(jí)特大地震影響涛救,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜业扒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一检吆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧程储,春花似錦咧栗、人聲如沸逆甜。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)交煞。三九已至,卻和暖如春斟或,著一層夾襖步出監(jiān)牢的瞬間素征,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工萝挤, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留御毅,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓怜珍,卻偏偏與公主長(zhǎng)得像端蛆,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子酥泛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容