Vue深刻響應式原理-隱式添加響應式

現象:node

在Vue開發的時候,data初始化一個對象沒有定義任何屬性,通過變量賦值的以後,不須要$set方法,該對象下面的屬性就也能變成響應式屬性react

提問:分析下面代碼,頁面首先先顯示什麼?先點擊changeTest1顯示什麼?而後點擊chageTest2顯示什麼?數組

<body>
  <div id="app">
    <button @click="changeTest1">changeTest1</button>
    <button @click="changeTest2">changeTest2</button>
    <div>
      {{ form.test1 }}-{{ form.test2 }}
    </div>
  </div>
  <script>
    let vm = new Vue({
      el: "#app",
      data: {
        form: {},
      },
      mounted() {
        this.form = { test1: 1};
        this.form.test2 = 2;
      },
      methods: {
        changeTest2() {
          this.form.test2 = 'change2';
        },
        changeTest1() {
          this.form.test1 = 'change1';
        },
      }
    });
  </script>
</body>
複製代碼
  • 公佈答案:

首先顯示:1-2, 點擊changeTest1顯示:change1-2,點擊changeTest2顯示:change1-2bash

1592670725\(1\).jpg

疑問:在data中的form對象沒有定義test1和test2屬性,test1能夠改變視圖,而test2卻不能改變視圖?

1): 打印一下form對象,能夠看到test1有set,get修飾符,而test2沒有get,set修飾符app

2): 原來test1可以改變視圖是由於被Vue用Object.defineProperties()處理,有get,set修飾符,收集到渲染watch。dom

3): 而test2沒有get,set修飾符,沒有收集到渲染watch。異步

4): 從這個表現就能知道test1能夠更新視圖,而test2不能夠更新視圖函數

1592670725\(1\).jpg
1592671231\(1\).jpg

疑問:爲何test1有修飾符,test2沒有修飾符

1): 上面代碼能夠注意到在mounted生命週期那裏,對this.form進行了賦值,因此觸發了form的set修飾符,它會執行如下函數中的set函數ui

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      console.log(obj, key)
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
複製代碼

2):能夠看到newVal是{test1: 1}, 拋掉全部if-else的判斷,會執行一行代碼 childOb = !shallow && observe(newVal),shallow是undefined,而後執行observe函數,參數爲{test1: 1}this

看一下observe函數的功能

export function observe (value: any, asRootData: ?boolean): Observer | void {
  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
}
複製代碼

3): 會發現最終會執行ob = new Observer(value), 再看Observer函數

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
複製代碼

4): 最終發現ob = new Observer(value),就是把對象定義響應式的入口函數

5): 原來值{ test1: 1 },就這樣被隱式添加了set,get修飾符

總結:對this.form = { test1: 1 } 進行賦值的時候,就會觸發到form的set函數。在set函數裏面,會對newVal也就是{test1: 1} 走一次new Obsever(value)來添加set,get的修飾符,所以test1就變成了響應式屬性

疑問:爲何test2沒有變成響應式屬性

1): 應爲對象沒法監聽新增和刪除,對this.form.test2 = 2的時候,沒有觸發form的set函數,於是沒有添加get,set修飾符

流程總結:

1): Vue在初始化階段對data定義的對象form添加set,get修飾符

2): 執行render函數(生成vnode的過程),form取值一次,觸發get函數,收集渲染watch。form.test1取值一次爲1,沒有get修飾符。form.test2取值一次爲2,沒有get修飾符

3): 把vnode(虛擬dom)變成真實dom,頁面顯示: 1-2

4): 在mounted生命週期中對this.form = { test1: 1 }進行了賦值,觸發了form的set函數,而後對{ test1: 1 }添加get,set修飾符,test1變成了響應式屬性

5): 而後執行dep.notify(),觸發渲染watch,執行render函數

6): 在生成vnode的時候(模板中的{{ form.test1 }} {{ form.test2 }}),須要form.test1,form.test2取值一次,這時候就觸發了test1的get修飾符,收集該渲染watch, 使得test1有了更新視圖的能力,而test2沒有get修飾符,沒法收集改渲染watch,沒有更新視圖的能力

7): 點擊chageTest1的時候,修改了test1的值,就會執行set修飾符,執行dep.notify()去更新視圖

8): 點擊changeTest2的時候,修改了test2的值,因爲test2沒有get,set修飾符,全部沒法更新視圖

疑問:數組是怎麼監聽自身的改變的呢

1): 原來Vue對數組的push,pop,shift,unshfit,sort,reverse方法額外處理,例如:當數組新增的時候,可以給新增的項添加get,set修飾符,使得它們變成響應式屬性,這就是對象和數組的區別

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original 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)
    // notify change
    ob.dep.notify()
    return result
  })
})
複製代碼

結尾:

給Vue中data的值賦值對象或者數組,Vue內部會自動幫咱們給該對象或數組添加get,set修飾符,而且爲每個屬性收集渲染watch,使得它們有更新視圖的能力

結束語:

文章大概說了Vue如何添加響應式屬性,如何觸發視圖的更新,忽略了不少細節,好比渲染watch,render函數,異步更新,虛擬dom變成真實dom等等。文章的目的是想說:什麼狀況屬性會變成響應式屬性,什麼狀況下沒有響應式屬性(固然手動調用$set能夠變成響應式屬性)

相關文章
相關標籤/搜索