实现一个简单的vue-响应式模拟(二)

379 阅读3分钟

前言

上一节讲了Vue响应式原理所相关的技术,这一节直接实现代码。目前我实现了v-text、v-html、v-model和v-on的指令,有兴趣的朋友可以自己进行优化调整。

整体分析

  • vue.js
    • 把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter
  • observer.js
    • 对数据对象的所有属性进行监听,如有变动可拿到最新值并通知 Dep
  • compiler.js
    • 解析每个元素中的指令/插值表达式,并替换成相应的数据
  • dep.js
    • 添加观察者(watcher),当数据变化通知所有观察者
  • watcher.js
    • 数据变化更新视图

Vue.js

功能

  • 负责接收初始化的参数(选项)
  • 负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter
  • 负责调用 observer 监听 data 中所有属性的变化
  • 负责调用 compiler 解析指令/插值表达式
class Vue {
  constructor(options) {
    this.$options = options || {}
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
    this.$data = options.data || {}
    let methods = this.$options.methods || {}
    
    // 把data的数据放到vue实例上,并做成响应式数据
    this._proxyData(this.$data)
    // 把methods的方法放到vue实例上
    this._addMethods(methods)
    // 调用 observer 监听 data 中所有属性的变化
    new Observer(this.$data)
    // 调用 compiler 解析指令/插值表达式
    new Compiler(this)
  }

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

  _addMethods(methods) { // 把methods里的所有方法放到vue实例上
    for (const handle in methods) {
      this[handle] = methods[handle]
    }
  }
}

Obeserver.js

功能

  • 负责把 data 选项中的属性转换成响应式数据
  • data 中的某个属性也是对象,把该属性转换成响应式数据
  • 数据变化发送通知
class Observer {
  constructor(data) {
    this.walk(data)
  }

  walk(data) {
    if(!data || typeof data != 'object') return

    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }

  defineReactive(data, key, val) {
    let _this = this
    // 创建dep对象,方便收集观察者依赖
    let dep = new Dep()
    // 如果val是对象,则重新执行walk方法,进行监听
    this.walk(val)
    
    Object.defineProperty(data, key, {
      configurable: true,
      enumerable: true,
      get() {
        // 收集观察者依赖 Dep.target。 Dep.target是在watcher里面创建的
        Dep.target && dep.addSub(Dep.target)
        return val
      },
      set(newVal) {
        if(val === newVal) return
        // 如果newval是对象,则执行walk方法,进行监听
        _this.walk(newVal)
        val = newVal
        // 数据变化后,通知观察者
        dep.notify()
      }
    })
  }

}

Compiler.js

功能

  • 负责编译模板,解析指令/插值表达式
  • 负责页面的首次渲染
  • 当数据变化后重新渲染视图
class Compiler {
  constructor(vm) {
    this.vm = vm // vm是vue实例
    this.$el = vm.$el

    // 编译模板
    this.compile(this.$el)
  }

  compile(el) {
    // 获取所有子节点
    let nodeList = el.childNodes
    Array.from(nodeList).forEach(node => {
      if(this.isTextNode(node)) { // 文本节点
        this.compileText(node)
      }else if(this.isElementNode(node)) { // 元素节点
        this.compileElement(node)
      }

      if(node.childNodes && node.childNodes.length) { // 如果节点下还有子节点,则递归
        this.compile(node)
      }
    })
  }

  isTextNode(node) {
    return node.nodeType === 3
  }

  isElementNode(node) {
    return node.nodeType === 1
  }

  isDirective(attrName) { // 是否是以'v-'开头的指令
    return attrName.startsWith('v-')
  }

  compileText(node) { // 编译文本节点
    const reg = /\{\{(.+)\}\}/
    const value = node.textContent // 文本内容
    if(reg.test(value)) {
      const key = RegExp.$1.trim() // 获取变量名
      node.textContent = value.replace(reg, this.vm[key]) // 把对应的变量替换
      // 创建一个 watcher,观察数据的变化
      new Watcher(this.vm, key, newValue => {
        node.textContent = newValue
      })
    }
  }

  compileElement(node) { // 编译元素节点
    // 获取元素的属性  Array.from把 伪数组转成数组
    Array.from(node.attributes).forEach(attr => {
      let attrName = attr.name
      // 是否是以'v-'开头的指令
      if(this.isDirective(attrName)) {
        let name = attrName.substr(2) // 指令名称
        let key = attr.value // 所赋值的变量
        // 根据指令执行对应的方法
        this.instructions(node, name, key)
      }
    })
  }

  instructions(node, name, key) {
    let instrucFn = ''
    let eventType = ''
    if(name.startsWith('on:')) { // 这里是v-on指令
      eventType = name.substr(3) // 事件名
      instrucFn = this.onInstrucFn
    }else {
      instrucFn = this[name + 'InstrucFn']
    }
    instrucFn && instrucFn.call(this, node, this.vm[key], key, eventType)
  }

  textInstrucFn(node, value, key) { // v-text对应的方法
    node.textContent = value
    // 每一个指令中创建一个 watcher,观察数据的变化
    new Watcher(this.vm, key, newValue => {
      node.textContent = newValue
    })
  }

  htmlInstrucFn(node, value, key) { // v-html对应的方法
    node.innerHTML = value
    // 每一个指令中创建一个 watcher,观察数据的变化
    new Watcher(this.vm, key, newValue => {
      node.innerHTML = newValue
    })
  }

  modelInstrucFn(node, value, key) { // v-model对应的方法
    node.value = value
    // 每一个指令中创建一个 watcher,观察数据的变化
    new Watcher(this.vm, key, newValue => {
      node.value = newValue
    })

    // 监听input事件,给对应的变量重新赋值
    node.addEventListener('input', ()=>{
      this.vm[key] = node.value
    })
  }

  onInstrucFn(node, value, key, eventType) { // v-on对应的方法
    node.addEventListener(eventType, ()=>{
      this.vm[key]()
    })
  }
}

Dep.js

功能

  • 收集依赖,添加观察者(watcher)
  • 通知所有观察者
class Dep {
  constructor() {
    // 存储所有观察者
    this.subs = []
  }

  addSub(sub) { // 添加观察者
    if(sub && sub.update) {
      this.subs.push(sub)
    }
  }

  notify() { // 通知所有观察者
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

Watcher.js

功能

  • 当数据变化触发依赖, dep 通知所有的 Watcher 实例更新视图
  • 自身实例化的时候往 dep 对象中添加自己
class Watcher {
  constructor(vm, key, callback) {
    this.vm = vm
    this.key = key // data 中的属性名称
    this.cb = callback // 当数据变化的时候,调用 cb 更新视图

    Dep.target = this // 在 Dep 的静态属性上记录当前 watcher 对象,当访问数据的时候把 watcher 添加到 dep 的 subs 中
    this.oldValue = vm[key] // 记录改变之前的变量值
    Dep.target = null // 重置,以防重复添加
  }

  update() {
    const newValue = this.vm[this.key]
    if(newValue === this.oldValue) return
    this.cb(newValue)
  }
}

页面上引用

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>实现一个简单的vue</title>
</head>
<body>
  <div id="app">
    <h2>差值表达式</h2>
    <div v-on:click="handleClick">msg: <b>{{ msg }}</b></div>
    <div>count: <b>{{ count }}</b></div>

    <h2>v-text</h2>
    <div v-text="msg"></div>

    <h2>v-html</h2>
    <div v-html="vHtml"></div>

    <h2>v-model</h2>
    <input type="text" v-model="msg" >
    <input type="text" v-model="count" >
  </div>

  <script src="./js/dep.js"></script>
  <script src="./js/watcher.js"></script>
  <script src="./js/compiler.js"></script>
  <script src="./js/observer.js"></script>
  <script src="./js/vue.js"></script>
  <script>
    let h = '你好啊'
    let vm = new Vue({
      el: '#app',
      data: {
        msg: 'hello world',
        count: 100,
        vHtml: `<p style="color: pink;">${h}</p>`,
        obj: {
          name: 'xxx'
        }
      },
      methods: {
        handleClick() {
          console.log(this.msg)
        }
      },
    })

  </script>
</body>
</html>