提到 Vue 的響應式,一般指的是視圖跟隨數據的改變而更新。開發上帶來的便利是,在須要更新視圖呈現時,只需修改視圖渲染所須要的數據便可,而不用手動操做DOM。從實現來講,能夠分爲兩個部分:html
咱們很熟悉如何監聽鼠標的點擊,鍵盤的輸入等用戶事件,可是不多直接去監聽一個數據改變的事件。雖然,不存在數據改變這個事件,可是監聽數據改變是能夠作到的,而且從程序設計角度來講,和給事件綁定一個回調函數沒有本質的不一樣。vue
爲了比較監聽普通事件和監聽數據改變的區別,咱們先使用事件的方式,來實現「響應式」視圖更新。node
下面的代碼中,咱們定義了數據變量data
和視圖更新函數update
。update
函數在更新視圖時,讀取了data
的text
屬性做爲視圖節點的文本內容。而後監聽一個input
元素的input
事件,事件的回調函數中,將用戶輸入的值替換data.text
的當前值,而後調用update
函數,通知視圖進行更新。react
<input id='text' /> <div id='app'></div> <script> /* 定義渲染數據和視圖更新函數 */ var data = { text: 'hello' } function update() { document.getElementById('app').textContent = data.text } update() /* 綁定 input 事件,在修改數據後更新視圖 */ var textElm = document.getElementById('text') textElm.value = data.text textElm.addEventListener('input', function() { data.text = this.value update() }) </script> 複製代碼
藉助input
事件,咱們間接實現了「響應式」,但它只是起到一個紐帶的做用,不能直接對數據的改變做出響應。瀏覽器
Object.defineProperty(obj, prop, descriptor)
能夠給對象添加或者修改已有屬性。函數接受三個參數:markdown
String
或Symbol
類型Object
類型這裏重點須要瞭解的是屬性描述符對象 descriptor
。descriptor
支持如下字段:app
configurable
: Boolean
,爲true
時,才能改變屬性描述符,以及刪除屬性enumerable
: Boolean
,爲true
時,能夠經過for ... in
或 Object.keys
方法枚舉value
: 該屬性對應的值。能夠是任何有效的 JavaScript 值writable
: Boolean
,爲true
時,屬性值,也就是 value
才能被賦值運算符改變get
: 屬性的 getter 函數,當訪問該屬性時,會調用此函數set
: 屬性的 setter 函數,當屬性值被修改時,會調用此函數其中 value
和 writable
只能出如今數據描述符
中;而get
和set
只能出如今存取描述符
中。一個屬性描述符descriptor
只能是其中之一,所以當定義了 value
或 writable
,就不能再定義 get
或 set
,不然報錯 Cannot both specify accessors and a value or writable attribute
。反之亦然。函數
因爲,咱們須要在對象屬性改變時得到通知,我須要使用存取描述符
來定義對象屬性,即定義set
來響應屬性值的修改,定義get
來響應屬性的訪問。oop
以上文的data
爲例,咱們但願在經過data.text = xxx
的方式改變對象的屬性值時,更新視圖,因此要從新定義屬性text
的描述符,在set
函數中調用視圖更新函數update
。這裏還須要定義get
,由於,我不但須要對屬性值更改時做出響應,同時在update
函數中,咱們還須要讀取data.text
的值,而若是不定義get
,獲取的值就爲undefined
。
var data = { text: 'hello' } var text = data.text Object.defineProperty(data, 'text', { get: function() { return text }, set: function(newValue) { if (text !== newValue) { text = newValue update() } } }) 複製代碼
這樣定義後,咱們即可以直接修改data.text
值更新視圖了。讀者能夠將如下完整代碼,保存到一個 html
文件中,而後在瀏覽器控制檯中經過data.text = 'world'
賦值的方式,查看視圖的變化。
<div id='app'></div> <script> /* 定義渲染數據和視圖更新函數 */ var data = { text: 'hello' } function update() { document.getElementById('app').textContent = data.text } update() /* 使用 Object.defineProperty 實現響應式視圖更新 */ var text = data.text Object.defineProperty(data, 'text', { get: function() { return text }, set: function(newValue) { if (text !== newValue) { text = newValue update() } } }) </script> 複製代碼
這裏只是針對data
的屬性text
定義響應式。爲了代碼更加通用,以用於任意對象,能夠編寫一個函數defineReactive(obj, key, update)
(函數名參考了 Vue2 的定義,讀者能夠在 Vue2 源碼中搜索該函數)。
function defineReactive(obj, key, update) { var value = obj[key] Object.defineProperty(obj, key, { get: function() { return value }, set: function(newValue) { if (value !== newValue) { value = newValue update() } } }) return obj } 複製代碼
因而上面的代碼能夠改寫成:
var data = { text: 'hello' } function update() { document.getElementById('app').textContent = data.text } update() defineReactive(data, 'text', update) 複製代碼
響應對象屬性改變,除了Object.definProperty
外,瀏覽器還支持另外一個全局的構造函數Proxy
,用於自定義對象的基本操做,如:屬性查找,賦值,枚舉,函數調用等。相比而言,前者只能自定義對象屬性的訪問和賦值。
Proxy
的使用方法以下:
const proxy = new Proxy(target, handler) 複製代碼
handelr
對象支持的方法(一般被稱爲traps
,中文翻譯爲陷阱,能夠理解爲鉤子或者執行某項操做的回調函數)有:
in
操做符時調用delete
操做符時調用Object.getOwnPropertyNames
方法和Object.getOwnPropertySymbols
方法時調用new
操做符時調用Object.defineProperty
方法時調用Object.getOwnPropertyDescriptor
方法時調用Object.getPrototypeOf
方法時調用Object.setPrototypeOf
方法時調用Object.isExtensible
方法時調用Object.preventExtensions
方法時調用能夠看到Proxy
對對象自定義行爲的控制比Object.defineProperty
更加全面。這裏,咱們重點關注和後者
相同部分,即get
和set
。雖然名稱都是get
和set
,但方法的傳參不一樣。Object.defineProperty
是針對對象的某個屬性定義get
和set
,而Proxy
是針對整個對象。而且經過Proxy
構造函數返回的是一個proxy
實例,而不是原對象。所以,Proxy
中的get
和set
參數比Object.defineProperty
的多了兩個參數:
target
之前文的data
對象爲例,定義get
和set
方法以下:
const dataProxy = new Proxy(data, { get(obj, key) { return obj[key] }, set(obj, key, newValue) { obj[key] = newValue // 表示成功 return true } }) 複製代碼
這裏和Object.defineProperty
還有最大不一樣的是,前者響應式在新返回的代理對象生效,而對原對象屬性盡心訪問和修改是不會觸發set
和get
回調的。所以,若是使用Proxy
重寫前文的響應式視圖更新,須要在讀取和設置對象屬性時使用dataProxy
,完整代碼以下:
<div id='app'></div> <script> function reactive(target, update) { var targetProxy = new Proxy(target, { get(obj, key) { return obj[key] }, set(obj, key, newValue) { obj[key] = newValue update() // 表示成功 return true } }) return targetProxy } var data = { text: 'hello' } var dataProxy = reactive(data, update) function update() { document.getElementById('app').textContent = dataProxy.text } update() </script> 複製代碼
若是一樣在瀏覽器控制檯修改數據,咱們應該使用dataProxy.text = 'xxx'
而不是 data.text = 'xxxx'
。
在《手寫 Vue (一)》中,咱們實現了基於虛擬 DOM 的視圖掛載。如今結合響應式實現虛擬 DOM 的到真實 DOM 的響應式更新。
完整代碼以下:
function Vue(options) { var vm = this function update () { vm.update() } var data = options.data var keys = Object.keys(data) for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i] this[key] = data[key] defineReactive(this, key, update) } this.$options = options } Vue.prototype.render = function() { var render = this.$options.render return render.call(this, createVNode) } Vue.prototype.update = function() { var vnode = this.render() this.$el = createElm(vnode, this.$el.parentNode, this.$el) } Vue.prototype.$mount = function (id) { this.$el = document.querySelector(id) this.update() return this } function createVNode(tag, data, children) { var vnode = { tag: tag, data: undefined, children: undefined, text: undefined } if (typeof data === 'string') { vnode.text = data } else { vnode.data = data if (Array.isArray(children)) { vnode.children = children } else { vnode.children = [ children ] } } return vnode } function createElm(vnode, parentElm, refElm) { var elm // 建立真實DOM節點 if (vnode.tag) { elm = document.createElement(vnode.tag) } else if (vnode.text) { elm = document.createTextNode(vnode.text) } // 將真實DOM節點插入到文檔中 if (refElm) { parentElm.insertBefore(elm, refElm) parentElm.removeChild(refElm) } else { parentElm.appendChild(elm) } // 遞歸建立子節點 if (Array.isArray(vnode.children)) { for (var i = 0, l = vnode.children.length; i < l; i++) { var childVNode = vnode.children[i] createElm(childVNode, elm) } } else if (vnode.text) { elm.textContent = vnode.text } return elm } function defineReactive(obj, key, update) { var value = obj[key] Object.defineProperty(obj, key, { get: function() { return value }, set: function(newValue) { if (value !== newValue) { value = newValue update() } } }) return obj } 複製代碼
將以上代碼保存到文件myvue_2.js
中,再新建html文件myvue_2.html
,替換如下內容:
<div id="app"></div> <script src="myvue_2.js"></script> <script> var vm = new Vue( { data: { text: 'hello world!' }, render(h) { return h('div', this.text) } } ).$mount('#app') </script> 複製代碼
嘗試在瀏覽器控制檯輸入:
vm.text = 'anything you like!!!' 複製代碼
若是看到顯示內容即時更新爲你修改的內容,那麼,恭喜你成功作到了和 Vue 同樣的響應式視圖更新。
咱們成功利用set
攔截,實現了響應式視圖更新,可是還不夠完美,由於,咱們對data
對象中任何屬性的賦值都會執行視圖更新操做,而無論update
是否用到了這個屬性。這意味着,若是data
有不少個屬性,但並不是全部屬性都會用於視圖的渲染,這樣咱們就會作一些多餘的視圖更新操做,顯然這是沒有意義的性能開銷。要作到自動根據update
中實際使用的到屬性,只對用到的屬性執行視圖更新,就涉及到依賴的蒐集
。關於依賴蒐集
的實現,咱們在下一篇文章中繼續探討。