继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

用原生js实现自定义组件,Vue3双向绑定

睡魔的谎言
关注TA
已关注
手记 81
粉丝 3
获赞 45

目标:用原生js实现自定义组件,Vue3双向绑定

学前知识储备:

必备知识1,自定义元素(customElement)

废话不多,先上代码:

//html: <user-card data-open="true"></user-card> //javascript: class Learn extends HTMLElement{     constructor(props) {         super(props);         console.log(this.dataset);         this.innerHTML = '这是我自定义的元素';         this.style.border = '1px solid #899';         this.style.borderRadius = '3px';         this.style.padding = '4px';     } } window.customElements.define('user-card',Learn); 复制代码

效果: http://img2.mukewang.com/5faca4f00001893f14140484.jpg 解析:通过window.customElements方法可以创建自定义元素,里面的define方法就是用来指定自定义元素的名称,以及自定义元素对应的类。

这里有一个细节,自定义元素中间一定要用中划线隔开,不然是无效的。

这时候在这个类里面就可以定义元素里的所有内容了,这和Vue里面的组件已经比较类似了,有了这个基础之后我们再往里面去进行拓展就可以实现组件了。

必备知识2,Proxy

这家伙估计大家都知道,Vue3数据响应的核心,Vue2用的是Object.defineProperty; 很强大,很好用,先来个简单的代码:

let obj = {     a:2938,     b:'siduhis',     item:'name' } obj = new Proxy(obj,{     set(target, p, value, receiver) {         console.log('监听到',p,'被修改了,由原来的:',target[p],'改成了:',value);     } }); document.onclick = ()=>{     obj.item = 'newValue'; } 复制代码

效果: http://img1.mukewang.com/5faca4f000011b6500010001.jpg 这个如果深入去讲的话有很多可以讲,比如说修改值的时候会触发set方法,读取值的时候会触发get方法等等,具体的大家去看看官网文档会更好。

必备知识3,事件代理

首先,我利用事件代理去处理组件中的事件,主要是写起来方便,拓展也很方便,先来看个最简单版本的事件代理:

//html <ul class="list">     <li class="item" data-first="true">这是第一个</li>     <li class="item">2222</li>     <li class="item">three</li>     <li class="item" data-open="true">打开</li>     <li class="item">这是最后一个</li> </ul> //javascript let list = document.querySelector('.list'); list.onclick = function(ev){     let target = ev.target;     console.log('点击了'+target.innerHTML); } 复制代码

效果: http://img1.mukewang.com/5faca4f10001f6a700170024.jpg 这是最简单版本,在ul身上绑定了点击事件,利用事件冒泡原理,点击任何一个li都会触发其父级ul的点击事件,通过ul的事件也可以反向找到被精确点击的li元素,从而把相应的li的内容打印出来,怎么样,很简单吧~

你可能注意到了上面代码中,有两个li的身上有data自定义属性,这个一会有用

再来看一个升级版本,在这里,可以通多判断li身上不同的属性,从而去执行不同的函数,这样的话就有点语法糖的意思了:

let eventfn = function(ev){     let target = ev.target;     let dataset = target.dataset;     for(b in dataset){         if(eventfn[b]){             eventfn[b]({obj:target,parent:this});         }     } } eventfn.first = function(){     console.log('点击了第一个,并且传了一些参数', arguments); } eventfn.open = function(){     console.log('点击了打开'); } list.onclick = eventfn; 复制代码

在这里,我去获取了被点击元素的data属性,然后看看这个属性有没有对应的事件函数,如果有,则执行,并且传递一些参数进去,这个参数以后可能会用到,这是一个拓展点。到这里,我们事件处理基本就成型了

第一步,创建组件内容

思路分析:

  • 1, 内容最好是直接写在页面上,然后需要填数据的地方用{{}}包起来

  • 2, template标签可以用来包裹模板,并且不会被显示在页面上

  • 3, 在组件里复制template里的内容作为组件的内容,并且解析里面的{{}}

  • 4, 还需要解析里面的各种指令,比如data-open这代表一个open事件

http://img2.mukewang.com/5faca4f10001542b11220452.jpg 这是效果图 上代码:

<template id="userCardTemplate">     <style>         .image {             width: 100px;         }         .container {             background: #eee;             border-radius: 10px;             width: 500px;             padding: 20px;         }     </style>     <img src="img/bg_03.png" class="image">     <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> 复制代码

第二步,开始写组件类

通过template的id获取到里面的内容,然后直接丢到组件里面,并且定义好数据:

class UserCard extends HTMLElement {     constructor() {         super();         var templateElem = document.getElementById('userCardTemplate');         var content = templateElem.content.cloneNode(true);         this.appendChild(content);         this._data = {             name:'用户名',             email:'yourmail@some-email.com',             message:'双向'         }     } } window.customElements.define('user-card',UserCard); 复制代码

这时候吧user-card这个元素往页面上丢,得到的效果就是这样的了:

第三步,解析

那么接下来要做的事情就是解析元素里面的子元素,看看里面是不是包含了{{}}这样的符号,并且要把中间的内容拿出来,和data里面的数据进行比对,如果对应上了,那就把数据填充到这个地方就可以了,说起来简单,做起来还是有一定难度的,这里面会用到正则匹配,于是我在class里写了这个么个方法:

compileNode(el){     let child = el.childNodes;//获取到所有的子元素     [...child].forEach((node)=>{//利用展开运算符直接转换成数组然后forEach         if(node.nodeType === 3){//判断是文本节点,于是直接正则伺候             let text = node.textContent;             let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g;             //大概的意思就是匹配前面有两个{{,后面也有两个}}的这么一串文本             if(reg.test(text)){//如果能找到这样的字符串                 let $1 = RegExp.$1;//那就把里面的内容拿出来,比如‘name’                 this._data[$1] && (node.textContent = text.replace(reg,this._data[$1]));//看看数据里面有没有name这么个东西,如果有,那就把数据里面name对应的值填到当前这个位置。             };         }     }) } 复制代码

把这个方法丢到constructor里面运行一下就可以了,得到效果:

第四步,实现数据视图绑定

到这里,还是只简单的把数据渲染到了页面上,如果数据再次发生变化,我们还没有找到通知机制让视图发生改变,怎么办呢? 这时候就需要用到Proxy了。这里还需要配合自定义事件,先来看Proxy部分,这里其实很简单,增加一个方法就可以了:

observe(){     let _this = this;     this._data = new Proxy(this._data,{//监听数据         set(obj, prop, value){//数据改变的时候会触发set方法             //事件通知机制,发生改变的时候,通过自定义事件通知视图发生改变             let event = new CustomEvent(prop,{                 detail: value//注意这里我传了个detail过去,这样的话更新视图的时候就可以直接拿到新的数据             });             _this.dispatchEvent(event);             return Reflect.set(...arguments);//这里是为了确保修改成功,不写其实也没关系         }     }); } 复制代码

事件通知有了,但是需要在解析函数里面监听一下事件,以便视图及时作出改变:

compileNode(el){     let child = el.childNodes;//获取到所有的子元素     [...child].forEach((node)=>{//利用展开运算符直接转换成数组然后forEach         if(node.nodeType === 3){//判断是文本节点,于是直接正则伺候             let text = node.textContent;             let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g;             //大概的意思就是匹配前面有两个{{,后面也有两个}}的这么一串文本             if(reg.test(text)){//如果能找到这样的字符串                 let $1 = RegExp.$1;//那就把里面的内容拿出来,比如‘name’                 this._data[$1] && (node.textContent = text.replace(reg,this._data[$1]));//看看数据里面有没有name这么个东西,如果有,那就把数据里面name对应的值填到当前这个位置。                 //增加了事件监听,监听每一个匹配到的数据,并且再一次更新视图                 //注意这里的e.detail是上面observe里面的自定义事件传过来的                 this.addEventListener($1,(e)=>{                     node.textContent = text.replace(reg,e.detail)                 })             };         }     }) } 复制代码

到这一步,我们就可以实现修改数据的时候,视图也发生改变了:

let card = document.querySelector('user-card'); document.onclick = function(){     console.log('点击了');     card._data.name = '新的用户名'; } 复制代码

第五步,实现双向绑定

估计你也看到了,我在template里面写了一个输入框,并且输入框上面还带了一个属性:v-model="message" 所以估计你也猜到我要做什么了,怎么做呢? 其实很简单: 在解析内容的时候,判断一下input元素,并且看看它身上是不是有v-model属性,如果有,监听它的input事件,并且修改数据。

再次修改解析函数:

compileNode(el){     let child = el.childNodes;     [...child].forEach((node)=>{         if(node.nodeType === 3){             let text = node.textContent;             let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g;             if(reg.test(text)){                 let $1 = RegExp.$1;                 this._data[$1] && (node.textContent = text.replace(reg,this._data[$1]));                 this.addEventListener($1,(e)=>{                     node.textContent = text.replace(reg,e.detail)                 })             };         }else if(node.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);//递归实现深度解析             }         }     }) } 复制代码

第六步,处理事件

先来看看完整的组件代码:

class UserCard extends HTMLElement {     constructor() {         super();         var templateElem = document.getElementById('userCardTemplate');         var content = templateElem.content.cloneNode(true);         this.appendChild(content);         this._data = {//定义数据             name:'用户名',             email:'yourmail@some-email.com',             message:'双向'         }         this.compileNode(this);//解析元素         this.observe();//监听数据         this.bindEvent();//处理事件     }     bindEvent(){         this.event = new popEvent({             obj:this,             popup:true         });     }     observe(){         let _this = this;         this._data = new Proxy(this._data,{             set(obj, prop, value){                 let event = new CustomEvent(prop,{                     detail: value                 });                 _this.dispatchEvent(event);                 return Reflect.set(...arguments);             }         });     }     compileNode(el){         let child = el.childNodes;         [...child].forEach((node)=>{             if(node.nodeType === 3){                 let text = node.textContent;                 let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g;                 if(reg.test(text)){                     let $1 = RegExp.$1;                     this._data[$1] && (node.textContent = text.replace(reg,this._data[$1]));                     this.addEventListener($1,(e)=>{                         node.textContent = text.replace(reg,e.detail)                     })                 };             }else if(node.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(){         console.log('触发了open方法');     } } 复制代码

可以发现在这里面多了两个方法,一个是bindEvent,没错,这个就是用来处理事件的了,方法的代码在下面,结合着第三个必备知识点去看就能看懂了。

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){             let target = ev.target;             let dataset,parent,num,b;             popup(target);             function popup(obj){                 if(obj === document){ return false;}                 dataset = obj.dataset;                 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.eventObj,{obj:obj,ev:ev,target:dataset[b],data:_this.eventObj});                         }                     }                     _this.popup && popup(parent);                 }             }         })     } } 复制代码

另外一个就是open方法,这个方法是干嘛用的呢?再回过头去看看template里面的代码:<p class="name" data-open="true">{{name}}</p>这一串是不是很熟悉,猜到我想做什么了么?

没错,实现事件指令

当点击含有自定义属性:data-open的元素的时候,就可以触发组件里的open方法,并且在open方法里还能够得到任何你需要的参数。: http://img3.mukewang.com/5faca4f200015b1e17920466.jpg 点击用户名的时候,触发了open方法。

完整代码奉上,注意代码最后的小细节哦~

<!DOCTYPE html> <html> <head>     <meta charset="UTF-8">     <title>Title</title>     <style>     </style> </head> <body>     <template id="userCardTemplate">         <style>             .image {                 width: 100px;             }             .container {                 background: #eee;                 border-radius: 10px;                 width: 500px;                 padding: 20px;             }         </style>         <img src="img/bg_03.png" class="image">         <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){                 let target = ev.target;                 let dataset,parent,num,b;                 popup(target);                 function popup(obj){                     if(obj === document){ return false;}                     dataset = obj.dataset;                     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.eventObj,{obj:obj,ev:ev,target:dataset[b],data:_this.eventObj});                             }                         }                         _this.popup && popup(parent);                     }                 }             })         }     }          class UserCard extends HTMLElement {         constructor() {             super();             var templateElem = document.getElementById('userCardTemplate');             var content = templateElem.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;             this._data = new Proxy(this._data,{                 set(obj, prop, value){                     let event = new CustomEvent(prop,{                         detail: value                     });                     _this.dispatchEvent(event);                     return Reflect.set(...arguments);                 }             });         }         compileNode(el){             let child = el.childNodes;             [...child].forEach((node)=>{                 if(node.nodeType === 3){                     let text = node.textContent;                     let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g;                     if(reg.test(text)){                         let $1 = RegExp.$1;                         this._data[$1] && (node.textContent = text.replace(reg,this._data[$1]));                         this.addEventListener($1,(e)=>{                             node.textContent = text.replace(reg,e.detail)                         })                     };                 }else if(node.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(){             console.log('触发了open方法');         }     }     window.customElements.define('user-card',UserCard);     let card = document.querySelector('user-card');     card.addevent['click'] = function(){         console.log('触发了点击事件!');     } </script> </body> </html>


作者:Mr_无忧


打开App,阅读手记
1人推荐
发表评论
随时随地看视频慕课网APP