(第五篇)仿写 ’Vue 生态 ’ 系列___” 解析模板事件 ”
本次任务
- 取消 ’eval’, 改为 ’new Function’.
- 支持用户使用 ’@’ 与 ’v-on’ 绑定各种事件.
- 支持初始化 ’methods’ 数据.
- 使用函数时可以传参与不传参, 可以使用 ’$event’.
- 实现 ’c-center’ 与 ’c-show’ 指令.
- 实现 ’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’ 就非常有趣了
- 控制 ’dom’ 的 ’display:none’ 属性, 为 ’true’ 的时候显示 , 为 ’false’ 的时候 ’dom’ 要消失.
- 这个属性不可以影响 dom 本身的行间样式, 比如用户定义的就是 ’none’, 当他为 ’true’ 的时候依然不可以显示 ’dom’ 元素.
- 这个属性不可以改变 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 要解决的问题形式
- add —> 直接调取.
- add() —> 括号调取.
- add() —> 夹杂空格.
- add(n, m, 9) —> 夹杂空格, 常量, 变量的传参.
- 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
这个框架刚刚做了一点点就已经出现很多性能问题了, 接下来我会针对取值问题进行一次深层次的优化, 想想还挺兴奋.
下一集:
- 优化取值.
- 添加 hook 生命周期钩子.
github: 链接描述
个人技术博客: 链接描述
更多文章,ui 库的编写文章列表 : 链接描述