1.创建组件
// html
// 1.创建组件内容
<template id='userCardTemplate'>
<style type="text/css">
.container{
background: #eee;
border-radius: 10px;
width: 500px;
padding: 20px
}
</style>
<div class="container">
<p class="name" data-open='true'>{{name}}</p>
<p class="email">{{email}}</p>
<input type="text" v-model='message'>
<span>{{message}}</span>
<button class="button">Follow</button>
</div>
</template>
<user-card data-click="123"></user-card>
2.组件类
customElements是Window对象上的一个只读属性,接口返回一个CustomElementRegistry对象的引用,可用于注册新的custom elements,或者获取之前定义过的自定义元素的信息。
CustomElementRegistry.define()方法用来注册一个custom element,该方法接收以下参数:
1.表示所创建的元素名称符合DOMString标准的字符串。注意,custom element的名称不能是个单个单词,且其中必须要有短横线。
2.用于定义元素行为的类。
3.可选参数,一个包含extends属性的配置对象,是可选参数,它指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。
// js
class UserCard extends HTMLElement{
constructor(){
super();
var templateEle = document.getElementById('userCardTemplate');
var content = templateEle.content.cloneNode(true);
this.appendChild(content);
this._data={
name:'用户名',
email:'yourmail@some-email.com',
message:'双向'
};
}
}
window.customElements.define('user-card',UserCard);
效果图
3.解析
接下来要做的事情就是解析元素里面的子元素,看看里面是不是包含了{{}}这样的符号,并且要把中间的内容拿出来,和data里面的数据进行比对,如果对应上了,那么就把数据填充到这个里面
// 解析
compileNode(el){
let child = el.childNodes;// 获取到所有子元素
[...child].forEach((node)=>{ // 利用展开运算符直接转换成数组然后forEach
if(node.nodeType == 3){ // nodeType 获取节点类型 nodeType == 3; 代表元素或属性中的文本内容
let text = node.textContent;
// 匹配前面有两个{{,后面也有两个}}的这么一串文本
let reg = /\{\{\s*([^\s\{\}]+)\s*}\}/g;
if(reg.test(text)){ // 如果能找到这样的字符串
// 将里面的数据拿出来,比如‘name’
let $1 = RegExp.$1; // 正则表达式匹配的第一个子匹配(以括号为标志)字符串
// 看看数据里面有没有name这个东西,如果有,那就把数据里面name对应的值天道当前这个位置
this._data[$1]&&(node.textContent = text.replace(reg,this._data[$1]));
// 增加了事件监听,监听每一个匹配到的数据,并且再一次更新视图;
// 注意这里的e.detail是上面observe里面的自定义事件传过来的
this.addEventListener($1,(e)=>{
node.textContent = text.replace(reg,e.detail)
})
}
}else if(node.nodeType ==1){ // 实现数据双向绑定 nodeType == 1; 代表元素
let attrs = node.attributes; // 获取元素属性的集合
if(attrs.hasOwnProperty('v-model')){ // 判断是不是有这个属性
let keyname = attrs['v-model'].nodeValue; // 属性的值
node.value = this._data[keyname];
node.addEventListener('input',(e)=>{ // 如果有,监听事件,修改数据
this._data[keyname] = node.value ; //修改数据
})
}
if(node.childNodes.length>0){
this.compileNode(node) ; // 递归实现深度解析
}
}
})
}
效果图
4.实现数据视图绑定
// 实现数据视图绑定
observe(){
let _this = this;
/*
Proxy的意思是代理,其作用是可以拦截对象上的一个操作;用法如下:
通过new的方式创建对象,第一个参数是被拦截的对象,第二个参数是对象操作的描述。实例化后返回一个新的对象,当我们对这个新的对象进行操作时就会调用我们描述中对应的方法。
Proxy区别于Object.defineProperty:
Object.defineProperty只能监听到属性的读写,而Proxy除读写外还可以监听属性的删除,方法的调用等。
通常情况下我们想要监听数组的变化,基本要依靠重写数组方法的方式实现,这也是Vue的实现方式,而Proxy可以直接监听数组的变化。
Proxy是已非入侵的方式监管了对象的读写,而defineProperty需要按特定的方式定义对象的属性
*/
this._data = new Proxy(this._data,{ // 监听数据
set(obj,prop,value){ // 数据改变的时候会触发set方法
// 事件通知机制,发生改变的时候,通过自定义事件通知视图发生改变
let event = new CustomEvent(prop,{
detail: value // 注意这里我传了个detail过去,这样的话更新视图的时候就可以直接拿到新的数据
});
_this.dispatchEvent(event); // 自定义事件的触发
// Refect是一个内建的对象,用来提供法法拦截JavaScript的操作。Reflect不是一个函数对象,所以它是不可构造的,也就是说它不是一个构造器,你不能通过 new 操作符去新建或者将其作为一个函数去调用Reflect对象,Reflect的所有属性和方法都是静态的
return Reflect.set(...arguments); // 这里是为了确保修改成功,不写其实也么有关系
}
})
}
到这一步,我们可以实现修改数据的时候,视图也发生改变
window.customElements.define('user-card',UserCard);
let card = document.querySelector('user-card');
document.onclick = function(){
console.log('点击了');
card._data.name = "新数据"
}
效果图
5.处理事件
bindEvent(){
this.event = new popEvent({
obj: this,
popup: true
});
}
class popEvent{
constructor(option){
/*
* 接收四个参数:
* 1,对象的this
* 2,要监听的元素
* 3,要监听的事件,默认监听点击事件
* 4,是否冒泡
* */
this.eventObj = option.obj;
this.target = option.target || this.eventObj;
this.eventType = option.eventType || 'click';
this.popup = option.popup || false;
this.bindEvent();
}
bindEvent(){
let _this = this;
_this.target.addEventListener(_this.eventType,function(ev){
console.log(_this.eventType,'eventType')
let target = ev.target;
let dataset,parent,num,b;
popup(target);
function popup(obj){
if(obj === document){return false}
/*
HTMLElement.dataset属性允许无论实在读取模式和写入模式下访问在HTML或DOM中的元素上设置的所有自定义数据属性(data-*)集。
它是一个DOMString的映射,每个自定义数据属性的一个条目
*/
dataset = obj.dataset;
/*
Object.keys(obj)
参数:要返回其枚举自身属性的对象
返回值:一个表示给定对象的所有可枚举属性的字符串数组
*/
num = Object.keys(dataset).length;
parent = obj.parentNode;
if(num<1){
_this.popup && popup(parent);
num = 0;
}else {
for(b in dataset){
if(_this.eventObj.__proto__[b]){
_this.eventObj.__proto__[b].call(_this.eventOjb,{obj:obj,ev:ev,target:dataset[b],data:_this.eventOjb})
}
}
_this.popup && popup(parent);
}
}
})
}
}
完整代码
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<!-- 1.创建组件内容 -->
<template id='userCardTemplate'>
<style type="text/css">
.container{
background: #eee;
border-radius: 10px;
width: 500px;
padding: 20px
}
</style>
<div class="container">
<p class="name" data-open='true'>{{name}}</p>
<p class="email">{{email}}</p>
<input type="text" v-model='message'>
<span>{{message}}</span>
<button class="button">Follow</button>
</div>
</template>
<user-card data-click="123"></user-card>
<script type="module">
class popEvent{
constructor(option){
/*
* 接收四个参数:
* 1,对象的this
* 2,要监听的元素
* 3,要监听的事件,默认监听点击事件
* 4,是否冒泡
* */
this.eventObj = option.obj;
this.target = option.target || this.eventObj;
this.eventType = option.eventType || 'click';
this.popup = option.popup || false;
this.bindEvent();
}
bindEvent(){
let _this = this;
_this.target.addEventListener(_this.eventType,function(ev){
console.log(_this.eventType,'eventType')
let target = ev.target;
let dataset,parent,num,b;
popup(target);
function popup(obj){
if(obj === document){return false}
/*
HTMLElement.dataset属性允许无论实在读取模式和写入模式下访问在HTML或DOM中的元素上设置的所有自定义数据属性(data-*)集。
它是一个DOMString的映射,每个自定义数据属性的一个条目
*/
dataset = obj.dataset;
/*
Object.keys(obj)
参数:要返回其枚举自身属性的对象
返回值:一个表示给定对象的所有可枚举属性的字符串数组
*/
num = Object.keys(dataset).length;
parent = obj.parentNode;
if(num<1){
_this.popup && popup(parent);
num = 0;
}else {
for(b in dataset){
if(_this.eventObj.__proto__[b]){
_this.eventObj.__proto__[b].call(_this.eventOjb,{obj:obj,ev:ev,target:dataset[b],data:_this.eventOjb})
}
}
_this.popup && popup(parent);
}
}
})
}
}
class UserCard extends HTMLElement{
constructor(){
super();
var templateEle = document.getElementById('userCardTemplate');
var content = templateEle.content.cloneNode(true);
this.appendChild(content);
this._data={
name:'用户名',
email:'yourmail@some-email.com',
message:'双向'
};
this.compileNode(this); // 解析元素
this.observe(this._data); //监听数据
this.bindEvent(); //处理事件
this.addevent = this.__proto__;
}
bindEvent(){
this.event = new popEvent({
obj: this,
popup: true
});
}
// 实现数据视图绑定
observe(){
let _this = this;
/*
Proxy的意思是代理,其作用是可以拦截对象上的一个操作;用法如下:
通过new的方式创建对象,第一个参数是被拦截的对象,第二个参数是对象操作的描述。实例化后返回一个新的对象,当我们对这个新的对象进行操作时就会调用我们描述中对应的方法。
Proxy区别于Object.defineProperty:
Object.defineProperty只能监听到属性的读写,而Proxy除读写外还可以监听属性的删除,方法的调用等。
通常情况下我们想要监听数组的变化,基本要依靠重写数组方法的方式实现,这也是Vue的实现方式,而Proxy可以直接监听数组的变化。
Proxy是已非入侵的方式监管了对象的读写,而defineProperty需要按特定的方式定义对象的属性
*/
this._data = new Proxy(this._data,{ // 监听数据
set(obj,prop,value){ // 数据改变的时候会触发set方法
// 事件通知机制,发生改变的时候,通过自定义事件通知视图发生改变
let event = new CustomEvent(prop,{
detail: value // 注意这里我传了个detail过去,这样的话更新视图的时候就可以直接拿到新的数据
});
_this.dispatchEvent(event); // 自定义事件的触发
// Refect是一个内建的对象,用来提供法法拦截JavaScript的操作。Reflect不是一个函数对象,所以它是不可构造的,也就是说它不是一个构造器,你不能通过 new 操作符去新建或者将其作为一个函数去调用Reflect对象,Reflect的所有属性和方法都是静态的
return Reflect.set(...arguments); // 这里是为了确保修改成功,不写其实也么有关系
}
})
}
// 解析
compileNode(el){
let child = el.childNodes;// 获取到所有子元素
[...child].forEach((node)=>{ // 利用展开运算符直接转换成数组然后forEach
if(node.nodeType == 3){ // nodeType 获取节点类型 nodeType == 3; 代表元素或属性中的文本内容
let text = node.textContent;
// 匹配前面有两个{{,后面也有两个}}的这么一串文本
let reg = /\{\{\s*([^\s\{\}]+)\s*}\}/g;
if(reg.test(text)){ // 如果能找到这样的字符串
// 将里面的数据拿出来,比如‘name’
let $1 = RegExp.$1; // 正则表达式匹配的第一个子匹配(以括号为标志)字符串
// 看看数据里面有没有name这个东西,如果有,那就把数据里面name对应的值天道当前这个位置
this._data[$1]&&(node.textContent = text.replace(reg,this._data[$1]));
// 增加了事件监听,监听每一个匹配到的数据,并且再一次更新视图;
// 注意这里的e.detail是上面observe里面的自定义事件传过来的
this.addEventListener($1,(e)=>{
node.textContent = text.replace(reg,e.detail)
})
}
}else if(node.nodeType ==1){ // nodeType == 1; 代表元素
let attrs = node.attributes; // 获取元素属性的集合
if(attrs.hasOwnProperty('v-model')){ // 判断是不是有这个属性
let keyname = attrs['v-model'].nodeValue; // 属性的值
node.value = this._data[keyname];
node.addEventListener('input',(e)=>{ // 如果有,监听事件,修改数据
this._data[keyname] = node.value ; //修改数据
})
}
if(node.childNodes.length>0){
this.compileNode(node) ; // 递归实现深度解析
}
}
})
}
open(){
/*
template里面的代码:<p class="name" data-open="true">{{name}}</p>,这是实现事件指令当点击含有自定义属性:data-open的元素的时候,就可以触发组件里的open方法,并且在open方法里还能够得到任何你需要的参数。
*/
console.log("触发了open方法")
}
}
/*
customElements是Window对象上的一个只读属性,接口返回一个CustomElementRegistry对象的引用,可用于注册新的custom elements,或者获取之前定义过的自定义元素的信息。
CustomElementRegistry.define()方法用来注册一个custom element,该方法接收以下参数:
1.表示所创建的元素名称符合DOMString标准的字符串。注意,custom element的名称不能是个单个单词,且其中必须要有短横线。
2.用于定义元素行为的类。
3.可选参数,一个包含extends属性的配置对象,是可选参数,它指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。
*/
window.customElements.define('user-card',UserCard);
let card = document.querySelector('user-card');
card.addevent['click'] = function(){
console.log('触发了点击事件')
}
</script>
</body>
</html>