理解數據驅動視圖原理

源代碼1

// 響應式原理 defineProperty
 
//數據
const data = {
  obj: {
    a: 4,
    b: 6
  },
  arr: [1, 5, 9]
}
 
// 觀察數據
function observe(data) {
  Object.keys(data).forEach(function(key) {
    let value = data[key]
    if (value && typeof value === 'object') observe(value) // 遞歸
    Object.defineProperty(data, key, {
      get() {
        console.log(`get ${key}`)
        return value
      },
      set(newVal) {
        console.log(`set ${key} = ${newVal}`)
        if (newVal && typeof newVal === 'object') observe(newVal)
        value = newVal
      }
    })
  })
 
}
 
observe(data)
 
let obj = data.obj
// get obj
let arr = data.arr
// get arr
 
 
obj.a = 8
// set a = 8
obj.a
// get a
delete obj.b
// 無反應
obj.c = 9
// 無反應
obj.c
// 無反應
 
data.obj = {...obj, c: 7}
// set obj = [object Object]
obj = data.obj
// get obj
 
obj.c = 9
// set c = 9
obj.c
// get c
 
arr.push(9) // 包括pop,shift,unshift,splice,sort,reverse
// 無反應
data.arr = [...arr,9]
// set arr = 1,5,9,9,9

講解

  1. vue只因此能實現雙向綁定,是利用es5裏面的Object.defineProperty(這就是爲何vue只支持es9及以上)
  2. 從以上代碼能夠看出,對象屬性的刪除(delete obj.b)和添加新屬性(obj.c = 9),不會觸發對應的set方法(vue對於初始化沒有定義的屬性,設置值不能觸發視圖層渲染)
  3. 在項目開發中確定會遇到有些屬性,初始化時沒有定義,經過改變其父元素的值去實現(data.obj = {...obj, c: 7}),父元素的值改變後,會觸發其set方法,會對其子元素從新進行雙向綁定
  4. 對於數組的處理,push,pop,shift,unshift,splice,sort,reverse都不會觸發set,但咱們看到的vue,這些方法是會觸發視圖層的變化,這是由於vue針對這些方法作了特殊的處理,原理如
const arr = [5, 9, 8]
const push = Array.prototype.push
Array.prototype.push = function () {
  const result = push.apply(this, arguments)
  console.log('作本身喜歡的事情')
  return result
}
arr.push(7)
console.log(arr)

vue代碼片斷javascript

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */
 
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // cache original method
  var original = arrayProto[method];
  def(arrayMethods, method, function mutator () {
    var args = [], len = arguments.length;
    while ( len-- ) args[ len ] = arguments[ len ];
 
    var result = original.apply(this, args);
    var ob = this.__ob__;
    var 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
  });
});
 
/*  */

源代碼2

// 響應式原理 defineProperty
 
//數據
const data = {
  obj: {
    a: 4,
    b: 6
  },
  arr: [1, 5, 9]
}
function Dep() {}
Dep.target = null // 當前函數
function watcher(fn) { // 函數
  Dep.target = fn
  fn()
}
 
// 初始化
function init() {
  const a = () => {
    console.log(data.obj.a)
  }
  const mix = () => {
    const c = data.obj.a + data.obj.b
    console.log(c)
  }
  watcher(a)
  watcher(mix)
}
 
// 觀察數據
function observe(data) {
  Object.keys(data).forEach(function(key) {
    let value = data[key]
    const dep = [] // 存放函數的容器
    if (value && typeof value === 'object') observe(value) // 遞歸
    Object.defineProperty(data, key, {
      get() {
        dep.push(Dep.target)
        return value
      },
      set(newVal) {
        if (newVal && typeof newVal === 'object') observe(newVal)
        value = newVal
        dep.forEach(fn => fn())
      }
    })
  })
 
}
 
observe(data)
init()
 
setTimeout(() => {
  data.obj.a = 10
}, 2000)

講解

以上代碼能夠看出,當obj.a值變化的時候,會觸發a函數和mix函數,具體執行步驟以下vue

  1. 先將屬性a綁定了對應的set和get方法
  2. 初始化init()時,會調用watcher(a),此時 Dep.target等於a函數
  3. 執行a()函數時,會執行裏面的console.log(data.obj.a)
  4. data.obj.a會調用a的get方法,此時dep.push(Dep.target)就至關於dep.push(a函數),mix函數同理
  5. 當屬性a的值改變時(data.obj.a = 10),會觸發其set方法,dep.forEach(fn => fn())將a函數和mix函數循環執行一遍,從而實現了數據驅動視圖

注意點

  1. 在驅動視圖以前都要先賦值,好比源代碼2的(value = newVal)和(dep.forEach(fn => fn()))不能互換位置
  2. 以上代碼不是vue的源代碼,但原理是一致的,幫助開發者更好的理解本身寫的代碼
相關文章
相關標籤/搜索