美文网首页自己动手实现一个简易版Vue
自己动手写一个简易版本Vue(一)

自己动手写一个简易版本Vue(一)

作者: 李徐安 | 来源:发表于2021-07-24 16:15 被阅读0次

Vue由于其高效的性能和灵活入门简单、轻量的特点下变得火热。在当今前端越来越普遍的使用,今天来动手写一下Vue

主要实现
1.构建Vue实例查找指令和模板
2.数据驱动界面更新
3.界面驱动数据更新
4.事件相关指令
5.Vuex基本实现
6.Vue-Router基本实现

一、构建Vue实例

大家至此应该已经在项目中使用Vue,在此就不过多讲解Vue使用,直接从0开始尽量模仿Vue的语法,首先我们看一下简单的Vue案例

<body>
<div id="app">
    <input type="text" v-model="name">
    <p>{{ name }}</p>
    <p>{{age}}</p>
    <ul>
        <li>6</li>
        <li>6</li>
        <li>6</li>
    </ul>
</div>
<!--1.下载导入Vue.js-->
<script src="js/vue.js"></script>
<script>
    // 2.创建一个Vue的实例对象
    let vue = new Vue({
        // 3.告诉Vue的实例对象, 将来需要控制界面上的哪个区域
        el: '#app',
        // 4.告诉Vue的实例对象, 被控制区域的数据是什么
        data: {
            name: "张三",
            age : '18'
        }
    });
    console.log(vue.$el);
    console.log(vue.$data);
</script>
</body>
  1. 通过观察我们得知,要想使用Vue必须先创建Vue的实例, 创建Vue的实例通过new来创建, 所以说明Vue是一个类
    所以我们要想使用自己的Vue,就必须定义一个名称叫做Vue的类

  2. 只要创建好了Vue的实例, Vue就会根据指定的区域和数据, 去编译渲染这个区域
    所以我们需要在自己编写的Vue实例中拿到数据和控制区域, 去编译渲染这个区域
    注意点: 创建Vue实例的时候指定的控制区域可以是一个ID名称, 也可以是一个Dom元素
    注意点: Vue实例会将传递的控制区域和数据都绑定到创建出来的实例对象上
    $el/$data

  3. 来到我们自己创建的js文件,简单命名为lue.js

Lue.js文件
class Lue {
    constructor(options){
        // 1.保存创建时候传递过来的数据
        if(this.isElement(options.el)){
            this.$el = options.el;
        }else{
            this.$el = document.querySelector(options.el);
        }
        this.$data = options.data;
        // 2.根据指定的区域和数据去编译渲染界面
        if(this.$el){
            new Compiler(this)
        }
    }
    // 判断是否是一个元素
    isElement(node){
        return node.nodeType === 1;
    }
}
class Compiler {
    constructor(vm){
        this.vm = vm;
    }
}

首先在第一步判断传递的数据是否是元素,如果是则直接赋值给this.$el,不是则通过选择器查找一下再赋值。然后再把外面传递的data赋值给this.$data
接下来在根据指定数据,指定的区域,去编译渲染到界面中,专门定义一个Compiler类用于编译渲染。

接下来就可以使用了,只需要导入我们刚刚创建的Lue.js文件,将Vue替换Lue即可

 let vue = new Lue({
       el: '#app',
       data: {
            name: "张三",
            age : '18'
        }
    });
    console.log(vue.$el);
    console.log(vue.$data);

二、提取元素到内存

在后面实现数据驱动界面时,数据发生改变,就替换元素。在js每次操作dom时都会对DOM进行一次重排,所谓重排也就是当元素的大小,位置结构发生变化的时候,就会引起浏览器对当前页面的结构进行一次重新的计算,这是非常耗费浏览器性能的。
虚拟DOM的出现很好的解决了这一问题,而js中的文档碎片就类似于虚拟DOM
可以使用文档碎片(Fragment),
类似于虚拟DOM的思想,在内存中先开辟出一块地方,对于所有节点的操作都在内存中先完成,完成后再一次更新到页面中,优化了浏览器的性能。

Compiler编译渲染的类
class Compiler {
    constructor(vm){
        this.vm = vm;
        // 1.将网页上的元素放到内存中
        let fragment = this.node2fragment(this.vm.$el);
        console.log(fragment);
        // 2.利用指定的数据编译内存中的元素
        // 3.将编译好的内容重新渲染会网页上
    }
    node2fragment(app){
        // 1.创建一个空的文档碎片对象
        let fragment = document.createDocumentFragment();
        // 2.编译循环取到每一个元素
        let node = app.firstChild;
        while (node){
            // 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
            fragment.appendChild(node);
            node = app.firstChild;
        }
        // 3.返回存储了所有元素的文档碎片对象
        return fragment;
    }
}

三、编译指令和模板数据

将网页中的元素放到内存中后,接下就是利用指定的数据编译内存中的元素。

class Compiler {
    constructor(vm){
        this.vm = vm;
        // 1.将网页上的元素放到内存中
        let fragment = this.node2fragment(this.vm.$el);
        // 2.利用指定的数据编译内存中的元素
        this.buildTemplate(fragment);
        // 3.将编译好的内容重新渲染会网页上
        this.vm.$el.appendChild(fragment);
    }
    node2fragment(app){
        // 1.创建一个空的文档碎片对象
        let fragment = document.createDocumentFragment();
        // 2.编译循环取到每一个元素
        let node = app.firstChild;
        while (node){
            // 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
            fragment.appendChild(node);
            node = app.firstChild;
        }
        // 3.返回存储了所有元素的文档碎片对象
        return fragment;
    }
    buildTemplate(fragment){
        //为了实现遍历,通过解构赋值将伪数组转成数组
        let nodeList = [...fragment.childNodes];
        nodeList.forEach(node=>{
            // 需要判断当前遍历到的节点是一个元素还是一个文本
            // 如果是一个元素, 我们需要判断有没有v-model属性
            // 如果是一个文本, 我们需要判断有没有{{}}的内容
            if(this.vm.isElement(node)){
                // 是一个元素
                this.buildElement(node);
                // 处理子元素(处理后代)
                this.buildTemplate(node);
            }else{
                // 不是一个元素
                this.buildText(node);
            }
        })
    }
    buildElement(node){
        let attrs = [...node.attributes];
        attrs.forEach(attr => {
            let {name, value} = attr; // v-model="name" / name:v-model / value:name
            if(name.startsWith('v-')){ // v-model / v-html / v-text / v-xxx
                let [_, directive] = name.split('-'); // v-model -> [v, model]
                CompilerUtil[directive](node, value, this.vm);
            }
        })
    }
    buildText(node){
        let content = node.textContent;
        let reg = /\{\{.+?\}\}/gi;
        if(reg.test(content)){
            console.log('是{{}}的文本, 需要我们处理', content);
        }
    }
}

buildTemplate方法中,通过fragment.childNodes可以拿到内存中所有的子节点,然后遍历判断是否时元素还是文本。
但是子节点中可能包含多个子节点,所以需要再次递归遍历下

 // 处理子元素(处理后代) this.buildTemplate(node);

如果是元素则来到buildElement方法中。在遍历元素的属性,name为属性名,value为属性值。再判断是否包含v-,包含的话代表是vue指令,取出指令赋值给directive。

CompilerUtil[directive](node, value, this.vm);,调用工具对象的方法给节点赋值。

接着看工具CompilerUtil的实现

let CompilerUtil = {
    //根据属性名称获取值 vm:vue实例对象,value : 指令的值,(v-model = "name",值就是name)
    getValue(vm, value){
        // time.h --> [time, h] ,https://www.jianshu.com/p/e375ba1cfc47
       return value.split('.').reduce((data, currentKey) => {
            // 第一次执行: data=$data, currentKey=time
            // 第二次执行: data=time, currentKey=h
            return data[currentKey];
        }, vm.$data);
    },
    /*
    node : 当前节点
    value : 指令的值,(v-model = "name",值name)
    vm : lue实例对象
     */
    model: function (node, value, vm) { // value=time.h
        /*node.value = vm.$data[value]; // vm.$data[time.h] --> vm.$data[time] --> time[h]*/
        let val = this.getValue(vm, value);
        node.value = val;
    },
    html: function (node, value, vm) {
        let val = this.getValue(vm, value);
        node.innerHTML = val;
    },
    text: function (node, value, vm) {
        let val = this.getValue(vm, value);
        node.innerText = val;
    }
}

在getValue(vm, value)方法中又有注意点,此时我们修改下html代码

<body>
<div id="app">
    <input type="text" v-model="name">
    <input type="text" v-model="time.h">
    <input type="text" v-model="time.m">
    <input type="text" v-model="time.s">
    <div v-html="html">abc</div>
    <div v-text="text">123</div>
    <p>{{ name }}</p>
    <p>{{age}}</p>
    <p>{{time.h}}</p>
    <p>{{name}}-{{age}}</p>
    <ul>
        <li>6</li>
        <li>6</li>
        <li>6</li>
    </ul>
</div>
<script>
    // 2.创建一个Vue的实例对象
    // let vue = new Vue({
    let vue = new Lue({
        // 3.告诉Vue的实例对象, 将来需要控制界面上的哪个区域
        el: '#app',
        // el: document.querySelector('#app'),
        // 4.告诉Vue的实例对象, 被控制区域的数据是什么
        data: {
            name: "张三",
            age: 18,
            time: {
                h: 11,
                m: 12,
                s: 13
            },
            html: `<div>我是div</div>`,
            text: `<div>我是div</div>`
        }
    });
</script>
</body>

在data中可能有多层数据,也就是对象类似的。

<input type="text" v-model="time.h">
<input type="text" v-model="time.m">
<input type="text" v-model="time.s">
time: {
   h: 11,
   m: 12,
   s: 13
 },

所以在CompilerUtil的model方法中仅仅使用 node.value = vm.$data[value]不行的,所以抽取一个getValue方法,通过reduce遍历,取出data.time.h的值

  //根据属性名称获取值 vm:vue实例对象,value : 指令的值,(v-model = "name",值就是name)
    getValue(vm, value){
        // time.h --> [time, h] ,https://www.jianshu.com/p/e375ba1cfc47
       return value.split('.').reduce((data, currentKey) => {
            // 第一次执行: data=$data, currentKey=time
            // 第二次执行: data=time, currentKey=h
            return data[currentKey];
        }, vm.$data);
    },

元素中带有v-model指令就能够实现绑定数据了。接下来在看插值语法绑定模板数据。

    buildText(node){
        let content = node.textContent;
        //看看是否匹配{{name}}(插值语法)
        let reg = /\{\{.+?\}\}/gi;
        if(reg.test(content)){
            CompilerUtil['content'](node, content, this.vm);
        }
    }

通过正则表达式匹配{{}},匹配到后再调用CompilerUtil工具对象的content方法。

    content: function (node, value, vm) {
        // console.log(value); // {{ name }} -> name -> $data[name]
        let val = this.getContent(vm, value);
        node.textContent = val;
    }
    getContent(vm, value){
        // console.log(value); //  {{name}}-{{age}} ->张三-{{age}}  -> 张三-18
        // (.+?) 取出值
        let reg = /\{\{(.+?)\}\}/gi;
        let val = value.replace(reg, (...args) => {
            // 第一次执行 args[1] = name
            // 第二次执行 args[1] = age
            // console.log(args);
            return this.getValue(vm, args[1]); // 张三, 18
        });
        // console.log(val);
        return val;
    },

注意点:可能数据是{{name}--{{age}},那么则用replace方法依次查找替换。

附上本章节lue.js代码

let CompilerUtil = {

    /*
       node : 当前接单
       value : 指令的值,(v-model = "name",值name)
       vm : Lue实例对象
    */
    //根据属性名称获取值 vm:vue实例对象,value : 指令的值,(v-model = "name",值就是name)
    getValue(vm, value){
        // time.h --> [time, h]
       return value.split('.').reduce((data, currentKey) => {
            // 第一次执行: data=$data, currentKey=time
            // 第二次执行: data=time, currentKey=h
            return data[currentKey.trim()];
        }, vm.$data);
    },
    getContent(vm, value){
        // console.log(value); //  {{name}}-{{age}} -> 张三-{{age}}  -> 张三-18
        // (.+?) 取出值
        let reg = /\{\{(.+?)\}\}/gi;
        let val = value.replace(reg, (...args) => {
            // 第一次执行 args[1] = name
            // 第二次执行 args[1] = age
            // console.log(args);
            return this.getValue(vm, args[1]); // 张三, 18
        });
        // console.log(val);
        return val;
    },
    model: function (node, value, vm) {
        let val = this.getValue(vm, value);
        node.value = val;
    },
    html: function (node, value, vm) {
        let val = this.getValue(vm, value);
        node.innerHTML = val;
    },
    text: function (node, value, vm) {
        let val = this.getValue(vm, value);
        node.innerText = val;
    },
    content: function (node, value, vm) {
        // console.log(value); // {{ name }} -> name -> $data[name]
        let val = this.getContent(vm, value);
        node.textContent = val;
    }
}
class Lue {
    constructor(options){
        // 1.保存创建时候传递过来的数据
        if(this.isElement(options.el)){
            this.$el = options.el;
        }else{
            this.$el = document.querySelector(options.el);
        }
        this.$data = options.data;
        // 2.根据指定的区域和数据去编译渲染界面
        if(this.$el){
            new Compiler(this);
        }
    }
    // 判断是否是一个元素
    isElement(node){
        return node.nodeType === 1;
    }
}
class Compiler {
    constructor(vm){
        this.vm = vm;
        // 1.将网页上的元素放到内存中
        let fragment = this.node2fragment(this.vm.$el);
        // 2.利用指定的数据编译内存中的元素
        this.buildTemplate(fragment);
        // 3.将编译好的内容重新渲染会网页上
        this.vm.$el.appendChild(fragment);
    }
    node2fragment(app){
        // 1.创建一个空的文档碎片对象
        let fragment = document.createDocumentFragment();
        // 2.编译循环取到每一个元素
        let node = app.firstChild;
        while (node){
            // 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
            fragment.appendChild(node);
            node = app.firstChild;
        }
        // 3.返回存储了所有元素的文档碎片对象
        return fragment;
    }
    buildTemplate(fragment){
        //为了实现遍历,通过结构赋值将伪数组变为数组
        let nodeList = [...fragment.childNodes];
        nodeList.forEach(node=>{
            // 需要判断当前遍历到的节点是一个元素还是一个文本
            if(this.vm.isElement(node)){
                // 是一个元素
                this.buildElement(node);
                // 递归,处理子元素(处理后代)
                this.buildTemplate(node);
            }else{
                // 不是一个元素
                this.buildText(node);
            }
        })
    }
    buildElement(node){
        let attrs = [...node.attributes];
        attrs.forEach(attr => {
            let {name, value} = attr; // v-model="name" / name:v-model / value:name
            if(name.startsWith('v-')){ // v-model / v-html / v-text / v-xxx
                let [_, directive] = name.split('-'); // v-model -> [v, model]
                CompilerUtil[directive](node, value, this.vm);
            }
        })
    }
    buildText(node){
        let content = node.textContent;
        //看看是否匹配{{name}}(插值语法),'.+?' :至少一位数,'g':全局匹配,'i':是忽略大小写
        let reg = /\{\{.+?\}\}/gi;
        if(reg.test(content)){
            CompilerUtil['content'](node, content, this.vm);
        }
    }
}

本章节就暂时结束,下一章讲解利用 Object.defineProperty,和发布订阅者模式实现数据驱动界面更新

文章主要代码参考李南江web前端课程
其他参考链接:
https://www.cnblogs.com/suihang/p/9491359.html
https://segmentfault.com/a/1190000016434836

相关文章

网友评论

    本文标题:自己动手写一个简易版本Vue(一)

    本文链接:https://www.haomeiwen.com/subject/zdxvmltx.html