vue1.x原理解析

782 阅读13分钟

一,前言

Vue 作为 MVVM框架,由数据来驱动视图更新,基本的使用方法如下:

  <body>
    <div id="app">
      <p>{{counter}}</p>
      <p v-text="counter"></p>
      <p v-html="desc"></p>
      <div class="text" v-on:click="add">点击</div>
    </div>
    <script src="fake-vue.js"></script>
    <script>
      const app = new FakeVue({
        el"#app",
        data: {
          counter1,
          desc`<span style="color: red">测试样式</span>`
        },
        methods:{
          add(){
            this.counter++
          }
        }
      });
    </script>
  </body>

当点击按钮的时候,counter数值加一,这时候会自动触发视图的更新。在MVVM框架出现之前,我们是通过js先取得视图中的dom,然后使用js变更dom的属性,从而更新dom。

而vue等MVVM框架则帮我们处理了视图更新这一步骤,我们只要将精力放置在数据的维护上,当数据发生变化,就会触发更新对应的视图。

二,原理分析

实现一个简单的vue,需要解决的问题有这么几个:

1,数据发生变化,如何被监听到?
2html模板中的{{}}和v-html等如何被解析?
3,监听到数据变化后,如何触发视图的更新?

首先,我们需要一个拦截器,把所有的data中的数据收集起来,取值时都是通过这个拦截器取值,而修改值时,也是通过这个拦截器修改值。这样一来我们就能通过这个拦截器知道,数据发生了变化。从而开始调用函数更新视图。

其次,「解析器」,也就是要获取和处理对应的dom,注意到我们在使用vue的时候,使用了el: "#app",将外层的节点的id传入,于是就可以利用这个获取到最外层dom,紧接着遍历子节点,看是否有vue的特殊指令如v-html或{{}}等,再根据不同的元素节点:

节点类型node.nodeType值
元素节点1
属性节点2
文本节点3

取得对应节点的属性node.attributes来调用不同的方法处理对应的视图,也就是,第一次初始化页面时,「找到对应的dom,然后更新视图这件事,是解析器做的」

最后,当data中的数据发生变化之后,我们需要能够更新html模板中所有使用到的data中的数据。为了实现这一点,我们需要一个观察者Watcher,当数据发生变化被拦截器感知,于是通知该数据的观察者,由观察者将新值传入,触发解析器中的视图更新函数,更新视图。

这三者的关系大致如下,其中dep是用来存储watcher的一个类,而updater则是更新视图的函数方法。

vue原理图.png

三,Observe的实现

// 对象响应式处理
function observe(obj) {
  if (typeof obj !== "object" || obj === null) {
    return;
  }
  new Observer(obj);
}

// 对象响应式原理
function defineReactive(obj, key, value) {
  // 解决递归嵌套问题
  observe(value);
  Object.defineProperty(obj, key, {
    get() {
      console.log("get", value);
      return value;
    },
    set(newValue) {
      console.log("set",newValue);
      if (newValue !== value) {
        value = newValue;
      }
    }
  });
}
class Observer {
  constructor(obj) {
    this.value = obj;
    this.walk(obj);
  }
  walk(obj) {
    Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
  }
}

class FakeVue {
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
    this.$methods = options.methods;
    observe(this.$data);//将数据响应式化
  }
}

这样一来,就能拦截data中的数据了,当取值时会触发get函数,赋值时触发set函数。

例如:

 <div id="app">
      <p>{{counter}}</p>
      <p v-text="counter"></p>
      <p v-html="desc"></p>
      <div class="text" v-on:click="add">点击</div>
 </div>
<script src="fake-vue.js"></script>
 <script>
      const app = new FakeVue({
        data: {
          counter:1,
          desc`<span style="color: red">测试样式</span>`
        },
        methods:{}
      });
      console.log(app.$data.counter)
      app.$data.counter=2
 </script>

可以看到控制台打印出:

get 1
1
set 2

四,data数据代理

注意到上文我们访问data中的数据是通过app.$data.counter,而实际上,我们在使用vue的时候,都是通过this.counter来使用,所以这里需要加一层代理,让我们能直接在vue实例上访问data数据。

于是FakeVue类变成:

class FakeVue {
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
    this.$methods = options.methods;
    observe(this.$data);//将数据响应式化
    this.proxy();//将data中的对象,让实例化对象最外层也能访问和修改,也就是再做一层代理,后续this.$vm[exp]才能直接访问,而不是this.$vm.data[key]
  }
  proxy() {
    Object.keys(this.$data).forEach(key => {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(v) {
          this.$data[key] = v;
        }
      });
    });
  }
}

有使用过nginx的人应该很好理解,就是这个Object.defineProperty给data做了一层代理,当我们访问this.key的时候,它会帮我们代理,然后返回this.$data[key],修改值也是一样。

代理的原理.PNG

五,Compile解析html

现在我们实例化vue出一个app,但是页面上还是这样显示的。并没有把data中的数据渲染到页面上。我们需要定义一个html解析器,来实现这一需求。

页面展示.PNG

1,获取最外层根节点

我们要解析html,第一步肯定是需要获取对应的dom。注意到我们实例化Vue的时候,会传一个参数:el:'#app',而html上根节点的id也是app。

image-20220826102338263.png

于是可以利用document.querySelector来获取根节点。

定义Compile类:

class Compile {
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);
    if (this.$el) {
      this.compile(this.$el);//取得根节点后解析html
    }
  }
  compile(dom) {
    //对根节点处理的逻辑
  }
}

2,解析html

上文说过,dom节点的类型常用的有三种:元素节点,属性节点,文本节点。处理的思路可以是这样:

1,对于文本节点,如果有{{}}这样的data值绑定,则需要将data中的数据值给对应node的innerHTML.
2,对于元素节点,如果有"v-"开头的属性,则需要解析,再细分是v-html、v-on、v-text等调用不同的处理逻辑。

获取dom.png

3,遍历子节点,区分出文本节点和元素节点:


class Compile {
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);
    if (this.$el) {
      this.compile(this.$el);
    }
  }
  compile(dom) {
      //遍历子节点
    dom.childNodes.forEach(node => {
      if (this.isElement(node)) {//元素节点,
        this.compileElement(node);
      } else if (this.isInter(node)) {//html中数据双向{{}},也就是文本节点
        this.compileText(node);
      }
      //有子节点则递归遍历
      if (node.childNodes) {
        this.compile(node);
      }
    });
  }
  compileElement(node) {
    //元素节点的处理
  }
  isElement(node) {
    return node.nodeType === 1;
  }
  isInter(node) {
    return node.nodeType === 3 && /{{(.*)}}/.test(node.textContent);
  }
}

4,对文本节点的处理

  //{{}}的处理
  compileText(node) { // 正则表达式解析的分组结果,会保存到全局的 RegExp 上
    this.update(node, RegExp.$1"text");
  }
  update(node, exp, dir) {
    const fn = this[dir + "Updater"];//拼接出v-value需要执行的函数
    fn && fn(node, this.$vm[exp]);//执行对应的函数,传入的参数是当前元素和app.data中对应的数据,这一步是初始化渲染使用
  }
  //v-text的处理
  textUpdater(node, value) {
    node.textContent = value;
  }

这样就能实现初始化的时候,html中的{{}}能够渲染出来了。

image-20220829100157474.png

5,遍历元素节点的属性,区分@和v-,并处理v-on和@绑定事件

  compileElement(node) {
    const nodeAttrs = node.attributes;//伪数组
    Array.from(nodeAttrs).forEach(attr => {//转化为真数组并遍历属性
      const attrName = attr.name;//属性名称
      const exp = attr.value;//属性的值
      //是自定义指令的处理,不是自定义指令的话不处理
      if (this.isDirective(attrName)) {
        const directName = attrName.substring(2);//把自定义指令的v-value中的value取出来,如v-model,v-html,v-text,v-on:click    
        if (this.isEventDirective(directName)) {
          //v-on的处理
          this.compileEvent(node,this.$vm, exp, directName);
        } else if(this.isModel(directName)){
          //v-model的处理
          this.compileModel(node,exp)
        }else if(this.isBind(directName)){
          //v-bind的处理
          this.compileBind(node,this.$vm, exp, directName);
        }else {
          //其他v-
          this.update(node, exp, directName);
        }
      }
      //是@事件绑定的
      if(this.isEvent(attrName)){
        const directName = attrName.substring(1)
        this.compileEvent(node,this.$vm, exp, directName);
      }
    });
  }
//@事件绑定的处理
  compileEvent(node,vm, exp, dir){
    let eventType
    if(dir.indexOf(':')!==-1){
      eventType= dir.split(':')[1];
    }else{
      eventType=dir;
    }
    var cb = vm.$methods && vm.$methods[exp]; //取得绑定的事件方法
    if (eventType && cb) {
      node.addEventListener(eventType, cb.bind(vm), false); //冒泡机制绑定事件
    }
  }
  //判断是否是v-on
  isEventDirective(dir) {
    return dir.indexOf('on:') === 0;
  }
  // 判断是不是@开头的方法
  isEvent(attr) {
    return attr.startsWith('@')
  }
  //判断是不是v-model
  isModel(attr){
    return  attr.startsWith('model')
  }
  //判断是不是v-bind
  isBind(attr){
    return attr.indexOf('bind:') === 0;
  }

这样之后,有用@绑定事件的便完成了事件的绑定。

6,v-model的处理

//v-model的处理
this.compileModel(node,exp)
  //v-model的处理
compileModel(node, exp){
  this.update(node, exp, 'model');
  //监听input输入
  node.addEventListener('input'(e) => {
  this.$vm[exp] = e.target.value
  })
}
update(node, exp, dir) {
    const fn = this[dir + "Updater"];//拼接出v-value需要执行的函数
    fn && fn(node, this.$vm[exp]);//执行对应的函数,传入的参数是当前元素和app.data中对应的数据
}
//v-model的处理
modelUpdater(node, value) {
    node.value = value
}

7,v-bind的处理

 //v-bind的处理
this.compileBind(node,this.$vm, exp, directName);
compileBind(node,vm, exp, dir){
     // 移除模版中的 v-bind 属性
    node.removeAttribute(`v-${dir}`)
    const newDir=dir.split(':')[1];
    cb();
    function cb(){
      node.setAttribute(newDir, vm[exp])
    }
}

8,v-text和v-html的处理

//其他v-
this.update(node, exp, directName);
update(node, exp, dir) {
    const fn = this[dir + "Updater"];//拼接出v-value需要执行的函数
    fn && fn(node, this.$vm[exp]);//执行对应的函数,传入的参数是当前元素和app.data中对应的数据,这
 }
//v-text的处理
  textUpdater(node, value) {
    node.textContent = value;
  }
//对于v-html的处理
  htmlUpdater(node, value) {
    node.innerHTML = value;
  }

经过上文的处理,我们已经能够把data中的数据渲染到页面上,methods中的事件也绑定到对应的dom上了。

但是值得注意的是,现在我们只是初始化的时候将data中的数据渲染到页面上。一旦data中的数据变更,并没有触发页面数据的变化。

上文说过,我们使用observe来劫持了data中的key-value,当数据发生变化的时候,我们能够准确知道是哪个数据发生了变化,于是这时候就可以通知观察者watcher来执行函数更新对应的dom。

六,watcher的实现

//监听器类,负责维护每一个数据自身的信息,更新函数一执行,则把该数据的最新值传入,调用对应的处理函数,渲染数据
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm;
    this.key = key;
    this.updateFn = updateFn;
    Dep.target = this;//在Dep上临时存储自己这个watcher
    this.vm[this.key];//再次触发一次get,从而将Dep类上临时存的watch,则添加到dep数组中
    Dep.target = null;//然后将临时存储的watcher清除
  }

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

可以看到Watcher的构造器函数存储了这个data数据基本的信息:

vm:就是我们创建的vue实例:app
key:就是data中的key值
updateFn:是传入的函数参数,也就是数据变更后要执行的函数

那这有啥用呢?

我们的目的是data中的一个数据比如说counter变化了,能够把页面上使用这个counter的地方全部更新一下。

那怎么才能找到所有的counter呢?我们在上文初始化的时候,是有遍历解析一次页面上所有dom的,这个时候呢,就可以把所有的counter收集起来。

现在问题转化为:「啥时候收集?怎么收集?又如何管理?如何更新页面数据?」

1,啥时候收集

先明确初衷,目的是数据变更,能重新更新页面数据。那就需要知道啥数据需要更新。

v-text,v-html,v-model,v-bind需要,而v-on,@则不需要。

对于v-v-text,v-html,v-model,因为都执行了解析器中的updata函数,于是update可以改写成:

 update(node, exp, dir) {
    const fn = this[dir + "Updater"];//拼接出v-value需要执行的函数
    fn && fn(node, this.$vm[exp]);//执行对应的函数,传入的参数是当前元素和app.data中对应的数据,这一步是初始化渲染使用
    new Watcher(this.$vm, exp, newValue => {
      fn && fn(node, newValue);//因为这里传入的是对应的node,所以每个watcher是会准确更新对应绑定数据的node,而不是整个页面重绘
    });
  }

而对于v-bind,则可以修改compileBind:

  compileBind(node,vm, exp, dir){
     // 移除模版中的 v-bind 属性
    node.removeAttribute(`v-${dir}`)
    const newDir=dir.split(':')[1];
    cb();//这个地方调用是初始化渲染页面用的
    function cb(){
      node.setAttribute(newDir, vm[exp])
    }
    new Watcher(vm, exp, cb);//这里传进去是为了数据变更后触发更新
  }

2,怎么收集

我们在常规使用时,data中往往会有许多数据,每一个数据比如counter又可能在html中使用多次。

那么,我们就需要一个类Dep,专门来存储这个watcher。每一个Dep实例。管理一个data数据的观察者。

于是Dep可以这样写:

//放置观察者的数组。其中的每一个dep都是一个观察者watcher,数据一旦变更,则遍历执行其中的观察者的更新函数
class Dep {
  constructor() {
    this.deps = [];
  }
  addDep(watcher) {
    this.deps.push(watcher);
  }
//由于一个key是可以多次使用,建立Dep,一个key只有一个dep但是可以有多个watcher, deps中管理多个watcher,在订阅的时候添加,并统一执行更新,做到精确更新。
  notify() {
    this.deps.forEach(watcher =>watcher.update());//由dep调用,
  }
}

那这个Dep又是怎么用的呢?

注意到上文的Watcher构造函数中这几行代码:

Dep.target = this;//在Dep上临时存储自己这个watcher
this.vm[this.key];//再次触发一次get,从而将Dep类上临时存的watch,则添加到dep数组中
Dep.target = null;//然后将临时存储的watcher清除

当我们在初始化解析dom的时候,例如第一次遇到{{counter}}的时候,new Watcher()了,就会执行构造器中的这三行代码。

第一行,在Dep类的target上暂时存储这个counter的watcher。

第二行,重新强制访问这个counter,于是会触发上文说的data拦截的get函数。这时候,我们只要改写对象响应式函数为:

// 对象响应式原理
function defineReactive(obj, key, value) {
  // 解决递归嵌套问题
  observe(value);
  const dep = new Dep();//在数据变化的时候进行订阅并执行对应的更新函数重新渲染。一个data[key]就实例化一个Dep
  Object.defineProperty(obj, key, {
    get() {
      console.log("get", value);
      Dep.target && dep.addDep(Dep.target);//发现Dep类上临时存的watch,则添加到dep数组中
      return value;
    },
    set(newValue) {
      if (newValue !== value) {
        // observe(newValue);
        value = newValue;
        dep.notify()//值发生变更了,则调用dep的notify,这个dep只是本函数中的,所以它的deps应该只有一个data[key]的watcher
      }
    }
  });
}

最开始的时候数据劫持,每遇到一个data,就实例化一个dep,当这一步重新强制访问这个counter的时候,就会执行get函数,然后发现上一行代码暂存的Dep.target中的watcher,就把它push进去。

一旦页面上有多处使用了counter,则这个deps数组就会有多个watcher,并且都是观察counter的。

第三步:清除掉Dep上暂存的watcher。

3,如何管理

2中说到,每一个data在最开始数据劫持的时候,都会实例化一个Dep类,就是说,每一个data数据,都有一个dep实例来管理它的观察者,并且,这个data在页面上使用了几次,dep.deps数据中就保存了几个它的watcher。

也就是说,一个key就会产生一个dep,而每个dep.deps数组中的观察者的数量等同于在html页面上的使用次数。这样一来,页面上的每个需要更新的数据,其实都有一个watcher进行观察。

4,如何更新页面数据

当我们数据变化的时候,就会被拦截器劫持,触发set函数,于是会执行:

// 对象响应式原理
function defineReactive(obj, key, value) {
  // 解决递归嵌套问题
  observe(value);
  const dep = new Dep();//在数据变化的时候进行订阅并执行对应的更新函数重新渲染。一个data[key]就实例化一个Dep
  Object.defineProperty(obj, key, {
    get() {
      console.log("get", value);
      Dep.target && dep.addDep(Dep.target);//发现Dep类上临时存的watch,则添加到dep数组中
      return value;
    },
    set(newValue) {
      if (newValue !== value) {
        // observe(newValue);
        value = newValue;
        dep.notify()//值发生变更了,则调用dep的notify
      }
    }
  });
}

主要就是这一行代码: dep.notify(),这个dep是在上文中说到的,初始化劫持data数据的时候实例化出来的,比如劫持counter的时候就会生成一个dep实例,这里如果counter发生了变化,就会执行它生成的dep实例的notify函数:

//由于一个key是可以多次使用,建立Dep,一个key只有一个dep但是可以有多个watcher, deps中管理多个watcher,在订阅的时候添加,并统一执行更新,做到精确更新。
 notify() {
    this.deps.forEach(watcher =>watcher.update());//由dep调用,
 }

可以看到,notify函数是把dep.deps数组中的watcher全部遍历执行update。

还是拿counter来举例,一个counter可以在页面上使用n次,于是,它的dep.deps数组中就会有n个watcher,而当我们代码更改了counter的值的时候,就需要把页面上所有用到的地方都更新一下,这就是这里遍历执行的意义。

于是我们接着看watcher中的update:

update() {
    this.updateFn.call(this.vm, this.vm[this.key]);//this就是watcher,在这里存储了每个数据的watcher信息,is.vm[this.key]就是该数据对应的值。
  }

上文中说到,一个watcher中存储了这三个信息:

vm:就是我们创建的vue实例:app
key:就是data中的key值
updateFn:是传入的函数参数,也就是数据变更后要执行的函数

这里就是执行这个更新函数。而更新函数我们在之前new Watcher的时候传入的:

  update(node, exp, dir) {
    const fn = this[dir + "Updater"];//拼接出v-value需要执行的函数
    fn && fn(node, this.$vm[exp]);
    new Watcher(this.$vm, exp, newValue => {
      fn && fn(node, newValue);//因为这里传入的是对应的node,所以每个watcher是会准确更新对应绑定数据的node,而不是整个页面重绘
    });
  }

注意这个fn的第一个参数,传的是node,也就是当前的data[key]绑定的dom。

也就是说它能准确地更新对应的dom。

七,总结

Vue 1 的解决方案,就是使用数据劫持,初始化的时候,劫持了数据的每个属性,这样数据发生变化的时候,我们就能精确地知道data数据中哪个 key 对应的值变了,去针对性修改对应的 DOM 即可,这一过程可以按如下方式解构:

vue观察者图.png

1,data中有n个key,就会在数据劫持阶段实例化n个dep。
2,对于data中的一个数据counter,如果页面上使用了k次,则它对应的依赖收集dep.deps数组中就有k个watcher。每个watcher负责dom上一个使用。当counter发生了变化,则遍历deps数组,执行它对应所有watcher的页面更新函数。
3,每个watcher准确更新自己负责的dom,而不会影响其他的dom.所以从这里看,vue1是很高效的。
4,vue1的缺点:每个数据都有自己的watcher,并且每个数据的watcher可以有多个。当开发大型项目时,data很多,并且每个数据在页面上多次使用,导致watcher变得非常多。而每个watcher都需要内存开销去存储,这就导致性能下降。

总的代码:

// 对象响应式处理
function observe(obj) {
  if (typeof obj !== "object" || obj === null) {
    return;
  }
  new Observer(obj);
}

// 对象响应式原理
function defineReactive(obj, key, value) {
  // 解决递归嵌套问题
  observe(value);
  const dep = new Dep();//在数据变化的时候进行订阅并执行对应的更新函数重新渲染。一个data[key]就实例化一个Dep
  Object.defineProperty(obj, key, {
    get() {
      console.log("get", value);
      Dep.target && dep.addDep(Dep.target);//发现Dep类上临时存的watch,则添加到dep数组中
      console.log("+++++",dep)
      return value;
    },
    set(newValue) {
      if (newValue !== value) {
        // observe(newValue);
        value = newValue;
        dep.notify()//值发生变更了,则调用dep的notify,这个dep只是本函数中的,所以它的deps应该只有一个data[key]的watcher
      }
    }
  });
}
class Observer {
  constructor(obj) {
    this.value = obj;
    this.walk(obj);
  }

  walk(obj) {
    Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
  }
}


class FakeVue {
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
    this.$methods = options.methods;
    observe(this.$data);//将数据响应式化
    this.proxy();//将data中的对象,让实例化对象最外层也能访问和修改,也就是再做一层代理,后续this.$vm[exp]才能直接访问,而不是this.$vm.data[key]
    new Compile(this.$options.el, this);
  }
  proxy() {
    Object.keys(this.$data).forEach(key => {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(v) {
          this.$data[key] = v;
        }
      });
    });
  }
}


class Compile {
  constructor(el, vm) {
    this.$vm = vm;

    this.$el = document.querySelector(el);
    if (this.$el) {
      this.compile(this.$el);
    }
  }
  compile(dom) {
    dom.childNodes.forEach(node => {
      if (this.isElement(node)) {//元素节点,有v-或者没有
        this.compileElement(node);
      } else if (this.isInter(node)) {//html中数据双向{{}}
        this.compileText(node);
      }
      //有子节点则遍历
      if (node.childNodes) {
        this.compile(node);
      }
    });
  }
  compileElement(node) {
    const nodeAttrs = node.attributes;//伪数组
    Array.from(nodeAttrs).forEach(attr => {//转化为真数组
      const attrName = attr.name;//属性名称
      const exp = attr.value;//属性的值
      //是自定义指令的处理,不是自定义指令的话不处理
      if (this.isDirective(attrName)) {
        const directName = attrName.substring(2);//把自定义指令的v-value中的value取出来,如v-model,v-html,v-text,v-on:click    
        if (this.isEventDirective(directName)) {
          //v-on的处理
          this.compileEvent(node,this.$vm, exp, directName);
        } else if(this.isModel(directName)){
          //v-model的处理
          this.compileModel(node,exp)
        }else if(this.isBind(directName)){
          //v-bind的处理
          this.compileBind(node,this.$vm, exp, directName);
        }else {
          //其他v-
          this.update(node, exp, directName);
        }
      }
      //是@事件绑定的
      if(this.isEvent(attrName)){
        const directName = attrName.substring(1)
        this.compileEvent(node,this.$vm, exp, directName);
      }
    });
  }

  isDirective(attrName) {
    return attrName && /^v-.+/.test(attrName);
  }

  update(node, exp, dir) {
    const fn = this[dir + "Updater"];//拼接出v-value需要执行的函数
    fn && fn(node, this.$vm[exp]);//执行对应的函数,传入的参数是当前元素和app.data中对应的数据,这一步是初始化渲染使用
    //这里第一次执行get,这时候dep.target里面是空的
    //初始化的时候,初始化一个watcher来监听对应的数据exp,这里传入的函数,最终在数据劫持的set中执行
    //这个watcher监视器,并不是监视data中的数据,而是监视html中的模板数据比如v-html,v-text,v-model,{{}}等,
    //当data中同一个数据被使用多次,那么deps数组中才会出现多个值。这时候dep.notify()中的遍历才有了意义,就是更新页面上所有用了它的地方
    new Watcher(this.$vm, exp, newValue => {
      fn && fn(node, newValue);//因为这里传入的是对应的node,所以每个watcher是会准确更新对应绑定数据的node,而不是整个页面重绘
    });
  }
  compileEvent(node,vm, exp, dir){
    let eventType
    if(dir.indexOf(':')!==-1){
      eventType= dir.split(':')[1];
    }else{
      eventType=dir;
    }
    var cb = vm.$methods && vm.$methods[exp]; //取得绑定的事件方法
    if (eventType && cb) {
      node.addEventListener(eventType, cb.bind(vm), false); //冒泡机制绑定事件
    }
  }
  compileBind(node,vm, exp, dir){
     // 移除模版中的 v-bind 属性
    node.removeAttribute(`v-${dir}`)
    const newDir=dir.split(':')[1];
    cb();
    function cb(){
      node.setAttribute(newDir, vm[exp])
    }
    new Watcher(vm, exp, cb);
  }

//v-text的处理
  textUpdater(node, value) {
    node.textContent = value;
  }
//对于v-html的处理
  htmlUpdater(node, value) {
    node.innerHTML = value;
  }
  //v-model的处理
  modelUpdater(node, value) {
    node.value = value
  }
  //v-model的处理
  compileModel(node, exp){
    this.update(node, exp, 'model');
    //监听input输入
    node.addEventListener('input', (e) => {
      this.$vm[exp] = e.target.value
    })
  }
  //{{}}的处理
  compileText(node) { // 正则表达式解析的分组结果,会保存到全局的 RegExp 上
    this.update(node, RegExp.$1, "text");
  }

  isElement(node) {
    return node.nodeType === 1;
  }
  isInter(node) {
    return node.nodeType === 3 && /{{(.*)}}/.test(node.textContent);
  }
  //判断是否是v-on
  isEventDirective(dir) {
    return dir.indexOf('on:') === 0;
  }
  // 判断是不是@开头的方法
  isEvent(attr) {
    return attr.startsWith('@')
  }
  //判断是不是v-model
  isModel(attr){
    return  attr.startsWith('model')
  }
  //判断是不是v-bind
  isBind(attr){
    return attr.indexOf('bind:') === 0;
  }
}
//监听器类,负责维护每一个数据自身的信息,更新函数一执行,则把该数据的最新值传入,调用对应的处理函数,渲染数据
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm;
    this.key = key;
    this.updateFn = updateFn;
    Dep.target = this;//在Dep上临时存储自己这个watcher
    console.log("存储进去了再次强制访问")
    this.vm[this.key];//再次触发一次get,从而将Dep类上临时存的watch,则添加到dep数组中
    Dep.target = null;//然后将临时存储的watcher清除
  }

  update() {
    this.updateFn.call(this.vm, this.vm[this.key]);//this就是watcher,在这里存储了每个数据的watcher信息,is.vm[this.key]就是该数据对应的值。
  }
}
//放置监听器的数组。其中的每一个deps都是一个监听器watcher,数据一旦变更,则遍历执行其中的监听器的更新函数
class Dep {
  constructor() {
    this.deps = [];
  }
  addDep(watcher) {
    this.deps.push(watcher);
  }
//由于一个key是可以多次使用,建立Dep,一个key只有一个dep但是可以有多个watcher, deps中管理多个watcher,在订阅的时候添加,并统一执行更新,做到精确更新。
  notify() {
    this.deps.forEach(watcher =>watcher.update());//由dep调用,
  }
}