( 第五篇 )仿寫'Vue生態'系列___"解析模板事件"

( 第五篇 )仿寫'Vue生態'系列___"解析模板事件"


本次任務

  1. 取消'eval', 改爲'new Function'.
  2. 支持用戶使用'@'與'v-on'綁定各種事件.
  3. 支持初始化'methods'數據.
  4. 使用函數時可以傳參與不傳參, 可以使用'$event'.
  5. 實現'c-center'與'c-show'指令.
  6. 實現'cc_cb'函數, 模板裏面也可以用if - else.

一. eval 與 Function

項目裏面的取值操作, 我之前一直採用的都是eval函數, 但是前段時間突然發現一個特別棒的函數Function, 下面我來演示一下他的神奇之處.

1. 可以執行字符串

 let fn1 = new Function('var a = 1;return a');
 console.log(fn1()); // 1

2. 可以傳遞參數
下面寫的name與age就是傳入函數的兩個參數,

let fn2 = new Function('name','age', ' return name+age');
console.log(fn2('lulu',24)); // lulu24

第二種傳參方式

let fn3 = new Function('name, age', ' return name+age');
console.log(fn3('lulu',24)); // lulu24

綜上我可以推斷, 他的原理是把最後一個參數當做執行體, 然後前面如果有參數就被當做新生成函數的參數.

3. 全局作用域
他執行的時候裏面的作用域是全局的, 就算在函數內部, 執行時候也取不到函數內部的值, 所以想要使用的值, 都需要我們手動傳進去.

// 報錯了, 找不到u
function cc(){
    let u = 777;
    let fn = new Function('var a = 5;console.log(u); return a');
    console.log(fn());
  }
 cc()
// 執行成功
function cc(){
    u = 777; // 直接掛在window上
    let fn = new Function('var a = 5;console.log(u); return a'); // 777
    console.log(fn()); // 5
  }
 cc()

我也試了一下, 裏面的var a 並不會污染全局, 放心使用吧;

把它介紹清楚了, 我就可以用它來替換之前寫的eval了
expression: 表達式, 例如 'obj[name].age'

getVal(vm, expression) {
    let result, __whoToVar = '';
    for (let i in vm.$data) {
      __whoToVar += `let ${i} = vm['${i}'];`;
    }
      __whoToVar = `${__whoToVar} return ${expression}`;
    result = new Function('vm', __whoToVar)(vm);
    return result;
  },

這裏以後還會改成一個公用的獲取變量的'池', 應該會下一章去做.

二. '@'與'v-on'

所謂指令當然是要綁定在元素的身上, 我們有一個compileElement方法來處理元素節點, 那麼正好利用他來讓我們分出一個指令處理模塊.
比如說指令, 本次我們來做v-show指令.
事件的話就是所有的原生事件.

compileElement(node) {
    let attributes = node.attributes;
    [...attributes].map(attr => {
      let name = attr.name,
        value = attr.value,
        obj = this.isDirective(name);
      if (obj.type === '指令') {
        CompileUtil.dir[obj.attrName] &&
          CompileUtil.dir[obj.attrName](
            this.vm,
            node,
            CompileUtil.getVal(this.vm, value),
            value
          );
      } else if (obj.type === '事件') {
        // 當前只處理了原生事件;
        if(CompileUtil.eventHandler.list.includes(obj.attrName)){
         CompileUtil.eventHandler.handler(obj.attrName,this.vm, node, value);
        }else{
          // eventHandler[obj.attrName] 這個事件不是原生掛載事件, 不能用handler 處理
        }
      }
    });
  }

上面有一個isDirective事件, 這個事件也是一個關鍵點.
我們現在分成四種形式.
判斷出類型, 切分出後面的指令名稱與參數, 返回給處理程序.

  isDirective(attrName) {
    if (attrName.startsWith('c-')) {
      return { type: '指令', attrName: attrName.split('c-')[1] };
    } else if (attrName.startsWith(':')) {
      return { type: '變量', attrName: attrName.split(':')[1] };
    } else if (attrName.startsWith('v-on:')) {
      return { type: '事件', attrName: attrName.split('v-on:')[1] };
    } else if (attrName.startsWith('@')) {
      return { type: '事件', attrName: attrName.split('@')[1] };
    }
    return {};
  }

cc_vue/src/CompileUtil.js
這裏面專門抽出一個指令處理模塊, 暫命名爲dir.
本次就以 c-html 與 c-show 爲例
c-html 顧名思義, 就是用戶傳一段html代碼, 然後我把它注入到dom結構中

dir: {
    html(vm, node, value, expr) {
    // 只有這樣一個操作就可以了, 沒有任何高深的東西
      node.innerHTML = value;
      // 這裏別忘了用watcher訂閱一下變化, 達到雙向綁定的效果.
      new Watcher(vm, expr, (old, newVale) => {
        node.innerHTML = newVale;
      });
    }
  },

熱身之後剩下的這個'c-center'與'c-show'就非常有趣了

  1. 控制'dom'的'display:none'屬性, 爲'true'的時候顯示 , 爲'false'的時候'dom'要消失.
  2. 這個屬性不可以影響dom本身的行間樣式, 比如用戶定義的就是'none', 當他爲'true'的時候依然不可以顯示'dom'元素.
  3. 這個屬性不可以改變dom本身的任何屬性, 但是優先級還要最高, 腦子裏一瞬間出現的竟然是'!important'.

綜上分析得出兩種方案:
第一種: 把所有外在因素全部考慮進來, 每次進行整體分析, 得出具體的結論到底是'block'還是'none' 也可能是 'flex' 與 'grid' 等等的.
第二種: 本次我想另闢蹊徑的方法, 動態插入'css'代碼, 這個想法挺有意思吧, 框架執行時, 先插入一段css代碼, 然後可以利用這個css做很多很多有趣的事, 這方面以後會有擴展.
獨立出一個插入'css'代碼的模塊.
單獨new一下
cc_vue/src/index.js

import CCStyle from './CCStyle.js';
class C {
  constructor(options) {
     for (let key in options) {
      this['$' + key] = options[key];
    }
    new CCStyle();
    // ...

cc_vue/src/CCStyle.js

class CCStyle {
  constructor() {
     // 我要把它插到最上, js裏面沒有插到第一個位置這樣的語句, 我只能獲取到第一個元素, 然後插在他的前面.
    let first = document.body.firstChild,
        style = document.createElement('style'); // 當然是做一個style標籤.
    // 這裏先定一個c-show的絕對隱藏屬性.
    style.innerText='.cc_vue-hidden{display:noneimportant}';
    // 放進去就生效了, 以後控制v-show就只需要爲元素添加與移除這個class名字就可以了.
    document.body.insertBefore(style, first);
  }
}

export default CCStyle;

上面的代碼明顯不符合設計模式, 我們來把它的'可擴展性'優化一下.

class CCStyle {
  constructor() {
    let first = document.body.firstChild,
      style = document.createElement('style'),
      typeList = this.typeList();
    // 不管具體的屬性是什麼, 我們只管在這裏面循環出來, 然後拼接上去,這裏我們自己壓縮一下他.
    for (let key in typeList) {
       style.innerText += `.${key}{${typeList[key]}}\n`;
    }
    document.body.insertBefore(style, first);
  }
// 這裏面我們可以分門別類的擴展很多屬性.
  typeList() {
     return {
      // 1: 控制元素隱藏的
      'cc_vue-hidden': 'display:none!important'

      // 2: 控制元素上下左右居中的
      'cc_vue-center':'display: flex;justify-content: center;align-items: center;'
    };
  }
}

export default CCStyle;

v-center 指令
cc_vue/src/CompileUtil.js

center(vm, node, value, expr) {
      value
        ? node.classList.remove('cc_vue-center')
        : node.classList.add('cc_vue-center');
      new Watcher(vm, expr, (old, newVale) => {
        newVale
          ? node.classList.remove('cc_vue-center')
          : node.classList.add('cc_vue-center');
      });
    }

c-show的原理與上面是一樣的

show(vm, node, value, expr) {
      value
        ? node.classList.remove('cc_vue-hidden')
        : node.classList.add('cc_vue-hidden');
      new Watcher(vm, expr, (old, newVale) => {
        newVale
          ? node.classList.remove('cc_vue-hidden')
          : node.classList.add('cc_vue-hidden');
      });
    },

三. methods 與 事件的綁定

methods 晚於 data定義, 在用戶出現重複定義的時候, 要給一個友好的提示.
cc_vue/src/index.js

class C {
  constructor(options) {
    // ...
    // proxyVm $data之後來處理$methods
    this.proxyVm(this.$methods, this, true);

綁定函數要稍作改變, 只要不傳target 就是與vm實例綁定, noRepeat是否檢測重複數據, 也就是報不報錯.

 proxyVm(data = {}, target = this, noRepeat = false) {
    for (let key in data) {
      if (noRepeat && target[key]) { // 防止data裏面的變量名與其他屬性重複
        throw Error(`變量名${key}重複`);
      }
      Reflect.defineProperty(target, key, {
        enumerable: true, // 描述屬性是否會出現在for in 或者 Object.keys()的遍歷中
        configurable: true, // 描述屬性是否配置,以及可否刪除
        get() {
          return Reflect.get(data, key);
        },
        set(newVal) {
          if (newVal !== data[key]) {
            Reflect.set(data, key, newVal);
          }
        }
      });
    }
  }

處理好methods的數據了, 就要處理事件的綁定了.
分配的邏輯之前已經展示過了

// 如果事件列表裏面有這個事件, 那麼就綁定這個事件.
if(CompileUtil.eventHandler.list.includes(obj.attrName)){
   CompileUtil.eventHandler.handler(obj.attrName,this.vm, node, value);
}

cc_vue/src/CompileUtil.js
專門處理事件的模塊

  eventHandler: {
     // 這個選項用來維護可處理的原生事件, 下面只是舉例並不全面.
    list: [
      'click',
      'mousemove',
      'dblClick',
      'mousedown',
      'mouseup',
      'blur',
      'focus'
    ],
    // 確定含有事件時進行的操作
    handler(eventName, vm, node, type) {
      // ...
     }
    }
  }

handler要解決的問題形式

  1. add ---> 直接調取.
  2. add() ---> 括號調取.
  3. add( ) ---> 夾雜空格.
  4. add(n, m, 9) ---> 夾雜空格,常量,變量的傳參.
  5. add(n, $event) ---> 用戶想要獲取事件對象$event.

那我們就來分步處理這幾種情況吧.

 handler(eventName, vm, node, type) {
    // 第一步: 匹配一個是否含有'()';
      if (/\(.*\)/.test(type)) {
        // 第二步: 把'()'裏面的內容拿出來
        let str = /\((.*)\)/.exec(type)[1];
        // 去除空格
        str = str.replace(/\s/g, '');
        // 以"("分割, 取到事件名字
        type = type.split('(')[0];
        // '()'裏面有內容才進行這一步;
        if (str) {
        // 第三步: 參數化'組'
          let arg = str.split(',');
          // 第四部: 綁定事件與解析參數
          node.addEventListener(
            eventName,
            e => {
            // 循環這個參數組
              for (let i = 0; i < arg.length; i++) {
                // 這樣就做到了$event的映射關係
                arg[i] === '$event' && (arg[i] = e);
              }
              vm[type].apply(vm, arg);
            },
            false
          );
          return;
        }
      }
      // 第二步: 不帶括號的直接掛就行了
      node.addEventListener(
        eventName,
        () => {
          vm[type].call(vm); // this肯定指向vm, 畢竟用戶要使用$data等等屬性
        },
        false
      );
    }

上面沒有對參數爲$data上的變量的情況時做處理, 因爲沒有太大的必要, 以後寫到 c-for的時候, 會着重的改寫一下這邊的邏輯.

四. 在模板內使用if

我們使用vue開發的時候, 只允許在模板中使用表達式, 這次我玩的這個項目, 允許用戶使用任何形式去寫, 當然了這樣有一些性能之類的弊端, 但是爲了好玩, 什麼我都願意嘗試, 摒棄了return出值的寫法, 採取了callback的模式.
關鍵字 cc_cb(value) value就是要傳出來的值.
用法如下:

<div>
{{ 
  if(n > 3){
    cc_cb(n) 
  }else{
    cc_cb('n小於等於3')
  };
}}
</div>

其實這種功能並不複雜, 只是書寫起來挺討厭的, 而且太太太違背設計模式了.
只需要改變getVal函數

  getVal(vm, expression) {
    let result,
      __whoToVar = '';
    for (let i in vm.$data) {
      __whoToVar += `let ${i} = vm['${i}'];`;
    }
    // 檢測到存在cc_cb被調用的情況時
    if (/cc_cb/.test(expression)) {
         // 無非就是把返回的值, return出來
      __whoToVar = `let _res;function cc_cb(v){ _res = v;}${__whoToVar}${expression};return _res`;
    } else {
      __whoToVar = `${__whoToVar} return ${expression}`;
    }
    result = new Function('vm', __whoToVar)(vm);
    return result;
  },

嘿嘿僅需小小的改動, 就做到了這麼神奇的事情.

end

這個框架剛剛做了一點點就已經出現很多性能問題了, 接下來我會針對取值問題進行一次深層次的優化, 想想還挺興奮.

下一集:

  1. 優化取值.
  2. 添加hook生命週期鉤子.

github:鏈接描述
個人技術博客:鏈接描述
更多文章,ui庫的編寫文章列表 :鏈接描述

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