手写vue源码(1)

240 阅读2分钟

前言

个人的学习笔记,用于方便日后复习用,顺便炒个冷饭。。。。。
讲道理,相信大部分人都大概知道响应式的原理,就是实例化的时候,遍历data和props上的属性,通过Object.defineProperty监听,下面就写一写低配版的源码。
这一篇更新响应式和异步渲染,后面会更新watch,computed,watchEffect,set,以及vue中改写数组方法的hack方法

监听

这里就不遍历对象了,可以通过写一个ref函数直接监听一个基础类型,

      let ref = function (value) {
        return Object.defineProperty({}, "value", {
          get() {
            console.log("get");
            return value;
          },
          set(newValue) {
            console.log("set");
            value = newValue;
          },
        });
      };
      let x = ref(1);
      console.log(x.value);
      x.value = 2;

依赖

监听完成之后就是需要收集依赖
可以用一个Dep类来储存不同属性各自得依赖,然后在获取数据得时候收集依赖,然后在更改值得时候,调用依赖中的方法,渲染DOM

      class Dep {
        constructor() {
          this.deps = new Set();
        }
        depend() {
          if (active) {
            this.deps.add(active);
          }
        }
        notify() {
          this.deps.forEach((dep) => dep());
        }
      }
      let ref = function (value) {
        let dep = new Dep();
        return Object.defineProperty({}, "value", {
          get() {
            dep.depend();
            return value;
          },
          set(newValue) {
            value = newValue;
            dep.notify();
          },
        });
      };

写一个渲染DOM的代码

      let active;
      //有点类似watchEffect
      let onChange = function (fn) {
        active = fn;
        active(); //上来就要执行一次,渲染页面
        active = null; //避免添加重复依赖,所以在get收集到依赖得时候,就要清空
      };
      onChange(() => {
        let str = `hello ${x.value}`;
        document.getElementById("app").innerText = str;
      });

异步渲染DOM

上面的代码已经可以随着x.value的改变而响应式的更新页面了,但是会有一个问题

      onChange(() => {
        console.log("渲染");
        let str = `hello ${x.value}`;
        document.getElementById("app").innerText = str;
      });

      x.value = 2;
      x.value = 3;
      x.value = 4;

这个时候会发现,随着x的更新,DOM会渲染3次,这样对性能不太友好,所以vue中通过nextTick来异步渲染,等本轮数据全部更新完毕之后,才会渲染DOM

nextTick

      let nextTick = function (cb) {
        Promise.resolve().then(cb);  //将渲染操作放在微任务中,异步渲染
      };

调用渲染方法

      let queue = [];  //储存渲染DOM的操作
      let queueJob = function (fn) {
        if (!queue.includes(fn)) {
          //因为更改的数据的依赖都是同一个,所以只会往queue中添加一个cb
          //并且会在数据全部更新之后渲染DOM
          queue.push(fn);
          nextTick(flushJobs);
        }
      };
      let flushJobs = function () {
        while (queue.length > 0) {
          let job = queue.shift(); //执行完之后删除,所以渲染时不会让之前渲染过的DOM再次渲染
          job && job();
        }
      };

notify

此时调用notify的时候,将方法放入异步渲染队列中

        notify() {
          this.deps.forEach((dep) => queueJob(dep));
        }

演示


可以发现,x.value改变了三次,此时只会渲染一次了(第一个渲染是x.value=1的时候调用的)

完整代码

      let active;
      let onChange = function (fn) {
        active = fn;
        active();
        active = null;
      };
      let nextTick = function (cb) {
        Promise.resolve().then(cb);
      };
      let queue = [];
      let queueJob = function (fn) {
        if (!queue.includes(fn)) {
          queue.push(fn);
          nextTick(flushJobs);
        }
      };
      let flushJobs = function () {
        while (queue.length > 0) {
          let job = queue.shift();
          job && job();
        }
      };
      class Dep {
        constructor() {
          this.deps = new Set();
        }
        depend() {
          if (active) {
            this.deps.add(active);
          }
        }
        notify() {
          this.deps.forEach((dep) => queueJob(dep));
        }
      }
      let ref = function (value) {
        let dep = new Dep();
        return Object.defineProperty({}, "value", {
          get() {
            dep.depend();
            return value;
          },
          set(newValue) {
            value = newValue;
            dep.notify();
          },
        });
      };
      let x = ref(1);
      onChange(() => {
        console.log("渲染");
        let str = `hello ${x.value}`;
        document.getElementById("app").innerText = str;
      });

      x.value = 2;
      x.value = 3;
      x.value = 4;