深入理解Vue 2.0原理

深入Vue原理

随着vue前端框架的广泛的运用在前端开发中,市场对vue前端开发工作者的要求也显著提高了,不仅要熟练使用vue进行日常业务的开发,同时又要熟悉并理解vue框架的底层原理,深入底层源码,这让前端开发工作者又将vue源码的理解加入了自己的学习清单中。所以,今天我在这里为大家分享自己研究vue原理的心路历程,避免踩坑。
先来上一张vue原理的解析图
在这里插入图片描述

Object.defineProperty介绍

Object.defineProperty(obj, prop, descriptor)
obj:必需。目标对象
prop:必需。需定义或修改的属性的名字
descriptor:必需。目标属性所拥有的特性
  • value:被定义的属性的值,默认为undefined

  • writable:是否可以被重写,true可以重写,false不能重写,默认为false。

  • enumerable:是否可以被枚举(使用for…in或Object.keys())。设置为true可以被枚举;设置为false,不能被枚举。默认为false。

  • configurable:是否可以删除目标属性或是否可以再次修改属性的特性(writable, configurable,
    enumerable)。设置为true可以被删除或可以重新设置特性;设置为false,不能被可以被删除或不可以重新设置特性。默认为false。

存取器getter/setter

注意:当使用了getter或setter方法,不允许使用writable和value这两个属性

getter:当访问该属性时,该方法会被执行。函数的返回值会作为该属性的值返回
setter:当属性值修改时,该方法会被执行。该方法将接受唯一参数,即该属性新的参数值。

var obj = {};
var initValue = 'hello';
Object.defineProperty(obj,"newKey",{
    get:function (){
        //当获取值的时候触发的函数
        return initValue;    
    },
    set:function (value){
        //当设置值的时候触发的函数,设置的新值通过参数value拿到
        initValue = value;
    }
});
//获取值
console.log( obj.newKey );  //hello

//设置值
obj.newKey = 'change value';

console.log( obj.newKey ); //change value

注意:不要在getter中再次获取该属性值,也不要在setter中再次设置改属性,否则会栈溢出

实现数据代理

我们知道在使用vue的时候,通过this.xxx就可以访问到data中的属性,并可以对其进行修改,进而可以在页面上进行实时更新,那么实现这种效果的原理是怎么样的呢?这里给几分钟思考。
其实这里用到了一个Object.defineProperty属性进行数据代理,起到主要作用的就是上面讲的存取器getter/setter
下面我们就来实现这样一个效果:
新建html文件如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>数据代理</title>
    <script src="./js/index.js"></script>  
    <!--  //这里引入index.js为我们的逻辑代码 -->
  </head>
  <body>
    <!-- //为什么在vue中通过this.xxx能够直接拿到data里面的元素,那是因为用到了Object.defineProperty方法进行了数据的代理,this.xxx实际上拿到的值是data.xxx,只不过是中间进行了一个代理的过程 -->
    <script>
      var vm = new Vue({
        data: {
          message: "hello world vue.js",
        },
      });
    </script>
  </body>
</html>

index.js如下:

class Vue {
    constructor(options){
        this.$options=options;
        this._data=options.data;
        this.initData(); //初始化
    }
    initData(){
        let data=this._data;
        let keys=Object.keys(data); //获取data中的每一个属性组成的数组,
        // 遍历数组 使用objectDefine.property()
        for(let i=0;i<keys.length;i++){       
            // this指向vue实例   注意这里要用关键字let 而不要用var 否则会出现变量名提升 导致访问不到data中的message
            Object.defineProperty(this,keys[i],{
                enumerable:true,
                configurable:true,
                get:function() {
                    return  this._data[keys[i]];
                },
                set:function(value) {
                    // 设置值
                    data[keys[i]]=value;
                }
            })
        }   
    }
}

运行之后打开浏览器的控制台,输入vm.message,然后就会获取到message,然后对其进行修改,vm.message=xxx,(随便赋值),最后再获取message的值,发现message的值已经变成更改之后的值了,以上就实现了一个简单的数据代理

实现数据劫持

 为了实现对data数据的响应,这里需要对data中的数据进行劫持
class Vue {
    constructor(options){
        this.$options=options;
        this._data=options.data;
        this.initData(); //初始化
    }
    initData(){
        let data=this._data;
        let keys=Object.keys(data); //获取data中的每一个属性组成的数组,
        // 遍历数组 使用objectDefine.property()
        for(let i=0;i<keys.length;i++){       
            // this指向vue实例   注意这里要用关键字let 而不要用var 否则会出现变量名提升 导致访问不到data中的message
            Object.defineProperty(this,keys[i],{
                enumerable:true,
                configurable:true,
                get:function() {
                    return  this._data[keys[i]];
                },
                set:function(value) {
                    // 设置值
                    data[keys[i]]=value;
                }
            })
        }
        // 为了实现对data数据的响应,这里需要对data中的数据进行劫持
        for(let i=0;i<keys.length;i++){
            let value=data[keys[i]];
            Object.defineProperty(data,keys[i],{
                enumerable:true,
                configurable:true,
                get:function reactiveGetter(){
                    console.log(`data的属性${keys[i]}的获取`);
                    return value;
                },
                set:function reactiveSetter(val){
                    console.log(`data设置属性${keys[i]}`)
                    value = val;
                }
            })

        }
       
    }
}

实现数据递归劫持

以上的数据劫持我们只做了针对简单的数据类型的劫持,而对复杂数据类型的数据(Object Array)等都没有做处理,所以接下来我们将对复杂数据类型的data响应做递归劫持:
将数据响应式的代码抽离到工厂函数中,并且新定义一个Observer类,为后续工作做铺垫

class Vue {
    constructor(options){
        this.$options=options;
        this._data=options.data;
        this.initData(); //初始化
    }
    initData(){
        let data=this._data;
        let keys=Object.keys(data); //获取data中的每一个属性组成的数组,
        // 遍历数组 使用objectDefine.property()
        for(let i=0;i<keys.length;i++){       
            // this指向vue实例   注意这里要用关键字let 而不要用var 否则会出现变量名提升 导致访问不到data中的message
            Object.defineProperty(this,keys[i],{
                enumerable:true,
                configurable:true,
                get:function() {
                    return  this._data[keys[i]];
                },
                set:function(value) {
                    // 设置值
                    data[keys[i]]=value;
                }
            })
        }
        //实现对data数据的响应
        observe(data);
      
       
    }
}

function observe(data){
     //判断data中的属性是简单数据类型还是复杂数据类型
     let type=Object.prototype.toString.call(data);
     //递归终止条件
     if(type!=='[object Object]'&&type!=='[object Array]'){
         return;
     }
     //复杂数据类型,对data进行递归处理
     new Observer(data);
}

function defineReactive(obj,key,value){
       //data中的属性进行扁平化处理
       observe(obj[key]);
      // 为了实现对data数据的响应,这里需要对data中的数据进行劫持
       //注意这里不要再代理里面再次获取改属性,即data[keys[i]],
    // 这样会触发get函数进入一个死循环,进而出现栈溢出的现象
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:true,
            get:function reactiveGetter(){
                console.log(`data的属性${key}的获取`);
                return value;
            },
            set:function reactiveSetter(val){
                console.log(`data设置属性${key}`)
                value = val;
            }
        })

    

}
class Observer{
    constructor(data){
        this.walk(data)
    }
    walk(data){
        let keys=Object.keys(data);  //获取data中的属性
        for(let i=0;i<keys.length;i++){
            defineReactive(data,keys[i],data[keys[i]]);  //传递当前的key和data中的属性值
        }

    }
}

实现Watcher

由于模板涉及到vue的编译和vdom等知识,所以先用watch选项与$watch api 来测试对属性的监听
在实现watcher之前呢,我们先来思考以下几个问题
问题一:首先,回调函数肯定不能硬编码在setter

因此,我们每个属性需要有个自己的“”,不管是使用watch选项初始化还是使用$watchapi来监听某个属性时,我们需要把这些回调添加到这个"筐"中,等到属性setter触发时,从“筐”中把收集的回调拿出来通知(notify)他们执行

问题二:有可能存在同一个回调可能依赖多个属性,例如模板或者computed

因此,我们可以使用对属性求值,来触发相应的getter,在getter中让“筐”去找当前的回调(depend),并且收集它

问题三:“筐”去哪里找当前的回调?

我们可以把当前需要被收集的回调在触发getter之前存在一个公共的地方,触发后再从公共的地方移除。就像从一个舞台上台再下台的过程。

根据面向对象的编程思想,我们把"筐"抽象成一个Dep类的实例,把回调抽象成一个Watcher类的实例

接下来我们将实现这个功能
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>实现watcher</title>
    <script src="./js/watch.js"></script>
</head>
<body>
    <script>
        var vm = new Vue({
          data: {
            message: "hello world vue.js",
            name:'12',
            person:{
                name:'zs',
                city:{
                    cityName:'大连'
                }
            }
          },
          watch:{
              message(){
                  this.name="ls"
                  console.log('message发生了改变1')

              }
          }
        });
        单个的顶层属性,第一个参数是字符串,第二个参数是函数。
        // 顶层属性
       vm.$watch('person', (newVal, oldVal) => {
         // 做点什么
         console.log("person发生了变化");
     })
        vm.message=2;
        vm.message=3;
        vm.message=4;
        vm.message=5;
        console.log(vm.name)  
        //先打印vm.name :12,后执行message回调:message发生了改变1
      </script>
    
</body>
</html>

在这里插入图片描述

js代码如下

class Vue {
    constructor(options){
        this.$options=options;
        this._data=options.data;
        this.initData(); //初始化
    }
    initData(){
        let data=this._data;
        let keys=Object.keys(data); //获取data中的每一个属性组成的数组,
        // 遍历数组 使用objectDefine.property()
        for(let i=0;i<keys.length;i++){       
            // this指向vue实例   注意这里要用关键字let 而不要用var 否则会出现变量名提升 导致访问不到data中的message
            Object.defineProperty(this,keys[i],{
                enumerable:true,
                configurable:true,
                get:function() {
                    return  this._data[keys[i]];
                },
                set:function(value) {
                    // 设置值
                    data[keys[i]]=value;
                }
            })
        }
        //实现对data数据的响应
        observe(data);
        this.initWatch();  //初始化watch
      
       
    }
    initWatch(){
        let watch=this.$options.watch;
        if(watch){
            let keys=Object.keys(watch);  //获取watch里面监听的对象属性
            for(let i=0;i<keys.length;i++){
                new Watcher(this,keys[i],watch[keys[i]]);  // vm, exp,cb
            }

        }
       

    }
    $watch(key,cb){
        new Watcher(this,key,cb);

    }
}

function observe(data){
     //判断data中的属性是简单数据类型还是复杂数据类型
     let type=Object.prototype.toString.call(data);
     if(type!=='[object Object]'&&type!=='[object Array]'){
         return;
     }
     //复杂数据类型,对data进行递归处理
     new Observer(data);
}

function defineReactive(obj,key,value){
       //data中的属性进行扁平化处理
       observe(obj[key]);

    //    初始化dep实例
     let dep=new Dep();
      // 为了实现对data数据的响应,这里需要对data中的数据进行劫持
       //注意这里不要再代理里面再次获取改属性,即data[keys[i]],
    // 这样会触发get函数进入一个死循环,进而出现栈溢出的现象
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:true,
            get:function reactiveGetter(){
                // console.log(`data的属性${key}的获取`);
                dep.depend(); //收集watcher
                return value;
            },
            set:function reactiveSetter(val){
                // console.log(`data设置属性${key}`)
                dep.notify();  //设置值的时候,通知watcher执行回调
                value = val;
            }
        })

    

}
class Observer{
    constructor(data){
        this.walk(data)
    }
    walk(data){
        let keys=Object.keys(data);
        for(let i=0;i<keys.length;i++){
            defineReactive(data,keys[i],data[keys[i]]);  //传递当前的data和data中的属性值
        }

    }
}
// 将回调函数装进框中  抽象Dep类

class Dep{
    constructor(){
       this.subs=[];   //手机依赖的数组
    }
    depend(){
        if(Dep.target){
            this.subs.push(Dep.target);
        }
    }
    notify(){
        // 将收集到的回调遍历,循环执行
        this.subs.forEach((watcher)=>{
            watcher.run();
        })
    }

}
class Watcher{
    constructor(vm,exp,cb){
        this.vm=vm;
        this.exp=exp;
        this.cb=cb;
        this.get()
    }
    get(){
        Dep.target=this;   //将watcher放到一个公共的地方
        this.vm[this.exp];  //取值
        Dep.target=null;  //清空
    }
    run(){
            this.cb.call(this.vm);
    }
}

运行以上代码,当执行到 多次对message赋值的时候,message监听的回调执行了4次,这个与实际的vue执行最新1次回调的结果不一样,所以这里我们需要用到宏任务和微任务的知识,使用一个watcher队列,每一次赋值时,给这个watcher回调打上一个标签(id),当要执行回调时,判断当前的id是否在watcher队列中存在,如果存在就return,反之就push进数组,同时这个回调的执行需要在下一次宏任务结束之后执行(即微任务)

    vm.message=2;
    vm.message=3;
    vm.message=4;
    vm.message=5;

实际在Vue中,watcher实例的求值和调用回调函数是异步调用的,并且在上一个事件循环中无论改变几次属性,回调只会异步调用一次,所以我们对Watcher及run方法进行改造:

class Vue {
    constructor(options){
        this.$options=options;
        this._data=options.data;
        this.initData(); //初始化
    }
    initData(){
        let data=this._data;
        let keys=Object.keys(data); //获取data中的每一个属性组成的数组,
        // 遍历数组 使用objectDefine.property()
        for(let i=0;i<keys.length;i++){       
            // this指向vue实例   注意这里要用关键字let 而不要用var 否则会出现变量名提升 导致访问不到data中的message
            Object.defineProperty(this,keys[i],{
                enumerable:true,
                configurable:true,
                get:function() {
                    return  this._data[keys[i]];
                },
                set:function(value) {
                    // 设置值
                    data[keys[i]]=value;
                }
            })
        }
        //实现对data数据的响应
        observe(data);
        this.initWatch();  //初始化watch
      
       
    }
    initWatch(){
        let watch=this.$options.watch;
        if(watch){
            let keys=Object.keys(watch);  //获取watch里面监听的对象属性
            for(let i=0;i<keys.length;i++){
                new Watcher(this,keys[i],watch[keys[i]]);  // vm, exp,cb
            }

        }
       

    }
    $watch(key,cb){
        new Watcher(this,key,cb);

    }
}

function observe(data){
     //判断data中的属性是简单数据类型还是复杂数据类型
     let type=Object.prototype.toString.call(data);
     if(type!=='[object Object]'&&type!=='[object Array]'){
         return;
     }
     //复杂数据类型,对data进行递归处理
     new Observer(data);
}

function defineReactive(obj,key,value){
       //data中的属性进行扁平化处理
       observe(obj[key]);

    //    初始化dep实例
     let dep=new Dep();
      // 为了实现对data数据的响应,这里需要对data中的数据进行劫持
       //注意这里不要再代理里面再次获取改属性,即data[keys[i]],
    // 这样会触发get函数进入一个死循环,进而出现栈溢出的现象
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:true,
            get:function reactiveGetter(){
                // console.log(`data的属性${key}的获取`);
                dep.depend(); //收集watcher
                return value;
            },
            set:function reactiveSetter(val){
                // console.log(`data设置属性${key}`)
                dep.notify();  //设置值的时候,通知watcher执行回调
                value = val;
            }
        })

    

}
class Observer{
    constructor(data){
        this.walk(data)
    }
    walk(data){
        let keys=Object.keys(data);
        for(let i=0;i<keys.length;i++){
            defineReactive(data,keys[i],data[keys[i]]);  //传递当前的data和data中的属性值
        }

    }
}
// 将回调函数装进框中  抽象Dep类

class Dep{
    constructor(){
       this.subs=[];   //手机依赖的数组
    }
    depend(){
        if(Dep.target){
            this.subs.push(Dep.target);
        }
    }
    notify(){
        // 将收集到的回调遍历,循环执行
        this.subs.forEach((watcher)=>{
            watcher.run();
        })
    }

}
//解决触发多次回调的问题
let watchId=0;
let watchQuene=[];
class Watcher{
    constructor(vm,exp,cb){
        this.vm=vm;
        this.exp=exp;
        this.cb=cb;
        this.id=++watchId;
        this.get()
    }
    get(){
        Dep.target=this;   //将watcher放到一个公共的地方
        this.vm[this.exp];  //取值
        Dep.target=null;  //清空
    }
    run(){
        if(watchQuene.indexOf(this.id)!== -1){
            // 如果存在watchId,
            return;
        }
        watchQuene.push(this.id);
        let index=watchQuene.length - 1;
        Promise.resolve().then(()=>{
            this.cb.call(this.vm);  //由于在watch中会用到data中依赖的属性,所以这里使用call来改变回调中的this.xxx this指向
            watchQuene.splice(index,1);
        })
    }
}

总结:至此,我们实现了一个基本的基于发布订阅的Dep和Watcher,但是目前仍然存在以下问题:

  1. 对象新增属性仍然无法触发回调
  2. 数组仍然没有做处理,如果使用Object.defineProperty对数组进行劫持会存在以下问题:
  • 改变了数组的顺序、改变了数组的长度、或者删除了数据。数组的下标全乱了。这时候怎么办?Object.defineProperty(array, '0', {}); 我们这个定义到底谁是谁?
  • 数组的原生方法进行增删改查无法触发回调
  • 与对象相似,对一个不存在的下标赋值也无法触发
    下面呢我们就来结局这个问题,其实呢vue2.0提供了一个方法就是来解决这个问题的,vue3.0对这个问题进行了升级方法,抛弃了Object.defineProperty()方法,而是使用了proxy vue3.0的proxy详解

接下来我们就来解决以上两个难点:

实现$set方法

对象上的__ob__是用来干什么的?

实际上__ob__就是Observer对象,并且对象上存储了一个Dep实例
在这里插入图片描述
可以看到,这个dep是和之前defineReactive闭包中的“筐”不同的另外一个“筐”,当属性的值是一个对象时,把触发getterwatcher也收集了一份在自己的subs中,这样就方便我们之后通过代码命令式地去触发这个属性对象的watcher

所以$set方法的实现思路基本如下:
1,在创建Observer实例时,也创建一个新的“筐”,挂在Observer实例上,然后把Observer实例挂载到对象的__ob__属性上。
2,触发getter时,不光把watcher收集一份到之前的”筐“里,也收集一份在这个新的“筐”里。
3,用户调用$set时,手动触发__ob__.deo.notify()
4,最后别忘了在notify()之前调用defineReactive把新的属性也定义成响应式

关键代码如下:

class Vue {
  //...
  $set(target,key,value) {
    let ob = target.__ob__
    defineReactive(target,key,value)
    ob.dep.notify()
  }
}
class Observer {
  constructor(data){
    this.dep = new Dep()
    this.walk(data)
    Object.defineProperty(data,'__ob__',{
      enumerable:false,
      configurable:false,
      value:this,
      writable:true,
    })
  }
  //...
}
function observe (data) {
  let type = Object.prototype.toString.call(data)
  if(type !== '[object Object]' && type !== '[object Array]'){
    return
  }
  if(data.__ob__){
    return data.__ob__
  }
  return new Observer(data)
}
function defineReactive(obj, key, value) {
  let childOb = observe(obj[key])
  let dep = new Dep()
  Object.defineProperty(obj,key,{
    enumerable:true,
    configurable:true,
    get:function reactiveGetter() {
      if(Dep.target){
        dep.depend()
        if(childOb){
          childOb.dep.depend()
        }
      }
      return value
    },
     set:function reactiveSetter(val){
                // console.log(`data设置属性${key}`)
                dep.notify();  //设置值的时候,通知watcher执行回调
                value= val;
            }
  
  })
}

以上实现的是对对象的处理,下面我们来对数组进行处理

实现对数组的处理

基于前面的__ob__来实现对数组的处理的思路:

  1. 因为前面已经说了,使用Object.defineProperty的办法劫持数组,会存在问题。所以在实现数据劫持的时候,数组本身不用管,而是去循环劫持数组的元素,因为元素也有可能是对象。这样也就变成了对对象和简单数据类型的劫持

    实现方法:数组的回调也通过__ob__.dep来收集,在数组调用push,pop等方法时手动去触发__ob__.dep.notify

  2. 原型对象Array.prototype上的方法不能直接修改,因为这样会破坏其他用到这些方法的代码的功能

    实现方法:在数组和Array.prototype的原型链上插入一个自定义的对象,拦截原来的push等方法,在自定义对象中的同名方法中先执行原本的方法,再去人为的调用__ob__.dep.notify()去执行之前收集的回调

    具体代码如下:
    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>set方法对数组的监测的处理</title>
    <script src="./js/setArray.js"></script>
</head>
<body>
    <script>
        var vm = new Vue({
          data: {
            message: "hello world vue.js",
            name:'12',
            person:{
                name:'zs',
                city:{
                    cityName:'大连'
                }
            },
            hobbys:['吃饭','睡觉','看书','写代码']
          },
          watch:{
              message(){
                  this.name="ls"
                  console.log('message发生了改变1')

              },
              hobbys(){
                  console.log('hobbys发生了改变')
              }
          }
        });
      
        // vm.$set(vm.person,'age',12)
        vm.hobbys.push('运动')
        vm.hobbys.push({name:'烧烤'})
      </script>
</body>
</html>

js文件

class Vue {
    constructor(options){
        this.$options=options;
        this._data=options.data;
        this.initData(); //初始化
    }
    initData(){
        let data=this._data;
        let keys=Object.keys(data); //获取data中的每一个属性组成的数组,
        // 遍历数组 使用objectDefine.property()
        for(let i=0;i<keys.length;i++){       
            // this指向vue实例   注意这里要用关键字let 而不要用var 否则会出现变量名提升 导致访问不到data中的message
            Object.defineProperty(this,keys[i],{
                enumerable:true,
                configurable:true,
                get:function() {
                    return  this._data[keys[i]];
                },
                set:function(value) {
                    // 设置值
                    data[keys[i]]=value;
                }
            })
        }
        //实现对data数据的响应
        observe(data);
        this.initWatch();  //初始化watch
      
       
    }
    initWatch(){
        let watch=this.$options.watch;
        if(watch){
            let keys=Object.keys(watch);  //获取watch里面监听的对象属性
            for(let i=0;i<keys.length;i++){
                new Watcher(this,keys[i],watch[keys[i]]);  // vm, exp,cb
            }

        }
    }
    $watch(key,cb){
        new Watcher(this,key,cb);
    }
    $set(target,key,value){
        defineReactive(target,key,value);  //将新加的属性也定义成相应式
        target.__ob__.dep.notify();
    }
}

function observe(data){
     //判断data中的属性是简单数据类型还是复杂数据类型
     let type=Object.prototype.toString.call(data);
     if(type!=='[object Object]'&&type!=='[object Array]'){
         return;
     }
     if(data.__ob__){
         return data.__ob__
     }
     //复杂数据类型,对data进行递归处理
    return new Observer(data);
}

function defineReactive(obj,key,value){
       //data中的属性进行扁平化处理
    let childOb=observe(obj[key]);
    //    初始化dep实例
     let dep=new Dep();
      // 为了实现对data数据的响应,这里需要对data中的数据进行劫持
       //注意这里不要再代理里面再次获取改属性,即data[keys[i]],
    // 这样会触发get函数进入一个死循环,进而出现栈溢出的现象
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:true,
            get:function reactiveGetter(){
                // console.log(`data的属性${key}的获取`);
                dep.depend(); //收集watcher
              if(childOb){
                  childOb.dep.depend();
              }
                return value;
            },
            set:function reactiveSetter(val){
                // console.log(`data设置属性${key}`)
                dep.notify();  //设置值的时候,通知watcher执行回调
                value = val;
            }
        })
}
class Observer{
    constructor(data){
        this.dep=new Dep();
        if(Array.isArray(data)){
            data.__proto__=arrayMethodsObj;
            this.observeArray(data); //如果数组中的属性是对象的话,需要对里面的元素进行观测
        }else{
            this.walk(data);
        }

        Object.defineProperty(data,'__ob__',{
            value:this,
            enumerable:false,
            writable:true,
            configurable:false
        })

    }
    walk(data){
        let keys=Object.keys(data);
        for(let i=0;i<keys.length;i++){
            defineReactive(data,keys[i],data[keys[i]]);  //传递当前的data和data中的属性值
        }

    }
    observeArray(arr){
        for(let i=0;i<arr.length;i++){
            observe(arr[i])
        }
        
    }
}
// 将回调函数装进框中  抽象Dep类

class Dep{
    constructor(){
       this.subs=[];   //手机依赖的数组
    }
    depend(){
        if(Dep.target){
            this.subs.push(Dep.target);
        }
    }
    notify(){
        // 将收集到的回调遍历,循环执行
        this.subs.forEach((watcher)=>{
            watcher.run();
        })
    }

}
let watchId=0;
let watchQuene=[];
class Watcher{
    constructor(vm,exp,cb){
        this.vm=vm;
        this.exp=exp;
        this.cb=cb;
        this.id=++watchId;
        this.get()
    }
    get(){
        Dep.target=this;   //将watcher放到一个公共的地方
        this.vm[this.exp];  //取值
        Dep.target=null;  //清空
    }
    run(){
        if(watchQuene.indexOf(this.id)!== -1){
            // 如果存在watchId,
            return;
        }
        watchQuene.push(this.id);
        let index=watchQuene.length - 1;
        Promise.resolve().then(()=>{
            this.cb.call(this.vm);  //由于在watch中会用到data中依赖的属性,所以这里使用call来改变回调中的this.xxx this指向
            watchQuene.splice(index,1);
        })
    }
}

//一下代码式处理数组的关键代码

let arrayMethodsObj={};
arrayMethodsObj.__proto__=Array.prototype;
const methods=[
    'push',
    'pop',
    //可以继续往下扩展
]
// 遍历方法
methods.forEach(method=>{
    arrayMethodsObj[method]=function (...args) {
        if(method=='push'){
            this.__ob__.observeArray(args);  //针对望数组中加数据需要将新加的数据变成响应式的,所以这里需要进行观测
        }
        let result=Array.prototype[method].apply(this,args);
        // 通知watcher执行回调
        this.__ob__.dep.notify();   //这里的this.__ob__这个属性是因为array数组有这个属性,所以能直接访问
        return result;
        
    }
})

实现computed

根据计算属性额几个特点设计思路

  1. 它的值是一个函数运行的返回结果
  2. 函数里用到的所有属性都会引起计算属性的变化

计算属性的本质人仍然是属于vue相应式实现的一种,本质上还是一个watcher,但是又似乎与之前的watcher实现有所不同,因为之前的watcher只能监听一个属性的变化
解决思路:
wathcer第二个参数exp也可以传递一个函数,然后运行这个函数并获取返回值,运行过程中,函数里面所有的this.xxx属性都会触发setter,这样一来,就可以让多个dep都能收集到这个watcher.
3. 计算属性不存在与daat选项中,需要单独进行初始化
4. 计算属性只能取,不能存,也就是计算属性的setter无效,考虑如下代码:

  let vm = new Vue({
      data:{
        a:3,
        b:5
      },
      watch:{
        x() {
          console.log('对x的监听回调触发');
        }
      },
      computed:{
        x() {
          console.log('开始计算')
          return this.a * this.b
        }
      }
    })

也意味着,计算属性本身不再需要筐去收集,对一个计算属性进行监听,回调触发的本质是计算属性依赖的其他属性发生了变化。
初步实现代码如下:

class Vue {
  constructor(options) {
    this.$options = options
    // TODO:data可能是函数
    this._data = options.data
    this.initData()
    this.initComputed()
    this.initWatch()
  }
  initData() {
      //...
  }
  initComputed(){
    let computed = this.$options.computed
    if(computed){
      let keys = Object.keys(computed)
      for (let i = 0; i < keys.length; i++) {
        const watcher = new Watcher(this, computed[keys[i]],function () {
          
        })
        Object.defineProperty(this,keys[i],{
          enumerable:true,
          configurable:true,
          get:function computedGetter(){
            watcher.get()
            return watcher.value
          },
          set:function computedSetter() {
            console.warn('请不要对计算属性赋值')
          }
        })
      }
    }
  }
  initWatch(){
    let watch = this.$options.watch
    if(watch){
      let keys = Object.keys(watch)
      for (let i = 0; i < keys.length; i++) {
        new Watcher(this, keys[i],watch[keys[i]])
      }
    }
  }
  //...
}
class Watcher {
  //...
  //求值
  get() {
    Dep.target = this
    if(typeof this.exp === 'function'){
      this.value = this.exp.call(this.vm)
    }else {
      this.value = this.vm[this.exp]
    }
    Dep.target = null
  }
  run() {
    //...
    Promise.resolve().then(()=>{
      this.get() //重新求值
      this.cb.call(this.vm)
      let index = watcherQueue.indexOf(this.id)
      watcherQueue.splice(index,1)
    })
  }
}
//...

继续解决还存在的问题:
以上代码执行完毕,在控制台打印x,貌似没有问题,但当对x计算属性所依赖的属性进行更改操作时,计算属性立刻就进行了重新计算,这个与vue中的计算属性的特性不符,他的特性如下:

  1. 计算属性是惰性的:计算属性依赖的其他属性发生变化时,计算属性不会立即重新计算,要等到对获取计算属性的值,也就是求值时才会重新计算。
  2. 计算属性是有缓存的:如果计算属性依赖的其他属性没有发生变化,即使对计算属性求值,也不会重新计算属性的。

考虑如下代码:

 let vm = new Vue({
      data: {
        a: 3,
        b: 5,
      },
      computed: {
        x() {
          console.log('计算x');
          return this.a * this.b
        }
      }
    })
    //没有任何打印
    vm.x
    //15
    //计算x
    vm.x
    //15
    vm.a = 4
    //没有任何打印
    vm.x
    //20
    //计算x

解决思路:给computed相关的watcher打一个标记this.lazy = true,代表这是一个lazy watcher,当dep通知watcher进行更新时,如果是lazy watcher,则只会给自己一个标记 this.dirty = true等到对计算属性进行求值时,如果watcher的dirty === true则会对watcher进行求值,并且把得到的值保存在watcher实例上(watcher.value),如果watcher的dirty === false则直接返回watcher.value

另外需要注意的一点:

let vm = new Vue({
  data:{
    a:3,
    b:5
  },
  watch:{
    x() { //2号watcher
      console.log('监听x');
    }
  },
  computed:{
    x() { 
      console.log('计算x');
      return this.a * this.b
    }
  }
})
//计算x
vm.a = 4
//计算x
//监听x

js如下:

class Vue {
  //...
  initComputed() {
    let computed = this.$options.computed
    if(computed){
      let keys = Object.keys(computed)
      for (let i = 0; i < keys.length; i++) {
        const watcher = new Watcher(this, computed[keys[i]],function () {},{lazy:true})
        Object.defineProperty(this,keys[i],{
          enumerable:true,
          configurable:true,
          get:function computedGetter() {
            if(watcher.dirty){
              watcher.get()
              watcher.dirty = false
            }
            return watcher.value
          },
          set:function computedSetter() {
            console.warn('请不要给计算属性赋值')
          }
        })
      }
    }
  }
}

class Dep {
  notify() {
    this.subs.forEach((watcher)=>{
      //依次执行回调函数
      watcher.update()
    })
  }
}
class Watcher {
  constructor(vm,exp,cb,options = {}) {
    this.dirty = this.lazy = !!options.lazy
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.id = ++watcherId
    this.lazy || this.get()
  }
  //求值
  get() {
    Dep.target = this
    if(typeof this.exp === 'function'){
      this.value = this.exp.call(this.vm)
    }else {
      this.value = this.vm[this.exp]
    }
    Dep.target = null
  }
  update() {
    if(this.lazy){
      this.dirty = true  //在通知更新的时候判断当前是否是计算属性的watcher
    }else {
      this.run()
    }
  }

}

目前仍然存在bug,考虑如下测试代码

 let vm = new Vue({
    data:{
      person:{
        name:'zs'
      }
    },
    watch:{
      x() { //2号watcher
        console.log('x监听');
      }
    },
    computed:{
      x() { //1号watcher
        console.log('x计算');
        return JSON.stringify(this.person)
      }
    }
  })
  vm.person = {name:'ls'} //没有任何打印

实际Vue中会先后打印 ‘x计算’,‘x监听’,我们的实现中仍然没有打印

说明2号watcher执行run的时候,会对x进行求值。因此watcher的run中不光要调用回调,也要调用get()

  run() {
    //...
    Promise.resolve().then(()=>{
      this.get()
      this.cb.call(this.vm)
      let index = watcherQueue.indexOf(this.id)
      watcherQueue.splice(index,1)
    })
  }

但是加上代码后我们的实现中仍然没有打印,问题出在哪里?

展开person对象:

实际Vue中:
在这里插入图片描述
我们的实现中:在这里插入图片描述
分析原因:

把computed中的watcher称为1号watcher,把watch中的watcher称为2号watcher。在initWatcher调用时,2号watcher上台,求值,触发了persongetter,触发1号watcherget()方法,1号watcher也上台,覆盖了2号watcher,person的筐开始收集台上的1号watcher,结束后清空舞台。person并没有收集到1号watcher

解决思路:

  • 维护一个,有新的watcher上台时入栈,下台时出栈,台上永远是栈顶的watcher
  • watcher被dep收集时,也收集dep,互相收集。这样的话,计算属性的getter完成后,检查舞台上还有没有watcher,有就把自己的watcher收集的dep拿出来通知,收集舞台上的watcher
class Vue {
  //...
  initComputed() {
    //...
        Object.defineProperty(this,keys[i],{
          enumerable:true,
          configurable:true,
          get:function computedGetter() {
            if(watcher.dirty){
              watcher.get()
              watcher.dirty = false
            }
            if(Dep.target){
              watcher.deps.forEach(dep => {
                dep.depend()
              })
            }
            return watcher.value
          },
          set:function computedSetter() {
            console.warn('请不要给计算属性赋值')
          }
        })
      }
    //...
  }
 //...
}
class Dep {
  constructor() {
    this.subs = []
  }
  addSub(watcher) {
    this.subs.push(watcher)
  }
  depend() {
    if(Dep.target){
      Dep.target.addDep(this)
    }
  }
  notify() {
    this.subs.forEach((watcher)=>{
      //依次执行回调函数
      watcher.update()
    })
  }
}
let targetStack = []
class Watcher {
  constructor(vm,exp,cb,options = {}) {
    this.dirty = this.lazy = !!options.lazy
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.id = ++watcherId
    this.deps = []
    if(!this.lazy){
      this.get()
    }
  }
  addDep(dep){
    if(this.deps.indexOf(dep) !== -1) {
      return
    }
    this.deps.push(dep)
    dep.addSub(this)
  }
  //求值
  get() {
    Dep.target = this
    targetStack.push(this)
    if(typeof this.exp === 'function'){
      this.value = this.exp.call(this.vm)
    }else {
      this.value = this.vm[this.exp]
    }
    targetStack.pop()
    Dep.target = targetStack.length ? targetStack[targetStack.length - 1] : null
  }
  update() {
    if(this.lazy){
      this.dirty = true
    }else {
      this.run()
    }
  }
  run() {

    if(watcherQueue.indexOf(this.id) !== -1){ //已经存在于队列中
      return
    }
    watcherQueue.push(this.id)

    Promise.resolve().then(()=>{
      this.get() //重新求值
      this.cb.call(this.vm)
      let index = watcherQueue.indexOf(this.id)
      watcherQueue.splice(index,1)
    })
  }
}

最后完整版的代码:

class Vue {
    constructor(options){
        this.$options=options;
        this._data=options.data;
        this.initData(); //初始化
        this.initComputed(); //computed需要在watch之前初始化,先有computed才能有watch
        this.initWatch();  //初始化watch
    }
    initData(){
        let data=this._data;
        let keys=Object.keys(data); //获取data中的每一个属性组成的数组,
        // 遍历数组 使用objectDefine.property()
        for(let i=0;i<keys.length;i++){       
            // this指向vue实例   注意这里要用关键字let 而不要用var 否则会出现变量名提升 导致访问不到data中的message
            Object.defineProperty(this,keys[i],{
                enumerable:true,
                configurable:true,
                get:function() {
                    return  this._data[keys[i]];
                },
                set:function(value) {
                    // 设置值
                    data[keys[i]]=value;
                }
            })
        }
        //实现对data数据的响应
        observe(data);   
    }
    initComputed(){
        let computed=this.$options.computed;
        if(computed){
            let keys=Object.keys(computed);
            for(let i=0;i<keys.length;i++){
                const watcher=new Watcher(this,computed[keys[i]],function(){

                },{lazy:true})
                
                Object.defineProperty(this,keys[i],{
                    enumerable:true,
                    configurable:true,
                    get:function computedGetter(){
                        if(watcher.dirty){
                            watcher.get();
                            watcher.dirty=false;
                        }
                        if(Dep.target){
                            watcher.deps.forEach(dep=>{
                                dep.depend();
                            })
                        }
                      
                        return watcher.value;
                    },
                    set:function computedSetter(){
                        console.warn('请不要对计算属性赋值');
                    }
                })
            }

        }
    }
    initWatch(){
        let watch=this.$options.watch;
        if(watch){
            let keys=Object.keys(watch);  //获取watch里面监听的对象属性
            for(let i=0;i<keys.length;i++){
                new Watcher(this,keys[i],watch[keys[i]]);  // vm, exp,cb
            }

        }
    }
    $watch(key,cb){
        new Watcher(this,key,cb);
    }
    $set(target,key,value){
        defineReactive(target,key,value);  //将新加的属性也定义成相应式
        target.__ob__.dep.notify();
    }
}

function observe(data){
     //判断data中的属性是简单数据类型还是复杂数据类型
     let type=Object.prototype.toString.call(data);
     if(type!=='[object Object]'&&type!=='[object Array]'){
         return;
     }
     if(data.__ob__){
         return data.__ob__
     }
     //复杂数据类型,对data进行递归处理
    return new Observer(data);
}

function defineReactive(obj,key,value){
       //data中的属性进行扁平化处理
    let childOb=observe(obj[key]);
    //    初始化dep实例
     let dep=new Dep();
      // 为了实现对data数据的响应,这里需要对data中的数据进行劫持
       //注意这里不要再代理里面再次获取改属性,即data[keys[i]],
    // 这样会触发get函数进入一个死循环,进而出现栈溢出的现象
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:true,
            get:function reactiveGetter(){
                // console.log(`data的属性${key}的获取`);
                dep.depend(); //收集watcher
              if(childOb){
                  childOb.dep.depend();
              }
                return value;
            },
            set:function reactiveSetter(val){
                // console.log(`data设置属性${key}`)
                dep.notify();  //设置值的时候,通知watcher执行回调
                value = val;
            }
        })
}
class Observer{
    constructor(data){
        this.dep=new Dep();
        if(Array.isArray(data)){
            data.__proto__=arrayMethodsObj;
            this.observeArray(data); //如果数组中的属性是对象的话,需要对里面的元素进行观测
        }else{
            this.walk(data);
        }

        Object.defineProperty(data,'__ob__',{
            value:this,
            enumerable:false,
            writable:true,
            configurable:false
        })

    }
    walk(data){
        let keys=Object.keys(data);
        for(let i=0;i<keys.length;i++){
            defineReactive(data,keys[i],data[keys[i]]);  //传递当前的data和data中的属性值
        }

    }
    observeArray(arr){
        for(let i=0;i<arr.length;i++){
            observe(arr[i])
        }
        
    }
}
// 将回调函数装进框中  抽象Dep类

class Dep{
    constructor(){
       this.subs=[];   //手机依赖的数组
    }
    addSub(watcher){
        this.subs.push(watcher)
    }
    depend(){
        if(Dep.target){
            // this.subs.push(Dep.target);
            Dep.target.addDep(this);
        }
    }
    notify(){
        // 将收集到的回调遍历,循环执行
        this.subs.forEach((watcher)=>{
            watcher.update();
        })
    }

}
let watchId=0;
let watchQuene=[];
let targetStack = []
class Watcher{
    constructor(vm,exp,cb,options={}){
        this.dirty=this.lazy=!!options.lazy;
        this.vm=vm;
        this.exp=exp;
        this.cb=cb;
        this.id=++watchId;
        this.deps=[];
        if(!this.lazy){
            this.get()
        }
    }
    update(){
        if(this.lazy){
            this.dirty = true;
        }else{
            this.run();
        }
    }
    addDep(dep){
        if(this.deps.indexOf(dep)!==-1){
            return
        }
        this.deps.push(dep); //收集dep
        dep.addSub(this);  //收集watcher
    }
    get(){
        Dep.target=this;   //将watcher放到一个公共的地方
        targetStack.push(this)
        if(typeof this.exp=='function'){
            this.value=this.exp.call(this.vm);  //执行回调,计算结果
        }else{
          this.value = this.vm[this.exp];  //取值

        }
        targetStack.pop()
        Dep.target = targetStack.length ? targetStack[targetStack.length - 1] : null;
    }
    run(){
        if(watchQuene.indexOf(this.id)!== -1){
            // 如果存在watchId,
            return;
        }
        watchQuene.push(this.id);
        let index=watchQuene.length - 1;
        Promise.resolve().then(()=>{
            this.get();
            this.cb.call(this.vm);  //由于在watch中会用到data中依赖的属性,所以这里使用call来改变回调中的this.xxx this指向
            watchQuene.splice(index,1);
        })
    }
}

let arrayMethodsObj={};
arrayMethodsObj.__proto__=Array.prototype;
const methods=[
    'push',
    'pop'
]
// 遍历方法
methods.forEach(method=>{
    arrayMethodsObj[method]=function (...args) {
        if(method=='push'){
            this.__ob__.observeArray(args);  //针对望数组中加数据需要将新加的数据变成响应式的,所以这里需要进行观测
        }
        let result=Array.prototype[method].apply(this,args);
        // 通知watcher执行回调
        this.__ob__.dep.notify();   //这里的this.__ob__这个属性是因为array数组有这个属性,所以能直接访问
        return result;
        
    }
})

实现对模板的编译

目前为止,我们已经实现Vue的响应式系统。现在需要对html进行响应。最简单的方法可以创建一个watcher:

    new Watcher(this, ()=>{
      document.querySelector('#app').innerHTML = `<p>${this.name}</p>`
    },()=>{})

在Vue中确实也是这么做的,这个watcher被称为render watcher,watcher中的求值函数并没有这么简单。
我们的实现存在一些问题:

  1. 用户是可以使用模板语法的,需要把模板进行一些处理,最终转换成一个执行dom更新的函数
  2. 直接替换所有的dom开销很大,最好按需更新dom

为了减少不必要的dom操作和实现跨平台的特性,Vue中引入了Virtual-DOM(虚拟DOM),以下简称VDOM
那么什么是Vdom呢?简单的说就是一个js对象,它可以用来描述当前的DOM长什么样。为了得到当前的Vue实例的Vdom,每个实例需要有一个函数来生成Vdom,被称为渲染函数(vm.$options.render)
Vue实例如果传入了Dom或者template,首先就是要把模板字符串转化成渲染函数,这个过程就是编译。

解析器(parser)

关于 Vue 编译原理这块的整体逻辑主要分三步,这三步是有前后关系的:

  • 第一步是将 模板字符串 转换成 element ASTs(解析器)
  • 第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)(本课程忽略)
  • 第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)

在这里插入图片描述
AST是什么?

简单的说一种代码转换成另外一种代码时,对源代码的描述。

JS AST在线生成

Vue中对模板parse后的AST长什么样?

{
  children: [{}],
  parent: {},
  tag: "div",
  type: 1, //1-元素节点 2-带变量的文本节点 3-纯文本节点,
  expression:'_s(name)', //type如果是2,则返回_s(变量)
  text:'{{name}}' //文本节点编译前的字符串
}

源代码生成AST一般包含两个步骤:词法分析和语法分析
在这里插入图片描述
Vue中的parse是每解析到一个token会立即对token进行处理

我们以最单纯的html模板为例。v-modelv-if,v-for,@click,以及html中的单标签元素注释等情况后续接上。

思路:
<为标识符,代表一个开始标签或者是结束标签,如果是开始标签,代表树的层级加了一层,如果是结束标签代表层级回退了一层。同时每一层要记录它的父级元素是谁。

所以可以使用一个去维护当前元素到了哪一层。有开始标签则入栈,结束标签则出栈。另外,标签之间是文本节点,文本节点不对栈进行操作

实现对HTML进行parse

function parser(html) {
  let stack = []
  let root
  let currentParent
  while (html) {
    let ltIndex = html.indexOf('<')
    if(ltIndex > 0){ //前面有文本
      //type 1-元素节点  2-带变量的文本节点  3-纯文本节点
      let text = html.slice(0,ltIndex)
      currentParent.children.push(element)
      const element = {
        type: 3,
        text,
        parent:currentParent
      }
      html = html.slice(ltIndex)
    }else if(html[ltIndex + 1] !== '/'){ //前面没有文本,且是开始标签
      let gtIndex = html.indexOf('>')
      const element = {
        type: 1,
        tag:html.slice(ltIndex + 1,gtIndex), //不考虑dom的任何属性
        parent: currentParent,
        children:[],
      }

      if(!root){
        root = element
      }else {
        currentParent.children.push(element)
      }
      stack.push(element)
      currentParent = element
      html = html.slice(gtIndex + 1)
    }else { //结束标签
      let gtIndex = html.indexOf('>')
      stack.pop()
      currentParent = stack[stack.length - 1]
      html = html.slice(gtIndex + 1)
    }
  }
  return root
}

实现对文本节点的parse

思路:

{{}}为标识符,对把插值变量名转换成_s(name)的形式。

function parseText(text) {
  let originText = text
  let tokens = []
  let type = 3
  while (text) {
    let start = text.indexOf('{{')
    let end = text.indexOf('}}')
    if(start !== -1 && end !== -1){
      type = 2
      if(start > 0){
        tokens.push(JSON.stringify(text.slice(0,start)))
      }
      let exp = text.slice(start+2,end)
      tokens.push(`_s(${exp})`)
      text = text.slice(end + 2)
    }else {
      tokens.push(JSON.stringify(text))
      text = ''
    }
  }
  let element = {
    type,
    text:originText,
  }
  type === 2 ? element.expression = tokens.join('+') : ''

  return element
}

代码生成器(codegenerator)

生成AST后需要把AST再转换成渲染函数

思路:

  1. 递归AST,遇到元素节点则生成如下格式的字符串_c(标签名, 属性对象, 后代数组)
  2. 遇到文本节点,如果是纯文本节点,则生成如下格式的字符串_v(文本字符串)
  3. 如果是带变量的文本节点,则生成如下格式的字符串_v(_s(变量名))
  4. 为了让变量能正常取到,生成最后将字符串包一层with(this)
  5. 最后把字符串作为函数体生成一个函数,挂载到vm.$options
function generate(ast) {
  const code = genElement(ast)
  return {
    render: `with(this){return ${code}}`,
  }
}
function genElement (el){
  const children = genChildren(el)
  let code = `_c('${el.tag}',{},${children})`
  return code
}
function genChildren (el){
  if (el.children.length) {
    return '[' + el.children.map(child=>genNode(child)).join(',') + ']'
  }
}
function genNode (node) {
  if (node.type === 1) {
    return genElement(node)
  } else {
    return genText(node)
  }
}
function genText (text){
  return `_v(${text.type === 2 ? text.expression : JSON.stringify(text.text)})`
}

实现Vdom

什么是VDOM?

简单的说就是一个JS对象,它可以用来描述当前DOM长什么样。

例如:

<ul>
    <li>1</li>
    <li>2</li>
</ul>

对应的vdom:

{
  tag: 'ul',
  attrs: {},
  children: [
    {
      tag: 'li',
      attrs: {},
      children: [
        {
          tag: null,
          attrs: {},
          children:[],
          text:'1'
        }
      ]
    },
    {
      tag: 'li',
      attrs: {},
      children: [
        {
          tag: null,
          attrs: {},
          children:[],
          text:'2'
        }
      ]
    }
  ]
}

VDOM有什么用?

1.大多数情况下,提供了比暴力刷新整个dom树更好的性能

操作JS对象是很快的,但是操作dom元素是很慢的。如果数据发生改变,视图应该怎样更新?

直接重新拼接html模板,然后把新的html挂载在根元素上?

显然这种方法会很耗费性能,因为它要大量的删除和创建dom

如果视图通过vdom来描述,那么当数据发生改变时,可以将新的vdom和旧的vdom进行对比,找到哪里发生了改变,再去对应的dom上改变相应的元素
在这里插入图片描述

上述步骤只有最后一步更新需要依赖dom api,意味着只要能跑js的地方就可以用vdom去描述当前视图,更新时只用调用相应平台的api就行,实现了跨平台

由渲染函数生成Vdom

定义一个简单的类VNode,描述一个DOM节点的相关信息,实现上一节渲染函数中未实现的_c,_v,_s函数。

//vnode.js
class VNode {
  constructor(tag, attrs, children,text) {
    this.tag = tag
    this.attrs = attrs
    this.children = children
    this.text = text
  }
}
//index.js
class Vue {
  //...
  _c(tag,attrs,children) {
    return new VNode(tag,attrs,children)
  }
  _v(text) {
    return new VNode(null,null,null,text)
  }
  _s(val){
    if(val === null || val === undefined){
      return ''
    }else if(typeof val === 'object'){
      return JSON.stringify(val)
    }else {
      return String(val)
    }
  }
}

目前为止,成功地用一个JS树状对象,描述了渲染后的HTML应该长什么样。运行vm.$options.render.call(vm)即可得到当前vdom

实现diff和patch

首先实现一个createElm函数将Vdom转化为真正的dom,稍后更新dom会用到此函数

function createElm(vnode) {
  if(!vnode.tag){
    const el = document.createTextNode(vnode.text)
    vnode.elm = el
    return el
  }
  const el = document.createElement(vnode.tag)
  vnode.elm = el
  vnode.children.map(createElm).forEach(childDom => {
    el.appendChild(childDom)
  })
  return el
}

延续模板编译里的思路,将原先粗暴式的代码进行改造。

思路:

  • 实现一个$mount函数,初次挂载到真实dom时调用,将原先的初始化render watcher的逻辑搬到$mount里
  • 实现一个_update函数,该函数接受一个新的vdom,然后对比新旧vdom并更新真实dom,render watcher中不再暴力更新dom,而是调用_update函数
//改造前
new Watcher(this, ()=>{
  document.querySelector('#app').innerHTML = `<p>${this.name}</p>`
},()=>{})

//改造后
class Vue {
  constructor(options) {
    //...
    if(options.el){
      let html = document.querySelector(options.el).outerHTML
      let ast = parser(html)
      let code = generate(ast).render
      this.$options.render = new Function(code)
      this.$mount(options.el)
    }
  }
  $mount(el) {
    this.$el = document.querySelector(el)
    this._watcher = new Watcher(this, ()=>{this._update(this.$options.render.call(this))}, ()=>{})
  }
  _update(vnode) {
    if(this._vnode){
      patch(this._vnode,vnode)
    }else {
      patch(this.$el,vnode)
    }
    this._vnode = vnode
  }
}

接下来,实现vdom机制中最核心的patch。vue中vdom进行patch的逻辑是基于snabbdom,有兴趣的朋友可以进一步阅读源码,我们目前不考虑节点属性和节点的key。

思路:

  • patch函数接受两个参数:旧的vdom和新的vdom
  • 当第一次挂载时旧的vdom是一个真实dom,单独处理
  • 后续更新时,分为如下三种情况
    1. 新节点不存在,则删除对应的dom
    2. 新旧节点标签不一样或文本不一样,则调用createElm生成新dom,并替换旧dom
    3. 旧节点不存在,新节点存在,则调用createElm生成新dom,并原dom后添加新dom
    4. 递归以上逻辑
function patch(oldNode,newNode,) {
  const isRealElement = oldNode.nodeType
  //如果是对真实dom进行patch
  if(isRealElement){
    let parent = oldNode.parentNode
    parent.replaceChild(createElm(newNode),oldNode)
    return
  }
  //当前vdom对应的真实dom
  let el = oldNode.elm
  //当前vdom对应的真实父级dom
  let parent = el.parentNode
  if(newNode){
    newNode.elm = el
  }
  if (!newNode) { //新节点不存在,删除
    parent.removeChild(el)
  } else if (changed(newNode, oldNode)) { //发生了变化,替换
    parent.replaceChild(createElm(newNode), el)
  } else if(newNode.children){
    
    const newLength = newNode.children.length
    const oldLength = oldNode.children.length
    for (let i = 0; i < newLength || i < oldLength; i++) {
      if (i >= oldLength) { //旧节点不存在,有多余的新节点,增加
        el.appendChild(createElm(newNode.children[i]))
      } else { //递归
        patch(oldNode.children[i], newNode.children[i])
      }
    }
  }
}
//由vdom创建真实dom
function createElm(vnode) {
  if(!vnode.tag){
    const el = document.createTextNode(vnode.text)
    vnode.elm = el
    return el
  }
  const el = document.createElement(vnode.tag)
  vnode.elm = el
  vnode.children.map(createElm).forEach(childDom => {
    el.appendChild(childDom)
  })
  return el
}
//判断是否是相同节点
function changed(newNode, oldNode) {
  return (newNode.tag !== oldNode.tag || newNode.text !== oldNode.text)
}

以上就是我对vue2.0的一个初步的研究解读,文中有些写的不够详细的部分后续会继续优化,欢迎同道中人批评指正。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: Vue 2.0官方离线文档是一份非常实用的技术资料,旨在帮助开发者快速了解和掌握Vue.js的核心概念和基本用法。该文档以整齐明了的章节结构,详细阐述了Vue.js的各个方面,从指令、组件到生命周期钩子等,无一不介绍得十分细致,让初学者对Vue.js的概念、原理和使用方法逐渐清晰起来。 同时,该离线文档的代码演示非常详尽且易懂,部分章节介绍的实战案例也极大地提高了学习者对Vue.js的实际运用能力。此外,文档还特别强调了Vue.js的响应式原理,让使用者能够清晰地理解Vue.js的数据绑定机制,这对于Vue.js的重要特性有着至关重要的意义。 总之,Vue 2.0官方离线文档提供了非常优秀的学习资源,可以说是Vue.js开发者的必备资料。它详实全面、易读易懂,对于了解Vue.js的学习者、前端开发初学者或是技术爱好者都十分有帮助。还有一点需要注意的是,由于该文档较为庞大,建议使用者在查阅时注意分类对应,以免出现查找困难。 ### 回答2: Vue.js是一款开源的JavaScript框架,Vue2.0是其较新版本。为了方便开发者学习和使用Vue2.0Vue官方提供了离线文档,可以在离线状态下查看和使用。这份文档包含了Vue2.0的基础知识、语法、组件、指令、模板等内容,同时也包含了一些高级用法和最佳实践。通过阅读官方文档,开发者可以更好地理解Vue2.0的内部实现和机制,从而更加高效地开发Vue应用。相比于在线文档,离线文档的优势在于可以随时查看,无需依赖网络,同时也保证了文档的版本一致性。开发者可以通过下载Vue2.0官方离线文档,将其保存在本地,并使用浏览器打开进行查看。总之,Vue2.0官方离线文档是Vue.js框架学习的重要参考资料之一,对于Vue开发者来说具有重要的意义。 ### 回答3: Vue.js是一个流行的JavaScript框架,用于构建Web应用程序。Vue 2.0官方离线文档使Vuejs的文档可以离线浏览,Linux用户可以在没有任何在线连接的情况下通过本地文档来学习Vuejs。离线文档中包含有关Vuejs的详细信息,包括其核心概念,指令,API,组件,路由,vuex等。这使得离线文档成为初学者和有经验的开发人员的理想资源。 离线文档中的章节和主题循序渐进,可以方便地跟随和学习。Vue.js的官方离线文档使得初学者可以通过本地文档来学习Vuejs,避免了需要在网络上搜索相关信息的麻烦,即使在没有网络连接的情况下,也可以把所需的资源都放在本地而不需要在寻找参考资料时冒着被各种广告干扰的风险,这样更加有利于开发人员的工作和学习。离线文档的主要优点是可以在没有网连接和依赖性的情况下使用和访问Vuejs文档并获得相关知识。此外,它还可以提高开发人员的工作效率,节省宝贵的时间,并减少由于网络延迟导致的等待时间。 总之,Vue 2.0官方离线文档是一个非常好用的资源,能够使开发人员方便的学习和使用Vue。它是准确,深入,具有有帮助的示例的绝佳资源。它使得初学者更容易掌握Vuejs,也为有经验的开发人员提供了许多有价值的信息。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值