《深入淺出vue.js》中變化偵測的源碼以及解析

《深入淺出vue.js》中變化偵測的源碼以及解析

背景

最近由於公司知識體系變動,需要學習vue來適應新的開發平臺,我選擇了《深入淺出vue.js》來進行學習,之前有了解過一些vue知識,這次打算深入學習一下,尤其是第一篇變化偵測,學習過程中對javascript也增加了不少理解,我把代碼都手打了出來,代碼中有很多按照自己理解打上的註釋,這裏分享給大家。

開門見山

這裏我不給大家解釋他是什麼,書中總結的很好了,我直接把代碼貼出來。下面有兩點值得注意:

  • 我使用了require以及 module.exports 這個是因爲node不支持es6特性
  • 運行環境爲node,需要自行安裝

@ Deppro.js 依賴收集,哪裏引用了reactive的變量,就把他加入Dep

let uid = 0
class Dep {
    constructor() {
        this.id = uid++
        this.subs = [];
    }

    addSub(sub) {
        this.subs.push(sub)
    }

    removeSub(sub) {
        const index = this.subs.indexOf(sub)
        if (index > -1) {
            this.subs.splice(index, 1)
        }
    }

    depend() {
        if(global.target) {
            this.addSub(global.target)
        }
    }

    notify (msg) {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i< l; i++) {
            if (msg) {
                subs[i].update(msg)
            } else {
                subs[i].update()
            }
        }
    }
}
module.exports = Dep;

@ common.js 這裏我提取了一些公共方法,使用的時候直接引用即可

function def (obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
    })
}

function parsePath(path) {
    const bailRE = /[^\w.$]/
    if (bailRE.test(path)) {
      return
    }
    const segments = path.split('.')
    return function (obj) {
      for (let i = 0; i < segments.length; i++) {
        if (!obj) {
          return // 如果.到當前沒有值,則選擇返回空
        }
        obj = obj[segments[i]] // 例如返回obj.a,下一次返回(obj.a).b
      }
      return obj
    }
  }

module.exports = {
    def,
    parsePath
}

@ arrayIntercept.js

const {def} = require('./common')
const ArrayProto = Array.prototype
/**
 * 對數組默認的增刪方法進行攔截
 */
const ArrayMethods = Object.create(ArrayProto)

;['push','pop','shift','unshift','splice','sort','reverse'].forEach(method => {
    // 緩存原始方法
    const original = ArrayProto[method]
    def(ArrayMethods, method, function mutator (...args) {
            const ob = this.__ob__
            let inserted
            let msg 
            switch (method) {
                case 'push':
                    msg = 'you just execute push opration'
                    break;
                case 'unshift':
                    inserted = args
                    break;
                case 'splice':
                    inserted = args.slice(2) // 獲得args第三個到第n個參數,即增加項
                    break;
            
                default:
                    break;
            }
            if (inserted) ob.observeArray(inserted) // 添加對新增元素的變化偵測
            ob.dep.notify(msg)
            /**
             * 改變this的指向,比如original是push函數,調用的時候肯定是[].push,此時push中的this是指向[]的
             * 如果這裏不操作,就會導致this鏈斷裂
             */
            return original.apply(this, args) 
    })
});

module.exports = ArrayMethods

@ Observer.js 這裏面我定義了 Observer 和 Watcher

const { def, parsePath } = require('./common')
const arrayMethods = require('./arrayIntercept')
const Dep = require('../DepPro')

// 是否支持proto
const hasProto = '__proto__' in {}

const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
/**
 * 不能對全局的Array.prototype 進行直接覆蓋,這樣會污染全局的Array
 * 實現針對性數組攔截
 */

class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm
    this.deps = []
    this.depIds = new Set()
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.cb = cb
    this.value = this.get()
  }

  get() {
    global.target = this
    let value = this.getter.call(this.vm, this.vm)
    global.target = undefined
    return value
  }

  update(msg) {
    if (msg) {
    //   this.cb.call(this.vm, msg)
      this.cb(msg)
    } else {
      const oldValue = this.value
      this.value = this.get() // 這裏又會執行一遍Object.definedProperty的get函數
      this.cb.call(this.vm, this.value, oldValue)
    }



  }
  addDep(dep) {
    const id = dep.id
    if (!this.depIds.has(id)) {
      this.depIds.add(id)
      this.deps.push(dep)
      dep.addSub(this)
    }
  }
  teardown() {
    let i = this.deps.length
    while (i--) {
      this.deps[i].removeSub(this)
    }
  }
}
/**
 * 數組增刪需要在攔截其中才能觸發依賴,setter中並不能監測到,需要同時滿足在getter中和攔截器中都能訪問到(一個push一個notify),所以放到Observe中
 */
class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep() // 一個Observer擁有一個dep
    // 將Ovserver實例賦給value.__ob__,第一,標記已響應,第二,通過__ob__拿到Observer實例,就可以在攔截器中調用__ob__上的dep了
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      this.observeArray(value) // 如果被偵測的數據是數組,則將數組每一個元素都轉換成響應式並偵測變化
      const augment = hasProto ? protoAugment : copyAugment
      augment(value, arrayMethods, arrayKeys)
    } else { // 如果是對象,則直接調用walk
      this.walk(value)
    }
  }

  observeArray(items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }

  walk(obj) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
}

let a = {
  b: [1, 2, 3],
  c: 'abk'
}
new Observer(a)
new Watcher(a, 'b', function (msg) {
    console.log(this.c)
  console.log(msg)
})
a.b.push(4)
console.log(a)
console.log(a.__ob__.value)

/**
 * 進一步封裝Observe, 返回Observe實例,如果已經存在則直接返回,不存在返回新建數據
 * 數組對象有了實例,代表已經是響應式數據,調用該方法,返回一個被攔截器修改的數組
 * 總結一下,通過const a = observe(value) 1、value從此擁有攔截器 2、a.value 等於 value ,a 等於 value.__ob__
 * 值得注意的是,value既可以是數組也可以是對象,是兼容的
 * @param {數組對象} value 
 * @param {暫無介紹} asRootData 
 */
function observe(value, asRootData) {
  if (typeof value !== 'object') {
    return null
  }
  let ob
  if (value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer) {
    ob = value__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}

function protoAugment(target, src, keys) {
  target.__proto__ = src
}

// 如果不支持,將攔截器中的值一個一個添加到數組對象
function copyAugment(target, src, keys) {
  for (let i = 0, l = keys.length; i < l; i++) { // 只需要機算一次keys.length的值
    const key = keys[i]
    def(target, key, src[key])
  }
}

function defineReactive(data, key, val) {
  let childOb = observe(val) // 對對象或者數組返回Observe實例,其他返回空
  let dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      dep.depend() // 如果傳值爲數組,則此dep監測數組引用變化,下方監測數據增刪改等變化
      if (childOb) {
        childOb.dep.depend()
      }
      return val
    },
    set: function (newVal) {
      if (val === newVal) {
        return
      }
      val = newVal
      dep.notify()
    }
  })
}

然後在控制檯環境中運行 node Observer.js,便可以得到如果所示的運行結果

在這裏插入圖片描述

說到最後

最後想說的是自己的一些總結,變化偵測映射到我們平時使用vue的時候,就好比在data中聲明瞭變量data,然後vue會對這個變量使用Observer(data),然後當DOM中有一個地方引用了這個變量,就相當於新建了一個watcher,並將該watcher放入data的dep中,當引用改變,會觸發nofity方法中從而調用watcherupdate方法,更新dom,這就實現了變化偵測。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章