11

( 第五篇 )仿写'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库的编写文章列表 :链接描述


lulu_up
5.7k 声望6.9k 粉丝

自信自律, 终身学习, 创业者