vue響應式原理學習

參考連接

https://www.cnblogs.com/canfo...html

前言

提到vue,你們確定會想到雙向數據綁定,數據驅動視圖,虛擬DOM,diff算法等等這些概念。在使用vue的時候,會感受到它的數據雙向綁定真的很爽啊。會不會在你用了很長時間後,會好奇到,這個是如何實現的?或者在遇到問題的時候,會不會想到,爲啥這個數據並無響應式的發生改變,視圖怎麼沒有變化...當你抱着這些疑問的時候你確定會想了解其中的原理了。那麼我也是..如今把以前學習和理解的內容整理一下,若是有什麼問題,請多多指教~vue

要了解的核心API

衆所周知,是個vue的使用者都知道其響應式數據是結合Object.defineProperty()這個方法實現,那麼關於這個方法的使用和做用,請自行了解..一個合格的jser應該都知道的。node

要了解的設計模式

核心是觀察者模式,數據是咱們的被觀察者,發生改變的時候,會通知咱們全部的觀察者算法

咱們來分析這個圖

vue響應式

關於上面這個圖,請仔細的看下,從vue官網拔過來的...。主要包括數據變化更新視圖,視圖變化更新數據。其中主要涉及ObserveWatcher,Dep這三個類,要了解vue的響應式原理,弄清楚這三個類是如何運做的,這樣就可以大概瞭解了。
Observe是數據監聽器,其實現方法就是Object.defineProperty
Watcher是咱們所說的觀察者。
Dep是能夠容納觀察者的一個訂閱器,主要收集訂閱者,而後在發生變化的時候通知每一個訂閱者。設計模式

實現一個Observe

observe的主要工做是針對vue中的響應式數據屬性進行監聽,因此經過遞歸的方法遍歷屬性。緩存

function observe(data) {
    if(!data || typeof data !== 'object') {
        return
    }
    
    Object.keys(data).map((k) => {
        defineReactive(data, k, data[k])
    })
}

function defineReactive(data, k, val) {
    observe(val)
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            return val
        },
        set: function(newval) {
            val = newval
        }
    })
}

建立一個Dep訂閱器

訂閱器負責收集觀察者,而後在屬性變化的時候通知觀察者進行更新。app

function Dep() {
    this.subs = []
}

Dep.prototype.addSub = function(sub) {
    this.subs.push(sub)
}
Dep.prototype.notify = function() {
    this.subs.forEach(v => {
        v.update()
    })
}

那麼何時將一個觀察者添加到Dep裏面呢?設計上是將觀察者的添加放在getter裏面。dom

實現一個Watcher

上面說到Watcher是在初始化的時候在getter裏面放進訂閱器Dep中。那麼在Watcher初始化的時候觸發getter。那麼還有個問題,在getter中是如何獲取到Watcher的?這個咱們能夠暫時緩存的放到Dep的靜態屬性Dep.target上面。函數

function Watcher(vm, exp, cb) {
    this.vm = vm
    this.exp = exp
    this.cb = cb
}
Watcher.prototype.update = function() {}
Watcher.prototype.run = function() {
  var value = this.vm.data[this.exp]; 
  var oldVal = this.value; 
  if (value !== oldVal) {      
    this.value = value; 
    this.cb.call(this.vm, value, oldVal);
  }         
}
Watcher.prototype.get = function() {
    Dep.target = this // 在target上進行緩存
    var value = this.vm.data[exp] //觸發getter
    Dep.target = null //清空null
    return value
}

這個時候咱們還須要修改下Observe:學習

function defineReactive(data, k, val) {
    observe(val)
    var dep = new Dep(); 
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            if(Dep.target) {
                dep.addSub(Dep.target)
            }
            return val
        },
        set: function(newval) {
            if(newval === val) {
                return
            }
            val = newval
            dep.notify()
        }
    })
}

看到這裏,大體應該都明白如何把Observe,Dep,Watcher如何運做到一塊兒了吧。

如何經過這三者完成一個數據的雙向綁定

function MyVue(data, el, exp) {
    this.data = data
    observe(this.data)
    el.innerHTML = this.data[exp]
    new Watcher(this, exp, function(value) {
        el.innerHTML = value
    })
}

優化MyVue,代理data上屬性

function MyVue(data, el, exp) {
    this.data = data
    Object.key(data).forEach(function(k) => {
        this.proxyKeys(k)
    })
    observe(data);
    el.innerHTML = this.data[exp];  // 初始化模板數據的值
    new Watcher(this, exp, function (value) {
        el.innerHTML = value;
    });
    return this;
}
MyVue.prototype.proxyKeys = function(k) {
    Object.defineProperty(this, k, {
        enumerable: false,
        configurable: true,
        get: function proxyGetter() {
            return this.data[key];
        },
        set: function proxySetter(newVal) {
            this.data[key] = newVal;
        }
    })
}

實現compile解析dom節點

上面的例子中沒有解析DOM節點的操做,只是針對指定dom節點進行內容的替換。那麼compile要進行哪寫操做呢?

  1. 解析模板指定,並替換模板數據
  2. 將模板指定對應的節點綁定對應的更新函數,並初始化相應的訂閱器。
function nodeToFragment(el) {
    var fragment = document.createDocumentFragment()
    var child = el.firstChild
    while(child) {
        fragment.appendChild(child)
        child = el.firstChild
    }
    return fragment
}

接下來須要遍歷各個節點,對含有相關指定的節點進行特殊處理。

function compileElement(el) {
    var childNodes = el.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {
        var reg = /\{\{(.*)\}\}/;
        var text = node.textContent;
 
        if (self.isTextNode(node) && reg.test(text)) {  // 判斷是不是符合這種形式{{}}的指令
            self.compileText(node, reg.exec(text)[1]);
        }
 
        if (node.childNodes && node.childNodes.length) {
            self.compileElement(node);  // 繼續遞歸遍歷子節點
        }
    });
}

function compileText (node, exp) {
    var self = this;
    var initText = this.vm[exp];
    updateText(node, initText);  // 將初始化的數據初始化到視圖中
    new Watcher(this.vm, exp, function (value) {  // 生成訂閱器並綁定更新函數
        self.updateText(node, value);
    });
},
function updateText (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value;
}

獲取到最外層節點後,調用compileElement函數,對全部子節點進行判斷,若是節點是文本節點且匹配{{}}這種形式指令的節點就開始進行編譯處理,編譯處理首先須要初始化視圖數據,對應上面所說的步驟1,接下去須要生成一個並綁定更新函數的訂閱器,對應上面所說的步驟2。這樣就完成指令的解析、初始化、編譯三個過程,一個解析器Compile也就能夠正常的工做了。爲了將解析器Compile與監聽器Observer和訂閱者Watcher關聯起來,咱們須要再修改一下類MySelf函數:

function MyVue(opt) {
    let self = this
    this.vm = this
    this.data = opt.data
    
    Object.key(this.data).forEach(function(k) {
        this.proxyKeys(k)
    })
    observe(this.data)
    new Compile(opt, this.vm)
    return this
}

感受大體說的差很少了...但願可以對你們有點點啓發,謝謝。

相關文章
相關標籤/搜索