Vue 双向数据绑定

原理分析

通过数据劫持实现数据双向绑定

  • 监听器Observer, 用来劫持并监听所有属性, 所发生变动则通知订阅者
  • 订阅者Watcher, 可接收到属性的变化通知并执行相应的函数, 从而更新视图
    • 由于订阅者有多个, 为方便统一管理, 通过一个消息订阅器Dep专门来收集订阅者
  • 指令解析器Complie,
    • 扫描和解析每个节点的相关指令, 并初始化模板数据以及初始化相应的订阅器

如图:
image.png

Version 1.0

实现 Observer

function Observer(data) {
  if (!data || typeof data !== 'object') {
    return;
  }
  Object.keys(data).forEach(key => {
    defineReactive(data, key, data[key]);
  });
}

function defineReactive(data, key, val) {
  //递归子属性
  Observer(val);
  //植入消息订阅器
  //为每个属性创建一个对应的 Dep 对象, 因为每个属性可能有多个订阅者
  let dep = new Dep();
  //在 getter 时收集订阅者 Watcher 到 Dep, 在 setter 时借由 Dep 通知所有的订阅者 Watcher
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      console.log(`getter -> key :: [[${key}]], value :: [[${val}]]`);
      if (Dep.target) { //是否需要添加订阅者
        dep.addSub(Dep.target); // 订阅者
      }
      return val;
    },
    set: function (newVal) {
      val = newVal;
      console.log(`setter -> key :: [[${key}]], new value :: [[${newVal}]]`);
      dep.notify(); // 通知订阅了该属性的所有订阅者更新
    }
  });
}

function Dep() {
  this.subs = [];
}
Dep.prototype.addSub = function (watch) {
  this.subs.push(watch);
}
Dep.prototype.notify = function () {
  this.subs.forEach(watch => watch.update());
}
复制代码

实现 Watcher

function Watcher(data, key, cb) {
  this.data = data;
  this.cb = cb;
  this.key = key;
  this.value = this.get();
}
Watcher.prototype = {
  get: function () {
    Dep.target = this; // 缓存当前 watcher 实例到一个全局变量 , 以便在 getter 中将该 watcher 实例收集到 Dep 中
    const value = this.data[this.key]; // 强制触发 data:key 属性的 getter
    Dep.target = null; //释放
    return value;
  },
  update: function () {
    this.cb();
  }
}
复制代码

demo

<body>
  <h1 id="name">name</h1>
  <script src="./myVue.js"></script>
  <script>
  
    let data = {
      name: 'zheling',
    };
    
    observer(data);
    
    new Watcher(data, 'name', function () {
      document.querySelector('#name').innerHTML = 'hello vue??';
    });

    setTimeout(function () {
      data.name = 'hello Vue'
    }, 1000);

  </script>
</body>
复制代码

Version 2.0

实现 Watcher 2.0

function Watcher(vm, key, cb) {
  this.vm = vm;
  this.data = this.vm.data;
  this.cb = cb;
  this.key = key;
  this.value = this.get();
}
Watcher.prototype = {
  get: function () {
    Dep.target = this; // 缓存当前 watcher 实例到一个全局变量 , 以便在 getter 中将该 watcher 实例收集到 Dep 中
    const value = this.data[this.key]; // 强制触发 data:key 属性的 getter
    Dep.target = null; //释放
    return value;
  },
  update: function () {
    let oldVal = this.value;
    let value = this.data[this.key];
    if (value !== oldVal) {
      this.value = value;
      this.cb.call(this.vm, value, oldVal);
    }
  }
}
复制代码

MyVue

将 Observer 和 Watcher 关联起来

function MyVue(data, el, key) {
  this.data = data;
  this.el = el;
  observer(this.data); //监听属性
  //代理属性: vm.data.key -> vm.key
  Object.keys(this.data).forEach(key => {
    this.proxyKeys(key);
  });
  new Watcher(this, key, function (value) {
    el.innerHTML = value;
  });
  return this;
}
MyVue.prototype.proxyKeys = function (key) {
  Object.defineProperty(this, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      return this.data[key];
    },
    set: function (newVal) {
      this.data[key] = newVal;
    }
  });
}
复制代码

demo

<body>
  <h1 id="name">name</h1>
  <script src="./myVue.js"></script>
  <script>

    let data = {
      name: 'zheling',
    };

    let vm = new MyVue(
      data,
      document.querySelector('#name'),
      'name'
    );

    setTimeout(function () {
      vm.data.name = 'hello Vue'
    }, 1000);

  </script>
</body>
复制代码

Version 3.0

MyVue

function MyVue(options) {
  let { el, data,methods } = options || {};
  this.data = data;
  this.el = el;
  this.methods = methods;
  observer(this.data);
  Object.keys(this.data).forEach(key => {
    this.proxyKeys(key);
  });
  // 解析 DOM , 创建订阅者并为其绑定对应的更新函数
  new Compile(this);
  return this;
}
MyVue.prototype.proxyKeys = function (key) {
  Object.defineProperty(this, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      return this.data[key];
    },
    set: function (newVal) {
      this.data[key] = newVal;
    }
  });
}
复制代码

Compile

// 实现编译器, 将上例初始化订阅者以及解析 DOM 封装出来
function Compile(vm) {
  this.vm = vm;
  this.el = document.querySelector(vm.el);
  // 拿到根容器下所有节点, 由于需要频繁操作 DOM 考虑到性能, 所以将其移动到一个 fragment 中来处理
  this.fragment = null;
  this.init();
}
Compile.prototype = {
  init: function () {
    this.fragment = this.nodeToFragment(this.el);
    this.compileElement(this.fragment);
    this.el.appendChild(this.fragment);
  },
  nodeToFragment: (el) => {
    var fragment = document.createDocumentFragment();
    var child = el.firstChild;
    while (child) {
      // 将DOM元素移入fragment中
      fragment.appendChild(child);
      child = el.firstChild
    }
    return fragment;
  },
  // 遍历 el 下的所有节点
  compileElement: function (el) {
    let childNodes = el.childNodes; // 伪数组对象
    let reg = /\{\{(.+)\}\}/;
    Array.from(childNodes).forEach(node => {
      let text = node.textContent;
      if (this.isTextNode(node) && reg.test(text)) { // 匹配 {{}} 指令
        console.log(node);
        this.compileText(node, reg.exec(text)[1]); // 获取{{key}}中的 key
      }
      //遍历子节点
      if (node.childNodes && node.childNodes.length) {
        this.compileElement(node);
      }
    });
  },
  compileText: function (node, key) {
    // 初始化数据
    this.updateText(node, this.vm[key]);
    // 创建订阅者并绑定更新函数
    new Watcher(this.vm, key, (value) => {
      this.updateText(node, value);
    });
  },
  updateText: function (node, text) {
    node.textContent = text ? text : '';
  },
  isTextNode: function (node) {
    return node.nodeType === 3;
  },
}
复制代码

demo

<body>
  <div id="app">
    <h1>{{name}}</h1>
    <span>{{age}}</span>
  </div>
  <script src="./myVue.js"></script>
  <script>
    let vm = new MyVue({
      el: '#app',
      data: {
        name: 'zheling',
        age: 99
      },
    });
    setTimeout(function () {
      vm.name = 'hello Vue'
    }, 1000);
  </script>
</body>
复制代码

Version 4.0

添加v-on / v-model 指令的处理

Compile

Compile.prototype = {
  init: function () {
    this.fragment = this.nodeToFragment(this.el);
    this.compileElement(this.fragment);
    this.el.appendChild(this.fragment);
  },
  nodeToFragment: (el) => {
    var fragment = document.createDocumentFragment();
    var child = el.firstChild;
    while (child) {
      // 将DOM元素移入fragment中
      fragment.appendChild(child);
      child = el.firstChild
    }
    return fragment;
  },
  // 遍历 el 下的所有节点
  compileElement: function (el) {
    let childNodes = el.childNodes; // 伪数组对象
    let reg = /\{\{(.+)\}\}/;
    Array.from(childNodes).forEach(node => {
      let text = node.textContent;
      // 是否为元素节点
      if (this.isElementNode(node)) {
        this.compile(node);
      } else if (this.isTextNode(node) && reg.test(text)) { // 匹配 {{}} 指令
        this.compileText(node, reg.exec(text)[1]); // 获取{{key}}中的 key
      }
      //遍历子节点
      if (node.childNodes && node.childNodes.length) {
        this.compileElement(node);
      }
    });
  },
  compileText: function (node, key) {
    // 初始化数据
    this.updateText(node, this.vm[key]);
    // 创建订阅者并绑定更新函数
    new Watcher(this.vm, key, (value) => {
      this.updateText(node, value);
    });
  },
  updateText: function (node, text) {
    node.textContent = text ? text : '';
  },
  compile: function (node) {
    var nodeAttrs = node.attributes;
    var self = this;
    // 遍历 node 的属性 attributes , 找到 v-* 开头的指令
    Array.from(nodeAttrs).forEach(function (attr) {
      var attrName = attr.name;
      if (self.isDirective(attrName)) {
        var exp = attr.value;
        var dir = attrName.substring(2); // 移除 v-* 中的 'v-'
        // 进一步解析指令, 是 model 还是 on 指令
        if (self.isEventDirective(dir)) {  // 事件指令
          self.compileEvent(node, self.vm, exp, dir);
        } else {  // v-model 指令
          self.compileModel(node, exp);
        }
        node.removeAttribute(attrName);
      }
    });
  },
  compileEvent: function (node, vm, key, dir) {
    var eventType = dir.split(':')[1];
    var cb = vm.methods && vm.methods[key];
    if (eventType && cb) {  // bind(vm) 使用时能快捷访问到 MyVue 实例
      node.addEventListener(eventType, cb.bind(vm), false);
    }
  },
  compileModel: function (node, key) {
    var self = this;
    var val = this.vm[key];
    // 初始化 node.value
    node.value = typeof val == 'undefined' ? '' : val;
    // 双向绑定之一 : data.field -> input
    new Watcher(this.vm, key, function (value) {
      node.value = typeof value == 'undefined' ? '' : value;
    });
    // 双向绑定之二 : input -> data.field
    node.addEventListener('input', function (e) {
      var newValue = e.target.value;
      if (val === newValue) {
        return;
      }
      self.vm[key] = newValue;
    });
  },
  isDirective: function (attr) {
    return attr.indexOf('v-') == 0;
  },
  isEventDirective: function (dir) {
    return dir.indexOf('on:') === 0;
  },
  isTextNode: function (node) {
    return node.nodeType === 3;
  },
  isElementNode: node => node.nodeType === 1,
}
复制代码

demo

<body>
  <div id="app">
    <h2>{{title}}</h2>
    <input v-model="name">
    <h1>name : {{name}}</h1>
    <button v-on:click="clickMe">click me!</button>
  </div>

  <script src="./myVue.js"></script>
  <script>

    let vm = new MyVue({
      el: '#app',
      data: {
        title: 'zheling',
        name: 'kangshi',
      },
      methods: {
        clickMe: function () {
          this.title = 'new title';
        }
      }
    });
    // 验证 v-model 指令 data.name -> input.value 方向是否正常同步
    setTimeout(function () {
      vm.name = 'hello Vue??'
    }, 5000);

  </script>
</body>
复制代码

参考文章

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享