2、vue 源碼分析之如何實現 observer 和 watcher以及修改observer數據可能發生的問題

https://segmentfault.com/a/1190000004384515

本文能幫你做什麼? 。。好奇vue雙向綁定的同學, 可以部分緩解好奇心 還可以幫你瞭解如何實現$watch

前情回顧

我之前寫了一篇沒什麼乾貨的文章。。並且刨了一個大坑。。 今天。。打算來填一天。。並再刨一個。。哈哈 不過話說說回來了.看本文之前,, 如果不知道Object.defineProperty,還必須看看解析神奇的 Object.defineProperty 不得不感慨vue的作者,人長得帥,碼寫的也好。 本文是根據作者源碼,摘取出來的

本文將實現什麼

正如上一篇許下的承諾一樣,本文要實現一個 $wacth

const v = new Vue({
  data:{
    a:1,
    b:2
  }
})
v.$watch("a",()=>console.log("哈哈,$watch成功"))
setTimeout(()=>{
  v.a = 5
},2000) //打印 哈哈,$watch成功

爲了幫助大家理清思路。。我們就做最簡單的實現。。只考慮對象不考慮數組

1. 實現 observer

思路:我們知道Object.defineProperty的特性了, 我們就利用它的set和get。。我們將要observe的對象, 通過遞歸,將它所有的屬性,包括子屬性的屬性,都給加上set和get, 這樣的話,給這個對象的某個屬性賦值,就會觸發set。。嗯。。開始吧

export default class  Observer{
  constructor(value) {
    this.value = value
    this.walk(value)
  }
  //遞歸。。讓每個字屬性可以observe
  walk(value){
    Object.keys(value).forEach(key=>this.convert(key,value[key]))
  }
  convert(key, val){
    defineReactive(this.value, key, val)
  }
}


export function defineReactive (obj, key, val) {
  var childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: ()=>val,
    set:newVal=> {      
     childOb = observe(newVal)//如果新賦值的值是個複雜類型。再遞歸它,加上set/get。。
     }
  })
}


export function observe (value, vm) {
  if (!value || typeof value !== 'object') {
    return
  }
  return new Observer(value)
}

代碼很簡單,就給每個屬性(包括子屬性)都加上get/set, 這樣的話,這個對象的,有任何賦值,就會觸發set方法。。 所以,我們是不是應該寫一個消息-訂閱器呢?這樣的話, 一觸發set方法,我們就發一個通知出來,然後,訂閱這個消息的, 就會怎樣?。。。對咯。。收到消息。。。觸發回調。

2. 消息-訂閱器

很簡單,我們維護一個數組,,這個數組,就放訂閱着,一旦觸發notify, 訂閱者就調用自己的update方法

export default class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub){
    this.subs.push(sub)
  }
  notify(){
    this.subs.forEach(sub=>sub.update())
  }
}

所以,每次set函數,調用的時候,我們是不是應該,觸發notify,對吧。所以 我們把代碼補充完整

    export function defineReactive (obj, key, val) {
      var dep = new Dep()
      var childOb = observe(val)
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: ()=>val,
        set:newVal=> {
          var value =  val
          if (newVal === value) {
            return
          }
          val = newVal
          childOb = observe(newVal)
          dep.notify()
        }
      })
    }

那麼問題來了。。誰是訂閱者。。對,是Watcher。。一旦 dep.notify() 就遍歷訂閱者,也就是Watcher,並調用他的update()方法

3. 實現一個 Watcher

我們想象這個Watcher,應該用什麼東西。update方法,嗯這個毋庸置疑, 還有呢,

    v.$watch("a",()=>console.log("哈哈,$watch成功"))

對錶達式(就是那個“a”) 和 回調函數,這是最基本的,所以我們簡單寫寫

export default class Watcher {
  constructor(vm, expOrFn, cb) {
    this.cb = cb
    this.vm = vm
    //此處簡化.要區分fuction還是expression,只考慮最簡單的expression
    this.expOrFn = expOrFn
    this.value = this.get()
  }
  update(){
    this.run()
  }
  run(){
    const  value = this.get()
    if(value !==this.value){
      this.value = value
      this.cb.call(this.vm)
    }
  }
  get(){
    //此處簡化。。要區分fuction還是expression
    const value = this.vm._data[this.expOrFn]
    return value
  }
}

那麼問題來了,我們怎樣將通過addSub(),將Watcher加進去呢。 我們發現var dep = new Dep() 處於閉包當中, 我們又發現Watcher的構造函數裏會調用this.get 所以,我們可以在上面動動手腳, 修改一下Object.definePropertyget要調用的函數, 判斷是不是Watcher的構造函數調用,如果是,說明他就是這個屬性的訂閱者 果斷將他addSub()中去,那問題來了, 我怎樣判斷他是Watcherthis.get調用的,而不是我們普通調用的呢。 對,在Dep定義一個全局唯一的變量,跟着思路我們寫一下

export default class Watcher {
  ....省略未改動代碼....
  get(){
    Dep.target = this
    //此處簡化。。要區分fuction還是expression
    const value = this.vm._data[this.expOrFn]
    Dep.target = null
    return value
  }
}

這樣的話,我們只需要在Object.definePropertyget要調用的函數裏, 判斷有沒有值,就知道到底是Watcher 在get,還是我們自己在查看賦值,如果 是Watcher的話就addSub(),代碼補充一下


export function defineReactive (obj, key, val) {
  var dep = new Dep()
  var childOb = observe(val)

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: ()=>{
      // 說明這是watch 引起的
      if(Dep.target){
        dep.addSub(Dep.target)
      }
      return val
    },
    set:newVal=> {
      var value =  val
      if (newVal === value) {
        return
      }
      val = newVal
      childOb = observe(newVal)
      dep.notify()
    }
  })
}

最後不要忘記,在Dep.js中加上這麼一句

Dep.target = null

4. 實現一個 Vue

還差一步就大功告成了,我們要把以上代碼配合Vue的$watch方法來用, 要watch Vue實例的屬性,算了,,不要理會我在說什麼,,直接看代碼吧

import Watcher from '../watcher'
import {observe} from "../observer"

export default class Vue {
  constructor (options={}) {
    //這裏簡化了。。其實要merge
    this.$options=options
    //這裏簡化了。。其實要區分的
    let data = this._data=this.$options.data
    Object.keys(data).forEach(key=>this._proxy(key))
    observe(data,this)
  }


  $watch(expOrFn, cb, options){
    new Watcher(this, expOrFn, cb)
  }

  _proxy(key) {
    var self = this
    Object.defineProperty(self, key, {
      configurable: true,
      enumerable: true,
      get: function proxyGetter () {
        return self._data[key]
      },
      set: function proxySetter (val) {
        self._data[key] = val
      }
    })
  }
}

非常簡單。。兩件事,observe自己的data,代理自己的data, 使訪問自己的屬性,就是訪問子data的屬性。。 截止到現在,在我們只考慮最簡單情況下。。整個流程終於跑通了。。肯定會有 很多bug,本文主要目的是展示整個工作流,幫助讀者理解。。 代碼在https://github.com/georgebbbb...,。

---------------------------------------------------------------------------------------------------------------------------

再說一下修改observer數據可能發生的問題

在vue的項目裏做數組的值匹配的時候,發現帶有__ob__: Observer的數組,無法取到裏邊的值,

__ob__: Observer這些數據是vue這個框架對數據設置的監控器,一般都是不可枚舉的。

首先我們要把這個數據獲取原始數據 JSON.stringify([data])   變成字符串

然後在用JSON.parse()將數據轉換爲 JavaScript 對象 

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