Write your own code to understand the data bidirectional binding of Vue

The function of important code is written in the code annotation.

Core point:
1. Object.defineProperty(), which is equivalent to adding a proxy to the value and assignment of related properties, can perform the functions in the proxy. Here, get is a two-way binding
2. The essence of Compiler is to traverse html documents and find out the fields according to the vue specification for processing. For example, after {deep.a}} is taken out, take out the real value from the vm instance and replace {deep.a}}}.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
  <div id="app">
    <div>{{deep.a}}</div>
    <input v-model="deep.a">
  </div>

  <script>

    class Dep {

      static target = null
      static targetStack = []
      static pushTarget(_target) {
        if (Dep.target) {
          Dep.targetStack.push(Dep.target)
        }
        Dep.target = _target
      }

      static popTarget() {
        Dep.target = Dep.targetStack.pop()
      }

      constructor() {
        this.subArr = []
      }

      addDepend() {
        Dep.target.addDep(this) //this.addSub(watcher), the watcher at this time is in the target
      }

      addSub(sub) {
        this.subArr.push(sub)
      }

      notify() {
        for (let sub of this.subArr) {
          sub.update()
        }
      }
    }

    class Watcher {
      //Take expression as deep.a as an example
      constructor(vm, expression, cb) {
        this.vm = vm
        this.expression = expression
        this.cb = cb
        this.value = this.getVal()
      }

      getVal () {
        Dep.pushTarget(this)
        let val = this.vm
        this.expression.split('.').forEach((key) => {
          val = val[key]//The get method will be triggered here, and the watcher will be plugged into the sub of Dep
        })
        Dep.popTarget()
        return val //According to the analysis in the compiler, here val=1, that is, the value of the watcher = 1
      }

      addDep(dep) {
        dep.addSub(this)
      }

      update() {
        let val = this.vm
        this.expression.split('.').forEach((key) => {
          val = val[key]
        })
        this.cb.call(this.vm, val, this.value)
      }
    }

    class Observer {
      constructor(obj) {
        this.walk(obj)
      }

      walk(obj) {
        Object.keys(obj).forEach(key=> {
          if (typeof obj[key] === 'object') {
            this.walk(obj[key])
          }

          this.defineReactive(obj, key, obj[key])
        })
      }

      defineReactive(obj, key, value) {

        let dep = new Dep()

        Object.defineProperty(obj, key, {

          set(newVal) {
            console.log('set')
            console.log(key + ': ' + value)
            if (newVal === value) {
              return
            }

            value = newVal
            dep.notify()
          },

          get() {
            if (Dep.target) {
              dep.addDepend()
            }
            return value
          },
        })
      }

    }

    class Compiler {
      constructor (el, vm) {

        vm.$el = document.querySelector(el)
        let fragment = document.createDocumentFragment()
        this.replace(vm.$el, vm)
      }

      replace (frag, vm) {
        Array.from(frag.childNodes).forEach(node => {
          let txt = node.textContent
          let reg = /\{\{(.*?)\}\}/g // Regular matching {}}

          if (node.nodeType === 3 && reg.test(txt)) { // In the case of text nodes and braces {}}

            //For example, here RegExp.  is deep.a, and reg.test(txt) removes the braces
            let arr = RegExp.$1.split('.')//['deep', 'a']
            let val = vm
            arr.forEach(key => {
              val = val[key]//Cycle twice, the first val[deep] = {a:1, b:2}, the second val[a] = 1, the final val = 1
            })
            // Remove the leading and trailing spaces with trim method. Here {{deep.a}} becomes 1
            node.textContent = txt.replace(reg, val).trim()
            //Enter 'deep.a' for two-way binding
            vm.$watch(RegExp.$1, function (newVal) {
              node.textContent = txt.replace(reg, newVal).trim()
            })//new Watcher(vm, 'deep.a', cb) cb is the second function parameter above
          }

          if (node.nodeType === 1) {  // Element node
            let nodeAttr = node.attributes // Get all the properties on the dom, which is an array of classes
            Array.from(nodeAttr).forEach(attr => {
              let name = attr.name
              let exp = attr.value
              if (name.includes('v-')){
                let val = vm
                let arr = exp.split('.')
                arr.forEach(key=> {
                  val = val[key]
                })
                node.value = val
                // node.value = vm[exp]
              }
              // Monitoring changes
              vm.$watch(exp, function(newVal) {
                node.value = newVal
              })

              node.addEventListener('input', e => {
                let newVal = e.target.value
                let arr = exp.split('.')
                let val = vm
                arr.forEach((key, i)=> {
                  if (i === arr.length - 1) {
                    val[key] = newVal
                    return
                  }
                  val = val[key]
                })
              })
            })
          }

          // If there are child nodes, continue to recursively replace
          if (node.childNodes && node.childNodes.length) {
            this.replace(node, vm)
          }
        })
      }
    }

    const LIFECYCLE_HOOKS = [
      'created',
      'mounted'
    ]

    function callHook (vm, hook) {
      const handlers = vm.$options[hook]
      if (handlers) {
        handlers.call(vm)
      }
    }

    class Due {
      constructor(options) {
        let vm = this
        vm.$options = options
        vm.$watch = function (key, cb) {
          new Watcher(vm, key, cb)
        }
        vm._data = vm.$options.data
        this.observe(vm)

        LIFECYCLE_HOOKS.forEach(hook => {
          vm.$options[hook] = vm.$options[hook] || function () {}
        })

        for (let key in vm._data) {
          this.proxy(vm, '_data', key)
        }

        callHook(vm, 'created')
        new Compiler(vm.$options.el, vm)
        callHook(vm, 'mounted')
      }

      proxy (target, sourceKey, key) {
        Object.defineProperty(target, key, {
          configurable: true,
          get: function proxyGetter() {
            return target[sourceKey][key]
          },
          set: function proxySetter(newVal) {
            target[sourceKey][key] = newVal
          }
        })
      }

      observe(obj) {
        if (!obj || typeof obj !== 'object') {
          return
        }

        return new Observer(obj)
      }
    }

    let app = new Due({
      el: '#app',
      data: {
        msg: 'hello wue',
        deep: {
          a: 1,
          b: 2
        }
      },
      created() {
        console.log('created')
      },
      mounted () {
        console.log('mounted')
        this.deep.a = 111
      }
    })


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

66 original articles published, 74 praised, 120000 visitors+
Private letter follow

Tags: Vue Fragment

Posted on Fri, 14 Feb 2020 08:00:00 -0800 by manlio