Когда интервьюер спрашивает вас о принципе реактивности Vue, вы можете ответить ему так:

внешний интерфейс JavaScript Vue.js

Студенты, читавшие официальные документы vue, должны быть хорошо знакомы с этой картиной.

Как реализована отзывчивость vue?

Слышал слишком много ответов, черезObject.defineProperty, но когда его спросили подробно, другая сторона была совершенно не в курсе.

Уважайте в первую очередь

const Observer = function(data) {
  // 循环修改为每个属性添加get set
  for (let key in data) {
    defineReactive(data, key);
  }
}

const defineReactive = function(obj, key) {
  // 局部变量dep,用于get set内部调用
  const dep = new Dep();
  // 获取当前值
  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) {
      if (newVal === val) {
        return;
      }
      val = newVal;
      // 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
      // 这里每个需要更新通过什么断定?dep.subs
      dep.notify();
    }
  });
}

const observe = function(data) {
  return new Observer(data);
}

const Vue = function(options) {
  const self = this;
  // 将data赋值给this._data,源码这部分用的Proxy所以我们用最简单的方式临时实现
  if (options && typeof options.data === 'function') {
    this._data = options.data.apply(this);
  }
  // 挂载函数
  this.mount = function() {
    new Watcher(self, self.render);
  }
  // 渲染函数
  this.render = function() {
    with(self) {
      _data.text;
    }
  }
  // 监听this._data
  observe(this._data);  
}

const Watcher = function(vm, fn) {
  const self = this;
  this.vm = vm;
  // 将当前Dep.target指向自己
  Dep.target = this;
  // 向Dep方法添加当前Wathcer
  this.addDep = function(dep) {
    dep.addSub(self);
  }
  // 更新方法,用于触发vm._render
  this.update = function() {
    console.log('in watcher update');
    fn();
  }
  // 这里会首次调用vm._render,从而触发text的get
  // 从而将当前的Wathcer与Dep关联起来
  this.value = fn();
  // 这里清空了Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,
  // 造成代码死循环
  Dep.target = null;
}

const Dep = function() {
  const self = this;
  // 收集目标
  this.target = null;
  // 存储收集器中需要通知的Watcher
  this.subs = [];
  // 当有目标时,绑定Dep与Wathcer的关系
  this.depend = function() {
    if (Dep.target) {
      // 这里其实可以直接写self.addSub(Dep.target),
      // 没有这么写因为想还原源码的过程。
      Dep.target.addDep(self);
    }
  }
  // 为当前收集器添加Watcher
  this.addSub = function(watcher) {
    self.subs.push(watcher);
  }
  // 通知收集器中所的所有Wathcer,调用其update方法
  this.notify = function() {
    for (let i = 0; i < self.subs.length; i += 1) {
      self.subs[i].update();
    }
  }
}

const vue = new Vue({
  data() {
    return {
      text: 'hello world'
    };
  }
})

vue.mount(); // in get
vue._data.text = '123'; // in watcher update /n in get

Здесь мы реализуем простую отзывчивость vue с менее чем 100 строками кода. Конечно, если здесь не рассматривать процесс периода, я считаю, что это можно сделать в рамках 40 строк кода. Но я не хочу опускать это здесь, почему? Я боюсь, что вы автоматически проигнорируете процесс, и что, когда кто-то вас о чем-то спросит, вы это видели, но у вас не будет дара речи. Короче говоря, я пью больше горячей воды ради вас же.

Какова роль Депа?

Сборщик зависимостей, это не официальное название clam, я сделал его сам, просто на память.

Давайте рассмотрим роль сборщика зависимостей на двух примерах.

  • Пример 1, бессмысленный рендеринг не нужен?

    const vm = new Vue({
        data() {
            return {
                text: 'hello world',
                text2: 'hey',
            }
        }
    })
    

    когдаvm.text2Когда значение изменится, он будет вызван сноваrender,а такжеtemplateв, но не используетсяtext2, так что разберитесь с этимrenderЭто бессмысленно?

    Для этого примера вы помните, что мы смоделировали выше?VueизrenderВ функции мы вызываем значение, связанное с этим рендерингом, поэтому значение, не связанное с рендерингом, не сработает.get, он не будет добавлен в слушатель в сборщике зависимостей (addSubметод не сработает), даже если вызовsetназначать,notifyсерединаsubsтоже пусто. Хорошо, продолжайте возвращаться к демоверсии и приступайте к небольшой волне тестов, чтобы подтвердить то, что я сказал.

    const vue = new Vue({
      data() {
        return {
          text: 'hello world',
          text2: 'hey'
        };
      }
    })
    
    vue.mount(); // in get
    vue._data.text = '456'; // in watcher update /n in get
    vue._data.text2 = '123'; // nothing
    
  • Пример 2, когда несколько экземпляров Vue ссылаются на одни и те же данные, кто уведомляется? Разве оба не должны быть уведомлены?

    
    let commonData = {
      text: 'hello world'
    };
    
    const vm1 = new Vue({
      data() {
        return commonData;
      }
    })
    
    const vm2 = new Vue({
      data() {
        return commonData;
      }
    })
    
    vm1.mount(); // in get
    vm2.mount(); // in get
    commonData.text = 'hey' // 输出了两次 in watcher update /n in get
    

Я надеюсь, что благодаря этим двум примерам вы, вероятно, понялиDepВам кажется, что это так? Вот так. Подводя итог (следующий сборщик зависимостей на самом делеDep):

  • vueБудуdataинициализирован вObserverи для каждого значения в объекте переопределитеget,set,dataкаждый изkey, у всех есть отдельный сборщик зависимостей.
  • существуетget, добавил прослушиватель в сборщик зависимостей
  • При монтировании экземплярWatcher, наводя цель коллектора на токWatcher
  • существуетdataСрабатывает при изменении значенияset, который инициирует обновление всех прослушивателей в сборщике зависимостей для запускаWatcher.update

Если вы все еще не чувствуете себя удовлетворенным после прочтения, вы можете прочитать другие статьи автора.

"Рука об руку я проведу вас через исходный код Vue.

"Что vue делает с шаблоном