如何从0一步步写出MVVM的框架vue(1)

从vue的的使用开始想,开始写

  1. vue的基本使用我想大家都会了,看下面的代码
 <div id="app">
        <input type="text" v-model="inputVal" style="width:600px"> 
        <br />
        这是输入框的值:{{inputVal}}
        <hr />
        我是{{person.name}},
        <br />
        我今年 {{person.age}}
        <ul>
            <li>das</li>
            <li>asd</li>
        </ul>
 </div>
<script src="../node_modules/vue/dist/vue.js"></script>
<script>
        new Vue({
            el:"#app",
            data:{
                inputVal: '输入框的值',
                person:{
                    name: 'jack',
                    age: 18
                }
            }
        })
</script>

最基本和重要的两点:

  • 数据的双向绑定(input值变,下面inputVal的值也变)
  • 模版渲染数据(将person的值渲染到了页面中去)
  1. 先不考虑这两点,我们该如何去实现我们的vue;
  • new Vue ,那说明vue一定是一个构造函数
  • 传递了两个参数(后期在扩充)el, data
    ok,那我们基于这两个点,先写一个构造函数vue
class Vue{
    constructor(options){
        this.$el = options.el;
        this.$data = options.data;
        if(this.$el){
            new Compiler(this.$el,this.$data,this)
        }
    }
}

显而易见,这里面就是一个简单的数据保存操作,然后我们需要新建一个编译类,这个类的目的就是为了编译,让vue这个基础类不做太多的逻辑处理,只是一个接受数据,来调度这个项目,便于维护,这是一个编程习惯吧!
ok,现在就去看看complier这个类怎么去做的。

  1. 创建好vue基类以后,去创建编译类方法,complier;

明确好这个complier类是要做什么

  • 把#app里面的dom节点全部转移到fragment这个文档随便中去
  • 编译我们的fragment里面的文档碎片的内容,将数据映射到视图
  • 再将fragment添加回#app中去;
    看代码
class Compiler{
    constructor(el,data,vm){
        this.$el = el;
        this.data = data;
        this.vm = vm;
        let node = this.isElementNode(this.$el) ? this.$el : document.querySelector(options.el)
        // 避免重绘回流,一次性将节点全部加载到文档碎片中去
        let fragment = this.nodeToFragment(node);
        // 编译
        this.compile(fragment)
        // 添加到文档中去
        node.appendChild(fragment)
    }

    // 判断用户传进来的是不是node节点
    isElementNode(el){
        return el.nodeType === 1;
    }
}

上面的代码注释已经很详细,大家又不懂的可以留言
上面这些代码完成了第一步的需求了,然后我们开始最重要的第二步;
这个compile函数也是最关键的,去看看compile做了什么把;

// 编译
    compile(fragment){
        [...fragment.childNodes].forEach((e)=>{
            if(e.nodeType === 1){
                // 因为里面还有子元素,所以还需要在遍历一遍
                this.compile(e);
                [...e.attributes].forEach((attr,index)=>{
                    // arrt是个对象
                    let { name ,value } = attr;
                    name.startsWith('v-') && this.handleElementNode(name,value,e);
                })
            }
            if(e.nodeType === 3){
                let reg = /\{\{.+?\}\}/;
                reg.test(e.textContent) && this.handleTextNode(e.textContent,e);
            }
        })
    }

接受转换好的fragment这个文档碎片进行遍历操作,
切记:这里面的fragment.childNodes是一个类数组,必须转为数组在能操作

然后我们进行判断,看他是一个元素节点还是一个文本节点。如果是一个元素节点,那我们继续递归,读出里面的子节点,直到文本节点为止;

  1. 如果是元素节点,那我们取下他们的属性,看看有没有v-开头的属性,我们取出来;
    提示:然后对属性再进行遍历,遍历的子节点是一个对象,对象,对象,真的很奇怪。。。,然后通过结构的方法我们拿到了v-model和后面的表达式;
    然后取出来我们在调用handleElementNode的方法转换,传入指令和指令后面的表达式,节点;等下看这个方法;

  2. 如果是文本节点,我们需要匹配到{{}} 这种差值表达式,那利用差值表达式即可,如果符合这种我们就调用handleTextNode方法,传入文本值和节点;

  3. handleElementNode && handleTextNode

 // 转换指令元素类
    handleElementNode(attr,val,node){
        let value = this.getVal(val);
        let [,dir] = attr.split('-');
        CompileUtil[dir](node,value)
    }

    // 转换差值表达式 文本类
    handleTextNode(val,node){
        let reg = /\{\{(.+?)\}\}/;
        let newVal = val.replace(reg, (...arg) => {
            return this.getVal(arg[1])
        });
        console.log(node)
        node.textContent = newVal;
    }

    // 根据传入的表达式获取data的数据
    getVal(expr){
        let data = this.data;
        return expr.split('.').reduce((pre,cur)=>{
            return pre[cur]
        },data)
    }

上面的代码很简单,获取值,然后赋值;

  • 就是getVal的方法不好理解,就是我们传入‘person.age’这样的表达式,然后去data上面去取值,显然我们需要先转为数组然后依次去取,那么reduce最好实现不过了,这个在lodash库中就是get方法,不理解也可以用数组循环做;

  • 还有一个replace方法,很显然我们有两个不同的正则表达式,
    let reg1 = /{{.+?}}/;
    let reg2 = /{{(.+?)}}/;
    下面就是比上面多一个括号,这个作用就是当我匹配到括号里面的内容的时候拿出来成为一个分组,我在match replace这种方法里面就能拿到分组,就是我们拿到了{{ person.name }}中的person.name;上面只是用来匹配这样的字符串,具体可以看replace方法,还可以看我git上面有一个关于正则的学习笔记地址

  • 在编译handleElementNode中有一个CompileUtil函数,我们在这里其实有一个CompileUtil工具函数,具体如下:

CompileUtil = {
   model(node,val){
       node.value = val;
   },
   html(){},
   text(){},
}

大家如果用过vue,那么一看就知道啥意思了,不做多解释了;


到现在为止,我们完成了第一步,data渲染成了view了,不信大家可以看看,html页面有没有渲染完成,然后我们进行比较重要的一步了,那就是数据双向绑定了;先歇一会把。在第二节接着去实现把,一口气打了这么多字有点小累了。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容