摸索着看vue源码 - 部分响应式原理

前言

阅读完该文章, 你不必定会掌握响应式原理, 但必定会有助于你掌握响应式原理, 源码这玩意儿若是光看看文章视频, 不本身亲手调试一下的话, 很难掌握.vue

准备工做

为了方便调试, 我这里调试的不是源码, 而是打包好后的vue/dist/vue.esm.js, 这样方便打日志, 也不用切不一样的文件. 因此准备工做就是用vue init webpack vuedemo初始化一个项目, 而后在main.js中初始化一些demo, 以下react

import Vue from 'vue'
/* eslint-disable no-new */
new Vue({
  el: '#app',
  data(){
    return {
      msg: '天气不错',
    }
  },
  methods: {
    click() {
      //一些逻辑
    }
  },
  template: `
  <div>
    <div>{{msg}}</div>
    <button @click='click'>按钮</button>
  </div>
  `
})

复制代码

ps: 1. 切记不要经过挂载App组件的方式调试, 直接用template, 若是挂载组件, 会多不少重复的日志, 很是不利于调试 2. 这里只是给你们一些调试的建议(由于一开始我挂了个APP, 调得我好麻烦), 下文中不会出现 日志 相关的内容webpack

再次前言

本文标题是 部分响应式原理, 响应式分三块: 侦听器(watch), computed(计算属性), render(模板渲染). 实现响应式的原理都是相同的, 只是在针对特性的业务逻辑上有些不一样. 本文就选render(模板渲染)的响应式原理具体展开. 接下来会以调用栈会主线, 进行原理的分析web

原理分析

为了缩减篇幅, 在一些代码截取上, 我会忽略报错的警示代码以及与响应式原理无关的逻辑代码, 经过...取代, 不过仍是建议你们看下原函数,帮助理解数组

  1. initmixin 入口函数
function initmixin(Vue) {
    Vue.prototype._init = function(options) {
        var vm  = this
        ...
        // 
        initState(vm)
        if (vm.$options.el) { 
            // 这行代码, 在第7步中'呼应1'会解释
            vm.$mount(vm.$options.el);
        }
    }
}
复制代码
  1. initState(vm), 入口函数, 逻辑很简单. 注意: 这里的opts.data不是咱们写的那个data(){return {}}方法, 而是一个name是mergedInstanceDataFn的通过包装的方法
var opts = vm.$options;
if (opts.data) {
    initData(vm);
} else {
    observe(vm._data = {}, true /* asRootData */);
}
复制代码
  1. initData(vm)
data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
// 这里获取的data, 若是用我给的demo就是{msg: '天气不错'}
var keys = Object.keys(data);
var i = keys.length;
  while (i--) {
    var key = keys[i];
    ...
    if (!isReserved(key)) {
      // isReserved 函数是用来判断 data中的属性是否已 $ 或者 _ 开头, 由于这俩开头的属性可能会和vue内置的属性, API冲突, 因此vue选择不代理他们
      // proxy 只是将data中的属性代理到vm实例上,这样就能够用this.xxx直接获取数据, 要实现响应式还得看下面的observe
      proxy(vm, "_data", key);
    }
  }
  ...
  //观察他们!
  observe(data, true /* asRootData */);
复制代码
  1. observer(value, asRootData)
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
  // 生成一个Observer实例
    ob = new Observer(value);
  }
复制代码
  1. class Observer 声明一个观察者类
if (Array.isArray(value)) {
  // 若是是数组, 则调用observeArray, 其最终仍是会走walk
    this.observeArray(value);
  } else {
    this.walk(value);
  }
复制代码
  1. Observer.prototype.walk
// 遍历每一个属性, 并执行defineReactive$$1
Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    defineReactive$$1(obj, keys[i]);
  }
};
复制代码
  1. defineReactive$$1(直译就是定义响应式), 这是很关键的一个函数, 这个函数中定义了每一个响应式属性的getter& setter, 并在getter中执行依赖收集, 在setter中执行派发更新. 看这个函数前, 我强烈建议读者打开源码一块儿往下走, 由于这里特别绕, 若是光看文章, 很难理解
// 这里进行了大幅的代码删减, 只为展现最直接的逻辑
function defineReactive$$1(obj, key, val, customSetter, shallow) {
    // Dep是一个依赖类, 看下面代码
    var dep = new Dep()
    Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    //标记①:
    get: function reactiveGetter () {
    // 注①
    // Dep.target 是一个Watcher实例(可理解为一个订阅者), 若是Dep.target 不为undefined, 则去收集依赖
      if (Dep.target) {//
        dep.depend();
      }
      return value
    },
    //标记②:
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val;
      dep.notify(); //派发更新
    }
  });
}
// Dep
var Dep = function Dep () {
  this.id = uid++; // id
  this.subs = [];  // 订阅者数组
};

复制代码

注①:bash

  1. Dep.target 何时被赋值, 它是个什么?

1.全局搜索Dep.target, 会有两个函数中对其进行了赋值, 1. pushTarget 2. popTarget 能够看出这是逻辑相反的两个函数, 咱们就看pushTarget
2. 全局搜索pushTarget, 会发现有5处地方调用了, 但只有Watcher.prototype.get中给他传参了, 由于传的是this, 因此很明显Dep.target是一个Watcher的实例(即订阅者)app

/**
 * Evaluate the getter, and re-collect dependencies.(翻译: 计算一个getter, 并从新收集依赖)
 */
Watcher.prototype.get = function get () {
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  } catch (e) {
    ...
  } finally {
    // "touch" every roperty so they are all tracked as
    // dependencies for deep watching
    // watch中 有deep:true 属性的 会进入traverse, 进行递归绑定, 这里咱们忽略递归绑定逻辑
    if (this.deep) {
      traverse(value);
    } 
    //这两步目前不用管
    popTarget();
    this.cleanupDeps();
  }
  return value
};
复制代码

3 找到了给Dep.target调用的地方, 也引入了一个Watcher的概念, 那系统是何时建立的Watcher实例的呢?全局搜索new Watcher你会发现3个地方用到了, 分别是在Vue.prototype.$watch(侦听器)中, initComputed(计算属性)中, 以及mountComponent(挂载组件)中(呼应1:mountComponent是在Vue.prototype.$mount中调用的),因此全部订阅者均来自于这三个地方.本要讲的也就是mountComponent时建立的订阅者. 因此若是已我本文开头是给的demo为例, 只会生成一个Watcher(解释一下: demo中我只订阅了msg一个依赖, 若是我多订阅几个依赖, 依旧是一个Watcher.而若是是侦听器或者计算属性,则会生成对应多个watcher)
4 那么Watcher.prototype.get是在何时调用的呢?在render Watcher(就是本文要讲的Watcher, 即模板渲染Watcher)中, 有两个地方调用了, 一个是在function Watcher的最下面,这个很明确, 意思就每新建一个Watcher实例, 必然会执行一次Watcher.prototype.get, 一个是在Watcher.prototype.run 中.这个根据调用栈 去倒推, 会发现是这样的调用栈: 1. 触发属性的set(标记②) => 2. dep.notify => 3.Watcher.prototype.update => 4. queueWatcher => 5. flushSchedulerQueue => 6. watcher.run()async

//function Watcher
...
  this.value = this.lazy
    ? undefined
    : this.get();  <= 这个就是调用```Watcher.prototype.get```
// Watcher.prototype.run
...
  if (this.active) {
    var value = this.get();<= 这个就是调用```Watcher.prototype.get```
复制代码

5 目前为止咱们知道了何时触发Watcher.prototype.get即(Dep.target何时是一个Watcher), 这个时候咱们还须要搞清楚何时触发属性的get(标记①).在Watcher.prototype.get函数

Watcher.prototype.get = function get () {
    ...
    try {
        console.log(this.getter)
        value = this.getter.call(vm, vm); <=这一行是触发了属性的get, 能够尝试注释它, 页面就会不渲染
    } catch (e) {
    ...
}
复制代码

log的getter

6 整理下触发订阅者进行依赖收集(呼应2:或者说依赖进行订阅者收集,后文会讲到)逻辑: 1. 执行模板渲染函数时, 若是有用到依赖属性, 则会触发依赖属性的get(好比我页面要渲染{{msg}}, 则会触发msg的get), 并执行依赖收集 2.当属性的值发生改变时, 会执行dep.notify通知视图层更新, 一样会触发依赖属性的get. 咱们知道了触发依赖收集的条件, 而后咱们研究下属性是如何执行依赖收集学习

//defineReactive$$1
function defineReactive$$1() {
    ...
    Object.defineProperty(obj, key, {
    ...
    get: function reactiveGetter () {
      if (Dep.target) {
        dep.depend();    <= 依赖收集入口
      }
      return value
    },
}
// depend
Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);    <= 注意这的Dep.target是一个Watcher
  }
};
// addDep
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);
    }
  }
};
// addSub
Dep.prototype.addSub = function addSub (sub) {
//呼应2: 依赖执行订阅者收集
  this.subs.push(sub);   
};

复制代码

7 收集完订阅者, 来看看如何通知订阅者完成派发更新

function defineReactive$$1(obj, key, val, customSetter, shallow) {
    ...
    Object.defineProperty(obj, key, {
    ...
    set: function reactiveSetter (newVal) {
      ...
      dep.notify(); //派发更新
    }
  });
}
// notify
Dep.prototype.notify = function notify () {
  var subs = this.subs.slice();
  if (process.env.NODE_ENV !== 'production' && !config.async) {
    // 对订阅者排序
    subs.sort(function (a, b) { return a.id - b.id; });
  }
  for (var i = 0, l = subs.length; i < l; i++) {
  // 呼应2: 挨个通知订阅者该更新了. 这也是为何为了便于理解,我偏向于叫订阅者收集,
  // 由于他派发更新的主逻辑是,依赖收集订阅者,而后依赖挨个通知订阅者
    subs[i].update();
  }
};

复制代码

8 subs[i].update()后还有一系列逻辑, 主要就是queueWatcher => flushSchedulerQueue => Watcher.prototype.run => Watcher.prototype.get => this.getter.call(vm, vm);执行渲染 并触发属性中get

以上就是实现响应式的一系列最简洁的逻辑

总结一下

  1. Dep类是一个依赖类, 有一个自增id属性和一个订阅者数组属性
  2. Watcher类是一个订阅者类, 有三种类型: 渲染Watcher, 侦听器Watcher, 计算属性Watcher.
  3. 触发依赖收集有两种逻辑,1. 每一个新建Watcher, 都会触发订阅者收集相关依赖 2. 当收到派发更新通知时, 会更新视图层, 并触发相关依赖的get, 并而后从新收集依赖(re-collect dep). 但本质都是触发渲染, 收集相关依赖

深刻一下

  1. Watcher.prototype.get中有一步是cleanupDeps()
Watcher.prototype.get = function get () {
  console.log('执行watcherget')
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    ...
  } catch (e) {
    ...
    ...
    this.cleanupDeps();  // 这里
  }
  return value
};
// 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;
};
复制代码

显然这一步是为了清洗依赖, 何时须要清洗依赖?

<template>
    // 手动的将v-if置为false时, 本来须要订阅的msg,就无需再订阅了, 这也是cleanupDeps的做用
    <div v-if=false>{{msg}}</div>
</template>
复制代码

最后

以上是我总结的响应式原理的最简化逻辑, 其实有不少须要拓展的分支, 但要经过文章媒介实在过于麻烦. 因此我以为想要学习源码的最好途径就是去debugger

相关文章
相关标签/搜索