前言:最近學了vue的響應式原理,可是學了以後,感受模模糊糊的,不知道本身是否真的理解其中的精髓,因此就本身動手簡單的實現一下vue的響應式原理。畢竟概念終究仍是概念,實踐纔是檢驗本身會不會的硬道理!javascript
在開始以前,咱們須要瞭解一下基礎的知識:
- Object.defineProperty():它的做用是直接在一個對象上定義一個屬性,或者去修改一個已經存在的屬性。
obj
:表示須要定義屬性的當前對象。prop
:當前須要定義的屬性名。desc
:屬性描述符,就是更精確的控制對象屬性。
Object.defineProperty(obj,prop,desc)
- vue的數據響應式原理:就是vm中的data數據在頁面上有渲染,當data數據改變的時候,頁面上渲染的數據也跟着改變。
例如頁面上我用了data中的msg渲染了,如圖:
當我data.msg的值改變了的時候:
進入正題:
目標:成品就跟上面截圖的那樣,當你的數據發生變化,頁面的也跟着變化。
須要知識:
Object.defineProperty()
- es6部分語法
實踐:
html頁面:html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title></title> </head> <body> <div id="app"> msg </div> </body> </html>
開始編寫js代碼:vue
第一步:定義咱們要渲染的數據
let data = { msg: '你好' }
解釋:定義一個咱們要渲染到頁面的數據。java
第二步:獲取頁面的變量並渲染
class Render { constructor() { this.app = document.querySelector('#app') this.arg = this.app.innerText this.render() } render() { this.app.innerText = data[this.arg] } }
解釋:爲了方便獲取參數,咱們就使用一個參數簡單模擬,不像vue那樣用{ {}}來包括變量,若是用{ {}}包裹變量,就須要利用正則表達式獲取裏面內容。node
第三步:監聽數據的改變。
class Observe { constructor() { this.init(data.msg) } init(value) { Object.defineProperty(data, 'msg', { set(newVal) { if (newVal !== value) { value = newVal dep.notify() } }, get() { return value } }) } }
解釋:咱們想要知道一個變量的值有沒有改變呢,就須要監聽數據的變化。vue中就是使用Object.defineProperty(obj,prop,desc)
這個方法中的set()
和get()
來監聽的。set()
就是當值要改變的時候,就觸發。而get()
就是當你獲取這個變量的時候觸發。咱們利用這個方法就能夠監聽獲得數據的變化了。es6
第三步:建立收集者
class Dep { constructor() { this.subs = [] } addSub(watcher) { this.subs.push(watcher) } notify() { this.subs.forEach(w => w.update()) } }
解釋:收集者的做用就存儲觀察者和通知觀察者去更新頁面的。(觀察者在下面。)web
第三步:建立觀察者
class Watcher { constructor(node, arg, callback) { this.node = node // 變量所在的節點 this.arg = arg // 變量名 this.oldVal = this.getOldVal() // 沒更新前的值 this.callback = callback // 值更新後執行的操做 } getOldVal() { Dep.target = this let oldVal = data[this.arg] Dep.target = null return oldVal } update() { this.callback(data.msg) } }
解釋:咱們想要知道一個值有沒有改變,改變以後從新渲染的操做是怎麼樣的。這個時候就須要一個觀察者了。在數據渲染到頁面的時候,爲這個數據添加一個觀察者,當這個數據改變的時候,就執行回調函數,去更新頁面。正則表達式
第五步:調用收集者和觀察者
let data = { msg: '你好' } class Dep { constructor() { this.subs = [] } addSub(watcher) { this.subs.push(watcher) } notify() { this.subs.forEach(w => w.update()) } } class Watcher { constructor(node, arg, callback) { this.node = node this.arg = arg this.oldVal = this.getOldVal() this.callback = callback } getOldVal() { Dep.target = this let oldVal = data[this.arg] Dep.target = null return oldVal } update() { this.callback(data.msg) } } class Render { constructor() { this.app = document.querySelector('#app') this.arg = this.app.innerText this.render() } render() { // 安排觀察者監視數據 new Watcher(this.app, this.arg, (newVal) => { this.app.innerText = newVal }) this.app.innerText = data[this.arg] } } class Observe { constructor() { this.init(data.msg) } init(value) { let dep = new Dep() // 建立收集者 Object.defineProperty(data, 'msg', { set(newVal) { if (newVal !== value) { value = newVal dep.notify() // 通知觀察者 } }, get() { Dep.target && dep.addSub(Dep.target) // 添加觀察者 return value } }) } } new Observe() new Render()
解釋:在有註釋的方法,就是使用收集者和觀察者的地方。app
代碼的基本運行邏輯:
- 首先咱們先使用
Object.defineProperty
去監聽全部數據,而後獲取頁面中的內容,看看頁面有沒有使用data中的屬性,若是有,就將對應的變量渲染成對應的值。 - 渲染的時候,咱們要幫他建立一個觀察者(watcher),傳入當前的dom節點、屬性名、還有一個回調函數,觀察者內部就會獲取參數的值,保存在
oldval
中。回調函數,就是當數據更新以後才觸發的。 - 咱們在獲取
oldVal
,先爲Dep.target設置爲this,而後再獲取oldVal
,獲取的時候就會觸發get方法,Dep.target
有值就會添加進收集者(Dep)中,只會把Dep.target
該成null,由於get方法會在不少時候觸發,添加進收集者中,我就們就不要要添加了。 - 在值改變的時候,就會觸發set()方法,
dep.notify()
就會通知它裏面的觀察者就進更新頁面的操做,watcher就會調用他們的callback函數更新頁面。
總的代碼:dom
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title></title> </head> <body> <div id="app"> msg </div> </body> <script> let data = { msg: '你好' } class Dep { constructor() { this.subs = [] } addSub(watcher) { this.subs.push(watcher) } notify() { this.subs.forEach(w => w.update()) } } class Watcher { constructor(node, arg, callback) { this.node = node this.arg = arg this.oldVal = this.getOldVal() this.callback = callback } getOldVal() { Dep.target = this let oldVal = data[this.arg] Dep.target = null return oldVal } update() { this.callback(data.msg) } } class Render { constructor() { this.app = document.querySelector('#app') this.arg = this.app.innerText this.render() } render() { new Watcher(this.app, this.arg, (newVal) => { this.app.innerText = newVal }) this.app.innerText = data[this.arg] } } class Observe { constructor() { this.init(data.msg) } init(value) { let dep = new Dep() Object.defineProperty(data, 'msg', { set(newVal) { if (newVal !== value) { value = newVal dep.notify() } }, get() { Dep.target && dep.addSub(Dep.target) return value } }) } } new Observe() new Render() </script> </html>
本文分享 CSDN - 冬天愛吃冰淇淋。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。