实现简单vue2

71 阅读9分钟

mvvm的雏形的实现

原理:数据并且数据在页面显示,改变数据时,同时也改变在页面显示的数据。

使用的原理是Object.defineProperty(get...);里的set方法,一旦数值改变就执行跟新的函数。

流程图:

    //我有一个数据
    let obj={}
    //对数据进行拦截从而执行响应的处理的函数
    //响应的数据都是放入到这个函数里边的
    function defineReactive(obj,key,val){

        Object.defineProperty(obj,key,{
            get(){
                console.log("获取get的"+val);
                return val;
            },
            set(newValue){
                console.log("设置val的操作"+newValue);
                val=newValue;
                //每次数据发生改变就更新页面数据,执行update()函数就是更新数据
                update();

            }
        });

    }
    defineReactive(obj,"foo","默认值");//定义一个响应式属性foo
    //响应视图
    function update(){
        document.getElementById("app").innerHTML=obj.foo;//响应式属性在页面中被刷新的操作
    }
    update();//执行就会在页面添加值。vm驱动
    obj.foo=new Date().toLocaleString();
    //如果设置一个定时器,在这个定时器里边每一秒就对这个变量赋值一个新的时间
    setInterval(()=>{
        obj.foo=new Date().toLocaleString();
    },1000);

对象中会有多个key属性的处理

上述只解决了一个key属性的处理方法,如果是多个key属性的话,我们就需要用循环来进行处理了。

定义一个方法用来处理多个对象中的key。单个也能够处理的方法。

如下代码:

        //多个属性的对象
        function observer(obj){
        //首先判断这个obj必须是一个对象
            if(typeof obj=="object" && obj!=null){
                //对obj的属性进行循环,然后依次添加上响应式函数的
                Object.keys(obj).forEach((item)=>{
                    console.log("make ...");
                    // console.log(item);
                    defineReactive(obj,item,obj[item]);
                })
            }
        }

        //给这数据添加多个属性,然后放入到observer里边进行响应式特性增加
        let obj={};
        obj.name="zhangsan";
        obj.age=30;
        obj.avatar="url";
        obj.other={a:1};
        obj.other.aihao="篮球";
        obj.other.tiZhong=155;
        observer(obj);

解读深度响应式的添加

响应式只是添加到第一层,从第二层开始就不是响应式了。所以需要解决这个问题。

深度响应式的解决。

只需要再一次进行判断,如果是对象那么就再调用本方法就可以。只有是值才是直接添加响应式的。

上述代码中的修改,如下:

        //多个属性的对象
        function observer(obj){
        //首先判断这个obj必须是一个对象
            if(typeof obj=="object" && obj!=null){
                //对obj的属性进行循环,然后依次添加上响应式函数的
                Object.keys(obj).forEach((item)=>{
                    //对当前值判断,是否是对象,如果是对象需要再一次调用本方法进行响应式添加,当然自己也要添加响应式
                    if(typeof obj[item]=="object" && obj[item]!=null){
                        observer(obj[item]);
                    }
                    defineReactive(obj,item,obj[item]);
                })
            }
        }

如果后续赋值对象,对象里边的属性进行响应式添加

其实只要在set语句中进行修改就可以,如下代码:

set(newValue){
  console.log("设置val的操作"+newValue);
  //设置值时如果是对象,则对新值中的属性进行响应式添加
  if(typeof newValue=="object" && newValue!=null){
    observer(newValue);
  }
  val=newValue;
  update();
}

this.$set 或者Vue.set的原理

我们的数据在后面添加的属性是不具备响应式的,因为并没有做响应式的处理。

但是我们可以通过this.$set和Vue.set的来实现。也就是执行这个方法就是在调用Observer函数,进行响应式的添加。在后边流程图可以看到。

数组的7个方法如何具备响应式

因为数组不是对象,对数组的添加和删除就不具备响应式操作了。因为响应式的基础是Object.definePrototype();

重写7个方法,一旦指向方法就执行更新方法。这样也就具备了响应式了。

重写7个方法,就更改实例的7个方法。并不是整个数组原型的7个方法。私有属性方法优先于原型方法。

响应式流程图

实现一个定时器控制页面数字实例

实现的是一个new Vue(el:..data:...setInterv...)这样的格式

开始Vue构造函数的编写了

通过使用的格式,所以知道Vue类是如何编写的,如下代码:

//响应式实现的函数
function defineReactive(obj,key,val){

    Object.defineProperty(obj,key,{
        get(){
            console.log("获取get的"+val);
            return val;
        },
        set(newValue){
            console.log("设置val的操作"+newValue);
            //设置值时如果是对象,则对新值中的属性进行响应式添加
            if(typeof newValue=="object" && newValue!=null){
                new Observer(newValue);
            }
            val=newValue;
            update();
        }
    });


}


//管理响应式实现的类
/**
 * 功能与响应式方法不一样的是,这个是深度添加
 * 对添加响应式属性的对象进行区分是数组还是Object字面量对象
 */
class Observer{

    constructor(obj){//需要响应式的对象
        this.obj=obj;//先把数据临时储存
        //判断数组还是对象然后进行响应式的添加
        this.isObjectOrArray(this.obj);
      
    }
    //判断是数组和还是对象
    isObjectOrArray(obj){

        //如果是对象
        if(typeof obj=="object" && obj!=null){
            this.deep(obj);
        }

        //如果是数组,课外作业

    }

    //实现深度响应式的方法
    deep(obj){
        //对obj的属性进行循环,然后依次添加上响应式函数的
        Object.keys(obj).forEach((item)=>{
            //对当前值判断,是否是对象,如果是对象需要再一次调用本方法进行响应式添加,当然自己也要添加响应式
            if(typeof obj[item]=="object" && obj[item]!=null){
                this.deep(obj[item]);
            }
            defineReactive(obj,item,obj[item]);
        });
    }

}

//vue类
class MyVue{

    constructor(options){//传递进来的参数
        //首先把参数绑定在this上
        this.$options=options;
        //数据绑定在实例上
        this.$data=options.data;
        //对数据进行响应式的处理的
        new Observer(this.$data);
        //数据代理的方法,只代理一层数据
        this.proxy(this,this.$data);

    }
  
		//数据代理的方法
    proxy(vm,data){
        //数据代理其实就是把data的属性添加到vm上同时也要具备响应式
        Object.keys(data).forEach(attr=>{
            let val=data[attr];
            Object.defineProperty(vm,attr,{
                get(){
                    return val;
                },
                set(newValue){
                    if(data[attr]!=newValue){
                        val=newValue;
                    }
                }
            });
        });

    }
}

数据代理的代码,就是data的数据代理到this上:

		//数据代理的方法
    proxy(vm,data){
        //数据代理其实就是把data的属性添加到vm上同时也要具备响应式
        Object.keys(data).forEach(attr=>{
            let val=data[attr];
            Object.defineProperty(vm,attr,{
                get(){
                    return val;
                },
                set(newValue){
                    if(data[attr]!=newValue){
                        val=newValue;
                    }
                }
            });
        });

    }

初始化视图(Compiler)

其实就是对dom节点进行编译。这个类是一个编译的类。

我们需要做一个Compiler的实现

需要对dom进行一个解析。

解析流程图:

文本节点的检测和解析:

  • 文本节点的判断:

如下代码:

    isText(node){
        //文本文件的内容
        let text=node.textContent;
        //要有两个条件成立,这个是条件一的成立
        let condition1=node.nodeType===3;
        //条件二是一个正则表达式,不仅仅要条件成立而且需要获取部分值。
        let reg=/({{[a-zA-Z0-9_]*}})/g;

        return condition1 && reg.test(text);
    }

node.textContent方法获取文本节点的内容。同时也可以给文本节点赋值。

  • 文本节点的解析:

使用技巧是,对字符串内容进行切割,而切割的条件是正则。

然后再把切割内容中满足条件的值进行替换后重新拼接成一个内容再进行赋值。

parseText(node,vm){
        
        let text=node.textContent;
        let reg=/({{[a-zA-Z0-9_]*}})/g;
        //使用正则表达式进行分割内容
        //分割的结果是:["{{content}}","总监","哈哈","{{name}}"....]
        let arrRes=text.split(reg);
        let content="";
        //通过正则把{{}}进行隔开,分成一个一个的。然后去掉大括号,去除里边的值进行变量的替换
        arrRes.forEach((item,index)=>{
            
            if(item[0]=="{" && item[1]=="{"){//如果以{开头的我们就判断是{{开头的
                let i=item=String(item).substring(2,String(item).length-2);
                content+=vm[i]?vm[i]:item;
            
            }else{
                content+=item;
            }
        });
        node.textContent=content; //文本节点的赋值
    }

元素节点的

  • 首先是判断元素节点
    //判断是否有元素节点
    isElement(node){
        return node.nodeType===1;//如果是1则表示是元素节点
    }
  • 元素节点属性的解析

元素节点的属性这里暂时分成两种,一种是指令 一种是事件

通过 属性的开头来判断

所有属性的获取 attributes

单个属性的name是属性的名称 value是属性的值

\

   //解析Element属性的方法
    parseElementAttr(node,attr){
        //属性名称的获取
        let name=attr.name;
        //属性值的获取
        let value=attr.value;
        console.log(typeof name,value);
        //进行判断,如果是指令则指令解析
        if(name.indexOf("my-")!=-1){//my-开头的
            this.parseCommand(node,name,value);
        }
        //事件解析

    }
  • 指令解析
    //解析指令的方法
    parseCommand(node,name,value){
        let command=name.substring(3);

        if(command=="text"){//文本赋值的指令
            node.textContent=this.$vm[value];
        }
        if(command=="html"){
            node.innerHTML=value;
        }
    }
  • 元素节点中的文本节点

元素节点里边是可以有文本节点的。所以需要递归,知道文本节点后,结束。

//元素节点和文本节点分别处理
    handleElementText(currentNode,vm){
        let children=currentNode.childNodes;
        //循环这一层所有的节点
        Array.from(children).forEach(node=>{
            //判断是element节点
            if(this.isElement(node)){
                //如果是节点则获取所有的属性,属性暂时设置两种指令和时间
                // console.log(node.attributes);
                Array.from(node.attributes).forEach((item,index)=>{
                    //解析元素节点属性
                    this.parseElementAttr(node,item);
                });
                this.handleElementText(node,vm);
            }else if(this.isText(node)){
               //解析text节点里边的内容
               this.parseText(node,vm);
            }
        });
    }

元素节点事件的处理

//解析事件的方法
    parseEvent(node,name,value){
        let evName=name.substring(1);
        console.log("evName",evName);
        if(evName=="click"){//点击事件
            //如果是事件,那就是给当前元素绑定一个事件以及执行执行的方法
            let currentMethod= this.$vm.$options.methods[value];
            if(typeof currentMethod=="function"){//如果方法存在,给这个节点添加一个方法
                node.addEventListener("click",currentMethod);
            }
        }
    }

双向绑定(作业)

更新操作

一个好的封装技巧:

实现方法是传入的参数+update 的方法名。

而这些方法名也都是存在的。

wathers的作用

我们把会更新的操作保存在wathers的容器里边。

例如 : 指令 {{}} 这一些值的变化 就执行更新的方法。

暂时我们这里的更新操作是粗糙的更新,是把所有的更新操作都执行。

我们在Compiler类里边进行更新的时候,把更新的操作都写到了方法里。然后再把这个方法的执行放入到wathers容器里。

切记,这里的更新是都更新,并没有针对性的进行更新。

wathrs里边有多个wather对象,wather对象是在编译类里边需要更新数据操作的地方创键的。

因为在初始化的时候,哪些数据就有可能发生变化,所以都给他们加上一个监听的操作。我们把这个监听的操作叫做watcher。

  • 首先会把更新的方法都写成node和key作为参数就能赋值的方法,如下:
    //更新文档里边变量的方法
    textNodeUpdate(node,key,content){//通过字符串替换的方法也是可以的
        // let content=node.textContent;//获取所有的字符串.
        let reg=/({{[a-zA-Z0-9_]*}})/g;
        let resReg=content.match(reg);
        resReg.forEach(item=>{//循环在文本里边,哪个变量发生改变就更新哪个变量
            let i=String(item).substring(2,String(item).length-2);
            content=content.replace("{{"+i+"}}",this.$vm[i]);
        });
        //截取有中间变量部分
        //用变量的值替换{{}}的内容
        //赋值
        node.textContent=content;

    }
  • 调用上述方法的代码是:
  this.update(node,i,"textNode",text);

但是你会发现这个是update方法,调用上述的textNodeUpdate方法就是在这个update方法里边。如下代码是update方法的代码:

    //更新的方法,这里赋值的value必须是data里边的属性。
    update(node,key,dir,content=""){
        let updateFn=this[dir+"Update"];
        let That=this;
        updateFn.call(this,node,key,content);
        if(updateFn){
            //赋值方法的最后一步,单独拎出来做一个方法来使用,这样所有的更新方法都会有这样的一个步骤
            new Watcher(That,key,function(){
                updateFn.call(That,node,key,content);
            });//创建一个watcher,watcher里边的方法执行赋值的操作

        }
    }

根据传递的参数进行拼接,会发现updateFn就是上边更新的方法。然后把updateFn传递给 Watcher类作为参数。有两个作用:第一个是该作用域会一直保存,所以当前节点node会一直保存。 第二个是在watcher实例会添加这个updateFn方法。在wather保存到一个全局的容器里边。

wather类的代码:

//用于存储Watcher
let watchers=[];

//监听更新属性的类
class Watcher{

    //key其实就是MyVue中的data的属性
    constructor(vm,key,updateFn){
        this.vm=vm;

        this.key=key;

        this.updateFn=updateFn;

        watchers.push(this);
    }

   update(){
       this.updateFn.call(this.vm,key);
   }

}
  • 响应式set方法更新的操作

因为更新的方法都在watchers的容器里边,那么就循环这个容器,执行一次所有的更新方法。

watchers.forEach(item=>{
  item.updateFn();
})
  • 图解析

Dep管理watcher

原理

  • 利用响应式的作用域,就是我们在给MyVue添加响应式属性时的作用域,那么跟这个属性相关的几个更新方法都放入到作用域里边。那么每次set方法时,就把这个几个方法执行一遍就可以。这样就起到了针对属性更新的方法。
  • 更新的方法在watcher里边,所以我们需要定义一个全局变量,把watcher通过全局变量转移过来。
  • 转移过来的watch保存在当前的某个容器里边,这个容器和作用域一直是存在的,那么set方法时只要执行这个容器里面的方法就行。

实现

  • dep管理器类:
//watcher管理器
class Dep{

    constructor(){
        //当前属性watcher容器
        this.deps=[];
    }

    //添加watcher
    addDep(watcher){
        this.deps.push(watcher);
    }

    //更新方法
    notify(key){//传入key,每个更新的方法都是传入key进行相关操作的
        //更新方法的执行
        this.deps.forEach(item=>{
            console.log("item",item);
            item.update(key);
        });
    }


}
  • 在代理响应式里边使用到的:

主要是关注dep的使用 什么时候创建dep,什么时候调用dep等操作

    proxy(vm,data){

        //执行代理时就创建dep
        let dep=new Dep();

        //数据代理其实就是把data的属性添加到vm上同时也要具备响应式
        Object.keys(data).forEach(attr=>{
            let val=data[attr];
            Object.defineProperty(vm,attr,{
                get(){
                    //当属性被调用时就添加watcher方法.Dep是全局变量把watcher导过来
                    let watcher=Dep.target;
                    watcher && dep.addDep(watcher);
                    return val;
                },
                set(newValue){
                    if(data[attr]!=newValue){
                        val=newValue;
                        // console.log("val",val);
                        //更新视图
                        dep.notify(attr);
                    }
                }
            });
        });

    }
  • 在watcher里边的操作
Dep.target=this;
this.compiler.$vm[key];//调用这个就是在get里边赋值watcher
Dep.target=null;//清空这个全局变量