( 第三篇 )仿寫'Vue生態'系列___"'枚舉'與'雙向綁定'"

( 第三篇 )仿寫'Vue生態'系列___" '枚舉' 與 '雙向綁定' "

圖片描述

本次任務

  1. 對'遍歷'這個名詞進行死磕.
  2. 對defineProperty進行分析.
  3. 實現cc_vue的數據雙向綁定.
  4. 爲下一篇 Proxy 代替 defineProperty 做預熱.

一. 'forEach' vs 'map'

很多文章都寫過他們兩個的區別

  1. forEach沒有返回值, map會返回一個數組
  2. map利於壓縮, 因爲畢竟只有三個字母.

但是這些區別只是表面上的, 我來展示一個有趣的👇

<div id="boss">
   <div>1</div>
   <div>2</div>
   <div>3</div>
</div>
<script>
  let oD = document.getElementById('boss');  
  // 正常執行
  oD.childNodes.forEach(element => {console.log(element); });
  // 報錯
  oD.childNodes.map(element => { console.log(element); });
</script>

oDs.childNodes 並不是個數組, 他仍然是僞數組, 但是他可以使用forEach, 這個挺唬人的, 第一反應是這兩個遍歷方法在實現的方式上是不是有什麼不同, 但轉念一想感覺自己想歪了, 答案其實是 oDs.childNodes這個僞數組形成的時候, 往身上掛了個forEach...
通過上面的問題我有了些思考

  1. map既然返回新數組, 那就說明他空間複雜度會大一些.
  2. 某些系統提供的僞數組, 本身會掛載forEach但不會掛載map.

綜上所述, 還是用forEach保險!
但是就想用map怎麼辦那?
1: slice的原理就是一個一個的循環放入一個新數組;

let slice = Array.prototype.slice;
slice.call(oD.childNodes).map(()=>{})

2: 擴展運算符原理不太一樣, 但他一樣可以把所有元素都拿出來, 接下來我們就對他進行死磕.

[...oD.childNodes].map(()=>{})

二. 擴展運算符

這個神奇的語法其實有很多門道的, 我們來一起死磕一下吧.
下面代碼會正確執行, 對象放入對象肯定沒問題

let obj = {a:1,b:2},
    result = {...obj};
console.log(result)

下面代碼會報錯, 因爲缺少iterable.

 let obj = {a:1,b:2,length:2},
     result = [...obj];
 console.log(result)

原因是'擴展運算符'不知道該怎麼擴展他, 我們要告訴如何擴展纔可以正確的執行.
Symbol.iterator是Symbol身上的屬性, 而iterable的key就是它.

  let obj = { '0': 'a', '1': 'b', length: 2 };
      obj[Symbol.iterator] = function() {
        let n = -1,
            _this = this,
            len = this.length;
        // 必須有返回值
        // 並且返回值必須是對象
        return {
          // 必須有next
          next: function() {
            n++;
            if (n < len) {
              return {
                value: _this[n], // 返回的值, 這個可以隨便控制
                done: false // 爲true就是結束, 爲false就是繼續
              };
            } else {
              return {
                done: true 
              };
            }
          }
        };
      };

      result = [...obj];
      console.log(result);

上面的方法可以滿足我的要求了, 但是寫法上真的不敢恭維, 代碼量太多了..
所以我更推薦採用第二種方式利用Genertor

let obj = { '0': 'a', '1': 'b', length: 2 };
      obj[Symbol.iterator] = function*() {
        let n = -1, len = this.length;
        while (len !== ++n) {
          yield this[n];
        }
      };
      result = [...obj];
      console.log(result);

😺整個世界都清爽了.

三. defineProperty

這個神奇的屬性做了很多很多神奇的事情, 可以說現在的前端如果不會用它的話真完全說不過去了...

功能
監控對象的某個屬性, 可以對取值與賦值做出相應, 屬於'元編程'
第一個參數是 監控對象
第二個參數是 key
第三個參數必須是一個對象, 也可以理解成config對象
比如 obj.name 這個會觸發get函數
obj.name = 'lulu' 這個會觸發set屬性, 但是要注意, 這個不會觸發get
這些動作都能夠被監控到, 那我們就可以爲所以爲了哈哈哈哈哈

let obj = {
    name: 'a'
  };
  function proxyObj(obj, name, val) {
    Object.defineProperty(obj, name, {
      enumerable: true, // 描述屬性是否會出現在for in 或者 Object.keys()的遍歷中
      configurable: true, // 描述屬性是否配置,以及可否刪除
      get() {
        return val;
      },
      set(newVal) {
        val = newVal;
      }
    });
  }
  proxyObj(obj,'name',obj['name'])
  console.log((obj.name = 2));

缺點

  1. 在set裏面沒辦法爲自己賦值, 不然會導致死循環.
  2. 只能監控對象的變化, 無法監控數組與基本類型.
  3. 目標如果不是對象的話會報錯...不知道爲啥要設計成這樣.
  4. Object身上的方法這個設定不太妥當, 下一章我們會聊聊Reflect.defineProperty.

四. 將$data代理到vm身上

當前本套工程裏面, 使用data裏面的數據需要this.$data.xxx, 我們把它變成this.xxx就可以直接訪問的形式.
cc_vue/src/index.js

constructor(options) {
    // 1: 不管你傳啥, 我都放進來, 方便以後的擴展;
    // ...
    -------新加的
    // 2: 把$data掛在vm身上, 用戶可以直接this.xxx獲取到值
    this.proxyVm(this.$data);
    -------新加的
    // end
    new Compiler(this.$el, this);
  }
   /**
   * @method 把某個對象的值, 代理到目標對象上
   * @param { data } 想要被代理的對象
   * @param { target } 代理到誰身上
   */
  proxyVm(data = {}, target = this) {
   // 默認就掛在框架的實例上
    for (let key in data) {
      Object.defineProperty(target, key, {
        enumerable: true,
        configurable: true,
        get() {
          return data[key];
        },
        set(newVal) {
          if (newVal !== data[key]) {
            data[key] = newVal;
          }
        }
      });
    }
  }

這樣以後再有訪問數據的操作就可以直接this.了
之前對模板取值的操作要改一下啦, 很簡單的就是去掉$data
cc_vue/src/CompileUtil.js

  getVal(vm, expression) {
    let result,
      __whoToVar = '';
    for (let i in vm.$data) {
      // data下期做代理, 並且去掉原型上的屬性
      let item = vm.$data[i];
      if (typeof item === 'function') {
        __whoToVar += `function ${i}(...arg){return vm['${i}'].call(vm,...arg)}`;
      } else {
        __whoToVar += `let ${i}=vm['${i}'];`;
      }
    }
    __whoToVar = `${__whoToVar}result=${expression}`;
    eval(__whoToVar);
    return result;
  },

五. 爲data所有數據添加劫持

這裏比較核心, 所以我們直接單獨抽離出一個'劫持模塊'.
當前步驟只是添加了劫持, 關於具體劫持之後幹什麼, 請看下一條
cc_vue/src/Observer.js

class Observer {
  constructor(data) {
    // 我只是負責初始化
    this.data = data;
    this.observer(data);
  }
  /**
   * @method 針對對象進行觀察
   * @param { data } 要觀察的對象
   */
  observer(data) {
  // 循環拿出對象身上的所有值, 進行監控
    if (data && typeof data === 'object'&& !Array.isArray(data)) {
      for (let key in data) {
        this.defineReactive(data, key, data[key]);
      }
    }
  }
  /**
   * @method 進行雙向綁定,每個值以後的操作動作,都會反應到這裏.
   * @param { obj } 要觀察的對象
   * @param { key } 要觀察的對象
   * @param { value } 要觀察的對象
   */
  defineReactive(obj, key, value) {
   // 因爲data數據可能會很深, 所以必須遞歸
    this.observer(obj[key]);
    let _this = this;
    Object.defineProperty(obj, key, {
      configurable: true, // 可改變可刪除
      enumerable: true, // 可枚舉
      get() {
        return value;
      },
      set(newVal) {
        if (value !== newVal) {
          // 如果用戶傳進來的新值是個對象, 那就重新觀察他
          _this.observer(newVal);
          value = newVal;
        }
      }
    });
  }
}

當然要在index裏面啓動這個模塊
cc_vue/src/index.js

class C {
  constructor(options) {
    // 1: 不管你傳啥, 我都放進來, 方便以後的擴展;
    for (let key in options) {
      this['$' + key] = options[key];
    }

    // 2: 劫持data上面的操作
    new Observer(this.$data);
    // ....

六. 添加Watch與Dep

'訂閱發佈'屬於是vue比較核心的功能了, 這裏也稍微有一點繞, 大家一起慢慢梳理.
現在data數據的改動已經被劫持, 思路梳理如下:

  1. 我要知道每個數據變化的時候, 我要做什麼, 比如{{name}}, name變化的時候, 我要重新獲取到name對應的值, 渲染到頁面哪裏?
  2. 比如一個變量有很多地方需要渲染我怎麼辦

cc_vue/src/Watch.js
發佈訂閱, 這個類很簡單, 只是實現了兩個功能, 讓如隊列與執行隊列

export class Dep {
  constructor() {
    this.subs = []; // 把訂閱者全部放在這裏
  }
  /**
   * @method 添加方法進訂閱隊列.
   */
  addSub(w) {
    this.subs.push(w);
  }
  /**
   * @method 發佈信息,通知所有訂閱者.
   */
  notify() {
    this.subs.forEach(w => w.update());
  }
}

cc_vue/src/Watch.js
觀察者, 就是他稍微有點繞

export class Watcher {
// vm 實例
// expr 執行的表達式
// cb 回調函數, 也就是變量更新時執行的方法
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    // 這裏取一下當前的value, 以後每次變化都對比一下oldvalue, 防止無用的更新
    this.oldValue = this.getOld();
  }
  /**
   * @method 只有第一次的取值會調用他,對老值的記錄,以及被訂閱.
   */
  getOld() {
   // 他只會被調用一次
   // Dep是引用類型, 它身上的值當然可以傳遞
    Dep.target = this; // 這個this指的就是watch自己
    // 獲取到這個值當前的value
    let value = CompileUtil.getVal(this.vm, this.expr.trim());
    // 操作完要制空
    Dep.target = null;
    // 給oldvalue賦值
    return value;
  }
  /**
   * @method 更新值.
   */
  update() {
// 拿到新的value, 先比一比, 有變化再更新
    let newVal = CompileUtil.getVal(this.vm, this.expr.trim());
    if (newVal !== this.oldValue) {
    this.cb();
    }
  }
}

Dep.target = this; 這句是點睛之筆, 我們來使用一下

cc_vue/src/CompileUtil.js

// 在解析模板的時候添加一個watch
text(node, expr, vm) {
    let content = expr.replace(/\{\{(.+?)\}\}/g, ($0, $1) => {
      // 因爲模板只解析一次, 所以不用擔心他被重複new
      new Watcher(vm, $1, () => {
       // 這裏的callback就是具體的更新操作
        this.updater.textUpdater(node, this.getContentValue(vm, expr));
      });
      return this.getVal(vm, $1);
    });
    this.updater.textUpdater(node, content);
  },

getContentValue 獲取元素內的所有文本信息
有的人會問爲什麼要把文本信息全更新, 而不是隻獲取變化的文本, 那是因爲 很多時候我很會寫出這樣的代碼 <p>{{a}}--{{b}}</p>, 那我們無法只單獨改變b的樣子, 因爲我們操作的是 p標籤的textContent屬性

getContentValue(vm, expr) {
    return expr.replace(/\{\{(.+?)\}\}/g, ($0, $1) => {
      $1 = $1.trim();
      return this.getVal(vm, $1);
    });
  },

上面的代碼, 我們在解析text文本的時候放入了一個watch, 那麼這個watch被new的一瞬間會執行getOldvalue方法, 那麼就有了如下代碼
cc_vue/src/Observer.js

  defineReactive(obj, key, value) {
    this.observer(obj[key]);
    // 1: 劫持某一個值的時候, 創建一個dep實例
    let dep = new Dep();
    let _this = this;
    Object.defineProperty(obj, key, {
      configurable: true,
      enumerable: true,
      get() {
        // 2: 獲取值的時候, 查看Dep這個類上面是否有target參數
        // 這個參數是我們獲取oldval時候掛上去的watch類
        // 如果有的話, 調用把這個watch類放入訂閱者裏面
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      set(newVal) {
        if (value !== newVal) {
          _this.observer(newVal);
          value = newVal;
          // 3: 每次更新數據, 都執行發佈者
          dep.notify()
        }
      }
    });
  }

其實想一想dep與watch也可以寫成一個class, 但是寫成兩個更貼合設計模式.

實驗
新建第二個文件夾, 專門用來檢測雙向數據綁定
cc_vue/use/2:雙向綁定

 <div id="app">
          <p>n: {{n}} </p>
          <p>n+m: {{n+m}} </p>
      </div>
  let vm = new C({
    el: '#app',
    data: {
      n: 1,
      m: 2
    }
  });
// 每秒給變一下n的值, n只要在屏幕上跟着發生變化就是成功了
  setInterval(() => {
    vm.n += 1;
  }, 1000);

webpack方面配置調整一下

new HtmlWebpackPlugin({
      filename: 'index.html',
      template: path.resolve(__dirname, '../use/2:雙向綁定/index.html'),

有興趣的朋友可以試驗一下我的工程的效果,

end

這次實現的只是初步的綁定操作.
下一集:

  1. 實現vue3.0的proxy模式的綁定, 我還沒看vue3.0是怎麼設計的, 先用自己的理解實現一下, 然後再學習他們的方法, 也是爲了培養自己的思維.
  2. 篇幅足夠的話會手寫一個簡易的axios, 方便測試代碼.

大家都可以一起交流, 共同學習,共同進步, 早日實現自我價值!!

github:還沒有star,期待您的支持
個人技術博客:個人博客
更多文章,ui庫的編寫文章列表 文章地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章