前言

vue 是一个易上手的结构,许多快捷功用都在其内部做了集成,其中最有区别性的功用便是其潜藏于底层的呼应式体系。组件状况都是呼应式的 JavaScript 方针。当更改它们时,视图会随即更新,这让状况办理愈加简略直观。那么,Vue 呼应性体系是怎么完成的呢?本文也是在阅读了 Vue 源码后的了解以及模仿完成,所以跟从作者的思路,咱们一起由浅入深的探究一下vue吧!

本文 Vue 源码版别:2.6.14,为了便于了解,代码都最简化。

Vue 是怎么完成的数据呼应式

当你把一个一般的 JavaScript 方针传入 Vue 实例作为 data 选项,Vue 将遍历此方针一切的 property,并使用 Object.defineProperty 把这些 property 悉数转为 getter/setter,然后围绕 getter/setter来运转。

一句话归纳Vue 的呼应式体系便是: 调查者形式 + Object.defineProperty 拦截getter/setter

MDN ObjdefineProperty

调查者形式

什么是Object.defineProperty ?

Object.defineProperty() 办法会直接在一个方针上界说一个新特点,或许修正一个方针的现有特点,并返回此方针。

简略的说,便是经过此方式界说的 property,履行 obj.xxx 时会触发 get,履行 obj.xxx = xxx会触发 set,这便是呼应式的关键。

Object.defineProperty 是 ES5 中一个无法 shim(无法经过polyfill完成) 的特性,这也便是 Vue 不支持 IE8 以及更低版别浏览器的原因。

呼应式体系根底完成

现在,咱们来基于Object.defineProperty完成一个简易的呼应式更新体系作为“开胃菜”

let data = {};
// 使用一个中心变量保存 value
let value = "hello";
// 用一个集合保存数据的呼应更新函数
let fnSet = new Set();
// 在 data 上界说 text 特点
Object.defineProperty(data, "text", {
  enumerable: true,
  configurable: true,
  set(newValue) {
    value = newValue;
    // 数据改变
    fnSet.forEach((fn) => fn());
  },
  get() {
    fnSet.add(fn);
    return value;
  },
});
// 将 data.text 渲染到页面上
function fn() {
  document.body.innerText = data.text;
}
// 履行函数,触发读取 get
fn();
// 一秒后改变数据,触发 set 更新
setTimeout(() => {
  data.text = "world";
}, 1000);

接下来咱们在浏览器中运转这段代码,会得到希望的效果

经过上面的代码,我想你对呼应式体系的工作原理已经有了一定的了解。为了让这个“开胃菜”易于消化,这个简易的呼应式体系还有许多缺点,例如:数据和呼应更新函数是经过硬编码强耦合在一起的、只完成了1对1的情况、不行模块化等等……所以接下来,咱们来一一完善。

规划一个完善的呼应式体系

要规划一个完善的呼应式体系,咱们需求先了解一个前置知识,什么是调查者形式?

什么是调查者形式?

它便是一种行为规划形式, 答应你界说一种订阅机制, 可在方针事件产生时告诉多个 “调查” 该方针的其他方针。

拥有一些值得重视状况的方针通常被称为方针,由于它自身状况产生改变时需求告诉其他方针,咱们也将其成为发布者(publisher) 。一切希望重视发布者状况改变的其他方针被称为订阅者(subscribers) 。此外,发布者与一切订阅者直接仅经过接口交互,都有必要具有相同的接口

Vue 呼应式完成原理浅显易懂

举个比如:

你(即使用中的订阅者)对某个书店的周刊感兴趣,你给老板(即使用中的发布者)留了电话,让老板一有新周刊就给你打电话,其他对这本周刊感兴趣的人,也给老板留了电话。新周刊到货时,老板就挨个打电话,告诉读者来取。

假设某个读者一不小心留的是 qq 号,不是电话号码,老版打电话时就会打不通,该读者就收不到告诉了。这便是咱们上面说的,有必要具有相同的接口。

了解了调查者形式后,咱们就开始着手规划呼应式体系。

笼统调查者(订阅者)类Watcher

在上面的比如中,数据和呼应更新函数是经过硬编码强耦合在一起的。而实践开发过程中,更新函数不一定叫fn,更有可能是一个匿名函数。所以咱们需求抽像一个调查者(订阅者)类Watcher来保存并履行更新函数,一起向外供给一个update更新接口。

// Watcher 调查者可能有 n 个,咱们为了区分它们,保证唯一性,添加一个 uid
let watcherId = 0;
// 当前活跃的 Watcher
let activeWatcher = null;
class Watcher {
  constructor(cb) {
    this.uid = watcherId++;
    // 更新函数
    this.cb = cb;
    // 保存 watcher 订阅的一切数据
    this.deps = [];
    // 初始化时履行更新函数
    this.get();
  }
  // 求值函数
  get() {
    // 调用更新函数时,将 activeWatcher 指向当前 watcher
    activeWatcher = this;
    this.cb();
    // 调用完重置
    activeWatcher = null;
  }
  // 数据更新时,调用该函数从头求值
  update() {
    this.get();
  }
}

笼统被调查者(发布者)类Dep

咱们再想一想,实践开发过程中,data 中肯定不止一个数据,并且每个数据,都有不同的订阅者,所以说咱们还需求笼统一个被调查者(发布者)Dep类来保存数据对应的调查者(Watcher),以及数据改变时告诉调查者更新。

class Dep {
  constructor() {
    // 保存一切该依靠项的订阅者
    this.subs = [];
  }
  addSubs() {
    // 将 activeWatcher 作为订阅者,放到 subs 中
    // 防止重复订阅
    if(this.subs.indexOf(activeWatcher) === -1){
      this.subs.push(activeWatcher);
    }
  }
  notify() {
    // 先保存旧的依靠,便于下面遍历告诉更新
    const deps = this.subs.slice()
    // 每次更新前,清除上一次搜集的依靠,下次履行时,从头搜集
    this.subs.length = 0;
    deps.forEach((watcher) => {
      watcher.update();
    });
  }
}

笼统 Observer

现在,WatcherDep只是两个独立的模块,咱们怎么把它们关联起来呢?

答案便是Object.defineProperty,在数据被读取,触发get办法,Dep 将当前触发 get 的 Watcher 作为订阅者放到 subs中,Watcher 就与 Dep树立联系;在数据被修正,触发set办法,Dep就遍历 subs 中的订阅者,告诉Watcher更新。

下面咱们就来完善将数据转换为getter/setter的处理。

上面根底的呼应式体系完成中,咱们只界说了一个呼应式数据,当 data 中有其他property时咱们就处理不了了。所以,咱们需求笼统一个 Observer类来完成对 data数据的遍历,并调用defineReactive转换为 getter/setter,终究完成呼应式绑定。

为了简化,咱们只处理data中单层数据。

class Observer {
  constructor(value) {
    this.value = value;
    this.walk(value);
  }
  // 遍历 keys,转换为 getter/setter
  walk(obj) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      defineReactive(obj, key, obj[key]);
    }
  }
}

这儿咱们经过参数 value 的闭包,来保存最新的数据,防止新增其他变量

function defineReactive(target, key, value) {
  // 每一个数据都是一个被调查者
  const dep = new Dep();
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    // 履行 data.xxx 时 get 触发,进行依靠搜集,watcher 订阅 dep
    get() {
      if (activeWatcher) {
        // 订阅
        dep.addSubs(activeWatcher);
      }
      return value;
    },
    // 履行 data.xxx = xxx 时 set 触发,遍历订阅了该 dep 的 watchers,
    // 调用 watcher.updata 更新
    set(newValue) {
      // 假如前后值持平,没必要跟新
      if (value === newVal) {
        return;
      }
      value = newValue;
      // 派发更新
      dep.notify();
    },
  });
}

至此,呼应式体系就大功告成了!!

测验

咱们经过下面代码测验一下:

let data = {
  name: "张三",
  age: 18,
  address: "成都",
};
// 模仿 render
const render1 = () => {
  console.warn("-------------watcher1--------------");
  console.log("The name value is", data.name);
  console.log("The age value is", data.age);
  console.log("The address value is", data.address);
};
const render2 = () => {
  console.warn("-------------watcher2--------------");
  console.log("The name value is", data.name);
  console.log("The age value is", data.age);
};
// 先将 data 转换成呼应式
new Observer(data);
// 实例调查者
new Watcher(render1);
new Watcher(render2);

在浏览器中运转这段代码,和咱们希望的相同,两个render都履行了,并且在控制台上打印了结果。

Vue 呼应式完成原理浅显易懂

咱们尝试修正 data.name = '李四 23333333',测验两个 render 都会从头履行:

Vue 呼应式完成原理浅显易懂

咱们只修正 data.address = '北京',测验一下是否只有render 1回调都会从头履行:

Vue 呼应式完成原理浅显易懂

都完美经过测验!!

总结

Vue 呼应式完成原理浅显易懂

Vue呼应式原理的中心便是ObserverDepWatcher,三者一起构成 MVVM 中的 VM

Observer中进行数据呼应式处理以及终究的WatcherDep联系绑定,在数据被读的时分,触发get办法,将 Watcher搜集到 Dep中作为依靠;在数据被修正的时分,触发set办法,Dep就遍历 subs 中的订阅者,告诉Watcher更新。

本篇文章属于入门篇,并非源码完成,在源码的根底上简化了许多内容,能够便于了解ObserverDepWatcher三者的作用和联系。

本文的源码,以及作者学习 Vue 源码完好的逐行注释源码地址:github.com/yue1123/vue…

参阅内容

v2.cn.vuejs.org/

developer.mozilla.org/zh-CN/

refactoringguru.cn/design-patt…