初步研究下虚拟dom
实现思路:
1、获取dom的js对象
将指定的dom字符串(指定的dom字符串是指书写dom代码时的字符串)在页面加载前转换为js对象(以下这个对象统称为domTree),对象格式参考后面代码domTree定量。
2、创建虚拟dom
将获取到的domTree进行递归遍历,判断对象的结构及元素创建对应的dom,然后将dom赋给domTree.el
3、添加监听
添加监听前将虚拟dom挂载到页面,之后为domTree添加监听,使用Object.defineProperty('', '', {get: ()=>{}, set: ()=>{}})方法拦截虚拟dom在内部get、set中做拦截后的操作(不要将所有属性都加入监听,监听些常用的值就行,越少越好,比如className、value)。使用new MutationObserver()方法拦截监听页面真实dom的变化。这时的真实dom跟domTree.el在内存中的地址是一样的,所以真实dom和domTree.el已经关联。
代码
实现效果如下:在图片中第一个输入框输入值,点击button按钮,下面输入框会获取值,并调用监听方法。
点击后效果如下:(只触发了
Object.defineProperty监听,因为没有直接操作加入监听的dom)
html 结构
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script type="module" src="./main.js"></script>
</head>
<body>
<input type="text" id="changeValue" placeholder="修改input的值" />
<button id="getValue">获取input的值</button>
<div id="app">
</div>
</body>
</html>
因为使用了import、export模块化语法,script标签类型为type="module"。直接打开html是无法访问引用的文件。可以使用vscode的Live Server插件,安装完成后在html页面右键点击Open with live Server就可以了。
main.js 入口
/**
* createVdom 创建虚拟dom和真实dom
* domTree dom树
*/
import {createVdom, domTree} from './vDom.js';
// inputBlur设置input元素change后赋值给domTree.data.value。searchMark 封装递归查询标记
import {inputBlur, searchMark} from './outher.js';
// 渲染后执行
window.onload = () => {
const domId = 'app'; // 主要 dom id
let app = document.getElementById(domId); // 获取dom元素
createVdom(app); // 创建虚拟dom
inputBlur();
let changeValue = document.getElementById('changeValue');
let getValue = document.getElementById('getValue');
let custom_value = '';
changeValue.onchange = (e) => {
custom_value = e.target.value;
}
getValue.onclick = () => {
let key = ['data', 'class'];
let value = 'vnode-input';
let vnode = searchMark(domTree, key, value); // 查询指定键、值对应的dom
console.log(vnode);
vnode[0].data.value = custom_value;
}
}
vDom.js 虚拟dom
/**
* 定义虚拟dom并创建真实dom
*/
// Object.defineProperty 数据拦截
import Observer from './observer.js';
// new MutationObserver() dom监听
import {domObserver} from './mutationObserver.js'
// searchMark 递归查询
import {searchMark} from './outher.js';
// domTree 转换比较繁琐,这里直接使用定量
export const domTree = {
tag: 'div',
data: {class: 'text-div', id: 'dinBox'},
key: '',
children: [
{
tag: 'p',
data: '',
key: '',
children: [
{
tag: 'span',
data: '',
key: '',
children: [],
text: 'p标签,'
},
{
tag: 'b',
data: '',
key: '',
children: [],
text: '<b标签>'
},
{
tag: '',
data: '',
key: '',
children: [],
text: 'class属性为text-p。'
}
],
text: '??'
},
{
tag: 'h3',
data: '',
key: '',
children: [],
text: '<h3标签>'
},
{
tag: 'input',
data: {
class: 'vnode-input',
value: 'input'
},
key: '',
children: [],
text: ''
},
],
text: ''
}
/**
* 监听domtree中el属性以外的值
* 在页面上做的操作增删改查都是js进行的一些操作,dom监听MutationObserver只监听input标签对domtree.data的value属性值进行更改
* 其他操作更改直接操作domtree的el树并更新domtree.data属性,实现操作虚拟dom更新页面(虚拟dom数据与真实dom使用的数据池子是相同的)
*/
// 主要方法创建、监听真实dom,监听domTree数据
export function createVdom(app) {
createEl(app);
domObserver(app); // 监听真实dom
new Observer(domTree, setDomTree, getDomTree); // 数据添加监听,dom渲染后监听,否则class无法获取
}
// 拦截获取回调
function getDomTree(value, _mark) {
// console.log('获取:', value);
}
// 拦截更改回调
function setDomTree(obj, key, newVal, _mark) {
console.log('修改值的父亲对象:', obj);
console.log('值的键名:', key);
console.log('更改后的值:', newVal);
console.log('_mark唯一标识:', _mark);
let attrAry = [
{data: 'value', el: 'value'},
{data: 'class', el: 'className'}
];
let vnode = searchMark(domTree, ['el', '_mark'], _mark);
attrAry.forEach(item => {
if(key === item.data) vnode[0].el[item.el] = newVal;
})
}
// 创建真实dom
function createEl(app) {
app.innerHTML = '';
app.appendChild(getVdom(domTree));
}
function vnode(tag, data, key, children, text) {
return {tag, data, key, children, text};
}
let dom_level = 0;
function getVdom (vnode, level) {
let {tag, data, key, children, text} = vnode;
dom_level = Number((dom_level + Math.random() + 0.00000001).toFixed(8));
level = dom_level;
if (typeof tag == 'string' && tag) {
vnode.el = document.createElement(tag);
vnode.el.textContent = text;
for(let key in data) vnode.el.setAttribute(key, data[key]);
children.forEach((item, index) => vnode.el.appendChild(getVdom(item, level)))
}
else vnode.el = document.createTextNode(text);
vnode.el._mark = (tag || '#text') + '_' + (level + '').replace('.', '');
return vnode.el;
}
outher.js 大杂烩
/**
* 其他通用的js都放在这里面
*/
// input输入框值改变给value属性赋值,让domObserver监听
export function inputBlur() {
let _input = document.getElementsByTagName('input');
for(let item of _input) {
item.onchange = () => {
item.setAttribute('value', item.value);
}
}
}
/**
* 回调查找domTree字段
* vnode: 数据 object
* key: 键值 array
* value: 要查询的值 any
* 返回值为array类型
*/
export function searchMark (vnode, key, value) {
let domAry = [];
let search = (vnode, key, value) => {
let _vnode = vnode;
key.forEach(key => {
_vnode = _vnode[key];
})
if (_vnode == value) domAry.push(vnode);
else vnode.children.forEach(item => search(item, key, value));
}
search(vnode, key, value);
return domAry;
}
observer.js 数据监听拦截
/**
* 数据拦截监听
* 主要使用 Object.defineProperty 递归循环执行,给domTree的el值外每个值添加了set,get函数
*/
class Observer {
// data 必须是对象
constructor(data, setCallback, getCallback) {
if (setCallback) this.setCallback = setCallback;
if (getCallback) this.getCallback = getCallback;
this._mark = false;
this.objFor(data);
}
objFor(data) {
this._mark = data.el ? data.el._mark : this._mark; // 记录 el._mark 标记
for(let key in data) this.observer(data, key, data[key], this._mark);
}
observer(obj, key, value, _mark) { //data为对象 , 此处只针对对象处理
let isRelease = true;
if(key == 'el') isRelease = false;
if(value instanceof Object && isRelease) {
this.objFor(value);
return false;
}else if(value instanceof Array) {
value.forEach(item => this.objFor(item));
return false;
}
//劫持
Object.defineProperty(obj, key, {
enumerable: false, //是否可遍历
configurable: false, //是否可更改编写
get: () => { //获取值时走这里
this.getCallback(value, _mark)
return value;
},
set: (newVal) => { //设置值走这里
// 如果与上次值不一样则每次赋新值
if(newVal !== value) value = newVal;
this.setCallback(obj, key, newVal, _mark);
}
})
}
}
export default Observer;
mutationObserver.js dom监听拦截
// 引入domTree主要对domTree.data做更改,操作真实dom的value更改的是domTree.el上的value。每次更改dom后做判断,如果该的是value,则调用typeAttributes递归函数
import {domTree} from './vDom.js';
const config = { attributes: true, childList: true, subtree: true }; // 观察器的配置(需要观察什么变动)
// 当观察到变动时执行的回调函数
const callback = function(mutationsList, observer) {
for(let mutation of mutationsList) {
mutationCallback(mutation);
}
};
const observer = new MutationObserver(callback); // 创建一个观察器实例并传入回调函数
// 监听dom放到导出
export function domObserver(app) {
// 以上述配置开始观察目标节点
observer.observe(app, config);
};
function mutationCallback(mutation) {
switch (mutation.type) {
case 'childList':
typeChildList(mutation, domTree);
break;
case 'attributes':
// 只判断了value属性更改的监听
if(mutation.attributeName == 'value') typeAttributes(mutation, domTree);
break;
}
}
function typeChildList(mutation, vnode) {
// console.log('节点变更', mutation);
}
function typeAttributes(mutation, vnode) {
if(vnode.el._mark == mutation.target._mark) {
console.log('属性更改', vnode);
vnode.data[mutation.attributeName] = mutation.target[mutation.attributeName];
return false;
}else{
vnode.children.forEach(item => {
typeAttributes(mutation, item);
})
}
}