Цель: реализация пользовательских компонентов с помощью собственного 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);
Эффект:Разобрать: пройти
window.customElementsметод для создания пользовательских элементов, внутриdefineМетод используется для указания имени пользовательского элемента и класса, соответствующего пользовательскому элементу.
Здесь есть одна деталь: середина пользовательского элемента должна быть отделена тире, иначе он будет недействителен.
В настоящее время все содержимое элемента может быть определено в этом классе, который похож на компонент в Vue.После того, как у нас есть эта основа, мы можем расширить ее внутри, чтобы реализовать компонент.
Базовые знания 2, Прокси
Все знают этого парня.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';
}
Эффект:Можно многое сказать, если углубиться в это, например, метод 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);
}
Эффект:Это самый простой вариант, т.
ulСобытие клика привязывается к телу, используя принцип всплытия событий, нажмите на любойliвызовет его родителяulсобытие клика, черезulСобытие также можно отменить, чтобы найти точный щелчокliэлементы, тем самым размещая соответствующиеliКонтент распечатывается, как насчет этого, это очень просто~
Вы могли заметить, что в приведенном выше коде есть дваliНа теле есть пользовательский атрибут данных, это будет полезно
Давайте посмотрим на обновленную версию Здесь вы можете судить о различных атрибутах 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;
Здесь я получаю атрибут данных выбранного элемента, а затем смотрю, есть ли соответствующая функция события для этого атрибута. Если да, то выполняю ее и передаю некоторые параметры. Этот параметр можно использовать позже. Это точка расширения. На данный момент наша обработка событий в основном сформирована
Первым шагом является создание содержимого компонента.
Анализ мыслей:
- 1 контент лучше писать прямо на странице, а потом использовать то место, где нужно заполнить данные
{{}}заворачивать - 2, тег шаблона может использоваться для переноса шаблона и не будет отображаться на странице.
- 3, Скопируйте содержимое в шаблоне как содержимое компонента в компоненте и проанализируйте {{}} внутри
- 4, Также необходимо разобрать различные инструкции внутри, такие как
data-openЭто открытое мероприятие
это рендеринг
Над кодом:
<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>
Второй шаг — начать писать класс компонента
Контент внутри получается через 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 на страницу, и эффект такой:
Третий шаг – анализ
Затем следующее, что нужно сделать, это проанализировать подэлементы в элементе, чтобы увидеть, содержит ли он такие символы, как {{}}, и вынуть содержимое в середине и сравнить его с данными в данных, если соответствующие Это легко сказать, но все равно сложно сделать, здесь будет использоваться обычное сопоставление, поэтому я написал этот метод в классе:
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. Здесь нам также нужно взаимодействовать с пользовательскими событиями.Давайте сначала рассмотрим часть прокси.На самом деле здесь все очень просто.Просто добавьте метод:
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 = '新的用户名';
}
Пятый шаг — добиться двусторонней привязки
Как вы, возможно, заметили, я написал поле ввода в шаблоне, и поле ввода также имеет атрибут:v-model="message"Так что я думаю, вы можете догадаться, что я собираюсь сделать, как это сделать? на самом деле очень просто:При синтаксическом анализе содержимого оцените входной элемент и посмотрите, есть ли у него атрибут v-model.Если да, прослушайте его входное событие и измените данные.
Снова измените функцию синтаксического анализа:
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);
}
}
})
}
}
Другой - открытый метод.Для чего используется этот метод? Вернитесь назад и посмотрите на код в шаблоне:<p class="name" data-open="true">{{name}}</p>Эта строка не очень знакома, угадайте, что я хочу сделать?
Правильно, реализовать директиву события
Если клик содержит настраиваемые атрибуты:data-openКогда элемент открыт, вы можете вызвать метод open в компоненте, и вы можете получить любые параметры, которые вам нужны, в методе open. :При щелчке имени пользователя запускается метод open.
Предоставляется полный код, обратите внимание на мелкие детали в конце кода~
<!DOCTYPE html>
<html lang="en">
<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>
Наконец
Просто расслабься~