試着用Proxy 完成一個簡樸mvvm

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(代辦)是做什麼用的,然後下面我們看下要做終究的圖騙。
《試着用Proxy 完成一個簡樸mvvm》

看到上面的圖片,起首我們新建一個index.html,然後內里的代碼是這模樣滴。很簡樸

<!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這個組織函數

起首聲明一個Mvvm函數,options看成參數傳進來,options就是上面代碼的設置,內里有eldatacomputed~~

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實例。

在代辦內里,阻攔了getsetget函數內里,返回this對象的對應的key的值,沒有就去this._data對象內里取對應的key,再沒有去this._computed對象內里去對應的key值。set函數就是直接返回修正this._data對應key

做好這些種種阻攔事情。我們就能夠直接從氣力上接見到我們相對應的值了。(mvvm使我們第一塊代碼天生的實例)

mvvm.b // 2
mvvm.a // 1
mvvm.language // "Javascript"

《試着用Proxy 完成一個簡樸mvvm》

如上圖看控制台。能夠設置值,能夠獵取值,然則這不是相應式的。

翻開控制台看一下
《試着用Proxy 完成一個簡樸mvvm》

能夠細緻的看到。只要_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對象都增添一遍,再看一下控制台

《試着用Proxy 完成一個簡樸mvvm》

很不錯,_data也有proxy了,很王祖藍式的圓滿。

看到我們的html的界面,都是沒有數據的,上面我們把數據都預備好了,下面我們就最先把數據結合到html的界面上。

套數據,完成hmtl界面

先把盤算屬性這個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}}的佔位符經由過程正則替代成實在的數據。

然後革新瀏覽器,噹噹檔噹噹檔,就湧現我們要的數據了。

《試着用Proxy 完成一個簡樸mvvm》

很好很好,然則我們如今的數據並非轉變了 就發生變化了。還須要定閱宣布和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)
    })
}

增添好有所的東西了,我們看一下控制台。修正發明果真起作用了。

《試着用Proxy 完成一個簡樸mvvm》

然後我們回憶一下統統的流程,然後瞥見陳舊(我也是別的處所弄來的)的一張圖。

協助邃曉嘛

《試着用Proxy 完成一個簡樸mvvm》

相應式的數據我們都已完成了,下面我們完成一下雙向綁定。

完成雙向綁定

看到我們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事宜觸發的時刻,轉變我們的值,而值的轉變會挪用setset中又會挪用notifynotify中挪用watcherupdate要領完成了更新操縱。

然後我們看一下,界面
《試着用Proxy 完成一個簡樸mvvm》

雙向數據綁定我們基本完成了,別忘了,我們上面另有個解釋掉的盤算屬性。

盤算屬性

先把<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()–>更新界面

末了看看圖片

《試着用Proxy 完成一個簡樸mvvm》

統統好像沒什麼缺點~~~~

增添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

    原文作者:naice
    原文地址: https://segmentfault.com/a/1190000015460479
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞