目录
开始
实现 Vue 类
实现模板解析 Compile 类
实现数据代理 Observer 类
实现管理器 Dep 类
实现订阅者 Watcher 类
埋坑
总结
首页 web前端 Vue.js 手把手教你怎么实现一个vue双向绑定

手把手教你怎么实现一个vue双向绑定

Mar 03, 2022 pm 07:41 PM
vue 双向绑定

vue怎么实现双向绑定?本篇文章就手把手教你写一个vue的双向绑定,让大家可以更好的理解双向绑定的逻辑走向,希望对大家有所帮助!

手把手教你怎么实现一个vue双向绑定

本文主要是一个写坑和填坑的过程,让观众更好的理解如何去实现双向绑定,双向绑定的逻辑走向,至此一步步来重头实现一个双向绑定。这是一个类教程文章,只要跟着文章走,仔细观察每个类其实也并不难实现。

开始

开局法师?带一条狗!!

好像不对

重来,开局一张图

1.png

从图上可以看出new Vue()分为了两步走

  • 代理监听所有数据,并与Dep进行关联,通过Dep通知订阅者进行视图更新。【相关推荐:vuejs视频教程

  • 解析所有模板,并将模板中所用到的数据进行订阅,并绑定一个更新函数,数据发生改变时Dep通知订阅者执行更新函数。

接下里就是分析如何去实现,并且都需要写什么,先看一段vue的基础代码,我们从头开始分析

<div id="app">
  <input v-model="message" />
  <p>{{message}}</p>
</div>
登录后复制
let app = new Vue({
    el:"#app",
    data:{
      message:"测试这是一个内容"
    }
})
登录后复制

从上面代码我们可以看到new Vue的操作,里面携带了eldata属性,这算是最基础的属性,而在html代码中我们知道<div id="app">是vue渲染的模板根节点,所以vue要渲染页面就要去实现一个模板解析的方法Compile类,解析方法中还需要去处理{{ }}v-model两个指令,除了解析模板之后我们还需要去实现数据代理也就是实现Observer

实现 Vue 类

如下代码所示,这就写完了Vue类,够简单吧,如果对class关键字不熟悉的,建议先去学习一下,从下面我们可能看到,这里实例化了两个类,一个是代理数据的类,一个是解析模板的类。

class Vue {
  constructor(options) {
    // 代理数据
    new Observer(options.data)
    // 绑定数据
    this.data = options.data
    // 解析模板
    new Compile(options.el, this)
  }
}
登录后复制

接着往下我们先写一个Compile类用于解析模板,我们再来分析一波,解析模板要做什么事

  • 我们要解析模板不可能直接对dom继续操作,所以我们要创建一个文档片段(虚拟dom),然后将模板DOM节点复制一份到虚拟DOM节点中,对虚拟DOM节点解析完成之后,再将虚拟DOM节点替换掉原来的DOM节点

  • 虚拟节点复制出来之后,我们要遍历整个节点树进行解析,解析过程中会对DOM的atrr属性进行遍历找到Vue相关的指令,除此之外还要对 textContent节点内容进行解析,判断是否存在双花括号

  • 将解析出来所用到的属性进行一个订阅

实现模板解析 Compile 类

下面我们将逐步实现

  • 构建Compile类,先把静态节点和Vue实例获取出来,再定义一个虚拟dom的属性用来存储虚拟dom

class Compile {
  constructor(el, vm) {
    // 获取静态节点
    this.el = document.querySelector(el);
    // vue实例
    this.vm = vm 
    // 虚拟dom
    this.fragment = null 
    // 初始化方法
    this.init()
  }
}
登录后复制
  • 实现初始化方法init(),该方法主要是用于创建虚拟dom和调用解析模板的方法,解析完成之后再将DOM节点替换到页面中

class Compile { 
  //...省略其他代码

  init() {
    // 创建一个新的空白的文档片段(虚拟dom)
    this.fragment = document.createDocumentFragment()
  	// 遍历所有子节点加入到虚拟dom中
    Array.from(this.el.children).forEach(child => {
      this.fragment.appendChild(child)
    })
    // 解析模板
    this.parseTemplate(this.fragment)
    // 解析完成添加到页面
    this.el.appendChild(this.fragment);
  }
}
登录后复制
  • 实现解析模板方法parseTemplate,主要是遍历虚拟DOM中的所有子节点并进行解析,根据子节点类型进行不同的处理。

class Compile { 
  //...省略其他代码

  // 解析模板 
  parseTemplate(fragment) {
    // 获取虚拟DOM的子节点
    let childNodes = fragment.childNodes || []
    // 遍历节点
    childNodes.forEach((node) => {
      // 匹配大括号正则表达式 
      var reg = /\{\{(.*)\}\}/;
      // 获取节点文本
      var text = node.textContent;
      if (this.isElementNode(node)) { // 判断是否是html元素
        // 解析html元素
        this.parseHtml(node)
      } else if (this.isTextNode(node) && reg.test(text)) { //判断是否文本节点并带有双花括号
        // 解析文本
        this.parseText(node, reg.exec(text)[1])
      }

      // 递归解析,如果还有子元素则继续解析
      if (node.childNodes && node.childNodes.length != 0) {
        this.parseTemplate(node)
      }
    });
  }
}
登录后复制
  • 根据上面的代码我们得出需要实现两个简单的判断,也就是判断是否是html元素和文字元素,这里通过获取nodeType的值来进行区分,不了解的可以直接看一下 传送门:Node.nodeType,这里还扩展了一个isVueTag方法,用于后面的代码中使用

class Compile { 
  //...省略其他代码

	// 判断是否携带 v-
  isVueTag(attrName) {
    return attrName.indexOf("v-") == 0
  }
  // 判断是否是html元素
  isElementNode(node) {
    return node.nodeType == 1;
  }
  // 判断是否是文字元素
  isTextNode(node) {
    return node.nodeType == 3;
  }
}
登录后复制
  • 实现parseHtml方法,解析html代码主要是遍历html元素上的attr属性

class Compile {
  //...省略其他代码

  // 解析html
  parseHtml(node) {
    // 获取元素属性集合
    let nodeAttrs = node.attributes || []
    // 元素属性集合不是数组,所以这里要转成数组之后再遍历
    Array.from(nodeAttrs).forEach((attr) => {
      // 获取属性名称
      let arrtName = attr.name;
      // 判断名称是否带有 v- 
      if (this.isVueTag(arrtName)) {
        // 获取属性值
        let exp = attr.value;
        //切割 v- 之后的字符串
        let tag = arrtName.substring(2);
        if (tag == "model") {
          // v-model 指令处理方法
          this.modelCommand(node, exp, tag)
        }
      }
    });
  }
}
登录后复制
  • 实现modelCommand方法,在模板解析阶段来说,我们只要把 vue实例中data的值绑定到元素上,并实现监听input方法更新数据即可。

class Compile {
	//...省略其他代码
  
   // 处理model指令
  modelCommand(node, exp) {
    // 获取数据
    let val = this.vm.data[exp]
    // 解析时绑定数据
    node.value = val || ""

    // 监听input事件
    node.addEventListener("input", (event) => {
      let newVlaue = event.target.value;
      if (val != newVlaue) {
        // 更新data数据
        this.vm.data[exp] = newVlaue
        // 更新闭包数据,避免双向绑定失效
        val = newVlaue
      }
    })
  }
}
登录后复制
  • 处理Text元素就相对简单了,主要是将元素中的textContent内容替换成数据即可

class Compile {
	//...省略其他代码
  
  //解析文本
  parseText(node, exp) {
    let val = this.vm.data[exp]
    // 解析更新文本
    node.textContent = val || ""
  }
}
登录后复制

至此已经完成了Compile类的初步编写,测试结果如下,已经能够正常解析模板

2.png

下面就是我们目前所实现的流程图部分

3.png

坑点一:

  • 在第6点modelCommand方法中并没有实现双向绑定,只是单向绑定,后续要双向绑定时还需要继续处理

坑点二:

  • 第7点parseText方法上面的代码中并没有去订阅数据的改变,所以这里只会在模板解析时绑定一次数据

实现数据代理 Observer 类

这里主要是用于代理data中的所有数据,这里会用到一个Object.defineProperty方法,如果不了解这个方法的先去看一下文档传送门:

文档:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

Observer类主要是一个递归遍历所有data中的属性然后进行数据代理的的一个方法

defineReactive中传入三个参数data, key, val

datakey都是Object.defineProperty的参数,而val将其作为一个闭包变量供Object.defineProperty使用

// 监听者
class Observer {
  constructor(data) {
    this.observe(data)
  }
  // 递归方法
  observe(data) {
    //判断数据如果为空并且不是object类型则返回空字符串
    if (!data || typeof data != "object") {
      return ""
    } else {
      //遍历data进行数据代理
      Object.keys(data).forEach(key => {
        this.defineReactive(data, key, data[key])
      })
    }
  }

  // 代理方法
  defineReactive(data, key, val) {
    // 递归子属性
    this.observe(data[key])
    Object.defineProperty(data, key, {
      configurable: true,  //可配置的属性
      enumerable: true, //可遍历的属性
      get() {
        return val
      },
      set(newValue) {
        val = newValue
      }
    })
  }
}
登录后复制

下面我们来测试一下是否成功实现了数据代理,在Vue的构造函数输出一下数据

class Vue {
  constructor(options) {
    // 代理数据
    new Observer(options.data)
    console.log(options.data)
    // 绑定数据
    this.data = options.data
    // 解析模板
    new Compile(options.el, this)
  }
}
登录后复制

结果如下,我们可以看出已经实现了数据代理。

4.png

对应的流程图如下所示

5.png

坑点三:

  • 这里虽然实现了数据代理,但是按照图上来说,还需要引入管理器,在数据发生变化时通知管理器数据发生了变化,然后管理器再通知订阅者更新视图,这个会在后续的填坑过程过讲到。

实现管理器 Dep 类

上面我们已经实现了模板解析到初始化视图,还有数据代理。而下面要实现的Dep类主要是用于管理订阅者和通知订阅者,这里会用一个数组来记录每个订阅者,而类中也会给出一个notify方法去调用订阅者的update方法,实现通知订阅者更新功能。这里还定义了一个target属性用来存储临时的订阅者,用于加入管理器时使用。

class Dep {
  constructor() {
    // 记录订阅者
    this.subList = []
  }
  // 添加订阅者
  addSub(sub) {
    // 先判断是否存在,防止重复添加订阅者
    if (this.subList.indexOf(sub) == -1) {
      this.subList.push(sub)
    }
  }
  // 通知订阅者
  notify() {
    this.subList.forEach(item => {
      item.update() //订阅者执行更新,这里的item就是一个订阅者,update就是订阅者提供的方法
    })
  }
}
// Dep全局属性,用来临时存储订阅者
Dep.target = null
登录后复制

管理器实现完成之后我们也就实现了流程图中的以下部分。要注意下面几点

  • Observer通知Dep主要是通过调用notify方法
  • Dep通知Watcher主要是是调用了Watcher类中的update方法

6.png


实现订阅者 Watcher 类

订阅者代码相对少,但是理解起来还是有点难度的,在Watcher类中实现了两个方法,一个是update更新视图方法,一个putIn方法(我看了好几篇文章都是定义成 get 方法,可能是因为我理解的不够好吧)。

  • update:主要是调用传入的cb方法体,用于更新页面数据
  • putIn:主要是用来手动加入到Dep管理器中。
// 订阅者
class Watcher {
  // vm:vue实例本身
  // exp:代理数据的属性名称
  // cb:更新时需要做的事情
  constructor(vm, exp, cb) {
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.putIn()
  }
  update() {
    // 调用cb方法体,改变this指向并传入最新的数据作为参数
    this.cb.call(this.vm, this.vm.data[this.exp])
  }
  putIn() {
    // 把订阅者本身绑定到Dep的target全局属性上
    Dep.target = this
    // 调用获取数据的方法将订阅者加入到管理器中
    let val = this.vm.data[this.exp]
    // 清空全局属性
    Dep.target = null
  }
}
登录后复制

坑点四:

  • Watcher类中的putIn方法再构造函数调用后并没有加入到管理器中,而是将订阅者本身绑定到target全局属性上而已

埋坑

通过上面的代码我们已经完成了每一个类的构建,如下图所示,但是还是有几个流程是有问题的,也就是上面的坑点。所以下面要填坑

7.png

埋坑 1 和 2

完成坑点一和坑点二,在modelCommandparseText方法中增加实例化订阅者代码,并自定义要更新时执行的方法,其实就是更新时去更新页面中的值即可

modelCommand(node, exp) {
  
  // ...省略其他代码
  
  // 实例化订阅者,更新时直接更新node的值
  new Watcher(this.vm, exp, (value) => {
    node.value = value
  })
}


parseText(node, exp) {
  
  //  ...省略其他代码
  
  // 实例化订阅者,更新时直接更新文本内容
  new Watcher(this.vm, exp, (value) => {
    node.textContent = value
  })
}
登录后复制

埋坑 3

完成坑点三,主要是为了引入管理器,通知管理器发生改变,主要是在Object.defineProperty set方法中调用dep.notify()方法

// 监听方法
defineReactive(data, key, val) {
  // 实例化管理器--------------增加这一行
  let dep = new Dep()
  
  // ...省略其他代码
  
    set(newValue) {
      val = newValue
      // 通知管理器改变--------------增加这一行
      dep.notify()
    }

}
登录后复制

埋坑 4

完成坑点四,主要四将订阅者加入到管理器中

defineReactive(data, key, val) {
  // ...省略其他代码
    get() {
      // 将订阅者加入到管理器中--------------增加这一段
      if (Dep.target) {
        dep.addSub(Dep.target)
      }
      return val
    },
  // ...省略其他代码
}
登录后复制

完成了坑点四可能就会有靓仔疑惑了,这里是怎么加入的呢Dep.target又是什么呢,我们不妨从头看看代码并结合下面这张图

8.png

至此我们已经实现了一个简单的双向绑定,下面测试一下

9.gif

完结撒花

总结

本文解释的并不多,所以才是类教程文章,如果读者有不懂的地方可以在评论去留言讨论

10.png

(学习视频分享:vuejs教程web前端

以上是手把手教你怎么实现一个vue双向绑定的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
2 周前 By 尊渡假赌尊渡假赌尊渡假赌
仓库:如何复兴队友
1 个月前 By 尊渡假赌尊渡假赌尊渡假赌
Hello Kitty Island冒险:如何获得巨型种子
4 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

vue中echarts怎么用 vue中echarts怎么用 May 09, 2024 pm 04:24 PM

在 Vue 中使用 ECharts 可让应用程序轻松添加数据可视化功能。具体步骤包括:安装 ECharts 和 Vue ECharts 包、引入 ECharts、创建图表组件、配置选项、使用图表组件、实现图表与 Vue 数据的响应式、添加交互式功能,以及使用高级用法。

vue中的export default的作用 vue中的export default的作用 May 09, 2024 pm 06:48 PM

问题:Vue 中 export default 的作用是什么?详细描述:export default 定义组件的默认导出。导入时,将自动导入组件。简化导入过程,提高清晰度和防止冲突。常用于导出单个组件、同时使用命名导出和默认导出以及注册全局组件。

vue中map函数的用法 vue中map函数的用法 May 09, 2024 pm 06:54 PM

Vue.js map 函数是一个内置的高阶函数,用于创建一个新数组,其中每个元素都是原始数组中的每个元素转换后的结果。其语法为 map(callbackFn),其中 callbackFn 接收数组中的每个元素作为第一个参数,可选地接收索引作为第二个参数,并返回一个值。map 函数不会改变原始数组。

vue中event和$event区别 vue中event和$event区别 May 08, 2024 pm 04:42 PM

Vue.js 中,event 为原生 JavaScript 事件,由浏览器触发,而 $event 是 Vue 特定抽象事件对象,在 Vue 组件中使用。一般使用 $event 更方便,因为它经过格式化和增强,支持数据绑定。当需要访问原生事件对象特定功能时,使用 event。

vue中onmounted作用 vue中onmounted作用 May 09, 2024 pm 02:51 PM

onMounted 是 Vue 中的组件挂载生命周期钩子,其作用是在组件挂载到 DOM 后执行初始化操作,例如获取 DOM 元素的引用、设置数据、发送 HTTP 请求、注册事件监听器等。它在组件挂载时仅调用一次,如果需要在组件更新后或销毁前执行操作,可以使用其他生命周期钩子。

vue中export与export default区别 vue中export与export default区别 May 08, 2024 pm 05:27 PM

Vue.js 中导出模块的方式有两种:export 和 export default。export用于导出命名实体,需要使用花括号;export default用于导出默认实体,不需要花括号。导入时,export导出的实体需要使用其名称,而export default导出的实体可以隐式使用。建议对于需要被多次导入的模块使用export default,对于只导出一次的模块使用export。

vue中的钩子是什么 vue中的钩子是什么 May 09, 2024 pm 06:33 PM

Vue 钩子是可在特定事件或生命周期阶段执行操作的回调函数。它们包括生命周期钩子(如 beforeCreate、mounted、beforeDestroy)、事件处理钩子(如 click、input、keydown)和自定义钩子。钩子增强组件控制,响应组件生命周期,处理用户交互并提高组件重用性。使用钩子,定义钩子函数、执行逻辑并返回可选值即可。

vue中的onmounted对应react哪个生命周期 vue中的onmounted对应react哪个生命周期 May 09, 2024 pm 01:42 PM

Vue 中的 onMounted 对应于 React 中的 useEffect 生命周期方法,带有空依赖项数组 [],在组件挂载到 DOM 后立即执行。

See all articles