vue2简单实现

99 阅读1分钟
//index.js
export class Vue {
  constructor(options = {}) {
    this.$options = options
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
    this.$data = options.data
    this.$methods = options.methods

    this.proxy(this.$data)

    // observer 拦截 this.$data
    new Observer(this.$data)

    new Compiler(this)
  }

  // 代理一下,this.$data.xxx -> this.xxx
  proxy(data) {
    Object.keys(data).forEach(key => {
      // this
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return data[key]
        },
        set(newValue) {
          // NaN !== NaN
          if (data[key] === newValue || __isNaN(data[key], newValue)) return
          data[key] = newValue
        }
      })
    })
  }
}

function __isNaN(a, b) {
  return Number.isNaN(a) && Number.isNaN(b)
}

class Dep {
  constructor() {
    this.deps = new Set()
  }
  add(dep) {
    if (dep && dep.update) this.deps.add(dep)
  }
  notify() {
    this.deps.forEach(dep => dep.update())
  } 
}

class Watcher {
  // vm - Vue 实例
  constructor(vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb

    // window.watcher = this
    Dep.target = this
    this.__old = vm[key] // 存下了初始值,触发 getter
    Dep.target = null
  }
  update() {
    let newValue = this.vm[this.key]
    if (this.__old === newValue || __isNaN(newValue, this.__old)) return
    this.cb(newValue)
  }
}

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(obj, key, value) {
    let that = this
    this.walk(value)
    let dep = new Dep()
    Object.defineProperty(obj, key, {
      configurable: true,
      enumerable: true,
      get() {
        // Watcher 实例
        Dep.target && dep.add(Dep.target)
        return value
      },
      set(newValue) {
        if (value === newValue || __isNaN(value, newValue)) return
        value = newValue
        that.walk(newValue)
        dep.notify()
      }
    })
  }
}

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

    this.compile(vm.$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)
      }

      if (node.childNodes && node.childNodes.length) this.compile(node)
      // ...
    })
  }
  // <input v-model="msg"/>
  compileElement(node) {
    if (node.attributes.length) {
      Array.from(node.attributes).forEach(attr => {
        let attrName = attr.name
        if (this.isDirective(attrName)) {
          // v-on:click  v-model
          attrName = attrName.indexOf(':') > -1 ? attrName.substr(5) : attrName.substr(2)
          let key = attr.value
          this.update(node, key, attrName, this.vm[key])
        }
        // ...
      })
    }
  }
  update(node, key, attrName, value) {
    if (attrName === 'text') {
      node.textContent = value
      new Watcher(this.vm, key, val => node.textContent = val)
    }
    else if (attrName === 'model') {
      node.value = value
      new Watcher(this.vm, key, val => node.value = val)
      node.addEventListener('input', () => {
        this.vm[key] = node.value
      })
    }
    else if (attrName === 'click') {
      node.addEventListener(attrName, this.methods[key].bind(this.vm))
    }
    // ....
  }
  // 'this is {{ count }}'
  compileText(node) {
    let reg = /\{\{(.+?)\}\}/
    let value = node.textContent
    if (reg.test(value)) {
      let key = RegExp.$1.trim()
      node.textContent = value.replace(reg, this.vm[key])

      new Watcher(this.vm, key, val => {
        node.textContent = val
      })
    }
  }
  isDirective(str) {
    return str.startsWith('v-')
  }
  isElementNode(node) {
    return node.nodeType === 1
  }
  isTextNode(node) {
    return node.nodeType === 3
  }
}
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script type='module'>
    import { Vue } from './index.js'
    let vm = new Vue({
      el: '#app',
      data: {
        msg: 'Hello Vue2.x',
        count: 666
      },
      methods: {
        increase() {
          this.count++
        }
      }
    })
  </script>
</head>
<body>
  <div id="app">
    <h3>{{ msg }}</h3>
    <h3>{{ count }}</h3>
    <h1>v-text</h1>
    <div v-text="msg"></div>
    <h1>v-model</h1>
    <input type="text" v-model="msg" >
    <input type="text" v-model="count">
    <button v-on:click="increase">按钮</button>
  </div>
</body>
</html>

总结:
1.html里遇到 {{XX}} 就会new Watcher();并且赋予一个cb的方法(简易版相当于渲染函数)new Watcher()的时候会调用vm[key]出发observer里 defineReactive defineProperty的get函数,把一新创建的watcher挂载到依赖里
2.当执行set函数,会执行notify函数,notify函数会执行update,执行update函数实际是调用了new Watcher时绑定的cb方法(渲染);
3.cb在实际中是会触发虚拟DOM进行一系列比对之后在执行渲染;

效果图:

image.png