实现vue2的响应式原理

138 阅读2分钟

实现vue2的响应式原理

参考视频:www.bilibili.com/video/BV193…

第一步:先定义好html的基本结构

  div id="app">
    <span>创始人:{{name}}</span>
    <input type="text" v-model="name">
    <span>更多:{{more.like}}</span>
    <input type="text" v-model="more.like">
  </div>  

第二步:定义Vue中实现响应的的data数据

const vm = new Vue({
  el: "#app",
      data: {
        name: '影子',
        more: {
          like: '星心'
        }
      }
    })

这里我们监听两个属性name和more,属性more的值又是一个对象。

第三步:直接看代码(配有详细注释,注意每个小点(1,2,...)的逻辑跳跃)

//一、首先我们创建一个Vue的类
class Vue {
  constructor(obj_instance) {
    // 1.将传过来的对象(obj_instance其实就是new Vue实例)身上的data属性传给当前实例里的$data属性
    this.$data = obj_instance.data;
    Observer(this.$data); //对data里面的每个属性进行数据劫持
    Compile(obj_instance.el, this);  //HTML解析模板
  }
}

二、数据劫持
// 2.数据劫持-监听实例里面的数据
function Observer(data_instance) {
  // 4.为递归找出口,当当前的属性为空或者没有检测到当前属性是对象时需要终止递归
  if (!data_instance || typeof data_instance !== 'object') return;
  const dependency = new Dependency();
  // Object.keys以数组的形式返回对象里面的每个key(属性),可以通过其遍历每个属性并对每个属性进行数据劫持
  Object.keys(data_instance).forEach(key => {
    let value = data_instance[key];//在进入Object.defineProperties之前先存下属性的值,否则当进入Object.defineProperties之后属性的值就已经发生改变了
    // 3.递归,对当前属性的值进行数据劫持,否则只能劫持到最表面的一层属性
    Observer(value)
    Object.defineProperty(data_instance, key, {
      enumerable: true,//表示属性可枚举
      configurable: true,//表示属性可修改
      get() {
        // console.log(`访问了属性${key}->值:${value}`);
        // 订阅者加入依赖实例的数组(Dependency.temp不为空说明当前有新的订阅者,将新的订阅者加入到订阅者数组中)
        Dependency.temp && dependency.addSub(Dependency.temp)
        return value;
      },
      set(newValue) {
        // console.log(`属性${key}的值${value}修改为->${newValue}`)
        value = newValue
        Observer(newValue);//递归,对新设置的值进行劫持监听,以防设置新的值也是一个对象时没有及时劫持到,如果传入的不是对象会直接return,如果传入的是对象会进行数据劫持的操作
        // 在修改数据的时候需要通知订阅者来更新
        dependency.notify();//当设置新的值的时候需要调用依赖的通知方法,通知所有订阅者更新数据
      },
    })
  })
}

// HTML模板解析-替换DOM
function Compile(element, vm) {//element表示挂载到哪个元素,vm表示Vue实例
  vm.$el = document.querySelector(element);
  const fragment = document.createDocumentFragment();
  let child;
  while (child = vm.$el.firstChild) {
    fragment.append(child);//将vm.$el身上的所有节点属性文本等都移到碎片文档中
  }
  fragment_compile(fragment);
  // 替换文档碎片内容
  function fragment_compile(node) {
    const pattern = /\{\{\s*(\S+)\s*\}\}/;//设置正则表达式来匹配html结构中的模板字符串部分
    if (node.nodeType === 3) { //文本的nodeType值为3对应的是span里面的文本
      const xxx = node.nodeValue;//提前保存这个值
      const result_regex = pattern.exec(node.nodeValue);//返回匹配成功结果的数组
      if (result_regex) {
        const arr = result_regex[1].split('.');//result_regex[1]是匹配出来的属性名,split('.')针对当属性名是多个‘.’连接的属性名时将其切割
        const value = arr.reduce((total, current) => total[current], vm.$data)
        node.nodeValue = xxx.replace(pattern, value)
        // 创建订阅者(result_regex[1]对应当前的属性 name 和 more.like )
        new Watch(vm, result_regex[1], (newValue) => {
          node.nodeValue = xxx.replace(pattern, newValue)
        });
      }
      return
    }
    if (node.nodeType === 1 && node.nodeName === 'INPUT') { //v-model绑定,获取节点值为1的节点(即input节点)
      const attr = Array.from(node.attributes);
      Array.from(node.attributes).forEach(i => {
        if (i.nodeName === 'v-model') {
          const value = i.nodeValue.split('.').reduce((total, current) => total[current], vm.$data); //获取将当前属性的值
          node.value = value;
          // 创建订阅者(i.nodeValue对应当前的属性 name 和 more.like )
          new Watch(vm, i.nodeValue, newValue => {
            node.value = newValue;
          });
          node.addEventListener('input', e => { 
            // ['more','like']
            const arr1 = i.nodeValue.split('.');
            //['more']
            const arr2 = arr1.slice(0, arr1.length - 1);
            // vm.$data.more
            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 Watch {
  constructor(vm, key, callback) {
    this.vm = vm;//vm表示Vue实例
    this.key = key;//key表示Vue实例对应的属性
    this.callback = callback;//callback记录如何更新文本内容、
    Dependency.temp = this;//临时属性-触发getter,说明此时有订阅者
    key.split('.').reduce((total, current) => total[current], vm.$data)
    Dependency.temp = null;//触发getter之后将其情况,防止订阅者多次加入到依赖实例数组里面
  }
  // 发布者通知订阅者可以更新了
  update() {
    const value = this.key.split('.').reduce((total, current) => total[current], this.vm.$data)
    this.callback(value);
  }
}