实现一个最小版本vue(三)之compiler

compiler

功能

  • 负责编译模板,解析指令和插值表达式
  • 负责页面的首次渲染
  • 当数据变化后重新渲染视图
  • 结构

实现思路

  1. 根据图示,这个类有两个属性以及一些操作属性的方法
  2. compiler类的构造函数,传入vue的实例,定义el和vm两个属性,分别记住模板和vue实例,紧接着调用this.compile编译模板,这里调用这个方法的目的是,当调用new Compile()时,会立即开始编译模板
  3. compile方法里,处理所有文本节点的差值表达式和元素节点的指令
    1. 首先编译el里的所有子节点
    2. 判断是文本节点还是元素节点,调用相对应的处理方法
    3. 这里仅仅处理了el下第一层子节点,还要判断子节点下有没有子节点,进行递归调用
  4. compileText方法,编译文本节点,处理差值表达式
    1. 定义正则,匹配类似{{ msg }}这种差值表达式
    2. 获取文本节点里内容
    3. 如果内容匹配到正则,匹配到的值,就是vue实例中,data属性的key
    4. 有了这个key,就可以通过this.vm[key]获取对应的值,最后替换到文本节点里
  5. compileElement方法,编译元素节点,处理指令
    1. 遍历所有节点的属性
    2. 获取节点属性的名称
    3. 通过isDirective()方法判断当前属性是否是指令(v-开头)
    4. 截取掉v-,只保留功能单词。v-text -> text
    5. 获取节点属性的value值,这个value也是vue实例data属性里的key
    6. 调用update处理指令相关功能
  6. update
    1. 需要3个参数,节点、key(vue实例data属性里的key)、指令去掉v-后的内容
    2. 通过字符串拼接,获取指令对应的方法,并执行
    3. 这样做的好处是:以后即使有新的指令需要处理,只需新定义处理指令的方法(xxxUpdater)

代码

class Compiler {
  constructor (vm) {
    this.el = vm.$el
    this.vm = vm
    this.compile(this.el)
  }

  // 编译模板,处理文本节点和元素节点
  compile (el) {
    // children子元素,要找子节点childNodes
    let childNodes = el.childNodes
    // 转化伪数组
    Array.from(childNodes).forEach(node => {
      // 处理文本节点
      if (this.isTextNode(node)) {
        this.compileText(node)
      }
      // 处理元素节点
      if (this.isElementNode(node)) {
        this.compileElement(node)
      }
      // node节点是否有子节点,如果有,递归调用compile
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }

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

  // 处理v-text指令
  textUpdater (node, value, key) {
    node.textContent = value
    new Watcher(this.vm, key, newValue => {
      node.textContent = newValue
    })
  }

  // 处理v-model
  modelUpdater (node, value, key) {
    node.value = value
    new Watcher(this.vm, key, newValue => {
      node.value = newValue
    })
    // 给表单元素注册事件,实现双向绑定
    // node就是input元素
    // 触发input事件后,事件处理函数会把文本框的值取出,并重新赋值vm[key] = 'xxx'
    // 给vm[key]赋值的时候,会触发响应式机制
    // 响应式机制是,当数据发生变化,会重新更新视图
    node.addEventListener('input', () => {
      this.vm[key] = node.value
    })
  }

  // 编译元素节点,处理指令
  compileElement (node) {
    // 获取所有属性节点
    // 遍历所有属性节点
    Array.from(node.attributes).forEach(attr => {
      // 判断是否是指令
      let attrName = attr.name
      if (this.isDirective(attrName)) {
        // v-text -> text
        attrName = attrName.substr(2)
        let key = attr.value
        this.update(node, key, attrName)
      }
    })
  }

  // 编译文本节点,处理差值表达式
  compileText (node) {
    // console.dir(node)
    let reg = /\{\{(.+?)\}\}/
    // 获取文本节点内容
    let value = node.textContent
    if (reg.test(value)) {
      let key = RegExp.$1.trim()
      node.textContent = value.replace(reg, this.vm[key])
      // 创建watcher对象,当数据改变更新视图
      new Watcher(this.vm, key, newValue => {
        node.textContent = newValue
      })
    }
  }

  // 判断元素属性是否是指令
  isDirective (attrName) {
    // 判断是否v-开头即可
    return attrName.startsWith('v-')
  }

  // 判断节点是否是文本节点
  isTextNode (node) {
    // nodeType 3=文本节点 1=元素节点
    return node.nodeType === 3
  }

  // 判断节点是否是元素节点
  isElementNode (node) {
    return node.nodeType === 1
  }
}
posted @ 2020-07-06 21:28  Evo1uti0n  阅读(339)  评论(0编辑  收藏  举报