Vue2响应式原理

利用 Object.defineProperty 实现数据劫持

首先利用 Object.defineProperty 来实现一个简单的数据劫持案例

在下面的例子中,我们通过数据劫持来实现类似于 Vue 的双向绑定

export const DefinePropertyComp = () => { const obj = { name: 'liwenkai', info: { iphone: 12138, age: 18 } } const observe = (obj) => { if (!obj || typeof obj !== 'object') return for (const k in obj) { let v = obj[k] observe(v) Object.defineProperty(obj, k, { get() { return v }, set(val) { if (val !== v) { v = val if (k === 'name') spanNameRef.current.innerHTML = val else if (k === 'iphone') spanIponeRef.current.innerHTML = val else if (k === 'age') spanAgeRef.current.innerHTML = val } } }) } } const [inputName, setInputName] = React.useState(obj.name) const [inputIphone, setInputIphone] = React.useState(obj.info.iphone) const [inputAge, setInputAge] = React.useState(obj.info.age) const inputChange = (v, k) => { observe(obj) if (k === 'name') { setInputName(v) obj.name = v } else if (k === 'iphone') { setInputIphone(v) obj.info.iphone = v } else if (k === 'age') { setInputAge(v) obj.info.age = v } } const spanNameRef = React.useRef(null) const spanIponeRef = React.useRef(null) const spanAgeRef = React.useRef(null) return ( {obj.name} <input className="flex-1 rounded border bg-blue-500 p-2" placeholder="输入名字" value={inputName} onChange={(e) => inputChange(e.target.value, 'name')} /> {obj.info.iphone} <input className="flex-1 rounded border bg-blue-500 p-2" placeholder="输入电话" value={inputIphone} onChange={(e) => inputChange(e.target.value, 'iphone')} /> {obj.info.age} <input className="flex-1 rounded border bg-blue-500 p-2" placeholder="输入年龄" value={inputAge} onChange={(e) => inputChange(e.target.value, 'age')} /> ) }

export const DefinePropertyComp = () => {
  const obj = {
    name: 'liwenkai',
    info: {
      iphone: 12138,
      age: 18
    }
  }
  const observe = (obj) => {
    if (!obj || typeof obj !== 'object') return
    for (const k in obj) {
      let v = obj[k]
      observe(v)
      Object.defineProperty(obj, k, {
        get() {
          return v
        },
        set(val) {
          if (val !== v) {
            v = val
            if (k === 'name') spanNameRef.current.innerHTML = val
            else if (k === 'iphone') spanIponeRef.current.innerHTML = val
            else if (k === 'age') spanAgeRef.current.innerHTML = val
          }
        }
      })
    }
  }
  const [inputName, setInputName] = React.useState(obj.name)
  const [inputIphone, setInputIphone] = React.useState(obj.info.iphone)
  const [inputAge, setInputAge] = React.useState(obj.info.age)
  const inputChange = (v, k) => {
    observe(obj)
    if (k === 'name') {
      setInputName(v)
      obj.name = v
    } else if (k === 'iphone') {
      setInputIphone(v)
      obj.info.iphone = v
    } else if (k === 'age') {
      setInputAge(v)
      obj.info.age = v
    }
  }
  const spanNameRef = React.useRef(null)
  const spanIponeRef = React.useRef(null)
  const spanAgeRef = React.useRef(null)
  return (
    <div className="flex flex-col">
      <span className="w-full rounded bg-blue-500 p-2" ref={spanNameRef}>
        {obj.name}
      </span>
      <input
        className="flex-1 rounded border bg-blue-500 p-2"
        placeholder="输入名字"
        value={inputName}
        onChange={(e) => inputChange(e.target.value, 'name')}
      />
      <span className="w-full rounded bg-blue-500 p-2" ref={spanIponeRef}>
        {obj.info.iphone}
      </span>
      <input
        className="flex-1 rounded border bg-blue-500 p-2"
        placeholder="输入电话"
        value={inputIphone}
        onChange={(e) => inputChange(e.target.value, 'iphone')}
      />
      <span className="w-full rounded bg-blue-500 p-2" ref={spanAgeRef}>
        {obj.info.age}
      </span>
      <input
        className="flex-1 rounded border bg-blue-500 p-2"
        placeholder="输入年龄"
        value={inputAge}
        onChange={(e) => inputChange(e.target.value, 'age')}
      />
    </div>
  )
}

Vue 类

通过数据劫持 + 发布订阅模式实现 Vue 类

export const VueComp = () => { class Vue { constructor(obj_instance) { this.$data = obj_instance.data Observer(this.$data) Compile(obj_instance.el, this) } } // 数据劫持 function Observer(data_instance) { if (!data_instance || typeof data_instance !== 'object') return const dependency = new Dependency() for (const key in data_instance) { let value = data_instance[key] Observer(value) Object.defineProperty(data_instance, key, { enumerable: true, configurable: true, get() { Dependency.temp && dependency.addSub(Dependency.temp) return value }, set(newValue) { if (value === newValue) return value = newValue Observer(newValue) dependency.notify() } }) } } // HTML模板解析 function Compile(element, vm) { vm.$el = document.querySelector(element) const fragment = document.createDocumentFragment() let child while ((child = vm.$el.firstChild)) { fragment.append(child) } fragment_compile(fragment) // 替换文档碎片内容 function fragment_compile(node) { const xxx = node.nodeValue const pattern = /((\s*(\S+)\s*))/ if (node.nodeType === 3) { const result_regex = pattern.exec(node.nodeValue) if (result_regex) { const arr = result_regex[1].split('.') const value = arr.reduce((total, current) => total[current], vm.$data) node.nodeValue = xxx.replace(pattern, value) // 创建订阅者 new Watcher(vm, result_regex[1], (newValue) => { node.nodeValue = xxx.replace(pattern, newValue) }) } return } if (node.nodeType === 1 && node.nodeName === 'INPUT') { const attr = Array.from(node.attributes) attr.forEach((i) => { if (i.nodeName === 'v-model') { const value = i.nodeValue.split('.').reduce((total, current) => total[current], vm.$data) node.value = value // 创建订阅者 new Watcher(vm, i.nodeValue, (newValue) => { node.value = newValue }) // 添加监听 node.addEventListener('input', (e) => { const arr1 = i.nodeValue.split('.') const arr2 = arr1.slice(0, arr1.length - 1) const final = arr2.reduce((total, current) => total[current], vm.$data) final[arr1[arr1.length - 1]] = e.target.value }) } }) } node.childNodes.forEach((child) => fragment_compile(child)) } vm.$el.appendChild(fragment) } // 依赖 - 收集和通知订阅者 class Dependency { constructor() { this.subscribers = [] } addSub(sub) { this.subscribers.push(sub) } notify() { this.subscribers.forEach((sub) => sub.update()) } } // 订阅者 class Watcher { constructor(vm, key, callback) { this.vm = vm this.key = key this.callback = callback // 临时属性 - 触发getter Dependency.temp = this key.split('.').reduce((total, current) => total[current], vm.$data) Dependency.temp = null } update() { const value = this.key.split('.').reduce((total, current) => total[current], this.vm.$data) this.callback(value) } } React.useEffect(() => { new Vue({ el: '#app', data: { name: 'liwenkai', info: { iphone: 12138, age: 18 } } }) }, []) return ( 名字:((name)) 电话:((info.iphone)) 年龄:((info.age)) ) }

export const VueComp = () => {
  class Vue {
    constructor(obj_instance) {
      this.$data = obj_instance.data
      Observer(this.$data)
      Compile(obj_instance.el, this)
    }
  }
  // 数据劫持
  function Observer(data_instance) {
    if (!data_instance || typeof data_instance !== 'object') return
    const dependency = new Dependency()
    for (const key in data_instance) {
      let value = data_instance[key]
      Observer(value)
      Object.defineProperty(data_instance, key, {
        enumerable: true,
        configurable: true,
        get() {
          Dependency.temp && dependency.addSub(Dependency.temp)
          return value
        },
        set(newValue) {
          if (value === newValue) return
          value = newValue
          Observer(newValue)
          dependency.notify()
        }
      })
    }
  }
  // HTML模板解析
  function Compile(element, vm) {
    vm.$el = document.querySelector(element)
    const fragment = document.createDocumentFragment()
    let child
    while ((child = vm.$el.firstChild)) {
      fragment.append(child)
    }
    fragment_compile(fragment)
    // 替换文档碎片内容
    function fragment_compile(node) {
      const xxx = node.nodeValue
      const pattern = /\(\(\s*(\S+)\s*\)\)/
      if (node.nodeType === 3) {
        const result_regex = pattern.exec(node.nodeValue)
        if (result_regex) {
          const arr = result_regex[1].split('.')
          const value = arr.reduce((total, current) => total[current], vm.$data)
          node.nodeValue = xxx.replace(pattern, value)
          // 创建订阅者
          new Watcher(vm, result_regex[1], (newValue) => {
            node.nodeValue = xxx.replace(pattern, newValue)
          })
        }
        return
      }
      if (node.nodeType === 1 && node.nodeName === 'INPUT') {
        const attr = Array.from(node.attributes)
        attr.forEach((i) => {
          if (i.nodeName === 'v-model') {
            const value = i.nodeValue.split('.').reduce((total, current) => total[current], vm.$data)
            node.value = value
            // 创建订阅者
            new Watcher(vm, i.nodeValue, (newValue) => {
              node.value = newValue
            })
            // 添加监听
            node.addEventListener('input', (e) => {
              const arr1 = i.nodeValue.split('.')
              const arr2 = arr1.slice(0, arr1.length - 1)
              const final = arr2.reduce((total, current) => total[current], vm.$data)
              final[arr1[arr1.length - 1]] = e.target.value
            })
          }
        })
      }
      node.childNodes.forEach((child) => fragment_compile(child))
    }
    vm.$el.appendChild(fragment)
  }
  // 依赖 - 收集和通知订阅者
  class Dependency {
    constructor() {
      this.subscribers = []
    }
    addSub(sub) {
      this.subscribers.push(sub)
    }
    notify() {
      this.subscribers.forEach((sub) => sub.update())
    }
  }
  // 订阅者
  class Watcher {
    constructor(vm, key, callback) {
      this.vm = vm
      this.key = key
      this.callback = callback
      // 临时属性 - 触发getter
      Dependency.temp = this
      key.split('.').reduce((total, current) => total[current], vm.$data)
      Dependency.temp = null
    }
    update() {
      const value = this.key.split('.').reduce((total, current) => total[current], this.vm.$data)
      this.callback(value)
    }
  }
  React.useEffect(() => {
    new Vue({
      el: '#app',
      data: {
        name: 'liwenkai',
        info: {
          iphone: 12138,
          age: 18
        }
      }
    })
  }, [])
  return (
    <div id="app" className="flex flex-col">
      <span className="w-full rounded bg-blue-500 p-2">名字:((name))</span>
      <input className="flex-1 rounded border bg-blue-500 p-2" placeholder="输入名字" v-model="name" />
      <span className="w-full rounded bg-blue-500 p-2">电话:((info.iphone))</span>
      <input className="flex-1 rounded border bg-blue-500 p-2" placeholder="输入电话" v-model="info.iphone" />
      <span className="w-full rounded bg-blue-500 p-2">年龄:((info.age))</span>
      <input className="flex-1 rounded border bg-blue-500 p-2" placeholder="输入年龄" v-model="info.age" />
    </div>
  )
}