手写vue双向数据绑定

x33g5p2x  于2022-03-05 转载在 Vue.js  
字(5.1k)|赞(0)|评价(0)|浏览(613)

一、写在前面
学习前端不得不学vue,学习vue不得不学习他的响应式原理,下面为我实现响应式的思路和代码,注释写在代码中。
二、实现

const CommandUtils = {
  setValue(vm, attr, newValue) {
    attr.split('.').reduce((data, currentAttr, index, arr) => {
      if (index === arr.length - 1) {
        data[currentAttr] = newValue;
      }
      return data[currentAttr];
    }, vm.$data)
  },
  getContent(vm, value) {
    let reg = /\{\{(.+?)\}\}/gi;
    let val = value.replace(reg, (...args) => {
      return this.getValue(vm, args[1]);
    });
    return val;
  },

  getValue: function (vm, value) {
    return value.split(".").reduce((data, key) => {
      return data[key.trim()]
    }, vm.$data)
  },
  model: function (vm, node, value) { //vm为options, node为节点,value为 data中属性
    const val = this.getValue(vm, value)
    new Watcher(vm, value, (newValue, oldValue) => {
      node.value = newValue
    })
    node.addEventListener("input", (e) => {
      const newValue = e.target.value
      this.setValue(vm, value, newValue);
    })
    node.value = val
  },
  html: function (vm, node, value) {
    new Watcher(vm, value, (newValue, oldValue) => {
      node.innerHTML = newValue
    })
    const val = this.getValue(vm, value)
    node.innerHTML = val
  },
  text: function (vm, node, value) {
    new Watcher(vm, value, (newValue, oldValue) => {
      node.textContent = newValue
    })
    const val = this.getValue(vm, value)
    node.textContent = val
  },
  content: function (vm, node) {
    const reg = /\{\{(.+?)\}\}/gi
    const content = node.textContent
    const val = content.replace(reg, (...args) => {
      new Watcher(vm, args[1], (newValue, oldValue) => {
        node.textContent = this.getContent(vm, content);
      });
      return this.getValue(vm, args[1])
    })
    node.textContent = val
  },
  on: function (node, value, vm, type) {
    node.addEventListener(type, (e) => {
      vm.$methods[value].call(vm, e)
    })
  }
}

export default class minVue {
  constructor(options) {
    if (this.isElement(options.el)) {
      this.$el = options.el
    } else {
      this.$el = document.querySelector(options.el)
    }
    this.$data = options.data
    this.proxyData();
    this.$methods = options.methods;
    new Observer(this.$data)
    if (this.$el) {
      // 将模板进行编译
      new Compiler(this)
    }
  }
  // 判断是否是标签元素
  isElement(node) {
    return node.nodeType === 1
  }

  
  // 将data中的数据放到vue实例上
  proxyData() {
    for (let key in this.$data) {
      Object.defineProperty(this, key, {
        get: () => {
          return this.$data[key]
        }
      })
    }
  }
}

// 封装对模板进行编译的类
class Compiler {
  constructor(vm) {
    this.$vm = vm
    this.fragment = this.fragmentTemplate()
    this.buildTemplate(this.fragment)
    this.$vm.$el.appendChild(this.fragment)
  }

  fragmentTemplate() {
    const fragment = document.createDocumentFragment()
    const parentNode = this.$vm.$el
    let childNode = parentNode.firstChild
    while (childNode) {
      fragment.appendChild(childNode)
      childNode = parentNode.firstChild
    }
    return fragment
  }
  // 编译模板
  buildTemplate(fragment) {
    const fragmentChildNos = [...fragment.childNodes]
    fragmentChildNos.forEach(node => {
      // 元素节点类型
      if (this.$vm.isElement(node)) {
        this.buildNode(node)
        this.buildTemplate(node)
      } else {
        // 文本节点
        this.buildText(node)
      }
    })
  }

  // 编译node节点
  buildNode(node) {
    const attrs = node.attributes
    const newAttrs = [...attrs]
    newAttrs.forEach(item => {
      const {
        name,
        value
      } = item //name 为 v-model  value 为 title
      if (name.startsWith("v-")) {
        const command = name.split("-")[1] //model
        const typeEvent = command.split(":")
        if (typeEvent.length === 1) {
          CommandUtils[command](this.$vm, node, value)
        } else {
          CommandUtils[typeEvent[0]](node, value, this.$vm, typeEvent[1])
        }
      }
    })
  }

  // 编译文本节点
  buildText(node) {
    const reg = /\{\{.+?\}\}/gi
    if (reg.test(node.textContent)) {
      CommandUtils["content"](this.$vm, node)
    }
  }
}


// 对全部的属性进行监听
class Observer {
  constructor(obj) {
    this.observer(obj)
  }

  observer(obj) {
    if (obj && typeof obj === "object") {
      Reflect.ownKeys(obj).forEach(key => {
        this.defineReactive(obj, key, obj[key])
      })
    }
  }

  defineReactive(obj, attr, value) {
    this.observer(value)
    let dep = new Dep()
    Object.defineProperty(obj, attr, {
      get() {
        Dep.target && dep.addWatchs(Dep.target)
        return value
      },
      set: (newValue) => {
        if (value !== newValue) {
          this.observer(newValue);
          value = newValue
          dep.notify()
        }
      }
    })
  }
}

// 保存依赖的类
class Dep {
  constructor() {
    this.subs = []
  }

  addWatchs(watcher) {
    this.subs.push(watcher)
  }

  notify() {
    this.subs.forEach(watch => watch.update())
  }
}

class Watcher {
  constructor(vm, attr, cb) {
    this.vm = vm
    this.attr = attr
    this.cb = cb
    this.oldValue = this.getOldValue()
  }

  getOldValue() {
    Dep.target = this
    let val = CommandUtils.getValue(this.vm, this.attr)
    Dep.target = null
    return val
  }

  update() {
    const newValue = CommandUtils.getValue(this.vm, this.attr)
    if (this.oldValue !== newValue) {
      this.cb(newValue, this.oldValue)
      this.oldValue = newValue
    }
  }
}

html代码展示

<!DOCTYPE html>
<html lang="cn">
<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>
</head>
<body>

  <div id="app">
    <h1>大家好,我是{{ name }}</h1>
    <h1>大家好,我的年龄是{{ age }}</h1>
    <input type="text" v-model="time.H">
    <input type="text" v-model="time.M">
    <input type="text" v-model="time.S">
    <input type="text" v-model="time.S">
    <h1>{{ time.S }}</h1>
    <button v-on:click="myFn">点击</button>
  </div>

  <script type="module">
    import minVue from './minVue.js'
    new minVue({
      el:"#app",
      data: {
        name: "dmc",
        age: 20,
        time: {
          H:"小时",
          M: "分钟",
          S: "秒钟"
        }
      },
      methods: {
        myFn:() => {
          console.log("hhhhh")
        }
      },
    })
  </script>
  
</body>
</html>

相关文章