跟我一起读源码丨Vue源码之依赖收集

阅读源码,个人觉得更多的收获是你从源码中提炼到了什么知识点,Vue的很多核心源码都十分精妙,让我们一起来关注它「依赖收集」的实现。

**tip:Vue版本:v2.6.12,浏览器:谷歌,阅读方式:在静态html 引用 Vue 包
进行断点阅读**

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

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

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 hello1
obj.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 `
${message}
`; } } var data = { message: 'hello~' }; render.call(data); //
hello~

这就是我们平时在