ES6高階(defineProperty,Proxy代理,數據劫持,es6模塊化、exports 和 import,AMD /CMD模塊化)

目錄

1.課堂主題及知識點

2.ES5的Object.defineProperty()實現數據劫持

2.1Object.defineProperty()下的配置

2.2使用Object.defineProperty()方式模擬Vue數據劫持,並更新視圖(數據雙向綁定)

3.ES6中new Proxy()實現數據劫持

4.es6模塊化

4.1導出方式

4.1.1導出 方式一 :

 4.1.2導出方式二 關鍵字 "as"

4.1.3導出方式三

4.1.4導出方式四

4.2導入方式

4.2.1export 使用對象導出的,命名要保持一致方式

4.2.2export導出的,命名可以自定義方式;

4.2.3通配符 "*"方式導入

5.AMD require.js的使用

5.1引入require.js

5.2加載模塊

5.3定義模塊

5.3.1無依賴定義

5.3.2模塊有依賴

5.3.3函數式寫法

6.模塊化優點

7.總結


1.課堂主題及知識點

##課堂主題

  1. 利用defineProperty實現數據劫持;
  2. 利用ES6中proxy實現數據劫持
  3. 數據劫持實現mvvm裏的表達式
  4. 利用自定義事件實現數據動態更新;
  5. 通過es6模塊化改造自己的mvvm框架;
  6. AMD模塊化require.js介紹;

##知識點

  • defineProperty;
  • Proxy代理
  • 數據劫持
  • es6模塊化、exports 和 import
  • AMD /CMD模塊化;
  • MVVM框架:數據驅動,數據優先(數據變化,視圖也跟着變化)

數據在前端使用得最多的即對象數組,如[{},{},{}]形式。

數據劫持:攔截數據變化,再將變化的數據更新到視圖。

2.ES5的Object.defineProperty()實現數據劫持

  • 參數一:被劫持的對象數據;
  • 參數二:要劫持的對象數據中的屬性;
  • 參數三:對象,裏面的get()/set()方法在數據改變時會自動監聽並執行(即數據劫持)
  • 每個屬性劫持都需要單獨使用Object.defineProperty()進行設置劫持,劫持後的數據都有自己的get()/set()方法(如obj.name會自動調用get()方法,obj.name = 'zs'時會自動調用set()方法),其返回值仍然會被劫持。
Object.defineProperty(obj,'name',{
        configurable:true,
        enumerable:true,
        get(){
            return value;
        },
        set(newValue){
            console.log("set...");
            value = newValue;
        }
    })

2.1Object.defineProperty()下的配置

  1. configurable:true,表示可配置,即是否可更改,默認爲true。如果設置了false,則delete Object.key(name)則不會生效;
  2. enumeable:true,表示可枚舉,默認爲true。如果設置爲false,則會影響for in循環,Object.keys(),JSON.stringfy(),Object.assign()等方法的使用。

2.2使用Object.defineProperty()方式模擬Vue數據劫持,並更新視圖(數據雙向綁定)

Vue案例:更改數據vue.message= "hello my vue"時,視圖也會隨之更改

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="app">{{message}}</div>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script>
        let vue = new Vue({
            el: "#app",
            data:{
                message:"hello vue"
            }
        })
    </script>
</body>

</html>

  效果:

Object.defineProperty()方式模擬Vue數據劫持使用及步驟解析:

  1. 通過document.querySelect(el)找到被數據劫持範圍內所有節點(nodes = eles.childNodes)。並進行初次視圖渲染;
  2. 通過node.nodeType===3或node.nodeType===1判斷節點爲文本還是節點,如果是文本節點直接使用正則let reg = /\{\{\s*(\S+)\s*\}\}/g 將正則中的組(\S+)替換爲要替換的數據(組內內容使用$1獲取)。node.textContent().replace(reg,value);
  3. 判斷node.childNodes.length大於0則表示還有 子節點,需要使用遞歸實現多層節點替換message;不大於0則表示不再有子節點;
  4. 使用Object.defineProperty()方法實現數據劫持並改變數據;
  5. 使用自定義事件(繼承TargetEvent,並使用CustomEvent類)實現視圖的再次渲染,通過let event = new CustomEvent(key,value);this.dispatchEvent(event)監聽數據變化(注意此處this和自定義事件this的使用),並將設置的值獲取並替換上一次的值即let oldValue = this.options.dada[$1]; let reg = new RegExt(oldValue,"g");  node.textContent = node.textContent.replace(reg,newValue);
  6. 問題:不支持多層數據劫持更新,且多次更新也有問題
  7. 輸入框中使用屬性v-html = "htmldata"指令進行更新。獲取節點中所有屬性node.attributes ,然後循環所有屬性attr.name attr.value ,使用let attrName = attr.name.substr(2)去掉v-html的‘v-’;然後判斷attrName = "html"時 node.innerHTML = this.options.data[attrValue];attrName = "model" 時node.value = this.options.data[attrValue];
  8. 因爲使用的數據雙綁定,所以在input輸入框中輸入時也需要將值綁定並更新到視圖中。在CustomEvent中通過e.detail獲取input的值,然後直接賦值,因爲已經對數據進行劫持,並且進行了二次渲染,所以賦值後會直接進行數據劫持和視圖更新
  9. 通過new Proxy()方法同樣實現數據劫持和視圖更新

案例實現Vue模擬:

  • 因爲傳入的el是#app所以用document.querySelect(el)而不是document.querySelectAll(el);
  • 自定義事件的使用:增加監聽事件;使用CustomEvent獲取監聽事件及傳遞的數據;dipatchEvent(event)觸發事件;觸發自定義事件時的this指向問題;
  • v-html和v-model指令更新時,是獲取v-html和v-model所在節點的屬性值,且此處做input輸入框數據雙向綁定時,監聽事件是加在node節點上,而不是this上(因爲繼承了TargetEvent,所以此處this指向TargetEvent,所以不能使用)

Kvue.html:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="Kvue-new-Proxy.js"></script>
    <title>Document</title>
</head>

<body>
    <div id="app">
        {{message}}外層
        <div>
            <span>{{message}}</span>
        </div>
        {{code}}
        <!-- 注意所有v-html或 v-model都必須在el屬性範圍內-->
        <div v-html="htmlData"></div>
        <!-- 數據雙綁定——輸入框輸入內容的數據也會被劫持 -->
        <input v-model="modelData"/> {{modelData}}

    </div>
    <script>
        let kVue = new Kvue({
            el: '#app',
            data: {
                message: "這是我的Kvue!",
                code: 303,
                // 注意此處屬性值必須和佈局中v-html 和v-model後面的屬性值保持一致
                htmlData:"html數據",
                modelData:"數據雙綁定"
            }
        });
        //監聽獲取數據
        // console.log(kVue.data);


    </script>
</body>

</html>

Kvue.js:

/**
 * 模擬實現vue功能
 * 功能三:輸入框中使用屬性v-html = "htmldata"指令進行更新
 */

class Kvue extends EventTarget {
    //options表示
    constructor(options) {
        super();
        this.options = options;
        this.data = this.options.data;
        this.compile();
        // 數據劫持
        this.observe(this.options.data);
    }
    // 監聽數據變化
    observe(data) {
        Object.keys(data).forEach(key => {//key即data中的屬性名
            this.observeData(data, key, data[key]);
        });
    }
    // 通過Object.defineProperty劫持數據變化
    observeData(data, key, value) {
        let _this = this;
        Object.defineProperty(data, key, {
            configurable: true,
            enumerable: true,
            get() {
                console.log("get----");
                return value;
            },
            set(newValue) {
                console.log("set----", newValue);
                let event = new CustomEvent(key, { detail: newValue });//參數一時間名,參數二傳遞的數據
                // 需要觸發事件
                _this.dispatchEvent(event);
                return value = newValue;//set()方法返回更新後的數據
            }
        });
    }

    //處理數據,渲染視圖
    compile() {
        // 注意此處傳入的爲#app只有一個元素
        let eles = document.querySelector(this.options.el);
        this.compileNode(eles.childNodes);

    }
    compileNode(childNodes) {
        childNodes.forEach(node => {
            // 循環所有nodes,當nodeType爲1時表示爲節點,需找到下一個節點直到找到的是文本;爲3表示爲文本直接正則匹配顯示
            if (node.nodeType === 1) {
                // 數據雙綁定
                let attrs = node.attributes;//獲取屬性及屬性值
                console.log(attrs);
                [...attrs].forEach(attr => {
                    let attrName = attr.name;
                    let attrValue = attr.value;
                    // 獲取v-html v-model的v-後面的內容
                    attrName = attrName.substr(2);
                    console.log(attrName,attrValue);
                    // 爲html時,通過innerHTML獲取設置值
                    if (attrName == "html") {
                        node.innerHTML = this.data[attrValue];

                    // 爲model時,是input框 通過value獲取設置值
                    } else if (attrName == "model") {
                        // 注意是給當前節點設置值和加監聽,所以使用node,次此處this指向EventTarget
                        // 給input數據框設置初始值
                        node.value = this.data[attrValue];
                        // 監聽input更改後更新input視圖
                        node.addEventListener("input",e=>{
                            console.log("attrValue設置了值;", e.target.value);//此處是input框,通過e.target.value獲取值
                            this.data.modelData = e.target.value;
                        });
                    }

                });


                // 判斷nodes.length>0則表示還有子節點,需要遞歸找到下一個節點直到找到的是文本;nodes.length不大於0表示只有一個文本節點,直接匹配顯示
                if (node.childNodes.length > 0) {
                    this.compileNode(node.childNodes);
                }
            } else if (node.nodeType === 3) {
                let reg = /\{\{\s*(\S+)\s*\}\}/g;//\{ \}表示精確匹配{},\s*表示message前後可能會有任意個空格,(\S+)表示用組匹配message
                let textContent = node.textContent;
                let test = reg.test(textContent);//注意要使用test方法測試是否匹配再替換
                if (test) {
                    // 初次渲染;
                    let $1 = RegExp.$1;
                    node.textContent = textContent.replace(reg, this.options.data[$1]);

                    // 頁面再次渲染
                    //設置監聽事件,監聽$1即每個data中的key
                    // 注意:自定義事件的用法,因爲此處設置了監聽,所以在observeData方法中,dispatchEvent(event)觸發事件時會自動被監聽
                    // 所以此處的e即CustomEvent對象,所以有e.detail屬性
                    // console.log(this);//EventTarget {options: {…}, data: {…}}
                    this.addEventListener($1, e => {
                        console.log("設置了值;", e.detail);
                        // 獲取設置的值
                        let newValue = e.detail;
                        // 原來數據中的值key ,如message
                        let oldValue = this.options.data[$1];
                        // 全局匹配所有的原有數據
                        let reg = new RegExp(oldValue, "g");
                        // 將原有數據改爲更改後的值
                        node.textContent = node.textContent.replace(reg, newValue);
                    });
                }
            }
        });
    }
}

3.ES6中new Proxy()實現數據劫持

  • 定義 :對象用於定義基本操作的自定義行爲(如屬性查找,賦值,枚舉,函數調用等)。

  • 基本使用

let obj = new Proxy({
                    name: "張三",
                    age: 20
              },{
//target即傳入的原始數據即第一個參數值
                    get(target, name) {
                        return target[name];
                    },
                    set(target,name,value){
                        target[name] = value;
                    }
               })
  • 相關配置參數

has(target, propKey):攔截propKey in proxy的操作,返回一個布爾值。
defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)
Object.defineProperties(proxy, propDescs),返回一個布爾值。

使用new Proxy()實現模擬Vue:

 Kvue.html同上;

Kvue-new-proxy.js:

  • new Proxy()需要使用this.data進行接收;
  • set()和get()中的target即new Proxy()時傳入的第一個參數值(即原始數據);
  • 仍然需要觸發監聽
/**
 * 模擬實現vue功能
 * 功能四:通過new Proxy實現和Object.property()同樣功能
 */

class Kvue extends EventTarget {
    //options表示
    constructor(options) {
        super();
        this.options = options;
        this.data = this.options.data;
        this.compile();
        // 數據劫持
        this.observe(this.options.data);
    }
    // 監聽數據變化
    observe(data) {
        // Object.keys(data).forEach(key => {//key即data中的屬性名
        //     this.observeData(data, key, data[key]);
        // });
        let _this = this;
        this.data = new Proxy(data, {
            // target即傳入的data原始數據
            get(target, key) {
                return target[key];
            },
            // 此處newValue即傳入的改變的數據
            set(target, key, newValue) {
                console.log(target);
                
                let event = new CustomEvent(key, { detail: newValue });//參數一時間名,參數二傳遞的數據
                // 需要觸發事件
                _this.dispatchEvent(event);
                return target[key] = newValue;
            }
        });
    }
    // 通過Object.defineProperty劫持數據變化
    // observeData(data, key, value) {
    //     let _this = this;
    //     Object.defineProperty(data, key, {
    //         configurable: true,
    //         enumerable: true,
    // get() {
    //     console.log("get----");
    //     return value;
    // },
    // set(newValue) {
    //     console.log("set----", newValue);
    //     let event = new CustomEvent(key, { detail: newValue });//參數一時間名,參數二傳遞的數據
    //     // 需要觸發事件
    //     _this.dispatchEvent(event);
    //     return value = newValue;//set()方法返回更新後的數據
    // }
    //     });
    // }

    //處理數據,渲染視圖
    compile() {
        // 注意此處傳入的爲#app只有一個元素
        let eles = document.querySelector(this.options.el);
        this.compileNode(eles.childNodes);

    }
    compileNode(childNodes) {
        childNodes.forEach(node => {
            // 循環所有nodes,當nodeType爲1時表示爲節點,需找到下一個節點直到找到的是文本;爲3表示爲文本直接正則匹配顯示
            if (node.nodeType === 1) {
                // 數據雙綁定
                let attrs = node.attributes;//獲取屬性及屬性值
                console.log(attrs);
                [...attrs].forEach(attr => {
                    let attrName = attr.name;
                    let attrValue = attr.value;
                    // 獲取v-html v-model的v-後面的內容
                    attrName = attrName.substr(2);
                    console.log(attrName, attrValue);
                    // 爲html時,通過innerHTML獲取設置值
                    if (attrName == "html") {
                        node.innerHTML = this.data[attrValue];

                        // 爲model時,是input框 通過value獲取設置值
                    } else if (attrName == "model") {
                        // 注意是給當前節點設置值和加監聽,所以使用node,次此處this指向EventTarget
                        // 給input數據框設置初始值
                        node.value = this.data[attrValue];
                        // 監聽input更改後更新input視圖
                        node.addEventListener("input", e => {
                            console.log("attrValue設置了值;", e.target.value);//此處是input框,通過e.target.value獲取值
                            this.data.modelData = e.target.value;
                        });
                    }

                });

                // 判斷nodes.length>0則表示還有子節點,需要遞歸找到下一個節點直到找到的是文本;nodes.length不大於0表示只有一個文本節點,直接匹配顯示
                if (node.childNodes.length > 0) {
                    this.compileNode(node.childNodes);
                }
            } else if (node.nodeType === 3) {
                let reg = /\{\{\s*(\S+)\s*\}\}/g;//\{ \}表示精確匹配{},\s*表示message前後可能會有任意個空格,(\S+)表示用組匹配message
                let textContent = node.textContent;
                let test = reg.test(textContent);//注意要使用test方法測試是否匹配再替換
                if (test) {
                    // 初次渲染;
                    let $1 = RegExp.$1;
                    node.textContent = textContent.replace(reg, this.options.data[$1]);

                    // 頁面再次渲染
                    //設置監聽事件,監聽$1即每個data中的key
                    // 注意:自定義事件的用法,因爲此處設置了監聽,所以在observeData方法中,dispatchEvent(event)觸發事件時會自動被監聽
                    // 所以此處的e即CustomEvent對象,所以有e.detail屬性
                    // console.log(this);//EventTarget {options: {…}, data: {…}}
                    this.addEventListener($1, e => {
                        console.log("設置了值;", e.detail);
                        // 獲取設置的值
                        let newValue = e.detail;
                        // 原來數據中的值key ,如message
                        let oldValue = this.options.data[$1];
                        // 全局匹配所有的原有數據
                        let reg = new RegExp(oldValue, "g");
                        // 將原有數據改爲更改後的值
                        node.textContent = node.textContent.replace(reg, newValue);
                    });
                }
            }
        });
    }
}

4.es6模塊化

  • 瀏覽器默認模塊化 script 里加入 "type=module";
  • 導出 關鍵字 export
  • export 可以導出多個,export default 只能導出一個;
  • 使用模塊化時,需要在服務器環境打開頁面,否則會報錯

4.1導出方式

4.1.1導出 方式一 :

export.js文件中

export { a ,b , c}

對應導入方式:

import {a,b,c} from './export.js';

 4.1.2導出方式二 關鍵字 "as"

export { a as aa ,b , c}

對應導入方式:

import { aa, b, c } from './export.js';
console.log(aa, b, c);//10 20 30

 

4.1.3導出方式三

export let d = ()=>{console.log("I am d function...")}

對應導入方式:

import {d} from './export.js';
console.log(d);
d();//I am d function...

4.1.4導出方式四

// export default a;//等同export {a as default};
export {b as default};

 對應導入方式:

//  import a from './export.js';
// console.log(a);//10
import b from './export.js';
console.log(b);//20

4.2導入方式

導入方式:關鍵字 import,js文件名前必須加'./'

4.2.1export 使用對象導出的,命名要保持一致方式

import {aa , b , c} from './moduleb.js';

4.2.2export導出的,命名可以自定義方式;

import myfn from './moduleb.js';

4.2.3通配符 "*"方式導入

import * as obj from './moduleb.js';

ES6導入導出示例:

module.html:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script type="module">
        // 注意使用import模塊化時需要運行在服務器上,否則會報錯
        // 導出方式一對應導入方式一:
        // import {a,b,c} from './export.js';
        // console.log(a, b, c);//10 20 30

        // 導出方式二對應導入方式二:
        // import { aa, b, c } from './export.js';
        // console.log(aa, b, c);//10 20 30

        // 導出方式三對應導入方式三:
        // import {d} from './export.js';
        // console.log(d);
        // d();//I am d function...

         // 導出方式四對應導入方式四:
        //  import a from './export.js';
        // console.log(a);//10
        //  import b from './export.js';
        //  console.log(b);//20

        // 導出方式五:可使用通配符*方式導入
        import * as obj from './export.js';
        console.log(obj);//得到的是整個模塊,Module {Symbol(Symbol.toStringTag): "Module"}可通過obj.a,obj.b獲取具體值
        


    </script>
</body>

</html>

 export.js:

let a = 10;
let b = 20;
let c = 30;

// 導出方式一:
export {a,b,c};

// 導出方式二:
// export {a as aa,b,c};

// 導出方式三:
// export let d = ()=>{console.log("I am d function...")}

// 導出方式四:
// export default a;//等同export {a as default};
// export {b as default};

5.AMD require.js的使用

5.1引入require.js

https://cdn.bootcss.com/require.js/2.3.6/require.js

5.2加載模塊

require(["a"]);

5.3定義模塊

5.3.1無依賴定義

define({
    method1:function(){
        console.log("a method...");
    },
    method2:function(){
        console.log("b method...");
    }
});

5.3.2模塊有依賴

define(["c"],{
    method1:function(){
        console.log("a method...");
    },
    method2:function(){
        console.log("b method...");
    }
});

5.3.3函數式寫法

define(["c"],function(){
    obj = {
        name:"張安",
        age:20
    }
    return obj;
});

6.模塊化優點

  • 防止作用域污染
  • 提高代碼的複用性
  • 維護成本降低

7.總結

  • defineProperty
  • Proxy
  • 數據劫持
  • 自定義事件
  • es6模塊化
  • AMD/CMD模塊化
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章