当前位置:网站首页 > Vue.js开发 > 正文

简易版实现vue1.0

一,前言

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: { 
      counter: 1, desc: `<span style="color: red">测试样式</span>` }, methods:{ 
      add(){ 
      this.counter++ } } }); </script> </body> 

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

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

二,原理分析

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

1,数据发生变化,如何被监听到?
2,html模板中的{
  
  
  
  
  
  
  
  {}}和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则是更新视图的函数方法。

在这里插入图片描述

三,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],修改值也是一样。

在这里插入图片描述

五,Compile解析html

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

在这里插入图片描述

1,获取最外层根节点

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

在这里插入图片描述

于是可以利用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等调用不同的处理逻辑。

在这里插入图片描述

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中的{ {}}能够渲染出来了。

在这里插入图片描述

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 即可,这一过程可以按如下方式解构:

在这里插入图片描述

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都需要内存开销去存储,这就导致性能下降。 

总的vue代码:

// 对象响应式处理 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调用, } } 
到此这篇简易版实现vue1.0的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!

版权声明


相关文章:

  • 使用vuepress搭建文档站点2024-11-28 21:45:07
  • vue3下的watcheffect2024-11-28 21:45:07
  • vue2源码阅读--(一)--flow2024-11-28 21:45:07
  • 基于vue2.x搭建组件库的流程-(一)-组件库搭建与发布流程2024-11-28 21:45:07
  • 基于vue2.x搭建组件库的流程-(二)-新组件文件的初始化2024-11-28 21:45:07
  • 原生js实现dom的获取及操作2024-11-28 21:45:07
  • vue3的自定义指令2024-11-28 21:45:07
  • webpack5+vue3搭建h5项目模板-(二)-eslint代码规范化2024-11-28 21:45:07
  • webpack5+vue3搭建h5项目模板-(一)-基础配置2024-11-28 21:45:07
  • js正则表达式--个人常用2024-11-28 21:45:07
  • 全屏图片