Proxy 能夠理解成,在目標對象以前架設一層「攔截」,外界對該對象的訪問,都必須先經過這層攔截,所以提供了一種機制,能夠對外界的訪問進行過濾和改寫。Proxy 這個詞的原意是代理,用在這裏表示由它來「代理」某些操做,能夠譯爲「代理器」。 出自阮一峯老師的ECMAScript 6 入門,詳細點擊es6.ruanyifeng.com/#docs/proxyhtml
例如:vue
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
複製代碼
上面代碼對一個空對象架設了一層攔截,重定義了屬性的讀取(get)和設置(set)行爲。這裏暫時先不解釋具體的語法,只看運行結果。對設置了攔截行爲的對象obj,去讀寫它的屬性,就會獲得下面的結果。node
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
複製代碼
var proxy = new Proxy(target, handler);
複製代碼
這裏有兩個參數,target
參數表示所要攔截的目標對象,handler
參數也是一個對象,用來定製攔截行爲。git
注意,要使得
Proxy
起做用,必須針對Proxy
實例(上例是proxy
對象)進行操做,而不是針對目標對象(上例是空對象)進行操做。es6
Reflect對
象與Proxy
對象同樣,也是 ES6
爲了操做對象而提供的新API
。github
Reflect
對象的方法與Proxy
對象的方法一一對應,只要是Proxy對象的方法,就能在Reflect
對象上找到對應的方法。這就讓Proxy
對象能夠方便地調用對應的Reflect方法,完成默認行爲,做爲修改行爲的基礎。也就是說,無論Proxy
怎麼修改默認行爲,你總能夠在Reflect
上獲取默認行爲。設計模式
一樣也放上阮一峯老師的連接es6.ruanyifeng.com/#docs/refle…數組
看到這裏,我就當你們有比較明白Proxy
(代理)是作什麼用的,而後下面咱們看下要作最終的圖騙。 瀏覽器
看到上面的圖片,首先咱們新建一個index.html
,而後裏面的代碼是這樣子滴。很簡單bash
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>簡單版mvvm</title>
</head>
<body>
<div id="app">
<h1>開發語言:{{language}}</h1>
<h2>組成部分:</h2>
<ul>
<li>{{makeUp.one}}</li>
<li>{{makeUp.two}}</li>
<li>{{makeUp.three}}</li>
</ul>
<h2>描述:</h2>
<p>{{describe}}</p>
<p>計算屬性:{{sum}}</p>
<input placeholder="123" v-module="language" />
</div>
<script>
// 寫法和Vue同樣
const mvvm = new Mvvm({
el: '#app',
data: {
language: 'Javascript',
makeUp: {
one: 'ECMAScript',
two: '文檔對象模型(DOM)',
three: '瀏覽器對象模型(BOM)'
},
describe: '沒什麼產品是寫不了的',
a: 1,
b: 2
},
computed: {
sum() {
return this.a + this.b
}
})
</script>
</body>
</html>
複製代碼
看到上面的代碼,大概跟vue
長得差很少,下面去實現Mvvm
這個構造函數
首先聲明一個Mvvm
函數,options
看成參數傳進來,options
就是上面代碼的配置,裏面有el
、data
、computed
~~
function Mvvm(options = {}) {
// 把options 賦值給this.$options
this.$options = options
// 把options.data賦值給this._data
let data = this._data = this.$options.data
let vm = initVm.call(this)
return this._vm
}
複製代碼
上面Mvvm函數很簡單,就是把參數options
賦值給this.$options
、把options.data
賦值給this._data
、而後調用初始化initVm
函數,並用call
改變this
的指向,方便initVm
函操做。而後返回一個this._vm
,這個是在initVm
函數生成的。
下面繼續寫initVm
函數,
function initVm () {
this._vm = new Proxy(this, {
// 攔截get
get: (target, key, receiver) => {
return this[key] || this._data[key] || this._computed[key]
},
// 攔截set
set: (target, key, value) => {
return Reflect.set(this._data, key, value)
}
})
return this._vm
}
複製代碼
這個init函數
用到Proxy
攔截了,this
對象,生產Proxy
實例的而後賦值給this._vm
,最後返回this._vm
,
上面咱們說了,要使得
Proxy
起做用,必須針對Proxy
實例。
在代理裏面,攔截了get
和set
,get函數
裏面,返回this
對象的對應的key
的值,沒有就去this._data
對象裏面取對應的key
,再沒有去this._computed
對象裏面去對應的key
值。set函數
就是直接返回修改this._data
對應key
。
作好這些各類攔截工做。咱們就能夠直接從實力上訪問到咱們相對應的值了。(mvvm使咱們第一塊代碼生成的實例)
mvvm.b // 2
mvvm.a // 1
mvvm.language // "Javascript"
複製代碼
如上圖看控制檯。能夠設置值,能夠獲取值,可是這不是響應式的。
打開控制檯看一下
能夠詳細的看到。只有_vm
這個是proxy
,咱們須要的是,_data
下面全部數據都是有攔截代理的;下面咱們就去實現它。
咱們首先在Mvvm
裏面加一個initObserve
,以下
function Mvvm(options = {}) {
this.$options = options
let data = this._data = this.$options.data
let vm = initVm.call(this)
+ initObserve.call(this, data) // 初始化data的Observe
return this._vm
}
複製代碼
initObserve
這個函數主要是把,this._data
都加上代理。以下
function initObserve(data) {
this._data = observe(data) // 把全部observe都賦值到 this._data
}
// 分開這個主要是爲了下面遞歸調用
function observe(data) {
if (!data || typeof data !== 'object') return data // 若是不是對象直接返回值
return new Observe(data) // 對象調用Observe
}
複製代碼
下面主要實現Observe類
// Observe類
class Observe {
constructor(data) {
this.dep = new Dep() // 訂閱類,後面會介紹
for (let key in data) {
data[key] = observe(data[key]) // 遞歸調用子對象
}
return this.proxy(data)
}
proxy(data) {
let dep = this.dep
return new Proxy(data, {
get: (target, key, receiver) => {
return Reflect.get(target, key, receiver)
},
set: (target, key, value) => {
const result = Reflect.set(target, key, observe(value)) // 對於新添加的對象也要進行添加observe
return result
}
})
}
}
複製代碼
這樣子,經過咱們層層遞歸添加proxy
,把咱們的_data
對象都添加一遍,再看一下控制檯
很不錯,_data
也有proxy
了,很王祖藍式的完美。
看到咱們的html的界面,都是沒有數據的,上面咱們把數據都準備好了,下面咱們就開始把數據結合到html的界面上。
先把計算屬性這個html註釋掉,後面進行實現
<!-- <p>計算屬性:{{sum}}</p> -->
複製代碼
而後在Mvvm函數中增長一個編譯函數,➕號表示是添加的函數
function Mvvm(options = {}) {
this.$options = options
let data = this._data = this.$options.data
let vm = initVm.call(this)
+ new Compile(this.$options.el, vm) // 添加一個編譯函數
return this._vm
}
複製代碼
上面咱們添加了一個Compile
的構造函數。把配置的el
做爲參數傳機進來,把生成proxy
的實例vm
也傳進去,這樣子咱們就能夠拿到vm
下面的數據,下面咱們就去實現它。順序讀註釋就能夠了,很好理解
// 編譯類
class Compile {
constructor (el, vm) {
this.vm = vm // 把傳進來的vm 存起來,由於這個vm.a = 1 沒毛病
let element = document.querySelector(el) // 拿到 app 節點
let fragment = document.createDocumentFragment() // 建立fragment代碼片斷
fragment.append(element) // 把app節點 添加到 建立fragment代碼片斷中
this.replace(fragment) // 套數據函數
document.body.appendChild(fragment) // 最後添加到body中
}
replace(frag) {
let vm = this.vm // 拿到以前存起來的vm
// 循環frag.childNodes
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent // 拿到文本 例如:"開發語言:{{language}}"
let reg = /\{\{(.*?)\}\}/g // 定義匹配正則
if (node.nodeType === 3 && reg.test(txt)) {
replaceTxt()
function replaceTxt() {
// 若是匹配到的話,就替換文本
node.textContent = txt.replace(reg, (matched, placeholder) => {
return placeholder.split('.').reduce((obj, key) => {
return obj[key] // 例如:去vm.makeUp.one對象拿到值
}, vm)
})
}
}
// 若是還有字節點,而且長度不爲0
if (node.childNodes && node.childNodes.length) {
// 直接遞歸匹配替換
this.replace(node)
}
})
}
}
複製代碼
上面的編譯函數,總之就是一句話,想方設法的把{{xxx}}的佔位符經過正則替換成真實的數據。
而後刷新瀏覽器,鐺鐺檔鐺鐺檔,就出現咱們要的數據了。
很好很好,可是咱們如今的數據並非改變了 就發生變化了。還須要訂閱發佈和watcher來配合,才能作好改變數據就發生變化了。下面咱們先實現訂閱發佈。
訂閱發佈實際上是一種常見的程序設計模式,簡單直白來講就是:
把函數push到一個數組裏面,而後循環數據調用函數。
例如:舉個很直白的例子
let arr = []
let a = () => {console.log('a')}
arr.push(a) // 訂閱a函數
arr.push(a) // 又訂閱a函數
arr.push(a) // 雙訂閱a函數
arr.forEach(fn => fn()) // 發佈全部
// 此時會打印三個a
複製代碼
很簡單吧。下面咱們去實現咱們的代碼
// 訂閱類
class Dep {
constructor() {
this.subs = [] // 定義數組
}
// 訂閱函數
addSub(sub) {
this.subs.push(sub)
}
// 發佈函數
notify() {
this.subs.filter(item => typeof item !== 'string').forEach(sub => sub.update())
}
}
複製代碼
訂閱發佈是寫好了,可是在何時訂閱,何時發佈??這時候,咱們是在數據獲取的時候訂閱watcher
,而後在數據設置的時候發佈watcher
,在上面的Observe
類裏面裏面,看➕號的代碼。 .
... //省略代碼
...
proxy(data) {
let dep = this.dep
return new Proxy(data, {
// 攔截get
get: (target, prop, receiver) => {
+ if (Dep.target) {
// 若是以前是push過的,就不用重複push了
if (!dep.subs.includes(Dep.exp)) {
dep.addSub(Dep.exp) // 把Dep.exp。push到sub數組裏面,訂閱
dep.addSub(Dep.target) // 把Dep.target。push到sub數組裏面,訂閱
}
+ }
return Reflect.get(target, prop, receiver)
},
// 攔截set
set: (target, prop, value) => {
const result = Reflect.set(target, prop, observe(value))
+ dep.notify() // 發佈
return result
}
})
}
複製代碼
上面代碼說到,watcher是什麼鬼?而後發佈裏面的sub.update()又是什麼鬼??
帶着一堆疑問咱們來到了watcher
看詳細註釋
// Watcher類
class Watcher {
constructor (vm, exp, fn) {
this.fn = fn // 傳進來的fn
this.vm = vm // 傳進來的vm
this.exp = exp // 傳進來的匹配到exp 例如:"language","makeUp.one"
Dep.exp = exp // 給Dep類掛載一個exp
Dep.target = this // 給Dep類掛載一個watcher對象,跟新的時候就用到了
let arr = exp.split('.')
let val = vm
arr.forEach(key => {
val = val[key] // 獲取值,這時候會粗發vm.proxy的get()函數,get()裏面就添加addSub訂閱函數
})
Dep.target = null // 添加了訂閱以後,把Dep.target清空
}
update() {
// 設置值會觸發vm.proxy.set函數,而後調用發佈的notify,
// 最後調用update,update裏面繼續調用this.fn(val)
let exp = this.exp
let arr = exp.split('.')
let val = this.vm
arr.forEach(key => {
val = val[key]
})
this.fn(val)
}
}
複製代碼
Watcher類就是咱們要訂閱的watcher,裏面有回調函數fn,有update函數調用fn,
咱們都弄好了。可是在哪裏添加watcher呢??以下代碼
在Compile裏面
...
...
function replaceTxt() {
node.textContent = txt.replace(reg, (matched, placeholder) => {
+ new Watcher(vm, placeholder, replaceTxt); // 監聽變化,進行匹配替換內容
return placeholder.split('.').reduce((val, key) => {
return val[key]
}, vm)
})
}
複製代碼
添加好有所的東西了,咱們看一下控制檯。修改發現果真起做用了。
而後咱們回顧一下全部的流程,而後看見古老(我也是別的地方弄來的)的一張圖。
幫助理解嘛
響應式的數據咱們都已經完成了,下面咱們完成一下雙向綁定。
看到咱們html裏面有個<input placeholder="123" v-module="language" />
,v-module
綁定了一個language
,而後在Compile類
裏面的replace函數
,咱們加上
replace(frag) {
let vm = this.vm
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent
let reg = /\{\{(.*?)\}\}/g
// 判斷nodeType
+ if (node.nodeType === 1) {
const nodeAttr = node.attributes // 屬性集合
Array.from(nodeAttr).forEach(item => {
let name = item.name // 屬性名
let exp = item.value // 屬性值
// 若是屬性有 v-
if (name.includes('v-')){
node.value = vm[exp]
node.addEventListener('input', e => {
// 至關於給this.language賦了一個新值
// 而值的改變會調用set,set中又會調用notify,notify中調用watcher的update方法實現了更新操做
vm[exp] = e.target.value
})
}
});
+ }
...
...
}
}
複製代碼
上面的方法就是,讓咱們的input
節點綁定一個input事件
,而後當input事件
觸發的時候,改變咱們的值,而值的改變會調用set
,set
中又會調用notify
,notify
中調用watcher
的update
方法實現了更新操做。
而後咱們看一下,界面
雙向數據綁定咱們基本完成了,別忘了,咱們上面還有個註釋掉的計算屬性。
先把<p>計算屬性:{{sum}}</p>
註釋去掉,覺得上面一開始initVm函數裏面,咱們加了這個代碼return this[key] || this._data[key] || this._computed[key]
,到這裏你們都明白了,只須要把this._computed也加一個watcher就行了。
function Mvvm(options = {}) {
this.$options = options
let data = this._data = this.$options.data
let vm = initVm.call(this)
initObserve.call(this, data)
+ initComputed.call(this) // 添加計算函數,改變this指向
new Compile(this.$options.el, vm)
return this._vm
}
function initComputed() {
let vm = this
let computed = this.$options.computed // 拿到配置的computed
vm._computed = {}
if (!computed) return // 沒有計算直接返回
Object.keys(computed).forEach(key => {
// 至關於把sum裏的this指向到this._vm,而後就能夠拿到this.a、this、b
this._computed[key] = computed[key].call(this._vm)
// 添加新的Watcher
new Watcher(this._vm, key, val => {
// 每次設置的時候都會計算
this._computed[key] = computed[key].call(this._vm)
})
})
}
複製代碼
上面的initComputed 就是添加一個watcher,大體流程:
this._vm改變 ---> vm.set() ---> notify() -->update()-->更新界面
最後看看圖片
一切彷佛沒什麼毛病~~~~
添加mounted也很簡單
// 寫法和Vue同樣
let mvvm = new Mvvm({
el: '#app',
data: {
...
...
},
computed: {
...
...
},
mounted() {
console.log('i am mounted', this.a)
}
})
複製代碼
在new Mvvm裏面添加mounted, 而後到function Mvvm裏面加上
function Mvvm(options = {}) {
this.$options = options
let data = this._data = this.$options.data
let vm = initVm.call(this)
initObserve.call(this, data)
initComputed.call(this)
new Compile(this.$options.el, vm)
+ mounted.call(this._vm) // 加上mounted,改變指向
return this._vm
}
// 運行mounted
+ function mounted() {
let mounted = this.$options.mounted
mounted && mounted.call(this)
+ }
複製代碼
執行以後會打印出
i am mounted 1
複製代碼
完結~~~~撒花
ps:編譯裏面的,參考到這個大神的操做。@chenhongdong,謝謝大佬
最後附上,源代碼地址,直接下載運行就能夠啦。