【高能】自己写一个Vue => 纯原生js实现Vue的双向数据绑定和渲染

自己实现一个简化版的Vue关于数据的操作(双向绑定, 插值表达式, 虚拟节点)

  • 编译模块

  • 虚拟节点模块

  • 渲染模块

  • 数据响应模块(Vue的双向数据绑定)

  • Vue构造函数模块

编译模块

提供一个compile函数, 将一个模板文本和环境对象编译成一个结果

function compile(template, envObj)

例如:

const result = compile("英雄名称: {{ name }}, 英雄职业: {{ job }}, 英雄大招: {{ skills.powerSkill }}", {
    name: '男枪',
    job: 'ADC',
    skills: {
        powerSkill: '终极爆弹'
    }
})
//  编译结果为 英雄名称: 男枪, 英雄职业: ADC, 英雄大招: 热情爆弹

注意我开始了

笔者新建了一个compile.js

// compile.js 代码

/**
 * 根据环境和模板对象, 得到要渲染的编译结果
 * @param {模板} template 
 * @param {环境对象} envObj 
 */
export default function compile(template, envObj) {
    // 提取template中的插值表达式 
    const frags = getFragments(template);
    console.log(frags);
    // 我们可能需要先保存一下这个template, 当我们将里面的插值表达式都替换以后直接返回result
    let result = template;
    frags.forEach(frag => {
        // 每遍历一次, 就将当次循环的插值表达式拿去跟result进行比对并进行替换
        let matchValue = getEnvValue(frag, envObj);
        result = result.replace(frag, matchValue); 
    })

    // result保存了最终的编译结果
    return result;
}

// 提供一个getFragments方法, 该方法接收一个模板, 并将模板中所有插值表达式提取出来并且返回
// 如 "英雄名称: {{ name }}, 英雄职业: {{ job }}, 英雄大招: {{ skills.powerSkill }}"
// 调用该函数以后 返回 [ {{name}}, {{ job }}, {{ skills.powerSkill }} ], 如果没有检查到
// 插值表达式, 则返回空数组
const getFragments = function(template) {
    // 正则匹配{{  }}
    return template.match(/{{ [^}]+ }}/g) || [];
}

// 提供一个getEnvValue方法, 提供一个插值表达式和一个环境对象, 返回该表达式在环境对象中对应的值
const getEnvValue = function(frag, envOvj) {
    // 将插值表达式中的变量真正的提取出来
    const variableExp = frag.replace('{{', "").replace("}}", "").trim();
    // 变量可能带.因此我们用split分割将它变成数组
    const props = variableExp.split('.');
    let result = envObj; // result为最后要返回出去的值
    // 遍历props数组, 开始取值
    // 每循环一次, 就将result的值重新赋值知道找到最后一个变量为止
    props.forEach(variable => result = result[variable]);
    return result || ''; 
}

至此, 编译模块compile.js就具备了将Vue的模板语法解析成正常文本的能力

虚拟节点模块

提供一个函数createVNode, 根据真实的dom对象构建出虚拟dom树

function createVNode(realDom);

例如:

<div id = '#app'>
    <p>{{ name }}</p>
    <p>{{ age }}</p>
</div>
const node = createVNode(document.querySelector('#app'));

然后我输出node 他要给我一个虚拟节点对象, 如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JG2vWeWU-1581684076229)('...')]

上方是虚拟节点对象, 实际上他是一棵树型结构, 如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XZvscmzZ-1581684076231)('..')]

注意我开始了

话不多说新建一个vNode.js

// 看上方的图打印出来的对象是一个对象前面还带一个vNode名字, 那么毋庸置疑这肯定是一个构造函数
// 构造虚拟节点的类, 
// realDom -> 真实dom  template -> 文字模板(只有遇到了文本节点才会安排template),
// 如果一直是dom节点那么我们要一直创建虚拟节点
class VNode {
    constructor(realDom, template) {
        this.realDom = realDom;
        this.template = template;
        this.children = []; // 存放虚拟子节点的数组, 因为在一个真实dom中会有很多子节点, 我们将这些子节点映射成虚拟dom放进children数组中, 整体形成一个树一样的结构 
    }
}

// 我们暴露出去一个createNode方法, 外界一调用这个方法必定可以创建一个虚拟dom树出来
// 该方法接收一个参数realDom -> 真实dom
export default function createVNode(realDom) {
    const root = new VNode(realDom, ''); // 将根节点创建一个虚拟dom,先姑且将它的template着设置为''空,

    // 来判断传进来的真实节点是不是文本节点如果是的话 那么就要给他的template设置值
    if(realDom.nodeType === Node.TEXT_NODE) {
        root.template = realDom.nodeValue;
    }else {
        // 如果不是文本节点, 直接开始循环真实dom的子节点
        for(let i = 0, len = realDom.childNodes.length; i < len; i++) {
            let realChildDom = realDom.childNodes[i];
            // 直接递归创建虚拟子节点
            let childNode = createVNode(realChildDom);
            root.children.push(childNode); // 同时将该节点push进它虚拟父节点的children数组
        }

    }
}

至此, 虚拟节点模块vNode.js就具备了将真实dom映射出虚拟dom的能力

渲染模块

用于提取虚拟节点中的文本节点, 将起模板编译结果设置到真实dom中, 对虚拟节点的子节点也做同样的操作

function render(vNode, envObj)

这个模块的功能就是渲染页面都没啥好说的, 直接开干

新建一个render.js

import compile from './compile.js'; // 导入我们写的编译模块, 笔者是放在同一级目录下

// 渲染函数, 接收一个vNode -> 虚拟节点, envObj -> 环境对象
export default function render(vNode, envObj) {
    // 如果传进来的vNode是文本节点, 我们直接开始编译, 并将编译结果直接赋值进去
    if(vNode.realDom.nodeType === Node.TEXT_NODE) {
        const result = compile(vNode.template, envObj);
        // 如果两次的节点值一样则直接不渲染
        if(result === vNode.realDom.nodeValue)return;
        vNode.realDom.nodeValue = result;
    }else {
        如果不是文本节点, 那就找子节点中的子节点, 如此递归
        for(let i = 0, len = vNode.children.length; i < len; i++) {
            // 如果不是文本节点, 直接继续找
            render(vNode.children[i], envObj);
        }
    }
}

如果以后需要重新渲染的话直接调用render就好

至此, 渲染模块render.js就具备了根据环境对象和虚拟节点渲染页面的能力

数据响应模块

主要负责将原始对象的数据附加到代理对象上, 代理对象能够监听到数据的更改, 当数据更改的时候, 执行某个回调函数

也就是Vue的核心功能双向绑定

function createResponsive(originalObj, targetObj, callback); 

*前方高能~~~~*

新建一个dataResponsive.js


/**
 * 将原始对象中所有的属性都提取到代理对象中来进行访问控制
 * @param {原始对象} originalObj 
 * @param {代理对象} proxyObj 
 * @param {当代理对象上的属性被重新赋值以后触发的回调} callback 
 */
export default function createResponsive(originalObj, proxyObj, callback) {
    for(let prop in originalObj) {
        proxyProp(originalObj, prop, proxyObj, callback);
    }
}

/**
 * 代理某个属性
 * @param {原始对象} originalObj 
 * @param {该属性} prop 
 * @param {代理对象} proxyObj 
 * @param {当代理对象上的该属性被重新赋值以后触发的回调} callback 
 */
const proxyProp = function(originalObj, prop, proxyObj, callback) {
    // 当需要感知的是一个对象
    if(typeof originalObj[prop] === 'object') {
        let newTarget = {}; // 定义一个新的代理对象
        createResponsive(originalObj[prop],newTarget, callback); // 递归调用自身
        Object.defineProperty(proxyObj, prop, {
            get() {
                // 当然不要忘记对这个对象也要进行代理, 但是访问的时候就不能返回originalObj[prop]
                // 了, 要返回newTarget, 因为当我们访问originalObj[prop]这个对象中某个属性的时候
                // 也会触发originalObj[prop]的访问, 而如果这里返回的不是newTarget那么自然也就监控
                // 不到newTarget中属性的变化了
                return newTarget;
            },
            set(newVal) {
                originalObj[prop] = newVal;
                newTarget = newVal;
                typeof callback === 'function' && callback(prop);
            }
        })
    }else {
        // 如果就是简单的原始值
        Object.defineProperty(proxyObj, prop, {
            get() {
                return originalObj[prop];
            },
            set(newVal) {
                // console.log('更改属性')
                originalObj[prop] = newVal;
                // 当修改值的时候触发callback回调
                typeof callback === 'function' && callback(prop);
            }
        })

    }
}

至此, 数据响应模块dataResponsive.js就具备了监听数据变化的能力

Vue构造函数模块

通过Vue构造函数可以创建一个Vue的实例, 在创建的过程中, 需要完成以下操作

  1. 保存el和data配置

  2. 根据el创建虚拟节点

  3. 将data中的数据附加到代理对象-vue实例中

有了之前的这些辅助函数以后我们Vue的构造函数会变得相当易写

import createVNode from './vNode.js';
import createResponsive from './dataResponsive.js'; 
import render from './render.js';

Vue.prototype.$render = render; // 将render方法注入到Vue的原型上
let _uid = 0;
export default function Vue(config) {
    if(!config) {
        throw new Error('expected a initail config');
    }
    // 保存el和data配置
    this.$el = config.el;
    this.$data = config.data;
    this._isVue = true;
    this._uid = ++ _uid;
    // 根据el来创建虚拟节点
    const realDom = document.querySelector(this.$el);
    console.log(realDom, this.$el, document.querySelector('#app'));
    this.$vnode = createVNode(realDom);
    // 初始渲染一次
    this.$render(this.$vnode, this.$data);
    // 将data中的数据附加到代理对象 - vue实例中
    createResponsive(this.$data, this, () => {
        // 只要代理数据一改变就触发这个回调
        console.log(this.$render);
        this.$render(this.$vnode, this.$data);
    })

}

至此, 构造函数Vue完成

试验

我们创建一个文件然后导入我们自己写好的Vue框架, 并且尝试接管html文档中的某块区域

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Vue</title>
</head>

<body>
    <div id="app">
        <p>英雄名称:{{ name }}</p>
        <p>英雄职业:{{ job }}</p>
        <p>英雄大招:{{ skills.powerSkill }}</p>
    </div>
    <script type="module">
    import Vue from './Vue.js';
    window.vm = new Vue({
        el: '#app',
        data: {
            name: '男枪',
            job: 'ADC',
            skills: {
                powerSkill: '终极爆弹'
            }
        }
    })
    </script>
</body>

</html>

页面成功渲染, 而Vue实例也成功建立

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HT9xBRkJ-1581684076231)(''')]

上方完成的仅仅是Vue的双向数据绑定等一些功能, 来日可能会封装一些Vue更加高深的功能, 笔者也会尝试看Vue的源码, 希望朋友有想法可以交流共同进步

发布了33 篇原创文章 · 获赞 11 · 访问量 2240

猜你喜欢

转载自blog.csdn.net/weixin_44238796/article/details/104319054