zoukankan      html  css  js  c++  java
  • vue双向数据绑定原理解析及js代码实现

    vue中v-module双向数据绑定理解:我们可以简单分为四个过程
      实现一个监听器Observer:对数据对象进行遍历,包括子属性对象的属性,利用Object.definePropery()对属性都加上setter和getter。这样的话,给这个对象的每个值赋值,就回触发setter,那么就能监听到数据变化。
     
      实现一个解析器Compile:解析Vue模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
     
      实现一个订阅者Watcher:Watcher订阅者是Observer和Compile之间的通信桥梁,主要的任务是订阅Observer中的属性值变化的消息,当收到属性变化的消息时,触发解析器Compile中对应的更新函数。
     
      实现一个订阅器Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者Watcher,对监听器Observer和订阅者Watcher进行统一管理。 
        
    js代码:
    class Dep{
      constructor() {
      this.listenFunc = []
      }
      addFunc(obj) {
        this.listenFunc.push(obj);
      }
      changeWatch() {
        this.listenFunc.forEach(val => {
          val.sendVal()
        })
      }
    }
    Dep.target = null;
    const dep = new Dep()
    class Watcher{
      constructor(data, key, cbk) {
      // 每一次实例watcher的时候,均会把当前实例赋值给Dep的target静态属性
      Dep.target = this;
      this.data = data;
      this.key = key;
      this.cbk = cbk;
      // 每一次的实例都会调用该函数
      this.init()
      }
      // 获取对应key的值
      init() {
        // 获取对应key的值
        this.value = utils.getValue(this.data, this.key);
        Dep.target = null;
        return this.value;
      }
      sendVal() {
        let newVal = this.init()
        this.cbk(newVal)
      }
    }
    class Observer{
      constructor(data) {
        if (!data || typeof data !== 'object') {
          return;
        }
        this.data = data;
        this.init()
      }
      init() {
        Object.keys(this.data).forEach(val => {
          this.observer(this.data, val, this.data[val])
        })
      }
      observer(obj, key, value) {
        // 通过递归实现每个属性的数据劫持
        new Observer(obj[key])
        Object.defineProperty(obj, key, {
          // 添加劫持之后的属性获取方法
          get() {
            if (Dep.target) {
              // 给dep实例属性listenFunc添加一个watcher实例
              dep.addFunc(Dep.target)
            }
            return value
          },
          // 添加劫持之后的属性设置方法
          set(newValue) {
            if (value === newValue) {
              return;
            }
            value = newValue;
            // 触发每一个listenFunc里面的watcher实例
            dep.changeWatch();
            // 为了兼容新值为一个对象的时候,该对象的属性也得添加劫持
            new Observer(value);
          }
        })
      }
    }
    const utils = {
      setValue(node, data, key) {
        node.value = this.getValue(data, key)
      },
      getValue(data, key) {
        if (key.indexOf('.') > -1) {
          let arr = key.split('.');
          for(let i = 0; i < arr.length; i++) {
            data = data[arr[i]]
          }
          return data
        } else {
          return data[key]
        }
      },
      getContent(node, key, data) {
        node.textContent = this.getValue(data, key)
      },
      // 2.在input事件发生之后,改变对应的属性值
      changeKeyVal(data, key, newVal) {
        if (key.indexOf('.') > -1) {
          let arr = key.split('.');
          for(let i = 0; i < arr.length - 1; i++) {
            data = data[arr[i]]
          }
          data[arr[arr.length - 1]] = newVal
        } else {
          data[key] = newVal
        }
      }
    }





    // 实现双向数据绑定
    class Mvvm{
      constructor({el, data}) {
        this.el = el;
        this.data = data;
        // 初始化执行数据绑定实例对象的过程(以及数据劫持)
        this.init();
        // 替换文本中的属性为真实的数据
        this.initDom();
      }
      init() {
        Object.keys(this.data).forEach(val => {
          this.observer(this, val, this.data[val])
        })
        // 给当前数据集合的每一个属性添加劫持
        new Observer(this.data)
      }
      observer(obj, key, value) {
        Object.defineProperty(obj, key, {
          get() {
            return value
          },
          set(newValue) {
            value = newValue
          }
        })
      }
      initDom() {
        this.$el = document.getElementById(this.el);
        // 文本碎片--> 避免因为操作DOM而导致浏览器的多次重绘(操作完成之后可把整个碎片添加进去,浏览器课一并识别渲染)
        let newFargment = this.createFragment();
        // 根据nodeType来替换对应的属性值
        this.compiler(newFargment);
        this.$el.appendChild(newFargment);
      }
      createFragment() {
        let newFragment = document.createDocumentFragment();
        let firstChild;
        while(firstChild = this.$el.firstChild) {
          newFragment.appendChild(firstChild);
        }
        return newFragment;
      }
      compiler(node) {
        if (node.nodeType === 1) {
          let attributes = node.attributes;
          Array.from(attributes).forEach(val => {
            if (val.nodeName === 'v-model') {
              // 1.捕捉input输入框的修改事件
              node.addEventListener('input', (e) => {
                utils.changeKeyVal(this.data, val.nodeValue, e.target.value)
              })
              utils.setValue(node, this.data, val.nodeValue)
            }
          })
        } else if (node.nodeType === 3) {
          let contentVal = node.textContent.indexOf("{{") > -1 && node.textContent.split('{{')[1].split('}}')[0];
          contentVal && utils.getContent(node, contentVal, this.data);
          // 添加属性监听
          contentVal && new Watcher(this.data, contentVal, (newVal) => {
            node.textContent = newVal
          })
        }
        // 通过递归的形式保证每一级的文本都可获取到并替换
        if (node.childNodes && node.childNodes.length > 0) {
          node.childNodes.forEach(val => {
            this.compiler(val)
          })
        }
      }
    }



  • 相关阅读:
    css3动画事件 animationend animationiteration animationstart
    dom对象---增加class属性,去除class属性
    数组的indexOf() 方法
    line-height中的五种取值方式和继承
    html 中line-height相关的四种box模型
    真正的能理解CSS中的line-height,height与line-height
    background-size:contain与cover的区别
    激活win10系统
    table-layout:fixed 应用
    js数组fill()方法
  • 原文地址:https://www.cnblogs.com/Alina-na/p/11726092.html
Copyright © 2011-2022 走看看