前端虚拟dom学习

232 阅读5分钟

初步研究下虚拟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在内部getset中做拦截后的操作(不要将所有属性都加入监听,监听些常用的值就行,越少越好,比如className、value)。使用new MutationObserver()方法拦截监听页面真实dom的变化。这时的真实dom跟domTree.el在内存中的地址是一样的,所以真实dom和domTree.el已经关联。

代码

实现效果如下:在图片中第一个输入框输入值,点击button按钮,下面输入框会获取值,并调用监听方法。 image.png 点击后效果如下:(只触发了Object.defineProperty监听,因为没有直接操作加入监听的dom) image.png

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>

因为使用了importexport模块化语法,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);
        })
    }
}