造轮子:Vue2响应式源码

144 阅读2分钟

4.gif

响应式

juejin.cn/post/695082…

juejin.cn/post/684490…

什么是响应式?数据发生改变的时候,视图会重新渲染,匹配更新为最新的值。

我们可以问出下面三个问题

1、Vue 是怎么知道数据改变 ?Object.defineProperty

2、Vue 在数据改变时,怎么知道通知哪些视图更新?依赖收集dep

3、Vue 在数据改变时,视图怎么知道什么时候更新?

vue响应式

背景

观察者模式

juejin.cn/post/705544…

defineProperty

Object.defineProperty可以为对象中的每一个属性,设置 get 和 set 方法

  • get 值是一个函数,当属性被访问时,会触发 get 函数
  • set 值同样是一个函数,当属性被赋值时,会触发 set 函数
 var obj = {
   name: ""
 }
 Object.defineProperty(obj, "name", {
   get() {
     console.log("get 被触发")
     return '神仙'
   },
   set(val) {
     console.log("set 被触发")
     return val;
   }
 })
 console.log(obj.name);
 ​
 //当我访问 obj.name 时,会打印 ' get 被触发 '
 //当我为 obj.name 赋值时,obj.name = 5,会打印 ' set 被触发 '

vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调

 const data = {};
 const input = document.getElementById('input');
 Object.defineProperty(data, 'text', {
   set(value) {
     input.value = value;
     this.value = value;
   }
 });
 input.onChange = function(e) {
   data.text = e.target.value;
 }

Watcher

Watcher是一个观察者对象(本质上是一个组件)

  • 一个组件生成一个Watcher实例
  • 依赖收集:执行addSub把Watcher实例保存在Dep实例的subs中
  • 更新:数据变动的时候Dep执行notify通知Watcher实例,然后由Watcher实例回调update进行视图的更新
const Watcher = function (vm, fn) {
    console.log(vm, fn);
    this.vm = vm;
    // 将当前Dep.target指向自己
    Dep.target = this;
    // 在 dep 中添加 watcher
    this.addDep = function (dep) {
      console.log("addDep-this", this); //Watcher实例
      dep.addSub(this);
    };
​
    // 更新方法,用于触发vm.$render
    this.update = function () {
      console.log(this, this, "update-this");
      console.log("in watcher update");
      fn.call(this.vm);
    };
    // 这里会首次调用vm._render,从而触发text的get
    // 从而将当前的Wathcer与Dep关联起来
    fn.call(this.vm);
    // 这里清空了Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,
    // 造成代码死循环
    Dep.target = null;
  };

依赖收集Dep

  • 生成Dep实例(一个目标对象obj.key(即data)会生成一个Dep实例)
  • getter读取响应式数据时,执行depend负责收集Watcher依赖
  • setter设置响应式数据时,执行notify通知 dep 中那些 watcher 去执行 update 方法
  const Dep = function () {
    this.target = null; // 收集目标
    this.subs = []; // dep收集器存储需要通知的Watcher
    this.addSub = function (watcher) {
      // 在 dep 中添加 watcher
      this.subs.push(watcher);
    };
    // 向 watcher 中添加 dep
    this.depend = function () {
      // 当有目标时,绑定Dep与Wathcer的关系
      if (Dep.target) {
        console.log("Dep.target", Dep.target);//
        Dep.target.addDep(this);
      }
    };
    this.notify = function () {
      // 通知收集器 dep 中的所有 watcher,执行 watcher.update() 方法
      for (let i = 0; i < this.subs.length; i += 1) {
        this.subs[i].update();
      }
    };
  };

Observer

Observer类是将每个目标对象(即data)的键值转换成getter/setter形式,用于进行依赖收集以及调度更新。

const defineReactive = function (obj, key) {
    // 实例化 dep,一个 key 一个 dep
    const dep = new Dep();
    console.log("dep", dep, obj, key);
    // 获取当前值
    let val = obj[key];
    Object.defineProperty(obj, key, {
      // 设置当前描述属性为可被循环
      enumerable: true,
      // 设置当前描述属性可被修改
      configurable: true,
      get() {
        console.log("in get");
        // 调用依赖收集器中的addSub,用于收集当前属性与Watcher中的依赖关系
        dep.depend();
        return val;
      },
      set(newVal) {
        console.log("in set");
        if (newVal === val) {
          return;
        }
        val = newVal;
        // 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
        // 这里每个需要更新通过什么断定?dep.subs
        dep.notify();
      },
    });
  }; 
const Observer = function (data) {
    // 循环修改为每个属性添加get set
    for (let key in data) {
      defineReactive(data, key);
    }
  };
​
  const observe = function (data) {
    return new Observer(data);
  };

代码

  • 代码梳理流程图

响应式流程图.png

  • console

image-20231218161651228.png

//只针对了对象进行响应式
<div id="app">
  <h3></h3>
</div><script>
  // 响应式的数据分为两类:
  // 对象,循环遍历对象的所有属性,为每个属性设置 getter、setter,以达到拦截访问和设置的目的,如果属性值依旧为对象,则递归为属性值上的每个 key 设置 getter、setter
  // 访问数据时(obj.key)进行依赖收集,在 dep 中存储相关的 watcher
  // 设置数据时由 dep 通知相关的 watcher 去更新
  // 数组,增强数组的那 7 个可以更改自身的原型方法,然后拦截对这些方法的操作
  // 添加新数据时进行响应式处理,然后由 dep 通知 watcher 去更新
  // 删除数据时,也要由 dep 通知 watcher 去更新
​
  /**
   * 一个 dep 对应一个 obj.key
   * 在读取响应式数据时,负责收集依赖,每个 dep(或者说 obj.key)依赖的 watcher 有哪些
   * 在响应式数据更新时,负责通知 dep 中那些 watcher 去执行 update 方法
   */
  const Dep = function () {
    this.target = null; // 收集目标
    this.subs = []; // dep收集器存储需要通知的Watcher
    this.addSub = function (watcher) {
      // 在 dep 中添加 watcher
      this.subs.push(watcher);
    };
    // 向 watcher 中添加 dep
    this.depend = function () {
      // 当有目标时,绑定Dep与Wathcer的关系
      if (Dep.target) {
        console.log("Dep.target", Dep.target);
        Dep.target.addDep(this);
      }
    };
    this.notify = function () {
      // 通知收集器 dep 中的所有 watcher,执行 watcher.update() 方法
      for (let i = 0; i < this.subs.length; i += 1) {
        this.subs[i].update();
      }
    };
  };
  /**
   * 拦截 obj[key] 的读取和设置操作:
   *   1、在第一次读取时收集依赖,比如执行 render 函数生成虚拟 DOM 时会有读取操作
   *   2、在更新时设置新值并通知依赖更新
   */
  const defineReactive = function (obj, key) {
    // 实例化 dep,一个 key 一个 dep
    const dep = new Dep();
    console.log("dep", dep, obj, key);
    // 获取当前值
    let val = obj[key];
    Object.defineProperty(obj, key, {
      // 设置当前描述属性为可被循环
      enumerable: true,
      // 设置当前描述属性可被修改
      configurable: true,
      get() {
        console.log("in get");
        // 调用依赖收集器中的addSub,用于收集当前属性与Watcher中的依赖关系
        dep.depend();
        return val;
      },
      set(newVal) {
        console.log("in set");
        if (newVal === val) {
          return;
        }
        val = newVal;
        // 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
        // 这里每个需要更新通过什么断定?dep.subs
        dep.notify();
      },
    });
  };
​
  const Observer = function (data) {
    // 循环修改为每个属性添加get set
    for (let key in data) {
      defineReactive(data, key);
    }
  };
​
  const observe = function (data) {
    return new Observer(data);
  };
​
  /**
   * 一个组件一个 watcher(渲染 watcher)或者一个表达式一个 watcher(用户watcher)
   * 当数据更新时 watcher 会被触发,访问 this.computedProperty 时也会触发 watcher
   */
  const Watcher = function (vm, fn) {
    console.log(vm, fn);
    this.vm = vm;
    // 将当前Dep.target指向自己
    Dep.target = this;
    // 在 dep 中添加 watcher
    this.addDep = function (dep) {
      console.log("addDep-this", this); //Watcher实例
      dep.addSub(this);
    };
​
    // 更新方法,用于触发vm.$render
    this.update = function () {
      console.log(this, this, "update-this");
      console.log("in watcher update");
      fn.call(this.vm);
    };
    // 这里会首次调用vm._render,从而触发text的get
    // 从而将当前的Wathcer与Dep关联起来
    fn.call(this.vm);
    // 这里清空了Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,
    // 造成代码死循环
    Dep.target = null;
  };
​
  const Vue = function (options) {
    // 将data赋值给this.$data,源码这部分用的Proxy所以我们用最简单的方式临时实现
    if (options && typeof options.data === "function") {
      this.$data = options.data.apply(this);
    }
    // 监听this.$data
    observe(this.$data);
  };
​
  // 挂载函数
  Vue.prototype.$mount = function () {
    const self = this;
    new Watcher(self, self.$render);
    // new Watcher(self, self.$render);
  };
​
  // 渲染函数
  Vue.prototype.$render = function () {
    //渲染视图
    //vue2渲染函数返回的是虚拟dom,这里暂时返回真实的dom
    console.log(this);
    const h3 = document.getElementsByTagName("h3")[0];
    h3.innerText = this.$data.text + `${this.$data.text1}`;
    return h3;
  };
​
  const myApp = new Vue({
    data() {
      return {
        text: "hello world",
        text1: "123",
      };
    },
    mounted() {
      setTimeout(() => {
        this.$data.text = "这是新的标题";
      }, 1000);
    },
  });
​
  myApp.$mount("app");
  // myApp.$data.text = "123"; // in watcher update /n in get
</script>