vue Realizes Simple Bidirectional Data Binding

  • 2021-10-16 00:39:53
  • OfStack

It is mainly realized through data hijacking and publishing and subscribing

When bidirectional data binding data updates, the data updates that can update the view view are that you can reverse update the model

Composition description

The Observe listener hijacks data, senses data changes, sends notifications to subscribers, and adds subscribers to subscribers in get The Dep message subscriber stores the subscriber and informs the subscriber to call the update function Subscriber Wather fetches the model value and updates the view Parser Compile parses instructions, updates template data, initializes views, instantiates one subscriber, binds update functions to subscribers, updates views twice after receiving notifications, and monitors input events for v-model to realize data flow from views to models

Basic structure

HTML template


  <div id="app">
    <form>
      <input type="text" v-model="username">
    </form>
    <p v-bind="username"></p>
  </div>
1 root node # app Form element, which contains input, uses v-model instructions to bind data username v-bind binding number username on p element

Class MyVue

Simple Simulated Vue Class

Save the option options and the data options. data during instantiation. In addition, get the dom element through options. el and store it on $el


    class MyVue {
      constructor(options) {
        this.$options = options
        this.$el = document.querySelector(this.$options.el)
        this.$data = options.data
      }
    }

Instantiate MyVue

Instantiate 1 MyVue and pass in the option, which specifies the bound element el and data object data


    const myVm = new MyVue({
      el: '#app',
      data: {
        username: 'LastStarDust'
      }
    })

Implementation of Observe Listener

Hijacking data is to perceive, notify and update the view when modifying data


    class MyVue {
      constructor(options) {
        // ...
        //  Attributes of Monitoring Data 
        this.observable(this.$data)
      }
      //  Recursively traverses all properties of a data object ,  Hijacking data attributes  { username: 'LastStarDust' }
      observable(obj) {
        // obj Is empty or not an object ,  Do nothing 
        const isEmpty = !obj || typeof obj !== 'object'
        if(isEmpty) {
          return
        }

        // ['username']
        const keys = Object.keys(obj)
        keys.forEach(key => {
          //  If the property value is an object , Recursive invocation 
          let val = obj[key]
          if(typeof val === 'object') {
            this.observable(val)
          }
          // this.defineReactive(this.$data, 'username', 'LastStarDust')
          this.defineReactive(obj, key, val)
        })

        return obj
      }

      //  Data hijacking , To modify the property get And set Method 
      defineReactive(obj, key, val) {
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get() {
            console.log(` Take out ${key} Attribute value :  Value is ${val}`)
            return val
          },
          set(newVal) {
            //  There has been no change ,  Do not update 
            if(newVal === val) {
              return
            }
            console.log(` Update properties ${key} The value of is : ${newVal}`)
            val = newVal
          }
        })
      }
    }

Dep Message Subscriber

The subscriber is stored, and when the notification is received, the subscriber is taken out and the subscriber's update method is called


    //  Defining a message subscriber 
    class Dep {
      //  Static attribute  Dep.target This is 1 Individual global only 1  Adj. Watcher , because in the same 1 There can only be time 1 Global  Watcher
      static target = null
      constructor() {
        //  Storage subscriber 
        this.subs = []
      }
      //  Add Subscriber 
      add(sub) {
        this.subs.push(sub)
      }
      //  Notice 
      notify() {
        this.subs.forEach(sub => {
          //  Call the subscriber's update Method 
          sub.update()
        })
      }
    }

Adding a message subscriber to the data hijacking process

Add subscribers for every 1 attribute


      defineReactive(obj, key, val) {
        const dep = new Dep()
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get() {
            //  Will be initialized ,  Trigger attribute get() Method , Come here Dep.target Valuable , Store it as a subscriber , Object that triggers the property set() Method , Call notify Method 
            if(Dep.target) {
              dep.add(Dep.target)
            }
            console.log(` Take out ${key} Attribute value :  Value is ${val}`)
            return val
          },
          set(newVal) {
            //  There has been no change ,  Do not update 
            if(newVal === val) {
              return
            }
            console.log(` Update properties ${key} The value of is : ${newVal}`)

            val = newVal
            dep.notify()
          }
        })
      }

Subscriber Wather

Fetch data from the model and update the view


    //  Defining Subscriber Classes 
    class Wather {
      constructor(vm, exp, cb) {
        this.vm = vm // vm Instances 
        this.exp = exp //  The string value corresponding to the instruction ,  Such as v-model="username", exp Equivalent to "username"
        this.cb = cb //  Back to Function   Called when the view is updated 
        this.value = this.get() //  Add yourself to the message subscriber Dep Medium 
      }

      get() {
        //  Regard the current subscriber as a global only 1 Adj. Wather, Add to Dep.target Upper 
        Dep.target = this
        //  Get data , Object that triggers the property getter Method 
        const value = this.vm.$data[this.exp]
        //  Add to Message Subscription in Execution Dep Posterior ,  Reset Dep.target
        Dep.target = null
        return value
      }

      //  Perform an update 
      update() {
        this.run()
      }

      run() {
        //  From Model Take out attribute values from the model 
        const newVal = this.vm.$data[this.exp]
        const oldVal = this.value
        if(newVal === oldVal) {
          return false
        }
        //  Execute callback function ,  Will vm Instances , New value , Pass the old value past 
        this.cb.call(this.vm, newVal, oldVal)
      }
    }

Parser Compile

Parsing template instructions, replacing template data and initializing views; Binding the node corresponding to the template instruction to the corresponding update function and initializing the corresponding subscriber; Initialize the compiler, store the dom element corresponding to el, store the vm instance, and call the initialization method In the initialization method, starting from the root node, all the child nodes of the root node are taken out, and the nodes are parsed one by one In the process of parsing nodes, The parsing instruction exists, takes out the binding value, replaces the template data, and completes the initialization of the first view The node corresponding to the instruction is bound with an update function, and a subscriber Wather is instantiated For the v-model instruction, listen to the 'input' event, and realize the view update is to update the data of the model

    //  Defining a parser 
    //  Parsing instruction , Replace template data , Initial view 
    //  Instruction binding update function of template ,  When data is updated ,  Update view 
    class Compile {
      constructor(el, vm) {
        this.el = el
        this.vm = vm
        this.init(this.el)
      }

      init(el) {
        this.compileEle(el)
      }
      compileEle(ele) {
        const nodes = ele.children
		//  Traverse the node for resolution 
        for(const node of nodes) {
		 //  If there are child nodes , Recursive invocation 
          if(node.children && node.children.length !== 0) {
            this.compileEle(node)
          }

          //  Instruction time v-model And the label is the input label 
          const hasVmodel = node.hasAttribute('v-model')
          const isInputTag = ['INPUT', 'TEXTAREA'].indexOf(node.tagName) !== -1
          if(hasVmodel && isInputTag) {
            const exp = node.getAttribute('v-model')
            const val = this.vm.$data[exp]
            const attr = 'value'
            //  Push the initial model value to the view layer , Initialize view 
            this.modelToView(node, val, attr)
            //  Instantiation 1 Subscribers ,  Bind the update function to the subscriber ,  Future data updates , You can update the view 
            new Wather(this.vm, exp, (newVal)=> {
              this.modelToView(node, newVal, attr)
            })

            //  Listen for changes in view 
            node.addEventListener('input', (e) => {
              this.viewToModel(exp, e.target.value)
            })
          }
		 
		 //  Instruction time v-bind
          if(node.hasAttribute('v-bind')) {
            const exp = node.getAttribute('v-bind')
            const val = this.vm.$data[exp]
            const attr = 'innerHTML'
            //  Push the initial model value to the view layer , Initialize view 
            this.modelToView(node, val, attr)
            //  Instantiation 1 Subscribers ,  Bind the update function to the subscriber ,  Future data updates , You can update the view 
            new Wather(this.vm, exp, (newVal)=> {
              this.modelToView(node, newVal, attr)
            })
          }
        }
      }
      //  Update model values to views 
      modelToView(node, val, attr) {
        node[attr] = val
      }
      //  Update view values to the model 
      viewToModel(exp, val) {
        this.vm.$data[exp] = val
      }
    }

Complete code


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <form>
      <input type="text" v-model="username">
    </form>
    <div>
      <span v-bind="username"></span>
    </div>
    <p v-bind="username"></p>
  </div>
  <script>

    class MyVue {
      constructor(options) {
        this.$options = options
        this.$el = document.querySelector(this.$options.el)
        this.$data = options.data

        // 监视数据的属性
        this.observable(this.$data)

        // 编译节点
        new Compile(this.$el, this)
      }
      // 递归遍历数据对象的所有属性, 进行数据属性的劫持 { username: 'LastStarDust' }
      observable(obj) {
        // obj为空或者不是对象, 不做任何操作
        const isEmpty = !obj || typeof obj !== 'object'
        if(isEmpty) {
          return
        }

        // ['username']
        const keys = Object.keys(obj)
        keys.forEach(key => {
          // 如果属性值是对象,递归调用
          let val = obj[key]
          if(typeof val === 'object') {
            this.observable(val)
          }
          // this.defineReactive(this.$data, 'username', 'LastStarDust')
          this.defineReactive(obj, key, val)
        })

        return obj
      }

      // 数据劫持,修改属性的get和set方法
      defineReactive(obj, key, val) {
        const dep = new Dep()
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get() {
            // 会在初始化时, 触发属性get()方法,来到这里Dep.target有值,将其作为订阅者存储起来,在触发属性的set()方法时,调用notify方法
            if(Dep.target) {
              dep.add(Dep.target)
            }
            console.log(`取出${key}属性值: 值为${val}`)
            return val
          },
          set(newVal) {
            // 没有发生变化, 不做更新
            if(newVal === val) {
              return
            }
            console.log(`更新属性${key}的值为: ${newVal}`)

            val = newVal
            dep.notify()
          }
        })
      }
    }

    // 定义消息订阅器
    class Dep {
      // 静态属性 Dep.target,这是1个全局唯1 的Watcher,因为在同1时间只能有1个全局的 Watcher
      static target = null
      constructor() {
        // 存储订阅者
        this.subs = []
      }
      // 添加订阅者
      add(sub) {
        this.subs.push(sub)
      }
      // 通知
      notify() {
        this.subs.forEach(sub => {
          // 调用订阅者的update方法
          sub.update()
        })
      }
    }

    // 定义订阅者类
    class Wather {
      constructor(vm, exp, cb) {
        this.vm = vm // vm实例
        this.exp = exp // 指令对应的字符串值, 如v-model="username", exp相当于"username"
        this.cb = cb // 回到函数 更新视图时调用
        this.value = this.get() // 将自己添加到消息订阅器Dep中
      }

      get() {
        // 将当前订阅者作为全局唯1的Wather,添加到Dep.target上
        Dep.target = this
        // 获取数据,触发属性的getter方法
        const value = this.vm.$data[this.exp]
        // 在执行添加到消息订阅Dep后, 重置Dep.target
        Dep.target = null
        return value
      }

      // 执行更新
      update() {
        this.run()
      }

      run() {
        // 从Model模型中取出属性值
        const newVal = this.vm.$data[this.exp]
        const oldVal = this.value
        if(newVal === oldVal) {
          return false
        }
        // 执行回调函数, 将vm实例,新值,旧值传递过去
        this.cb.call(this.vm, newVal, oldVal)
      }
    }

    // 定义解析器
    // 解析指令,替换模板数据,初始视图
    // 模板的指令绑定更新函数, 数据更新时, 更新视图
    class Compile {
      constructor(el, vm) {
        this.el = el
        this.vm = vm
        this.init(this.el)
      }

      init(el) {
        this.compileEle(el)
      }
      compileEle(ele) {
        const nodes = ele.children
        for(const node of nodes) {
          if(node.children && node.children.length !== 0) {
            // 递归调用, 编译子节点
            this.compileEle(node)
          }

          // 指令时v-model并且是标签是输入标签
          const hasVmodel = node.hasAttribute('v-model')
          const isInputTag = ['INPUT', 'TEXTAREA'].indexOf(node.tagName) !== -1
          if(hasVmodel && isInputTag) {
            const exp = node.getAttribute('v-model')
            const val = this.vm.$data[exp]
            const attr = 'value'
            // 初次模型值推到视图层,初始化视图
            this.modelToView(node, val, attr)
            // 实例化1个订阅者, 将更新函数绑定到订阅者上, 未来数据更新,可以更新视图
            new Wather(this.vm, exp, (newVal)=> {
              this.modelToView(node, newVal, attr)
            })

            // 监听视图的改变
            node.addEventListener('input', (e) => {
              this.viewToModel(exp, e.target.value)
            })
          }

          if(node.hasAttribute('v-bind')) {
            const exp = node.getAttribute('v-bind')
            const val = this.vm.$data[exp]
            const attr = 'innerHTML'
            // 初次模型值推到视图层,初始化视图
            this.modelToView(node, val, attr)
            // 实例化1个订阅者, 将更新函数绑定到订阅者上, 未来数据更新,可以更新视图
            new Wather(this.vm, exp, (newVal)=> {
              this.modelToView(node, newVal, attr)
            })
          }
        }
      }
      // 将模型值更新到视图
      modelToView(node, val, attr) {
        node[attr] = val
      }
      // 将视图值更新到模型上
      viewToModel(exp, val) {
        this.vm.$data[exp] = val
      }
    }

    const myVm = new MyVue({
      el: '#app',
      data: {
        username: 'LastStarDust'
      }
    })

    // console.log(Dep.target)
  </script>
</body>
</html>

The above is the vue implementation of simple bi-directional data binding details, more about vue implementation of bi-directional data binding information please pay attention to other related articles on this site!


Related articles: