源碼學習VUE之響應式原理

首先咱們先本身嘗試實現一下數據監測。所謂數據監測就是當一個值改變時,用到這個值得地方作出相應改變。裏面的核心就是 Object.defineProperty數組

Object.defineProperty

var obj = {a: 1};
Object.defineProperty(obj, a, {
    enumerable: true,
    configurable: true,
    get: function(){
        // 當調用get方法是,就代表用到了該屬性。
        //這邊的問題就卡在  須要收集什麼
    }
})

收集的依賴是什麼

咱們收集依賴的目的就是當值改變時,依賴的部分也要作出相應的改變。
可是如今的問題是依賴的地方會有不少,類型也不同。多是template裏面用到,多是computed裏面計算用到。也有可能用戶本身watch監聽。先拋開用到地方應該怎麼變,想一想,一個動做觸發一個事件,不就是回調函數的邏輯嗎?
拿最簡單的watch來講:app

var callback = function (newVal, oldVal) {
  // do something
}
vm.$watch(obj.a, callback)

這麼一看就簡單了,咱們收集的依賴就是"回調函數"。
就像上面說的,一個屬性用到的地方可能會有不少,所以須要收集的回調函數也不少,所以咱們用一個數組來保存。
同時呢,這是個通用方法,咱們能夠封裝一下提出來。
所以上面的代碼能夠改爲函數

var obj = {a: 1};
function defineReactive (data, key, val) {
    var dep = [];
    Object.defineProperty(obj, a, {
        enumerable: true,
        configurable: true,
        get: function(){
            dep.push(callback); // 先無論callback哪來的
        },
        set: function(newVal){
            if(val === newVal) return
            val = newVal;
            dep.forEach(function(callback, index){
                callback();
            })
        }
    })
}

去耦合

能夠把dep封裝成一個對象oop

class Dep {
    constructor(id){
        this.id = id;
        this.deps = [];
    }
    addSub( sub ){
        this.subs.push(sub)
    }
    removeSub (sub){
        reomve(this.subs, sub)
    }
    notify(){
        this.deps.forEach(function(callback, index){
            callback();
        })
    }
}

function defineReactive (data, key, val) {
    var dep = new Dep();
    Object.defineProperty(obj, a, {
        enumerable: true,
        configurable: true,
        get: function(){
            dep.addSub(callback); // 先無論callback哪來的
        },
        set: function(newVal){
            if(val === newVal) return
            val = newVal;
            dep.notify();
        }
    })
}

Watcher

上面雖然藉助callback來幫助理解,但真正實現確定不可能真是callback。任何一個函數在不一樣的上下文中執行結果都不相同,光拿到要執行的函數確定不行,還得有執行的上下文。所以咱們可用個類包裝一下,observe中不關心怎麼執行callback,只須要通知一個監聽者本身去作更新操做就好。這個監聽者就是watcher.this

class Watcher {
    constructors(component, getter, cb){
        this.cb = cb // 對應的回調函數,callback
        this.getter = getter;
        this.component = component; //這就是執行上下文
    }
    
    //收集依賴
    get(){
        Dep.target = this;
        
        this.getter.call(this.component)
        
        Dep.target = null;
    }
    
    update(){
        this.cb()
    }
}

既然咱們將callback換成了Watcher實例,注意這邊,Dep裏面收集的Watcher實例,可不是Wacther構造函數。那麼在數據的getter方法中就要想辦法拿到。咱們將實例存放在Dep中,一個函數對象上。Dep.target = this,當依賴收集完就銷燬 Dep.target = null。所以Observe代碼能夠改爲。.net

function defineReactive (data, key, val) {
    var dep = new Dep();
    Object.defineProperty(obj, a, {
        // ....
        get: function(){
            if(Dep.target){
                 dep.addSub(Dep.target); // Dep.target是Watcher的實例
            }
        },
       // ...
    })
}

class Dep {
    //...
    notify(){
        this.deps.forEach(function(watcher, index){
            watcher.update();
        })
    }
}

VUE源碼

大概流程通了,咱們再作點完善。Observe,Dep, Watcher三個關係弄清楚了。如今的問題是,怎麼收集依賴和回調。舉例來講:prototype

<div id="app">
  <input type="text" v-model="name"/>
  <div>{{name}}</div>
</div>

new Vue({
    el: "#app",
    data: {
        name: "默認值",
        age: 29
    }
})

一: template中的依賴

name屬性直接在template中用到。那麼只要觸發render,就能夠收集到依賴。固然,收集到依賴後,須要及時更新。把DOM中的{{name}}替換成Data中對應的值。code

這部分代碼在 lifecycle.jsmountComponent方法中,能夠精簡爲component

export function mountComponent(){
    ...
    callHook(vm, 'beforeMount') //生命週期函數
    var updateComponent = () => {
    // 先經過render收集依賴,再經過update將虛擬DOM中的值同步到真實節點中
        vm._update(vm._render(), hydrating)
    }
    new Watcher(vm, updateComponent, emptyFunc, {
            before () {
                if (vm._isMounted) {
                    callHook(vm, 'beforeUpdate') //生命週期函數
                  }
            }
    }
    
    vm._isMounted = true
    callHook(vm, 'mounted')  //生命週期函數
}

每一個模板實例化一個Watcher實例。這也與官網的流程圖一致
server

$watch

<div id="app">
  <input type="text" v-model="name"/>
  <div>{{name}}</div>
</div>

new Vue({
    el: "#app",
    data: {
        name: "默認值",
        age: 29
    },
    watch: {
        age: function(newValue, oldValue){
            console.log("新的值爲:" + newValue)
        }
    }
})

傳進來的options會在initState中處理

export function initState (vm: Component) {
  //vm
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

.....

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}
function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

能夠看到對於options中的watch其實就是執行$watch方法。

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    options = options || {}
    options.user = true
    // expOrFn: key, cb: callback watch中的每一個key都實例出Wacther實例
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // immediate: 若是爲true就馬上執行一次,不然第一次進來不執行,當data改變纔會觸發執行
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

computed

同watch,計算屬性computed也是在initData中處理。

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
 
   for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    
    // 能夠看到,VUE爲每一個computed屬性也都生成了一個watcher實例。
    //而這邊的getter就是計算屬性的計算函數。必須先計算一次才能觸發依賴的屬性的get方法,收集依賴
    watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
    
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}

結論

這邊就再也不放出VUE的數據監聽部分源碼,能夠本身閱讀watcher.js,dep.js,observer/index.js。整體代碼和咱們本身實現的很像,只是比咱們代碼更縝密,多了些其餘功能。好比$watcher以後會返回一個取消函數,能夠取消監聽。
就像上面分析的,一個監聽流程的完成必須包含:

  1. 數據自己可被監聽(定義了set,get)。
  2. 這個數據被收集了依賴。也就是有人監聽。

咱們知道VUE爲template,watch,和computed中的屬性實例化了Watcher。而只有在data中的屬性纔會再initState時進行監聽操做。所以咱們能夠得出結論,

  1. data中的屬性才能夠被監聽。
  2. 只有在templete中用到的屬性或被手動watch的,或計算屬性用到的,數值改變時纔會執行相應的操做。
相關文章
相關標籤/搜索