浏览源码,集体感觉更多的播种是你从源码中提炼到了什么知识点,Vue的很多外围源码都非常精妙,让咱们一起来关注它「依赖收集」的实现。

**tip:Vue版本:v2.6.12,浏览器:谷歌,浏览形式:在动态html 援用 Vue 包
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> 进行断点浏览**

文章篇幅有点长,泡杯咖啡,缓缓看 ~

我从「依赖收集」中学习到了什么?

1. 观察者模式

观察者模式的基本概念:

察看指标发生变化 -> notify[告诉] -> 观察者们 -> update[更新]

上面这段代码是 Vue 源码中通过运算的后果,能够让小伙伴们的脑袋瓜先有个简略的构造:

名词解释:
dep:depend[依赖],这里的“依赖”,咱们能够了解成 “察看指标” 。
subs:subscribers[订阅者],这里的“订阅者”等价“观察者”。
// 根底数据data: {  a: 1, // 关联 dep:id=0 的对象,a如果发生变化,this.a=3,调用 notify,  b: 2, // 关联 dep:id=1 的对象...  // ...}dep = {  id: 0,  // 告诉观察者们  notify() {    this.subs.forEach(item => {      item.update();    });  },  // 观察者们  subs: [    {      id: 1,      update() {        // 被指标者告诉,做点什么事      }    },    {      id: 2,      update() {        // 被指标者告诉,做点什么事      }    }  ]};dep = {  id: 1,  //...

2. defineProperty 对一级/多级对象进行拦挡

对于一级对象的拦挡置信小伙伴们都会啦。
这里论述一下对于多级对象设置拦截器的封装,看下这段代码:

const obj = { message: { str1: 'hello1', str2: 'hello2' } };function observer(obj) {  if (!(obj !== null && typeof obj === 'object')) {    return;  }  walk(obj);}function walk(obj) {  let keys = Object.keys(obj);  for (let i = 0; i < keys.length; i++) {    defineReactive(obj, keys[i]);  }}function defineReactive(obj, key) {  let val = obj[key];  observer(val);  Object.defineProperty(obj, key, {    enumerable: true,    configurable: true,    get() {      console.log('get :>> ', key, val);      return val;    },    set(newVal) {      console.log('set :>> ', key, newVal);      observer(newVal);      val = newVal;    }  });}observer(obj);

解释:observer 这个办法示意如果以后是一个对象,就会持续被遍历封装拦挡。

咱们对 obj 进行操作,看控制台的输入:

obj.message// get :>>  message { str1: "hello1", str2: "hello2"}/* 这个例子阐明了:不论是在 get/set str1,都会先触发 message 的 get*/obj.message.str1// get :>>  message { str1: "hello1", str2: "hello2" }// get :>>  str1 hello1obj.message.str1="123"// get :>>  message { str1: "123", str2: "hello2" }// set :>>  str1 123// 重点:obj.message={test: "test"}// set :>>  message { test: "test" }obj.message.test='test2'// get :>>  message { test: "test2" }// set :>>  test test2/* 有些小伙伴可能会有纳闷,这里进行 obj.message={test: "test"} 赋值一个新对象的话,不就无奈检测到属性的变动,为什么执行 obj.message.test='test2' 还会触发到 set 呢?返回到下面,在 defineReactive 办法拦截器 set 中,咱们做了这样一件事:set(newVal) {  // 这里调用 observer 办法从新遍历,如果以后是一个对象,就会持续被遍历封装拦挡  observer(newVal)  // ...}*/

延长到理论业务场景:「获取用户信息而后进行展现」。我在 data 设置了一个 userInfo: {},ajax 获取到后果进行赋值 this.userInfo = { id: 1, name: 'refined' },就能够显示到模板 {{ userInfo.name }},之后再进行 this.userInfo.name = "xxx",也会进行响应式渲染了。

3. defineProperty 对数组的拦挡丨Object.create 原型式继承丨原型链丨AOP

咱们都晓得 defineProperty 只能拦挡对象,对于数组的拦挡 Vue 有奇妙的扩大:

var arrayProto = Array.prototype;var arrayMethods = Object.create(arrayProto);var methodsToPatch = [  'push',  'pop',  'shift',  'unshift',  'splice',  'sort',  'reverse'];methodsToPatch.forEach(function (method) {  var original = arrayProto[method];  Object.defineProperty(arrayMethods, method, {    enumerable: true,    configurable: true,    value: function mutator(...args) {      console.log('set and do something...');      var result = original.apply(this, args);      return result;    }  });});function protoAugment(target, src) {  target.__proto__ = src;}var arr = [1, 2, 3];protoAugment(arr, arrayMethods);arr.push(4)// set and do something...

解释:Object.create(arrayProto); 为原型式继承,即 arrayMethods.__proto__ === Array.prototype === true ,所以当初的 arrayMethods 就能够用数组的所有办法。

代码中的 target.__proto__ = src,即 arr.__proto__ = arrayMethods,咱们曾经对 arrayMethods 本人定义了几个办法了,如 push。

当初咱们进行 arr.push,就能够调用到 arrayMethods 自定义的 push 了,外部还是有调用了 Array.prototype.push 原生办法。这样咱们就实现了一个拦挡,就能够检测到数组内容的批改。

原型链机制:Array.prototype 自身是有 push 办法的,但原型链的机制就是,arr 通过 __proto__ 找到了 arrayMethods.push,曾经找到了,就不会往下进行找了。

能够留神到,封装的这几个办法 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse',都是波及到数组内容会被扭转的,那如果我要调用 arr.map 办法呢?还是刚刚讲的 原型链 机制,arrayMethods 没有 map 办法,就持续顺着 __proto__ 往下找,而后找到 Array.prototype.map

不得不说,这个数组的扩大封装,能够学习到很多,赞赞赞 ~

下面讲的例子都是对一个数组内容的扭转。细节的小伙伴会发现,如果我对整个数组进行赋值呢,如:arr = [4,5,6],拦挡不到吧,是的。其实我只是把这个例子和下面第二点的例子拆分进去了。咱们只须要对下面 observer 办法,进行这样一个判断,即

function observer(value) {  if (!(value !== null && typeof value === 'object')) {    return;  }  if (Array.isArray(value)) {    protoAugment(value, arrayMethods);  } else {    walk(value);  }}

多级对象和数组的拦挡概念其实很像,只是对象只须要逐级遍历封装拦截器,而数组须要用AOP的思维来封装。

4. 微工作(microtask)的妙用丨event loop

间接来一手例子:

var waiting = false;function queue(val) {  console.log(val);  nextTick();}function nextTick() {  if (!waiting) {    waiting = true;    Promise.resolve().then(() => {      console.log('The queue is over, do something...');    });  }}queue(1);queue(2);queue(3);// 1// 2// 3// The queue is over, do something...

解释:主程序办法执行结束之后,才会执行 promise 微工作。这也能够解释,为什么 Vue 更新动作是异步的【即:咱们没方法立刻操作 dom 】,因为这样做能够进步渲染性能,前面会具体讲这块。

5. 闭包的妙用

这里也间接来一手例子,集体认为这个闭包用法是成就了依赖收集的要害 ~

var id = 0;var Dep = function () {  this.id = id++;};Dep.prototype.notify = function notify() {  console.log('id :>> ', this.id, ',告诉依赖我的观察者们');};function defineReactive(obj, key) {  var dep = new Dep();  Object.defineProperty(obj, key, {    enumerable: true,    configurable: true,    get() {},    set() {      dep.notify();    }  });}var obj = { str1: 'hello1', str2: 'hello2' };defineReactive(obj, 'str1');defineReactive(obj, 'str2');obj.str1 = 'hello1-change';obj.str2 = 'hello2-change';// id :>>  0 ,告诉依赖我的观察者们// id :>>  1 ,告诉依赖我的观察者们

这也是第一点讲到的关联 dep 对象,当初每个属性都能够拜访到词法作用域的属于本人的 dep 对象,这就是闭包。

6. with 扭转作用域

这里只是模仿一下 Vue 的渲染函数

function render() {  with (this) {    return `<div>${message}</div>`;  }}var data = { message: 'hello~' };render.call(data);// <div>hello~</div>

这就是咱们平时在 <template> 中不必写 {{ this.message }} 的起因,而是如:

<template>  <div> {{ message }} </<div></template>

下面这 6 点是集体感觉有学习到货色的中央,当然要深刻了解依赖收集,咱们须要走一遍流程。如果你以后在电脑前,我会通知你须要打第几行的断点,让咱们一起读源码吧,go go go ~

深刻源码

tip:为了浏览品质,我会把一些绝对与流程无关的代码省略掉,代码中相似「✅ :123」,示意须要打的断点,谷歌浏览器上开启调试 ctrl + o ,输出 :123 即可跳转至 123 行。

本地新建 index.html,引入 Vue 包,关上浏览器浏览

<body>  <div id="app">    <div>{{message }}</div>    <button @click="handleClick">change</button>  </div>  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>  <script>    var app = new Vue({      el: '#app',      data: {        message: 'hello world'      },      methods: {        handleClick() {          this.message = 'hello world 2';        }      }    });  </script></body>

断点 ✅ :4700 initData办法

顾名思义,初始化咱们写的 data 数据并做一些操作,在这个办法里有两个办法值得咱们关注,proxy(vm, "_data", key);observe(data, true);

function initData (vm) {  var data = vm.$options.data;  data = vm._data = typeof data === 'function'    ? getData(data, vm)    : data || {};  var keys = Object.keys(data);  var i = keys.length;  while (i--) {    var key = keys[i];✅ :4734  proxy(vm, "_data", key);  }✅ :4738  observe(data, true);}

tip:在遇到办法的时候,咱们用步入的形式能够疾速定位到办法,如图:

步入到 proxy 办法

function proxy (target, sourceKey, key) {   sharedPropertyDefinition.get = function proxyGetter () {    return this[sourceKey][key]  };  sharedPropertyDefinition.set = function proxySetter (val) {    this[sourceKey][key] = val;  };  Object.defineProperty(target, key, sharedPropertyDefinition);✅ :4633  }

剖析:这个办法是在 while 里的,这里循环遍历了咱们写在 data 上的对象。以后 target = vm ,key = message,走到这个 4633 断点,控制台打印 target,如图:

下面咱们提到了一个 with 的例子:Vue会进行 render.call(vm)。这样子咱们就会触发到 message 的 get 办法,这是一个入口,后续会做一系列的操作。

步入到 observe 办法

function observe (value, asRootData) {  if (!isObject(value)) {    return  }  var ob;✅ :4633  ob = new Observer(value);}

剖析:能够了解成这个办法开始正在对 data 上的 可察看数据 进行察看的一些提前准备,如:往属性上附加 get/set 拦截器,而后别离在 get/set 里做点什么...

步入到 new Observer [可观测类]

这是第一个外围类,接下来咱们还会别离讲到其余的两个类,每个类都是外围

var Observer = function Observer (value) {  this.dep = new Dep();  if (Array.isArray(value)) {    protoAugment(value, arrayMethods);  } else {✅ :935  this.walk(value);  }};

剖析:这里有个数组 or 对象的判断,对于数组拦挡咱们在下面曾经有讲过了,咱们当初关注 walk 办法。

步入到 walk 办法

Observer.prototype.walk = function walk (obj) {  var keys = Object.keys(obj);  for (var i = 0; i < keys.length; i++) {✅ :947  defineReactive$$1(obj, keys[i]);  }};  

持续步入到 defineReactive$$1 办法

function defineReactive$$1 (  obj, // obj -> data  key, // key -> 'message'  val) {✅ :1021  var dep = new Dep();   val = obj[key];   var childOb = observe(val);  Object.defineProperty(obj, key, {    enumerable: true,    configurable: true,    get: function reactiveGetter () {      var value = val;✅ :1041  if (Dep.target) {        dep.depend();        if (childOb) {          childOb.dep.depend();          if (Array.isArray(value)) {            dependArray(value);          }        }      }      return value    },    set: function reactiveSetter (newVal) {      var value = val;      if (newVal === value || (newVal !== newVal && value !== value)) {        return      }      val = newVal;      childOb = observe(newVal);✅ :1070 dep.notify();    }  });}

剖析:这个办法能够说是依赖收集中的外围,通过 get 办法增加依赖,通过 set 办法告诉观察者。咱们下面讲到的 proxy 办法,能够把它当做第一层拦截器,当咱们触发一级拦截器之后,就会到二级拦截器 defineReactive$$1 里定义的 get/set 办法。

new Dep() [察看指标类] 这个是第二个外围类。

还记得咱们在下面说过这个办法是一个 “闭包” 吗?是的,在以后办法外部 Object.defineProperty(obj, key, { 以上的所有变量/办法,是各个属性各自独立领有的。

至此,咱们对于 data 上属性的 get/set 封装 就讲完了 。

如何对数据进行依赖收集?

断点 ✅ :4074

updateComponent = function () {✅ :4067  vm._update(vm._render(), hydrating);};✅ :4074 new Watcher(vm, updateComponent);

剖析:Watcher类,这个是第三个外围类,观察者类。和下面说的 Observer[可观擦类]、Dep[察看指标类],总共三个。这个代码片段是在 mounted 钩子之前调用的,也就是咱们之前对 data 数据先进行了 get/set 封装之后,就要开始进行 render 了,在 render 之前,须要创立 render 观察者,为了不便咱们这里叫它 renderWatcher。除了 renderWatcher,咱们还有 computedWatcherwatchWatcher,这两个别离是 计算属性侦听器 观察者,在 Vue 中次要是这三个类型的观察者。

步入到 new Watcher [观察者类]

var Watcher = function Watcher (  vm,  expOrFn) {  this.getter = expOrFn;  this.deps = [];  this.newDeps = [];  this.depIds = new Set();  this.newDepIds = new Set();✅ :4467  this.get();};

剖析:

  • deps:缓存每次执行观察者函数时所用到的dep所有实例。
  • depIds:缓存每次执行观察者函数时所用到的dep所有实例 id,用于判断。
  • newDeps:存储本次执行观察者函数时所用到的dep所有实例。
  • newDepIds:存储本次执行观察者函数时所用到的dep所有实例 id,用于判断。

步入到 get 办法

Watcher.prototype.get = function get () {✅ :4474  pushTarget(this);  var vm = this.vm;✅ :4478  this.getter.call(vm, vm);✅ :4491  popTarget();✅ :4492  this.cleanupDeps();};

剖析【这段剖析比拟具体】:pushTarget 和 popTarget 是一对办法,别离用来记录以后的观察者,和剔除以后观察者

Dep.target = null;var targetStack = [];function pushTarget (target) {  targetStack.push(target);  Dep.target = target;}function popTarget () {  targetStack.pop();  Dep.target = targetStack[targetStack.length - 1];}

Dep.target 为全局惟一的,因为在一个时刻内,就只会有一个观察者函数在执行,把以后的 观察者实例 赋值给 Dep.target, 后续只有拜访 Dep.target 就能晓得以后的观察者是谁了。

咱们持续步入 this.getter.call(vm, vm),【以下这几个步入咱们就简略过一下】

updateComponent = function () {✅ :4067  vm._update(vm._render(), hydrating);};

步入 vm._update(vm._render(), hydrating)

Vue.prototype._render = function () {✅ :3551  vnode = render.call(vm._renderProxy, vm.$createElement);};

步入 render.call(vm._renderProxy, vm.$createElement),在谷歌会新关上一个 tab 用来执行上面这个函数

(function anonymous() {with(this){return _c('div',{attrs:{"id":"app"}},[_c('div',[_v(_s(message))]),_v(" "),_c('button',{on:{"click":handleClick}},[_v("change")])])}})

要害局部来了,这个是 Vue 的渲染函数。咱们当初只有关注,它这里是会读取到 this.message 的,所以会触发 message 的 get 办法,也就是说以后观察者 renderWatcher 依赖了 message ,所以就会开始对它进行 “收集”。

谷歌浏览器器,间接点击下一步「 ||> 」,

咱们就能够看到光标跳到了 defineReactive$$1 办法外部咱们的 get 办法,开始进行“依赖收集” 了

    get: function reactiveGetter () {      var value = val;✅ :1041  if (Dep.target) {        dep.depend();        if (childOb) {          childOb.dep.depend();          if (Array.isArray(value)) {            dependArray(value);          }        }      }      return value    },

以后的 Dep.target 是有值的,所以执行 dep.depend 开始进行依赖,
步入 dep.depend

Dep.protJavaScriptotype.depend = function depend () {  if (Dep.target) {✅ :731  Dep.target.addDep(this);  }  };

步入 Dep.target.addDep(this)

Watcher.prototype.addDep = function addDep (dep) {  var id = dep.id;  if (!this.newDepIds.has(id)) {    this.newDepIds.add(id);    this.newDeps.push(dep);    if (!this.depIds.has(id)) {      dep.addSub(this);    }  }✅ :4059  };

dep.addSub(this) 把以后的 watcher 实例 push 到 subs 数组,并且判断如果以后 观察者 被 察看指标 增加到 subs 数组里,就不会持续增加,过滤反复数据。
走到这个 4059 断点,控制台打印 dep,如:

dep = {  id:3,  subs:[    renderWatcher 实例  ]}

跳出持续往下走会调用 4491 popTarget() ,剔除以后 观察者。

接着步入 this.cleanupDeps()

Watcher.prototype.cleanupDeps = function cleanupDeps () {  var i = this.deps.length;  while (i--) {    var dep = this.deps[i];    if (!this.newDepIds.has(dep.id)) {      dep.removeSub(this);    }  }  var tmp = this.depIds;  this.depIds = this.newDepIds;  this.newDepIds = tmp;  this.newDepIds.clear();  tmp = this.deps;  this.deps = this.newDeps;  this.newDeps = tmp;  this.newDeps.length = 0;};

这里把 this.deps = this.newDeps,缓存到 deps 里,而后清空newDeps,来做下一次的收集。

至此,咱们就实现了一个 依赖收集 ~

更新依赖数据如何 notify 观察者做出 update ?

官网:只有侦听到数据变动,Vue 将开启一个队列,并缓冲在同一事件循环中产生的所有数据变更。如果同一个 watcher 被屡次触发,只会被推入到队列中一次。这种在缓冲时去除反复数据对于防止不必要的计算和 DOM 操作是十分重要的。而后,在下一个的事件循环“tick”中,Vue 刷新队列并执行理论 (已去重的) 工作。

当用户点击change按钮

this.message = 'hello world 2';

光标主动跳转至 message 对应的 set 办法,执行 dep.notify() 进行告诉观察者进行 update 动作

Dep.prototype.notify = function notify () {  for (var i = 0, l = subs.length; i < l; i++) {✅ :745  subs[i].update();  }};

步入 subs[i].update()

Watcher.prototype.update = function update () {✅ :4543  queueWatcher(this);};

步入 queueWatcher()

function queueWatcher (watcher) {  var id = watcher.id;  if (has[id] == null) {    has[id] = true;    if (!flushing) {      queue.push(watcher);    } else {      var i = queue.length - 1;      while (i > index && queue[i].id > watcher.id) {        i--;      }      queue.splice(i + 1, 0, watcher);    }    if (!waiting) {      waiting = true;✅ :4403  nextTick(flushSchedulerQueue);    }  }}

flushSchedulerQueue 办法

function flushSchedulerQueue () {  flushing = true;  var watcher, id;   queue.sort(function (a, b) { return a.id - b.id; });  for (index = 0; index < queue.length; index++) {    watcher = queue[index];    id = watcher.id;    has[id] = null;✅ :4311  watcher.run();  }}

剖析[联合下面 queueWatcherflushSchedulerQueue 两个办法]:

flushSchedulerQueue 办法:

queue.sort 须要排序是起因:

确保 watcher 的更新程序与它们被创立的程序统一。

  1. 对于父子组件来说,组件的创立程序是父组件先被创立,而后子组件再被创立,所以父组件的renderWatcher的id是小于子组件的。
  2. 对于用户自定义watcher【watchWatcher】和 renderWatcher,用户自定义watcher是先于组件的renderWatcher被创立的。
  3. 如果子组件在父组件的监督程序运行期间被销毁,则会跳过子组件的watcher。

queueWatcher 办法:

  1. 这里进行了 watcher id 的反复判断,因为在一个 renderWatch 中可能会依赖多个察看指标,当咱们同时扭转多个依赖的值 ,通过判断 watcher.id 一样就不必把两次更新 push 到 队列,防止渲染性能耗费,如:
this.message1 = 'hello world 1';this.message2 = 'hello world 2';// 更多...      

或 循环扭转同一个依赖

for (let i = 0; i < 10; i++) {  this.message++;}
  1. flushing 示意 queue 队列的更新状态,flushing=true 代表队列正在更新中。

这里的 else 分支,次要是判断一种边界状况,
i--,从后往前遍历,其实目标是看刚进入的这个 watcher 在不在以后更新队列中。留神这里的 index 是来自 flushSchedulerQueue 办法外部定义的,是全局的。
咱们能够看到跳出 while 的条件为:

  • queue[i].id === watcher.id

咱们能够这样了解,以后在更新一个 id 为 3 的 watcher,而后又进来了一个 watcher,id 也为3。相当于须要从新更新一次 id 为 3 的 watcher,这样能力获取到最新值保障视图渲染正确。用代码解释如:

// ...<div>{{ message }}</div>// ...new Vue({  el: '#app',  data: {    message: 'hello world'  },  watch: {    message() {      this.message = 'hello world 3';    }  },  methods: {    handleClick() {      this.message = 'hello world 2';    }  }});

点击按钮更新 message 之后,又用 watch 监听其变动,而后在外部再对 message 进行更新,咱们试着读一下这段代码的更新流程。首先,用户自定义watcher【watchWatcher】是先于 renderWatcher 被创立的,所以咱们在更新 message 的时候,会先执行 watch ,触发到外部办法又更新了一次 message,为了保障视图渲染正确,咱们须要在执行一次这个 watcher 的 update。

  • queue[i].id < watcher.id

剖析:更新队列中有 id 为1,2,5 三个 watcher,以后正在更新id为 2 的watcher,当 queueWatcher 被调用并传进来一个 id 为 3 的watcher,于是就将这个 watcher 放到 2 的前面,确保 watcher 的更新程序与它们被创立的程序统一。

咱们都晓得,flushSchedulerQueue 办法是一个微工作。在对queue操作之后,主程序办法执行结束之后,开始执行微工作,进行 queue 的调度更新,watcher.run()

至此,咱们就实现了当察看指标扭转时告诉观察者更新的动作。

总结

以上举的例子是一个简略 renderWatcher 的一个流程闭环,依赖收集告诉更新。Vue 有renderWatcher【视图观察者】,computedWatcher【计算属性观察者】 和 watchWatcher【侦听器观察者】,次要这三个类型的观察者。

次要的三个类 Dep【察看指标类】,Observe【可观测类】,Watcher【观察者类】。

咱们能够了解,在依赖被扭转的时候告诉观察者的一过程,一切都是为了视图渲染,在这过程中会进行一些性能优化 / 解决一些边界状况,最终保障视图渲染的完整性。

集体感觉源码有点艰涩难懂,但还是得本人多过几遍能力相熟。这边还是倡议亲自浏览几遍源码,看一些别人的总结还是会有点含糊,所以本篇文章提供了 断点 参考。帮忙小伙伴疾速定位源码比拟精华的地位。

理解源码的运作,也能够让咱们更加的晓得,咱们须要怎么去调用框架提供的 api 会更加优化。

后话

Vue3 曾经进去了,咱们看完 Vue2 就能够比照看看 Vue3 的更弱小之处了,
这边就不再举例 computedWatcherwatchWatcher 了,小伙伴们能够入手 debug 看看 ~ 。

能够从页面的 initState 办法作为入口:

function initState (vm) {  vm._watchers = [];  var opts = vm.$options;  if (opts.props) { initProps(vm, opts.props); }  if (opts.methods) { initMethods(vm, opts.methods); }  if (opts.data) {    initData(vm);  } else {    observe(vm._data = {}, true /* asRootData */);  }✅ :4645  if (opts.computed) { initComputed(vm, opts.computed); }  if (opts.watch && opts.watch !== nativeWatch) {✅ :4647  initWatch(vm, opts.watch);  }}

感兴趣的小伙伴也能够 debug 看 computed 这种场景

computed: {  c1() {    return this.c2 + 'xxx';  },  c2() {    return this.message + 'xxx';  }}

computed 是 “lazy” 的,它不参加 queue 的更新,而是如果在模板上有用到 computed 属性,才会去进行获取计算后的值。