摘要:套數據,實現界面先把計算屬性這個注釋掉,后面進行實現計算屬性然后在函數中增加一個編譯函數,號表示是添加的函數添加一個編譯函數上面我們添加了一個的構造函數。
Proxy、Reflect的簡單概述
Proxy 可以理解成,在目標對象之前架設一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。Proxy 這個詞的原意是代理,用在這里表示由它來“代理”某些操作,可以譯為“代理器”。
出自阮一峰老師的ECMAScript 6 入門,詳細點擊http://es6.ruanyifeng.com/#docs/proxy
例如:
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,去讀寫它的屬性,就會得到下面的結果。
obj.count = 1 // setting count! ++obj.count // getting count! // setting count! // 2
var proxy = new Proxy(target, handler);
這里有兩個參數,target參數表示所要攔截的目標對象,handler參數也是一個對象,用來定制攔截行為。
注意,要使得Proxy起作用,必須針對Proxy實例(上例是proxy對象)進行操作,而不是針對目標對象(上例是空對象)進行操作。
Reflect對象與Proxy對象一樣,也是 ES6 為了操作對象而提供的新 API。
Reflect對象的方法與Proxy對象的方法一一對應,只要是Proxy對象的方法,就能在Reflect對象上找到對應的方法。這就讓Proxy對象可以方便地調用對應的Reflect方法,完成默認行為,作為修改行為的基礎。也就是說,不管Proxy怎么修改默認行為,你總可以在Reflect上獲取默認行為。
同樣也放上阮一峰老師的鏈接http://es6.ruanyifeng.com/#docs/reflect
初始化結構看到這里,我就當大家有比較明白Proxy(代理)是做什么用的,然后下面我們看下要做最終的圖騙。
看到上面的圖片,首先我們新建一個index.html,然后里面的代碼是這樣子滴。很簡單
簡單版mvvm 開發語言:{{language}}
組成部分:
- {{makeUp.one}}
- {{makeUp.two}}
- {{makeUp.three}}
描述:
{{describe}}
計算屬性:{{sum}}
看到上面的代碼,大概跟vue長得差不多,下面去實現Mvvm這個構造函數
實現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的界面上。
套數據,實現hmtl界面先把計算屬性這個html注釋掉,后面進行實現
然后在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看詳細注釋
// 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里面有個,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方法實現了更新操作。
然后我們看一下,界面
雙向數據綁定我們基本完成了,別忘了,我們上面還有個注釋掉的計算屬性。
計算屬性先把
計算屬性:{{sum}}
注釋去掉,以為上面一開始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鉤子添加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,謝謝大佬
最后附上,源代碼地址,直接下載運行就可以啦。
源碼地址:https://github.com/naihe138/proxy-mvvm
預覽地址:http://gitblog.naice.me/proxy-mvvm/index.html
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/95880.html
摘要:前言在說架構之前,先說說框架吧。在架構中就是這個轉接頭。當一個新框架誕生后,關注點從學習這個框架,慢慢變成了這個框架是如何設計的,解決什么樣的問題。前幾年使用過各種框架,小到,大到。 前言 在說 MVC 架構之前,先說說PHP框架吧。很多很多學完PHP語言的人,面對的就是PHP各種各樣的框架。什么TP啊、Yii啊、CI啊,還有很流行的laravel啊等等。 他們的大部分都會說自己是基于...
摘要:綁定實現的歷史綁定的基礎是事件。但臟檢查機制隨之帶來的就是性能問題。是谷歌對于簡化雙向綁定機制的嘗試,在中引入。掙扎了一段時間后谷歌團隊宣布收回的提議,并在中完全刪除了實現。自然全軍覆沒其他各大瀏覽器實現的時間也較晚。 綁定實現的歷史 綁定的基礎是 propertyChange 事件。如何得知 viewModel 成員值的改變一直是開發 MVVM 框架的首要問題。主流框架的處理有一下三...
摘要:綁定實現的歷史綁定的基礎是事件。但臟檢查機制隨之帶來的就是性能問題。是谷歌對于簡化雙向綁定機制的嘗試,在中引入。掙扎了一段時間后谷歌團隊宣布收回的提議,并在中完全刪除了實現。自然全軍覆沒其他各大瀏覽器實現的時間也較晚。 綁定實現的歷史 綁定的基礎是 propertyChange 事件。如何得知 viewModel 成員值的改變一直是開發 MVVM 框架的首要問題。主流框架的處理有一下三...
摘要:在中,格式是,所以需要把格式統一為注冊表的標準。注冊表的二進制值及關鍵信息如下開關長度地址是否跳過本地代理地址通過在中導入文件的方式執行并立即生效。本代碼可以根據需要自動設置代理。 聲明下:不同于網絡上千百篇方法,下文是經過各種嚴格測試都通過的,同時也是一個實驗的過程,排除了各種不靠譜的方法。有需要的可以評論來討論,想要源碼和相關參考文獻或筆記的,也可以找我。 思路及啟發 先說一下我這...
閱讀 3114·2021-11-23 09:51
閱讀 1974·2021-09-09 09:32
閱讀 1084·2019-08-30 15:53
閱讀 2957·2019-08-30 11:19
閱讀 2464·2019-08-29 14:15
閱讀 1432·2019-08-29 13:52
閱讀 553·2019-08-29 12:46
閱讀 2818·2019-08-26 12:18