欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Vue解讀之響應(yīng)式原理源碼剖析

 更新時間:2021年10月12日 11:10:44   作者:沐華  
Vue 最獨特的特性之一,是其非侵入性的響應(yīng)式系統(tǒng),下面這篇文章主要給大家介紹了關(guān)于Vue響應(yīng)式原理源碼剖析的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下

先看張圖,了解一下大體流程和要做的事

初始化

在 new Vue 初始化的時候,會對我們組件的數(shù)據(jù) props 和 data 進(jìn)行初始化,由于本文主要就是介紹響應(yīng)式,所以其他的不做過多說明來,看一下源碼

源碼地址:src/core/instance/init.js - 15行

export function initMixin (Vue: Class<Component>) {
  // 在原型上添加 _init 方法
  Vue.prototype._init = function (options?: Object) {
    ...
    vm._self = vm
    initLifecycle(vm) // 初始化實例的屬性、數(shù)據(jù):$parent, $children, $refs, $root, _watcher...等
    initEvents(vm) // 初始化事件:$on, $off, $emit, $once
    initRender(vm) // 初始化渲染: render, mixin
    callHook(vm, 'beforeCreate') // 調(diào)用生命周期鉤子函數(shù)
    initInjections(vm) // 初始化 inject
    initState(vm) // 初始化組件數(shù)據(jù):props, data, methods, watch, computed
    initProvide(vm) // 初始化 provide
    callHook(vm, 'created') // 調(diào)用生命周期鉤子函數(shù)
    ...
  }
}

初始化這里調(diào)用了很多方法,每個方法都做著不同的事,而關(guān)于響應(yīng)式主要就是組件內(nèi)的數(shù)據(jù) props、data。這一塊的內(nèi)容就是在 initState() 這個方法里,所以我們進(jìn)入這個方法源碼看一下

initState()

源碼地址:src/core/instance/state.js - 49行

export function initState (vm: Component) {
  vm._watchers = []
  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 {
    // 沒有 data 的話就默認(rèn)賦值為空對象,并監(jiān)聽
    observe(vm._data = {}, true /* asRootData */)
  }
  // 初始化 computed
  if (opts.computed) initComputed(vm, opts.computed)
  // 初始化 watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

又是調(diào)用一堆初始化的方法,我們還是直奔主題,取我們響應(yīng)式數(shù)據(jù)相關(guān)的,也就是 initProps()、initData()、observe()
一個一個繼續(xù)扒,非得整明白響應(yīng)式的全部過程

initProps()

源碼地址:src/core/instance/state.js - 65行

這里主要做的是:

  • 遍歷父組件傳進(jìn)來的 props 列表
  • 校驗每個屬性的命名、類型、default 屬性等,都沒有問題就調(diào)用 defineReactive 設(shè)置成響應(yīng)式
  • 然后用 proxy() 把屬性代理到當(dāng)前實例上,如把 vm._props.xx 變成 vm.xx,就可以訪問
function initProps (vm: Component, propsOptions: Object) {
  // 父組件傳入子組件的 props
  const propsData = vm.$options.propsData || {}
  // 經(jīng)過轉(zhuǎn)換后最終的 props
  const props = vm._props = {}
  // 存放 props 的 key,就算 props 值空了,key 也會在里面
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // 轉(zhuǎn)換非根實例的 props
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    // 校驗 props 類型、default 屬性等
    const value = validateProp(key, propsOptions, propsData, vm)
    // 在非生產(chǎn)環(huán)境中
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(`hyphenatedKey 是保留屬性,不能用作組件 prop`)
      }
      // 把 props 設(shè)置成響應(yīng)式的
      defineReactive(props, key, value, () => {
        // 如果用戶修改 props 發(fā)出警告
        if (!isRoot && !isUpdatingChildComponent) {
          warn(`避免直接改變 prop`)
        }
      })
    } else {
      // 把 props 設(shè)置為響應(yīng)式
      defineReactive(props, key, value)
    }
    // 把不在默認(rèn) vm 上的屬性,代理到實例上
    // 可以讓 vm._props.xx 通過 vm.xx 訪問
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

initData()

源碼地址:src/core/instance/state.js - 113行

這里主要做的是:

  • 初始化一個 data,并拿到 keys 集合
  • 遍歷 keys 集合,來判斷有沒有和 props 里的屬性名或者 methods 里的方法名重名的
  • 沒有問題就通過 proxy() 把 data 里的每一個屬性都代理到當(dāng)前實例上,就可以通過 this.xx 訪問了
  • 最后再調(diào)用 observe 監(jiān)聽整個 data
function initData (vm: Component) {
  // 獲取當(dāng)前實例的 data 
  let data = vm.$options.data
  // 判斷 data 的類型
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(`數(shù)據(jù)函數(shù)應(yīng)該返回一個對象`)
  }
  // 獲取當(dāng)前實例的 data 屬性名集合
  const keys = Object.keys(data)
  // 獲取當(dāng)前實例的 props 
  const props = vm.$options.props
  // 獲取當(dāng)前實例的 methods 對象
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    // 非生產(chǎn)環(huán)境下判斷 methods 里的方法是否存在于 props 中
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(`Method 方法不能重復(fù)聲明`)
      }
    }
    // 非生產(chǎn)環(huán)境下判斷 data 里的屬性是否存在于 props 中
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(`屬性不能重復(fù)聲明`)
    } else if (!isReserved(key)) {
      // 都不重名的情況下,代理到 vm 上
      // 可以讓 vm._data.xx 通過 vm.xx 訪問
      proxy(vm, `_data`, key)
    }
  }
  // 監(jiān)聽 data
  observe(data, true /* asRootData */)
}

observe()

源碼地址:src/core/observer/index.js - 110行

這個方法主要就是用來給數(shù)據(jù)加上監(jiān)聽器的

這里主要做的是:

  • 如果是 vnode 的對象類型或者不是引用類型,就直接跳出
  • 否則就給沒有添加 Observer 的數(shù)據(jù)添加一個 Observer,也就是監(jiān)聽者
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 如果不是'object'類型 或者是 vnode 的對象類型就直接返回
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 使用緩存的對象
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 創(chuàng)建監(jiān)聽者
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Observer

源碼地址:src/core/observer/index.js - 37行

這是一個類,作用是把一個正常的數(shù)據(jù)成可觀測的數(shù)據(jù)

這里主要做的是:

  • 給當(dāng)前 value 打上已經(jīng)是響應(yīng)式屬性的標(biāo)記,避免重復(fù)操作
  • 然后判斷數(shù)據(jù)類型
    • 如果是對象,就遍歷對象,調(diào)用 defineReactive()創(chuàng)建響應(yīng)式對象
    • 如果是數(shù)組,就遍歷數(shù)組,調(diào)用 observe()對每一個元素進(jìn)行監(jiān)聽
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // 根對象上的 vm 數(shù)量
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // 給 value 添加 __ob__ 屬性,值為value 的 Observe 實例
    // 表示已經(jīng)變成響應(yīng)式了,目的是對象遍歷時就直接跳過,避免重復(fù)操作
    def(value, '__ob__', this)
    // 類型判斷
    if (Array.isArray(value)) {
      // 判斷數(shù)組是否有__proty__
      if (hasProto) {
        // 如果有就重寫數(shù)組的方法
        protoAugment(value, arrayMethods)
      } else {
        // 沒有就通過 def,也就是Object.defineProperty 去定義屬性值
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  // 如果是對象類型
  walk (obj: Object) {
    const keys = Object.keys(obj)
    // 遍歷對象所有屬性,轉(zhuǎn)為響應(yīng)式對象,也是動態(tài)添加 getter 和 setter,實現(xiàn)雙向綁定
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  // 監(jiān)聽數(shù)組
  observeArray (items: Array<any>) {
    // 遍歷數(shù)組,對每一個元素進(jìn)行監(jiān)聽
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

defineReactive()

源碼地址:src/core/observer/index.js - 135行

這個方法的作用是定義響應(yīng)式對象

這里主要做的是:

  • 先初始化一個 dep 實例
  • 如果是對象就調(diào)用 observe,遞歸監(jiān)聽,以保證不管結(jié)構(gòu)嵌套多深,都能變成響應(yīng)式對象
  • 然后調(diào)用 Object.defineProperty() 劫持對象屬性的 getter 和 getter
  • 如果獲取時,觸發(fā) getter 會調(diào)用 dep.depend() 把觀察者 push 到依賴的數(shù)組 subs 里去,也就是依賴收集
  • 如果更新時,觸發(fā) setter 會做以下操作
    • 新值沒有變化或者沒有 setter 屬性的直接跳出
    • 如果新值是對象就調(diào)用 observe() 遞歸監(jiān)聽
    • 然后調(diào)用 dep.notify() 派發(fā)更新
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {

  // 創(chuàng)建 dep 實例
  const dep = new Dep()
  // 拿到對象的屬性描述符
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // 獲取自定義的 getter 和 setter
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // 如果 val 是對象的話就遞歸監(jiān)聽
  // 遞歸調(diào)用 observe 就可以保證不管對象結(jié)構(gòu)嵌套有多深,都能變成響應(yīng)式對象
  let childOb = !shallow && observe(val)
  // 截持對象屬性的 getter 和 setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 攔截 getter,當(dāng)取值時會觸發(fā)該函數(shù)
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 進(jìn)行依賴收集
      // 初始化渲染 watcher 時訪問到需要雙向綁定的對象,從而觸發(fā) get 函數(shù)
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    // 攔截 setter,當(dāng)值改變時會觸發(fā)該函數(shù)
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      // 判斷是否發(fā)生變化
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // 沒有 setter 的訪問器屬性
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 如果新值是對象的話遞歸監(jiān)聽
      childOb = !shallow && observe(newVal)
      // 派發(fā)更新
      dep.notify()
    }
  })
}

上面說了通過 dep.depend 來做依賴收集,可以說 Dep 就是整個 getter 依賴收集的核心了

依賴收集

依賴收集的核心是 Dep,而且它與 Watcher 也是密不可分的,我們來看一下

Dep

源碼地址:src/core/observer/dep.js

這是一個類,它實際上就是對 Watcher 的一種管理

這里首先初始化一個 subs 數(shù)組,用來存放依賴,也就是觀察者,誰依賴這個數(shù)據(jù),誰就在這個數(shù)組里,然后定義幾個方法來對依賴添加、刪除、通知更新等

另外它有一個靜態(tài)屬性 target,這是一個全局的 Watcher,也表示同一時間只能存在一個全局的 Watcher

let uid = 0
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor () {
    this.id = uid++
    this.subs = []
  }
  // 添加觀察者
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  // 移除觀察者
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  depend () {
    if (Dep.target) {
      // 調(diào)用 Watcher 的 addDep 函數(shù)
      Dep.target.addDep(this)
    }
  }
  // 派發(fā)更新(下一章節(jié)介紹)
  notify () {
    ...
  }
}
// 同一時間只有一個觀察者使用,賦值觀察者
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

Watcher

源碼地址:src/core/observer/watcher.js

Watcher 也是一個類,也叫觀察者(訂閱者),這里干的活還挺復(fù)雜的,而且還串連了渲染和編譯

先看源碼吧,再來捋一下整個依賴收集的過程

let uid = 0
export default class Watcher {
  ...
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // Watcher 實例持有的 Dep 實例的數(shù)組
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.value = this.lazy
      ? undefined
      : this.get()
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
  }
  get () 
    // 該函數(shù)用于緩存 Watcher
    // 因為在組件含有嵌套組件的情況下,需要恢復(fù)父組件的 Watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 調(diào)用回調(diào)函數(shù),也就是upcateComponent,對需要雙向綁定的對象求值,從而觸發(fā)依賴收集
      value = this.getter.call(vm, vm)
    } catch (e) {
      ...
    } finally {
      // 深度監(jiān)聽
      if (this.deep) {
        traverse(value)
      }
      // 恢復(fù)Watcher
      popTarget()
      // 清理不需要了的依賴
      this.cleanupDeps()
    }
    return value
  }
  // 依賴收集時調(diào)用
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        // 把當(dāng)前 Watcher push 進(jìn)數(shù)組
        dep.addSub(this)
      }
    }
  }
  // 清理不需要的依賴(下面有)
  cleanupDeps () {
    ...
  }
  // 派發(fā)更新時調(diào)用(下面有)
  update () {
    ...
  }
  // 執(zhí)行 watcher 的回調(diào)
  run () {
    ...
  }
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

補充:

我們自己組件里寫的 watch,為什么自動就能拿到新值和老值兩個參數(shù)?

就是在 watcher.run() 里面會執(zhí)行回調(diào),并且把新值和老值傳過去

為什么要初始化兩個 Dep 實例數(shù)組

因為 Vue 是數(shù)據(jù)驅(qū)動的,每次數(shù)據(jù)變化都會重新 render,也就是說 vm.render() 方法就又會重新執(zhí)行,再次觸發(fā) getter,所以用兩個數(shù)組表示,新添加的 Dep 實例數(shù)組 newDeps 和上一次添加的實例數(shù)組 deps

依賴收集過程

在首次渲染掛載的時候,還會有這樣一段邏輯

mountComponent 源碼地址:src/core/instance/lifecycle.js - 141行

export function mountComponent (...): Component {
  // 調(diào)用生命周期鉤子函數(shù)
  callHook(vm, 'beforeMount')
  let updateComponent
  updateComponent = () => {
    // 調(diào)用 _update 對 render 返回的虛擬 DOM 進(jìn)行 patch(也就是 Diff )到真實DOM,這里是首次渲染
    vm._update(vm._render(), hydrating)
  }
  // 為當(dāng)前組件實例設(shè)置觀察者,監(jiān)控 updateComponent 函數(shù)得到的數(shù)據(jù),下面有介紹
  new Watcher(vm, updateComponent, noop, {
    // 當(dāng)觸發(fā)更新的時候,會在更新之前調(diào)用
    before () {
      // 判斷 DOM 是否是掛載狀態(tài),就是說首次渲染和卸載的時候不會執(zhí)行
      if (vm._isMounted && !vm._isDestroyed) {
        // 調(diào)用生命周期鉤子函數(shù)
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  // 沒有老的 vnode,說明是首次渲染
  if (vm.$vnode == null) {
    vm._isMounted = true
    // 調(diào)用生命周期鉤子函數(shù)
    callHook(vm, 'mounted')
  }
  return vm
}

依賴收集:

  • 掛載之前會實例化一個渲染 watcher ,進(jìn)入 watcher 構(gòu)造函數(shù)里就會執(zhí)行 this.get() 方法
  • 然后就會執(zhí)行 pushTarget(this),就是把 Dep.target 賦值為當(dāng)前渲染 watcher 并壓入棧(為了恢復(fù)用)
  • 然后執(zhí)行 this.getter.call(vm, vm),也就是上面的 updateComponent() 函數(shù),里面就執(zhí)行了 vm._update(vm._render(), hydrating)
  • 接著執(zhí)行 vm._render() 就會生成渲染 vnode,這個過程中會訪問 vm 上的數(shù)據(jù),就觸發(fā)了數(shù)據(jù)對象的 getter
  • 每一個對象值的 getter 都有一個 dep,在觸發(fā) getter 的時候就會調(diào)用 dep.depend() 方法,也就會執(zhí)行 Dep.target.addDep(this)
  • 然后這里會做一些判斷,以確保同一數(shù)據(jù)不會被多次添加,接著把符合條件的數(shù)據(jù) push 到 subs 里,到這就已經(jīng)完成了依賴的收集,不過到這里還沒執(zhí)行完,如果是對象還會遞歸對象觸發(fā)所有子項的getter,還要恢復(fù) Dep.target 狀態(tài)

移除訂閱

移除訂閱就是調(diào)用 cleanupDeps() 方法。比如在模板中有 v-if 我們收集了符合條件的模板 a 里的依賴。當(dāng)條件改變時,模板 b 顯示出來,模板 a 隱藏。這時就需要移除 a 的依賴

這里主要做的是:

  • 先遍歷上一次添加的實例數(shù)組 deps,移除 dep.subs 數(shù)組中的 Watcher 的訂閱
  • 然后把 newDepIds 和 depIds 交換,newDeps 和 deps 交換
  • 再把 newDepIds 和 newDeps 清空
// 清理不需要的依賴
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

派發(fā)更新

notify()

觸發(fā) setter 的時候會調(diào)用 dep.notify() 通知所有訂閱者進(jìn)行派發(fā)更新

notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // 如果不是異步,需要排序以確保正確觸發(fā)
      subs.sort((a, b) => a.id - b.id)
    }
    // 遍歷所有 watcher 實例數(shù)組
    for (let i = 0, l = subs.length; i < l; i++) {
      // 觸發(fā)更新
      subs[i].update()
    }
  }

update()

觸發(fā)更新時調(diào)用

  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 組件數(shù)據(jù)更新會走這里
      queueWatcher(this)
    }
  }

queueWatcher()

源碼地址:src/core/observer/scheduler.js - 164行

這是一個隊列,也是 Vue 在做派發(fā)更新時的一個優(yōu)化點。就是說在每次數(shù)據(jù)改變的時候不會都觸發(fā) watcher 回調(diào),而是把這些 watcher 都添加到一個隊列里,然后在 nextTick 后才執(zhí)行

這里和下一小節(jié) flushSchedulerQueue() 的邏輯有交叉的地方,所以要聯(lián)合起來理解

主要做的是:

  • 先用 has 對象查找 id,保證同一個 watcher 只會 push 一次
  • else 如果在執(zhí)行 watcher 期間又有新的 watcher 插入進(jìn)來就會到這里,然后從后往前找,找到第一個待插入的 id 比當(dāng)前隊列中的 id 大的位置,插入到隊列中,這樣隊列的長度就發(fā)生了變化
  • 最后通過 waiting 保證 nextTick 只會調(diào)用一次
export function queueWatcher (watcher: Watcher) {
  // 獲得 watcher 的 id
  const id = watcher.id
  // 判斷當(dāng)前 id 的 watcher 有沒有被 push 過
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      // 最開始會進(jìn)入這里
      queue.push(watcher)
    } else {
      // 在執(zhí)行下面 flushSchedulerQueue 的時候,如果有新派發(fā)的更新會進(jìn)入這里,插入新的 watcher,下面有介紹
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // 最開始會進(jìn)入這里
    if (!waiting) {
      waiting = true
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      // 因為每次派發(fā)更新都會引起渲染,所以把所有 watcher 都放到 nextTick 里調(diào)用
      nextTick(flushSchedulerQueue)
    }
  }
}

flushSchedulerQueue()

源碼地址:src/core/observer/scheduler.js - 71行

這里主要做的是:

  • 先排序隊列,排序條件有三點,看注釋
  • 然后遍歷隊列,執(zhí)行對應(yīng) watcher.run()。需要注意的是,遍歷的時候每次都會對隊列長度進(jìn)行求值,因為在 run 之后,很可能又會有新的 watcher 添加進(jìn)來,這時就會再次執(zhí)行到上面的 queueWatcher
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // 根據(jù) id 排序,有如下條件
  // 1.組件更新需要按從父到子的順序,因為創(chuàng)建過程中也是先父后子
  // 2.組件內(nèi)我們自己寫的 watcher 優(yōu)先于渲染 watcher
  // 3.如果某組件在父組件的 watcher 運行期間銷毀了,就跳過這個 watcher
  queue.sort((a, b) => a.id - b.id)

  // 不要緩存隊列長度,因為遍歷過程中可能隊列的長度發(fā)生變化
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      // 執(zhí)行 beforeUpdate 生命周期鉤子函數(shù)
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // 執(zhí)行組件內(nèi)我們自己寫的 watch 的回調(diào)函數(shù)并渲染組件
    watcher.run()
    // 檢查并停止循環(huán)更新,比如在 watcher 的過程中又重新給對象賦值了,就會進(jìn)入無限循環(huán)
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(`無限循環(huán)了`)
        break
      }
    }
  }
  // 重置狀態(tài)之前,先保留一份隊列備份
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  resetSchedulerState()
  // 調(diào)用組件激活的鉤子  activated
  callActivatedHooks(activatedQueue)
  // 調(diào)用組件更新的鉤子  updated
  callUpdatedHooks(updatedQueue)
}

updated()

終于可以更新了,updated 大家都熟悉了,就是生命周期鉤子函數(shù)

上面調(diào)用 callUpdatedHooks() 的時候就會進(jìn)入這里, 執(zhí)行 updated 了

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}

至此 Vue2 的響應(yīng)式原理流程的源碼基本就分析完畢了,接下來就介紹一下上面流程中的不足之處

defineProperty 缺陷及處理

使用 Object.defineProperty 實現(xiàn)響應(yīng)式對象,還是有一些問題的

  • 比如給對象中添加新屬性時,是無法觸發(fā) setter 的
  • 比如不能檢測到數(shù)組元素的變化

而這些問題,Vue2 里也有相應(yīng)的解決文案

Vue.set()

給對象添加新的響應(yīng)式屬性時,可以使用一個全局的 API,就是 Vue.set() 方法

源碼地址:src/core/observer/index.js - 201行

set 方法接收三個參數(shù):

  • target:數(shù)組或普通對象
  • key:表示數(shù)組下標(biāo)或?qū)ο蟮?key 名
  • val:表示要替換的新值

這里主要做的是:

  • 先判斷如果是數(shù)組,并且下標(biāo)合法,就直接使用重寫過的 splice 替換
  • 如果是對象,并且 key 存在于 target 里,就替換值
  • 如果沒有 __ob__,說明不是一個響應(yīng)式對象,直接賦值返回
  • 最后再把新屬性變成響應(yīng)式,并派發(fā)更新
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 如果是數(shù)組 而且 是合法的下標(biāo)
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    // 直接使用 splice 就替換,注意這里的 splice 不是原生的,所以才可以監(jiān)測到,具體看下面
    target.splice(key, 1, val)
    return val
  }
  // 到這說明是對象
  // 如果 key 存在于 target 里,就直接賦值,也是可以監(jiān)測到的
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  // 獲取 target.__ob__
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  // 在 Observer 里介紹過,如果沒有這個屬性,就說明不是一個響應(yīng)式對象
  if (!ob) {
    target[key] = val
    return val
  }
  // 然后把新添加的屬性變成響應(yīng)式
  defineReactive(ob.value, key, val)
  // 手動派發(fā)更新
  ob.dep.notify()
  return val
}

重寫數(shù)組方法

源碼地址:src/core/observer/array.js

這里做的主要是:

  • 保存會改變數(shù)組的方法列表
  • 當(dāng)執(zhí)行列表里有的方法的時候,比如 push,先把原本的 push 保存起來,再做響應(yīng)式處理,再執(zhí)行這個方法
// 獲取數(shù)組的原型
const arrayProto = Array.prototype
// 創(chuàng)建繼承了數(shù)組原型的對象
export const arrayMethods = Object.create(arrayProto)
// 會改變原數(shù)組的方法列表
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
// 重寫數(shù)組事件
methodsToPatch.forEach(function (method) {
  // 保存原本的事件
  const original = arrayProto[method]
  // 創(chuàng)建響應(yīng)式對象
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // 派發(fā)更新
    ob.dep.notify()
    // 做完我們需要的處理后,再執(zhí)行原本的事件
    return result
  })
})

總結(jié)

到此這篇關(guān)于Vue解讀之響應(yīng)式原理源碼剖析的文章就介紹到這了,更多相關(guān)Vue響應(yīng)式原理源碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • vue動態(tài)添加store、路由和國際化配置方式

    vue動態(tài)添加store、路由和國際化配置方式

    這篇文章主要介紹了vue動態(tài)添加store、路由和國際化配置方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-03-03
  • 基于element-ui封裝表單金額輸入框的方法示例

    基于element-ui封裝表單金額輸入框的方法示例

    這篇文章主要介紹了基于element-ui封裝表單金額輸入框的方法示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-01-01
  • Vue v-for循環(huán)之@click點擊事件獲取元素示例

    Vue v-for循環(huán)之@click點擊事件獲取元素示例

    今天小編就為大家分享一篇Vue v-for循環(huán)之@click點擊事件獲取元素示例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2019-11-11
  • Vue3+Element?Plus使用svg加載iconfont的處理方法

    Vue3+Element?Plus使用svg加載iconfont的處理方法

    這篇文章主要介紹了Vue3+Element?Plus使用svg加載iconfont的解決方案,本文詳細(xì)介紹了如何在Element?Plus中使用iconfont,簡單的說就是要將其封裝成SVG,并且支持動態(tài)修改顏色,需要的朋友可以參考下
    2022-08-08
  • vue3?hook自動導(dǎo)入原理解析

    vue3?hook自動導(dǎo)入原理解析

    這篇文章主要介紹了vue3?hook自動導(dǎo)入的原理,介紹了API的自動導(dǎo)入及組件的自動導(dǎo)入,本文結(jié)合實例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下
    2022-09-09
  • 八個一看就覺得很棒的Vue開發(fā)技巧分享

    八個一看就覺得很棒的Vue開發(fā)技巧分享

    在用Vue開發(fā)的這幾年里,我學(xué)到了很多有用的技巧,所以這篇文章主要給大家分享介紹了八個一看就覺得很棒的Vue開發(fā)技巧,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2023-04-04
  • vue如何給自定義的組件綁定點擊事件

    vue如何給自定義的組件綁定點擊事件

    這篇文章主要介紹了vue如何給自定義的組件綁定點擊事件,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-04-04
  • vue3+vite+TS中使用element-plus按需引入ElLoading、ElMessage樣式失效解決

    vue3+vite+TS中使用element-plus按需引入ElLoading、ElMessage樣式失效解決

    在項目中使用elementui確實是很方便的一件事,下面這篇文章主要給大家介紹了關(guān)于vue3+vite+TS中使用element-plus按需引入ElLoading、ElMessage樣式失效解決的相關(guān)資料,需要的朋友可以參考下
    2023-02-02
  • vue實現(xiàn)打卡功能

    vue實現(xiàn)打卡功能

    這篇文章主要為大家詳細(xì)介紹了vue實現(xiàn)打卡功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-08-08
  • vue如何在data中引入圖片的正確路徑

    vue如何在data中引入圖片的正確路徑

    這篇文章主要介紹了vue如何在data中引入圖片的正確路徑,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-06-06

最新評論