1. 程式人生 > 程式設計 >Vue解讀之響應式原理原始碼剖析

Vue解讀之響應式原理原始碼剖析

目錄
  • 初始化
    • initState()
    • initProps()
    • initData()
    • observe()
    • Observer
    • defineReactive()
  • 依賴收集
    • Dep
    • Watcher
    • 依賴收集過程
    • 移除訂閱
  • 派發更新
    • notify()
    • update()
    • queueWatcher()
    • flushSchedulerQueue()
    • updated()
  • defineProperty 缺陷及處理
    • .set()
    • 重寫陣列方法
  • 總結

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

    Vue解讀之響應式原理原始碼剖析

    初始化

    在 new Vue 初始化的時候,會對我們元件的資料 props 和 data 進行初始化,由於本文主要就是介紹響應式,所以其他的不做過多說明來,看一下原始碼

    原始碼地址:src/core/instance/init. - 15行

    export function initMixin (Vue: Class<Component>) {
      // 在原型上新增 _init 方法
      Vue.prototype._init = function (options?: Object) {
        ...
        vm._self = vm
        initLifecycle(vm) // 初始化例項的屬性、資料:$parent,$children,$refs,$root,_watcher...等
        initEvents(vm) // 初始化事件:$on,$off,$emit,$once
        initRender(vm) // 初始化渲染: render,mixin
        callHook(vm,'beforeCreate') // 呼叫生命週期鉤子函式
        initInjections(vm) // 初始化 inject
        initState(vm) // 初始化元件資料:props,data,methods,watch,computed
        initProvide(vm) // 初始化 provide
        callHook(vm,'created') // 呼叫生命週期鉤子函式
        ...
      }
    }
    

    初始化這裡呼叫了很多方法,每個方法都做著不同的事,而關於響應式主要就是元件內的資料 props、data。這一塊的內容就是在 initState() 這個方法裡,所以我們進入這個方法原始碼看一下

    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 的話就預設賦值為空物件,並監聽
        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()、initData()、observe()
    一個一個繼續扒,非得整明白響應式的全部過程

    initProps()

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

    這裡主要做的是:

    • 遍歷父元件傳進來的 props 列表
    • 校驗每個屬性的命名、型別、default 屬性等,都沒有問題就呼叫 defineReactive 設定成響應式
    • 然後用 proxy() 把屬性代理到當前例項上,如把 vm._props.xx 變成 vm.xx,就可以訪問
    function initProps (vm: Component,propsOptions: Object) {
      // 父元件傳入子元件的 props
      const propsData = vm.$options.propsData || {}
      // 經過轉換後最終的 props
      const props = vm._props = {}
      // 存放 props 的 key,就算 props 值空了,key 也會在裡面
      const keys = vm.$options._propKeys = []
      const isRoot = !vm.$parent
      // 轉換非根例項的 props
      if (!isRoot) {
        toggleObserving(false)
      }
      for (const key in propsOptions) {
        keys.push(key)
        // 校驗 props 型別、default 屬性等
        const value = validateProp(key,propsOptions,propsData,vm)
        // 在非生產環境中
        if (process.env.NODE_ENV !== 'production') {
          const hyphenatedKey = hyphenate(key)
          if (isReservedAttribute(hyphenatedKey) ||
              config.isReservedAttr(hyphenatedKey)) {
            warn(`hyphenatedKey 是保留屬性,不能用作元件 prop`)
          }
          // 把 props 設定成響應式的
          defineReactive(props,key,value,() => {
            // 如果使用者修改 props 發出警告
            if (!isRoot && !isUpdatingChildComponent) {
              warn(`避免直接改變 prop`)
            }
          })
        } else {
          // 把 props 設定為響應式
    www.cppcns.com      defineReactive(props,value)
        }
        // 把不在預設 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 裡的每一個屬性都代理到當前例項上,就可以通過 this.xx 訪問了
    • 最後再呼叫 observe 監聽整個 data
    function initData (vm: Component) {
      // 獲取當前例項的 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(`資料函式應該返回一個物件`)
      }
      // 獲取當前例項的 data 屬性名集合
      const keys = Object.keys(data)
      // 獲取當前例項的 props 
      const props = vm.$options.props
      // 獲取當前例項的 methods 物件
      const methods = vm.$options.methods
      let i = keys.length
      while (i--) {
        const key = keys[i]
        // 非生產環境下判斷 methods 裡的方法是否存在於 props 中
        if (process.env.NODE_ENV !== 'production') {
          if (methods && hasOwn(methods,key)) {
            warn(`Method 方法不能重複宣告`)
          }
        }
        // 非生產環境下判斷 data 裡的屬性是否存在於 props 中
        if (props && hasOwn(props,key)) {
          process.env.NODE_ENV !== 'production' && warn(`屬性不能重複宣告`)
        } else if (!isReserved(key)) {
          // 都不重名的情況下,代理到 vm 上
          // 可以讓 vm._data.xx 通過 vm.xx 訪問
          proxy(vm,`_data`,key)
        }
      }
      // 監聽 data
      observe(data,true /* asRootData */)
    }
    

    observe()

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

    這個方法主要就是用來給資料加上監聽器的

    這裡主要做的是:

    • 如果是 vnode 的物件型別或者不是引用型別,就直接跳出
    • 否則就給沒有新增 Observer 的資料新增一個 Observer,也就是監聽者
    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
      ) {
        // 建立監聽者
        ob = new Observer(value)
      }
      if (asRootData && ob) {
        ob.vmCount++
      }
      return ob
    }
    

    Observer

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

    這是一個類,作用是把一個正常的資料成可觀測的資料

    這裡主要做的是:

    • 給當前 value 打上已經是響應式屬性的標記,避免重複操作
    • 然後判斷資料型別
      • 如果是物件,就遍歷物件,呼叫 defineReactive()建立響應式物件
      • 如果是陣列,就遍歷陣列,呼叫 observe()對每一個元素進行監聽
    export class Observer {
      value: any;
      dep: Dep;
      vmCount: number; // 根物件上的 vm 數量
      constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        // 給 value 新增 __ob__ 屬性,值為value 的 Observe 例項
        // 表示已經變成響應式了,目的是物件遍歷時就直接跳過,避免重複操作
        def(value,'__ob__',this)
        // 型別判斷
        if (Array.isArray(value)) {
          // 判斷陣列是否有__proty__
          if (hasProto) {
            // 如果有就重寫陣列的方法
            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)
        // 遍歷物件所有屬性,轉為響應式物件,也是動態新增 getter 和 setter,實現雙向繫結
        for (let i = 0; i < keys.length; i+http://www.cppcns.com+) {
          defineReactive(obj,keys[i])
        }
      }
      // 監聽陣列
      observeArray (items: Array<any>) {
        // 遍歷陣列,對每一個元素進行監聽
        for (let i = 0,l = items.length; i < l; i++) {
          observe(items[i])
        }
      }
    }
    

    defineReactive()

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

    這個方法的作用是定義響應式物件

    這裡主要做的是:

    • 先初始化一個 dep 例項
    • 如果是物件就呼叫 observe,遞迴監聽,以保證不管結構巢狀多深,都能變成響應式物件
    • 然後呼叫 Object.defineProperty() 劫持物件屬性的 getter 和 getter
    • 如果獲取時,觸發 getter 會呼叫 dep.depend() 把觀察者 push 到依賴的陣列 subs 裡去,也就是依賴收集
    • 如果更新時,觸發 setter 會做以下操作
      • 新值沒有變化或者沒有 setter 屬性的直接跳出
      • 如果新值是物件就呼叫 observe() 遞迴監聽
      • 然後呼叫 dep.notify() 派發更新
    export function defineReactive (
      obj: Object,key: string,val: any,customSetter?: ?Function,shallow?: boolean
    ) {
    
      // 建立 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 是物件的話就遞迴監聽
      // 遞迴呼叫 observe 就可以保證不管物件結構巢狀有多深,都能變成響應式物件
      let childOb = !shallow && observe(val)
      // 截持物件屬性的 getter 和 setter
      Object.defineProperty(obj,{
        enumerable: true,configurable: true,// 攔截 getter,當取值時會觸發該函式
        get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
          // 進行依賴收集
          // 初始化渲染 watcher 時訪問到需要雙向繫結的物件,從而觸發 get 函式
          if (Dep.target) {
            dep.depend()
            if (childOb) {
              childOb.dep.depend()
              if (Array.isArray(value)) {
                dependArray(value)
              }
            }
          }
          return value
        },// 攔截 setter,當值改變時會觸發該函式
        set: function reactiveSetter (newVal) {
          const value = getter ? getter.call(obj) : val
          // 判斷是否發生變化
          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
          }
          // 如果新值是物件的話遞迴監聽
          childOb = !shallow && observe(newVal)
          // 派發更新
          dep.notify()
        }
      })
    }
    

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

    依賴收集

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

    Dep

    原始碼地址:src/core/observer/dep.js

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

    這裡首先初始化一個 subs 陣列,用來存放依賴,也就是觀察者,誰依賴這個資料,誰就在這個數組裡,然後定義幾個方法來對依賴新增、刪除、通知更新等

    另外它有一個靜態屬性 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) {
          // 呼叫 Watcher 的 addDep 函式
          Dep.target.addDep(this)
        }
      }
      // 派發更新(下一章節介紹)
      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 也是一個類,也叫觀察者(訂閱者),這裡乾的活還挺複雜的,而且還串連了渲染和編譯

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

    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 例項的陣列
        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 () 
        // 該函式用於快取 Watcher
        // 因為在元件含有巢狀元件的情況下,需要恢復父元件的 Watcher
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          // 呼叫回撥函式,也就是upcateComponent,對需要雙向繫結的物件求值,從而觸發依賴收集
          value = this.getter.call(vm,vm)
        } catch (e) {
          ...
        } finally {
          // 深度監聽
          if (this.deep) {
            traverse(value)
          }
          // 恢復Watcher
          popTarget()
          // 清理不需要了的依賴
          this.cleanupDeps()
        }
        return value
      }
      // 依賴收集時呼叫
      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)) {
            // 把當前 Watcher push 進陣列
            dep.addSub(this)
          }
        }
      }
      // 清理不需要的依賴(下面有)
      cleanupDeps () {
        ...
      }
      // 派發更新時呼叫(下面有)
      update () {
        ...
      }
      // 執行 watcher 的回撥
      run () {
        ...
      }
      depend () {
        let i = this.deps.length
        while (i--) {
          this.deps[i].depend()
        }
      }
    }
    

    補充:

    我們自己元件裡寫的 watch,為什麼自動就能拿到新值和老值兩個引數?

    就是在 watcher.run() 裡面會執行回撥,並且把新值和老值傳過去

    為什麼要初始化兩個 Dep 例項陣列

    因為 Vue 是資料驅動的,每次資料變化都會重新 render,也就是說 vm.render() 方法就又會重新執行,再次觸發 getter,所以用兩個陣列表示,新新增的 Dep 例項陣列 newDeps 和上一次新增的例項陣列 deps

    依賴收集過程

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

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

    export function mountComponent (...): Component {
      // 呼叫生命週期鉤子函式
      callHook(vm,'beforeMount')
      let updateComponent
      updateComponent = () => {
        // 呼叫 _update 對 render 返回的虛擬 DOM 進行 patch(也就是 Diff )到真實DOM,這裡是首次渲染
        vm._update(vm._render(),hydrating)
      }
      // 為當前元件例項設定觀察者,監控 updateComponent 函式得到的資料,下面有介紹
      new Watcher(vm,updateComponent,noop,{
        // 當觸發更新的時候,會在更新之前呼叫
        before () {
          // 判斷 DOM 是否是掛載狀態,就是說首次渲染和解除安裝的時候不會執行
          if (vm._isMounted && !vm._isDestroyed) {
            // 呼叫生命週期鉤子函式
            callHook(vm,'beforeUpdate')
          }
        }
      },true /* isRenderWatcher */)
      // 沒有老的 vnode,說明是首次渲染
      if (vm.$vnode == null) {
        vm._isMounted = true
        // 呼叫生命週期鉤子函式
        callHook(vm,'mounted')
      }
      return vm
    }
    

    依賴收集:

    • 掛載之前會例項化一個渲染 watcher ,進入 watcher 建構函式裡就會執行 this.get() 方法
    • 然後就會執行 pushTarget(this),就是把 Dep.target 賦值為當前渲染 watcher 並壓入棧(為了恢復用)
    • 然後執行 this.getter.call(vm,vm),也就是上面的 updateComponent() 函式,裡面就執行了 vm._update(vm._render(),hydrating)
    • 接著執行 vm._render() 就會生成渲染 vnode,這個過程中會訪問 vm 上的資料,就觸發了資料物件的 getter
    • 每一個物件值的 getter 都有一個 dep,在觸發 getter 的時候就會呼叫 dep.depend() 方法,也就會執行 Dep.target.addDep(this)
    • 然後這裡會做一些判斷,以確保同一資料不會被多次新增,接著把符合條件的資料 push 到 subs 裡,到這就已經完成了依賴的收集,不過到這裡還沒執行完,如果是物件還會遞迴物件觸發所有子項的getter,還要恢復 Dep.target 狀態

    移除訂閱

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

    這裡主要做的是:

    • 先遍歷上一次新增的例項陣列 deps,移除 dep.subs 陣列中的 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
      }
    

    派發更新

    notify()

    觸發 setter 的時候會呼叫 dep.notify() 通知所有訂閱者進行派發更新

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

    update()

    觸發更新時呼叫

      update () {
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          // 元件資料更新會走這裡
          queueWatcher(this)
        }
      }
    

    queueWatcher()

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

    這是一個佇列,也是 Vue 在做派發更新時的一個優化點。就是說在每次資料改變的時候不會都觸發 watcher 回撥,而是把這些 watcher 都新增到一個佇列裡,然後在 nextTick 後才執行

    這裡和下一小節 flushSchedulerQueue() 的邏輯有交叉的地方,所以要聯合起來理解

    主要做的是:

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

    flushSchedulerQueue()

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

    這裡主要做的是:

    • 先排序佇列,排序條件有三點,看註釋
    • 然後遍歷佇列,執行對應 watcher.ruMtGjlkZybxn()。需要注意的是,遍歷的時候每次都會對佇列長度進行求值,因為在 run 之後,很可能又會有新的 watcher 新增進來,這時就會再次執行到上面的 queueWatcher
    function flushSchedulerQueue () {
      currentFlushTimestamp = getNow()
      flushing = true
      let watcher,id
    
      // 根據 id 排序,有如下條件
      // 1.元件更新需要按從父到子的順序,因為建立過程中也是先父後子
      // 2.元件內我們自己寫的 watcher 優先於渲染 watcher
      // 3.如果某元件在父元件的 watcher 執行期間銷燬了,就跳過這個 watcher
      queue.sort((a,b) => a.id - b.id)
    
      // 不要快取佇列長度,因為遍歷過程中可能佇列的長度發生變化
      for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        if (watcher.before) {
          // 執行 beforeUpdate 生命週期鉤子函式
          watcher.before()
        }
        id = watcher.id
        has[id] = null
        // 執行元件內我們自己寫的 watch 的回撥函式並渲染元件
        watcher.run()
        // 檢查並停止迴圈更新,比如在 watcher 的過程中又重新給物件賦值了,就會進入無限迴圈
        if (process.env.NODE_ENV !== 'production' && has[id] != null) {
          circular[id] = (circular[id] || 0) + 1
          if (circular[id] > MAX_UPDATE_COUNT) {
            warn(`無限迴圈了`)
            break
          }
        }
      }
      // 重置狀態之前,先保留一份佇列備份
      const activatedQueue = activatedChildren.slice()
      const updatedQueue = queue.slice()
      resetSchedulerState()
      // 呼叫元件啟用的鉤子  activated
      callActivatedHooks(activatedQueue)
      // 呼叫元件更新的鉤子  updated
      callUpdatedHooks(updatedQueue)
    }
    

    updated()

    終於可以更新了,updated 大家都熟悉了,就是生命週期鉤子函式

    上面呼叫 callUpdatedHooks() 的時候就會進入這裡, 執行 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 的響應式原理流程的原始碼基本就分析完畢了,接下來就介紹一下上面流程中的不足之處

    defineProperty 缺陷及處理

    使用 Object.defineProperty 實現響應式物件,還是有一些問題的

    • 比如給物件中新增新屬性時,是無法觸發 setter 的
    • 比如不能檢測到陣列元素的變化

    而這些問題,Vue2 裡也有相應的解決文案

    Vue.set()

    給物件新增新的響應式屬性時,可以使用一個全域性的 API,就是 Vue.set() 方法

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

    set 方法接收三個引數:

    • target:陣列或普通物件
    • key:表示陣列下標或物件的 key 名
    • val:表示要替換的新值

    這裡主要做的是:

    • 先判斷如果是陣列,並且下標合法,就直接使用重寫過的 splice 替換
    • 如果是物件,並且 key 存在於 target 裡,就替換值
    • 如果沒有 __ob__,說明不是一個響應式物件,直接賦值返回
    • 最後再把新屬性變成響應式,並派發更新
    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)}`)
      }
      // 如果是陣列 而且 是合法的下標
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length,key)
        // 直接使用 splice 就替換,注意這裡的 splice 不是原生的,所以才可以監測到,具體看下面
        target.splice(key,1,val)
        return val
      }
      // 到這說明是物件
      // 如果 key 存在於 target 裡,就直接賦值,也是可以監測到的
      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 裡介紹過,如果沒有這個屬性,就說明不是一個響應式物件
      if (!ob) {
        target[key] = val
        return val
      }
      // 然後把新新增的屬性變成響應式
      defineReactive(ob.value,val)
      // 手動派發更新
      ob.dep.notify()
      return val
    }
    

    重寫陣列方法

    原始碼地址:src/core/observer/array.js

    這裡做的主要是:

    • 儲存會改變陣列的方法列表
    • 當執行列表裡有的方法的時候,比如 push,先把原本的 push 儲存起來,再做響應式處理,再執行這個方法
    // 獲取陣列的原型
    const arrayProto = Array.prototype
    // 建立繼承了陣列原型的物件
    export const arrayMethods = Object.create(arrayProto)
    // 會改變原陣列的方法列表
    const methodsToPatch = [
      'push','pop','shift','unshift','splice','sort','reverse'
    ]
    // 重寫陣列事件
    methodsToPatch.forEach(function (method) {
      // 儲存原本的事件
      const original = arrayProto[method]
      // 建立響應式物件
      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)
        // 派發更新
        ob.dep.notify()
        // 做完我們需要的處理後,再執行原本的事件
        return result
      })
    })
    

    總結

    到此這篇關於Vue解讀之響應式原理原始碼剖析的文章就介紹到這了,更多相關Vue響應式原理原始碼內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!