实现Vue的模板编译-数据劫持-双向数据绑定

前言

Vue这个框架自己也用了两年多了,发现自己对它的了解不是很深刻,对vue也只是停留在会用的地方,对于一些细节的地方了解的不是很深,最近有时间就在b站看了路白大佬的公开课,结合vue官网的源码理解,打算自己照着模仿写一个mini版的vue框架,方便自己加深印象,有兴趣的也可以自己去b站看视频,或者看官网。
b站链接:https://www.bilibili.com/video/BV1Lo4y1277S
官网链接: https://vue-js.com/learn-vue/start

内容梳理

此次实现的mini版的vue主要包括了大家比较关心的以下几点:
1、数据的双向绑定
2、数据劫持,也就是实现依赖收集
3、模板编译,主要实现几个简单的命令,比如v-html,v-text,v-model以及click事件

大致内容是以上几点,除此之外我们还要理解几个文件的内容
在这里插入图片描述
上图是我们的项目目录,项目名是myvue,index.html是我们的入口文件,modules下面有一下几个文件
1、complier.js用于模板编译
2、dep.js用于依赖收集
3、index.js也是入口文件,在index .html内引入
4、observe.js用于实现双向数据绑定
5、vue.js包含了vue的类
6、watcher.js用于通知观察者,也就是数据的依赖

到这里大家应该有大致的了解了,接下来是编码环节

index.html

index.html就是一个普通的html页面,内容只有一个id为app的根结点

<!DOCTYPE html>
<html lang="cn">
<head>
</head>
<body>
  <div id="app">
  </div> 
</body>
</html>

vue.js

vue.js里面是我们的一个类,它需要接受一个options参数,这个options参数就是我们new Vue()时候传进来的,具体有哪些内容呢,我们可以联想到一个Vue页面就是一个vue的实例,它包括了一下内容:
1、el 就是我们的根结点
2、data 就是我们常见的vue页面里面的data
3、methods 一些事件,触发的方法

当然了,一个完整的vue肯定还有很多,今天我们只实现一部分,因此,我们的options包含了以上的内容,

export default class Vue{
  constructor(options = {}){
    this.$options = options
    this.$data = options.data
    this.$methods = options.methods

    this.isElement(options.el)

    this._proxyData(this.$data)
  }

  isElement(el){
    if(typeof el == 'string'){
      this.$el = document.querySelector(el)
    }else if(el instanceof HTMLElement){
      this.$el = el
    }

    if(!this.$el){
      throw new Error('请传入字符串节点')
    }
  }

  _proxyData(data){
    Object.keys(data).forEach(key => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get(){
          return data[key]
        },
        set(newValue){
          if(newValue == data[key]){
            return
          }
          data[key] = newValue
        }
      })
    })
  }
}

拿到传入的options,我们先在当前的实例先保存一下,这里我们要判断一下传入的节点el是否是合法的,如果传的节点找不到,或者传的是其他类型的数据我们要提示一下,因此借助一个isElement方法来判断el的合法性,如果传入的不是字符串(id,类名之类的)也不是一个html节点元素就提示传入的节点不合法,否则我们就把拿到的el挂载到当前的实例上。
其次我们还要把data挂载在实例上,这里借助于Object.defineProperity,这里有小伙伴可能要问为什么要把data挂载到实例上了?回想我们在vue开发的过程,如果在data 里面定了一个变量,我们是不是直接this.xxx就能拿到了,这一步就是为了实现这个效果;其次有可能还会问,如果是对象不是需要递归去挂载么?这里其实不需要,我们在这里不是实现双向绑定,只是把data里面的东西挂载到实例上,因此不需要。

index.js

上面我们写了最基本的vue类的内容,先在我们把vue.js引入到我们的入口文件,并初始化看看初始化了什么。

import Vue from './vue.js'
const vm = new Vue({
  el: '#app',
  data: {
    msg: 'hello world',
  },
  methods: {
    handle(){
      alert(111)
    }
  }
})
console.log(vm)

别忘了我们还要在index.html内引入index.js,在index.html内加上这句

<script src="./modules/index.js" type="module"></script>

在这里插入图片描述
在控制台我们可以看到vue实例已经初始化出来了,data的msg直接挂载到了实例上,上面还有msg的 get和set方法。想要试试传错误el的小伙伴可自行尝试,到这里初始化的工作就做的差不多了,接下来是我们比较麻烦的一部分。

捋清关系

这里我们先捋一捋数据流,看看他们之间的关系,逐个细说
1、dep.js 用来存储依赖,收集依赖,通知依赖,既然知道了作用我们先建一个dep的类

export default class Dep{
  constructor(){
    this.subs = [] //依赖的集合
  }

  // 添加依赖的方法 watcher是观察者,也就是数据的依赖者
  addSubs(watcher){

  }

  // 用于通知watcher更新
  notify(){

  }
}

2、wtacher.js 实际上数据的更细最终是在这里执行,dep负责通知watcher,

import Dep from './dep.js'
export default class Watch{
  /**
   * @description: 
   * @param {*} vm 当前vue实例
   * @param {*} key data的key
   * @param {*} cb update回调函数
   * @return {*}
   */  
  constructor(vm, key, cb){
    this.vm = vm
    this.key = key
    this.cb = cb

    Dep.target = this
    this.oldValue = vm[key]
    Dep.target = null
  }

  update(){
    // 判断新值与旧值是否相等
    if(this.vm[this.key] === this.oldValue){
      return 
    }
    // 执行回调更新
    this.cb(this.vm[this.key])
  }
}

到了这里大家和我一样可能都有些迷糊了,我来和大家细说一下,我们的watcher初始化的时候会接收三个参数,vm是当前的都vue实例;key是我们datā都key,比如你在data定义了一个a变量,那么这个key就是a;cb是一个回调函数,用于数据的更新;初始化的时候我们先把这三个保存到a变量的watcher中,每一个变量都有属于自己的watcher,互不干扰;这里还有一步更精妙的就是在Dep上挂载了一个target,因为我们在获取旧值的时候会触发我们的变量的get方法,我们需要把依赖收集到dep中,有小伙伴可能会疑惑,不设置target我一样可以添加依赖啊,是的,的确可以,但是这样有可能会造成多次添加依赖,我们的target是为了确保唯一性,只添加一次,不重复添加,并且添加完马上设置为null。

这时候回过头去看dep.js就很简单了,直接上代码

export default class Dep{
  constructor(){
    this.subs = [] //依赖的集合
  }

  // 添加依赖的方法 watcher是观察者,也就是数据的依赖者
  addSubs(watcher){
    if(watcher && watcher.update){
      this.subs.push(watcher)
    }
  }

  // 用于通知watcher更新
  notify(){
  	this.subs.forEach(watcher => {
      watcher.update()
    })
  }
}

依赖的收集和更新都写好了,接下来就到我们模板的编译了,首先我们得知道complier.js是用来做什么的,我们比如有下面一段这样的template:


  <div id="app">
    <h3 v-html='msg'></h3>
    <h3 v-text='msg'></h3>
    <input type="text" v-model="msg">
    <button v-on:click="handle"></button>
  </div> 

首先我们怎么拿到这段模板呢,
1、首先我们需要先拿到根结点
2、拿根结点的集合去遍历,判断他是文本节点还是标签节点
3、如果是文本节点直接解析它的nodeContent,如果是表达式,类似{{ msg }}的,直接用正则表达式替换掉内容
如果是标签节点,拿到该节点的属性进行遍历,如果是v-开头的 指令就进行下一步的指令解析。
4、如果指令是v-html,v-text这种,我们就截取指令v-的后半部分,如果是v-on:click就截取v-on:后面的内容;并把截取到的字符串拼接一个对应的事件,比如html就定义一个htmlHandle专门去处理v-html指令

大致步骤如上,接下来我们开始编码,根据上面的内容,我们需要初始化complier的时候需要传入一个vm实例,然后把实例上的内容保存起来

import Watcher from './watcher.js'
export default class Complier{
  constructor(vm){  
    this.vm = vm
    this.data = vm.$data
    this.methods = vm.$methods

    this.initTemplate(vm.$el)
  }

  // 解析模板
  initTemplate(el){
    let childNodes = Array.from(el.childNodes)
    childNodes.forEach(node => {
      // 判断是否是文本节点
      if(this.isTextNode(node.nodeType)){
        this.initTextNode(node)
      }else if(this.isTagNode(node.nodeType)){
        // 标签节点
        this.initTagNode(node)
      }
      
      // 如果节点下面还有子节点 递归遍历
      if(node.childNodes && node.childNodes.length > 0){
        this.initTemplate(node)
      }
    })
  }
  initTextNode(node){
    let reg = /\{\{(.+)\}\}/ // 正则匹配{{}}
    let value = node.textContent // 获取文本节点内容
    if(reg.test(value)){ 
      let key = RegExp.$1.trim() // 获取当前上下文第一个正则表达式的结果
      node.textContent = value.replace(reg, this.vm[key]) // 替换掉节点的内容
    }
  }

  initTagNode(node){
    // v-html v-text  v-on:click
    let attrs = Array.from(node.attributes) // 获取标签节点属性
    attrs.forEach(attr => {
      if(this.isDirective(attr.name)){ //判断是否是vue的指令
        // v-text v-on:click根据不同指令截取不同内容
        let name = attr.name.indexOf(':') > -1 ? attr.name.slice(5) : attr.name.slice(2)
        // 生成fn
        let fn = this[ name + 'Handle']
        fn && fn.call(this, node, name, attr.value)
      }
    })
  }

  clickHandle(node, name, key){
    // click事件直接在节点上添加dom事件,回调方法就是methods中的方法,例如v-on:click=”handle“,key就是handle
    node.addEventListener(name, this.methods[key])
  }

  textHandle(node, name, key){
    // v-text直接替换掉内容
    node.textContent = this.data[key]
    // 这里需要添加一个watcher,因为{{ msg }}也是数据的依赖者
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue
    })
  }

  htmlHandle(node, name, key){
    // v-html与v-text大同小异
    node.innerHTML = this.data[key]
    new Watcher(this.vm, key, (newValue) => {
      node.innerHTML = newValue
    })
  }

  modelHandle(node, name, key){
    node.value = this.data[key]
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue
    })

    // v-model还需要在输入的时候手动更新data的值,达到数据双向绑定
    node.addEventListener('input', () => {
      this.vm[key] = node.value
    })

  }

  isDirective(name){
    return name.startsWith('v-')
  }

  isTextNode(type){
    return type === 3
  }

  isTagNode(type){
    return type === 1
  }
}

complier.js内容比较多,但不复杂,处理的细节比较多,需要多看看。

最后一个就是observe.js了,在这里实现我们的双向数据绑定

import Dep from './dep.js'
export default class Observer{
  constructor(vm){
    this.data = vm.$data
    this.vm = vm
    this.traverse(this.data)
  }
// 遍历data
  traverse(data){
    if(!data || typeof data != 'object'){
      return
    }
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }

  defineReactive(data, key, value){
    let dep = new Dep()
    let that = this
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get(){
        	// 这里结合watcher那一段看,只有初始化watcher的时候,Dep.target才会存在,所以get的时候需要添加到依赖
          Dep.target && dep.addSubs(Dep.target)
          return value
        },
        set(newValue){
          if(newValue == value){
            return
          }
          value = newValue
          // 设置新值的时候如果newValue是obj需要在遍历一次
          that.traverse(newValue)
          // 数据更新通知依赖发生了改变
          dep.notify()
        }
      })
  }
}

到这里我们的mini版vue就写完了,大家可以运行一遍试试,其实不需要大家去死记硬背,最重要的是大家要去了解vue每一步到底干了什么,面试的时候可能会问你,什么时候收集依赖?什么时候通知订阅者数据发生了更新,在哪里通知?vue是如何解析模板的,等等这些问题,看完文章我也相信大家有了一定的了解,由于我的文章比较烂,还是希望大家多看看官网的源码系列以及b站教程的,链接在上面,我也是照着b站自己撸一遍,来这里写篇文章加深印象,希望大家和我一样有所收获,文章有错误的地方欢迎指出,多多相互学习交流。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 内容概要 《计算机试卷1》是一份综合性的计算机基础和应用测试卷,涵盖了计算机硬件、软件、操作系统、网络、多媒体技术等多个领域的知识点。试卷包括单选题和操作应用两大类,单选题部分测试学生对计算机基础知识的掌握,操作应用部分则评估学生对计算机应用软件的实际操作能力。 ### 适用人群 本试卷适用于: - 计算机专业或信息技术相关专业的学生,用于课程学习或考试复习。 - 准备计算机等级考试或职业资格认证的人士,作为实战演练材料。 - 对计算机操作有兴趣的自学者,用于提升个人计算机应用技能。 - 计算机基础教育工作者,作为教学资源或出题参考。 ### 使用场景及目标 1. **学习评估**:作为学校或教育机构对学生计算机基础知识和应用技能的评估工具。 2. **自学测试**:供个人自学者检验自己对计算机知识的掌握程度和操作熟练度。 3. **职业发展**:帮助职场人士通过实际操作练习,提升计算机应用能力,增强工作竞争力。 4. **教学资源**:教师可以用于课堂教学,作为教学内容的补充或学生的课后练习。 5. **竞赛准备**:适合准备计算机相关竞赛的学生,作为强化训练和技能检测的材料。 试卷的目标是通过系统性的题目设计,帮助学生全面复习和巩固计算机基础知识,同时通过实际操作题目,提高学生解决实际问题的能力。通过本试卷的学习与练习,学生将能够更加深入地理解计算机的工作原理,掌握常用软件的使用方法,为未来的学术或职业生涯打下坚实的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值