vue 响应式原理模拟3 - Compiler

至此我们已经将数据都存储好,而且将 data 中的属性都转换为响应式的了,接下来我们需要做的就是编译插值表达式和指令,将 html 中的 {{}}v-textv-model 都解析成 data 中的属性。

Compiler 类结构

我们定义一个 Compiler 类来完成编译插值表达式和指令,这个类里我们定义 el(vm.$el是一个Dom对象) 和 vm(vue实例) 两个属性,定义 compile 方法来进行编译。

compile(el) 方法遍历 el 这个 Dom 对象的所有节点,调用 isTextNode(node) 和 isElementNode(node) 方法判断这些节点类型,如果是文本节点则调用 compileText(node) 解析插值表达式, 如果是元素节点则调用 compileElement(node) 解析指令。在 compileElement 内部调用 isDirective(attrName),判断 attrName 这个属性是否是指令。

我们按照以上所说的功能先描绘出 Compiler 类的大致结构:

js 复制代码
class Compiler {
  constructor(vm) {
    this.el = vm.$el
    this.vm = vm
    this.compile(this.el)
  }
  
  compile(el) {}
  compileElement(node) {}
  compileText(node) {}
  
  isTextNode(node) {
    return node.nodeType === 3   // 3 为文本节点
  }
  isElementNode(node) {
    node.nodeType === 1   // 1 为元素节点
  }
  isDirective(attrName) {
    return attrName.startsWith('v-')   // 指令以 v- 开头
  } 
}

接下来需要完善 compilecompileElementcompileText 方法。

使用 Compiler

在完善方法之前,我们先引入 Compiler

在 Vue 类中创建实例

js 复制代码
new Observer(this.$data)
+ new Compiler(this)

在 html 中引入 compiler 文件

compile 方法

compile 方法中首先要遍历 el 中的所有子节点,如果是文本节点按照插值表达式处理,如果是元素节点按照指令处理,如果子节点中还有子节点,则需要更深层次的遍历。

js 复制代码
compile(el) {
    // 遍历 el 这个 Dom 对象的所有节点
    // el.children 只返回所有子元素节点,不包括文本节点和注释节点。而 el.childNodes 返回所有子节点
    let childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      if (this.isTextNode(node)) {
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        this.compileElement(node)
      }
      // 处理深层次的子节点,如果 node 还有子节点要继续遍历
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }

compileText 方法

compileText 用于编译文本节点,处理插值表达式,可以利用 nodeValuetextContent 属性获取文本节点的内容,然后利用正则表达式匹配插值表达式 {{}} 取出里面的变量,再用对应的值替代插值表达式。

js 复制代码
compileText(node) {
    console.dir(node)  // 以对象的形式打印出文本节点
    let value = node.textContent
    let reg = /\{\{(.+?)\}\}/
    if (reg.test(value)) {
      node.textContent = value.replace(reg, (match, p1) => this.vm[p1.trim()])
    }
}

当完善这个方法后,我们可以在模拟实例中看到插值表达式的 msgcount 都显示成了 data 中的值。

compileElement 方法

这个方法我们倒着来看怎么写,首先这个方法是处理指令的,不同的指令有不同的操作,在这个模拟实例上我们暂时只写了 v-textv-model 指令,所以我们先来写一写这两个指令的代码。

v-text

js 复制代码
textUpdater (node, value, key) {
  node.textContent = value
}

v-model

暂时对于 v-model 只考虑显示,不考虑输入的功能。

js 复制代码
modelUpdater (node, value, key) {
  node.value = value
}

update

我们注意到对于 textUpdatermodelUpdater 两个方法的命名,是根据指令名称去掉 v- 前缀再拼接 Updater 得到的,这样做的好处是,无论有再多的指令我们只需要加一个按照这个规律命名的方法即可,不需要修改其他代码。

那我们在统一的 update 方法中处理这个逻辑:

js 复制代码
update (node, key, attrName) {
  attrName = attrName.substr(2)             // 去除前缀 v-
  let updateFn = this[attrName + 'Updater']
  updateFn && updateFn(node, this.vm[key])
}

compileElement

指令的处理逻辑已经在上面写好了,那接下就是我们如何在 Dom 中找出指令,并将指令的名称和值传递给 update 方法。在模拟实例中,v-text 的使用如下:

html 复制代码
<div v-text="msg"></div>

这个元素节点会进入 compileElement 的处理逻辑,而我们需要遍历这个元素节点的属性节点(node.attributes),并判断这个属性是否为 v- 指令,如果是指令则获取指令的名称(attr.name)和指令的值(attr.value

js 复制代码
  compileElement (node) {
    Array.from(node.attributes).forEach(attr => {
      let attrName = attr.name
      if (this.isDirective(attrName)) {
        let key = attr.value
        this.update(node, key, attrName)
      }
    })
  }

完整代码

js 复制代码
class Compiler {
  constructor (vm) {
    this.el = vm.$el
    this.vm = vm
    this.compile(this.el)
  }
  // 编译模板,处理文本节点和元素节点
  compile (el) {
    let childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      if (this.isTextNode(node)) {
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        this.compileElement(node)
      }
      // 判断node节点,是否有子节点,如果有子节点,要递归调用compile
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }
  // 编译元素节点,处理指令
  compileElement (node) {
    Array.from(node.attributes).forEach(attr => {
      let attrName = attr.name
      if (this.isDirective(attrName)) {
        let key = attr.value
        this.update(node, key, attrName)
      }
    })
  }

  update (node, key, attrName) {
    attrName = attrName.substr(2)
    let updateFn = this[attrName + 'Updater']
    updateFn && updateFn.call(this, node, this.vm[key], key)
  }

  textUpdater (node, value, key) {
    node.textContent = value
  }
  modelUpdater (node, value, key) {
    node.value = value
  }

  // 编译文本节点,处理差值表达式
  compileText (node) {
    let reg = /\{\{(.+?)\}\}/
    let value = node.textContent
    if (reg.test(value)) {
      node.textContent = value.replace(reg, (match, p1) => this.vm[p1.trim()])
    }
  }
  // 判断元素属性是否是指令
  isDirective (attrName) {
    return attrName.startsWith('v-')
  }
  // 判断节点是否是文本节点
  isTextNode (node) {
    return node.nodeType === 3
  }
  // 判断节点是否是元素节点
  isElementNode (node) {
    return node.nodeType === 1
  }
}
相关推荐
HaanLen2 小时前
React19源码系列之 Hooks (useState、useReducer、useOptimistic)
服务器·前端
yuanyxh4 小时前
《精通正则表达式》精华摘要
前端·javascript·正则表达式
小飞大王6665 小时前
简单实现HTML在线编辑器
前端·编辑器·html
Jimmy5 小时前
CSS 实现卡牌翻转
前端·css·html
百万蹄蹄向前冲5 小时前
大学期末考,AI定制个性化考试体验
前端·人工智能·面试
向明天乄6 小时前
在 Vue 3 项目中集成高德地图(附 Key 与安全密钥申请全流程)
前端·vue.js·安全
sunshine_程序媛6 小时前
vue3中的watch和watchEffect区别以及demo示例
前端·javascript·vue.js·vue3
电商数据girl6 小时前
【经验分享】浅谈京东商品SKU接口的技术实现原理
java·开发语言·前端·数据库·经验分享·eclipse·json
Senar7 小时前
听《富婆KTV》让我学到个新的API
前端·javascript·浏览器
烛阴7 小时前
提升Web爬虫效率的秘密武器:Puppeteer选择器全攻略
前端·javascript·爬虫