vue响应式详解(重学前端-vue篇1)

27,397 阅读16分钟

1 什么是Vue响应式呢

数据发生变化后,会重新对页面渲染,这就是Vue响应式

响应式图例

响应式图例

2 想完成这个过程,我们需要做些什么

  • 侦测数据的变化
  • 收集视图依赖了哪些数据
  • 数据变化时,自动“通知”需要更新的视图部分,并进行更新

「它们对应专业俗语分别是:」

  • 数据劫持 / 数据代理
  • 依赖收集
  • 发布订阅模式

3.什么是发布订阅模式

  • 观察者模式是由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的
  • 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

image.png

vue响应式采用的就是发布订阅模式

举个例子说明:

image.png

4. 如何侦测数据的变化

有两种办法可以侦测到变化:
使用Object.defineProperty和ES6的Proxy,这就是进行数据劫持或数据代理。

3.1 Object.defineProperty实现

Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。

代码如下:

function render () {
//set的时候会走这里,重新渲染
  console.log('模拟视图渲染')
}
let data = {
  name: '浪里行舟',
  location: { x: 100, y: 100 }
}
// 请看下一节
observe(data)

定义核心函数

function observe (obj) { // 我们来用它使对象变成可观察的
  /**
      判断类型,obj可能是vue中的data对象,也可以是data中的某个属性值,
      例如:data:{ name: '浪里行舟', location:{  x: 100, y: 100 } },这里的值可能是整个data,
      也可能是location的值,当location值也是对象的时候就需要再次监听location值对象。
      而属性name和location在监听data的时候就会监听这2个属性。
  */
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
  
  //监听对象的每个属性
  function defineReactive (obj, key, value) {
    // 递归子属性
    observe(value)
    Object.defineProperty(obj, key, {
      enumerable: true, //可枚举(可以遍历)
      configurable: true, //可配置(比如可以删除)
      get: function reactiveGetter () {
        console.log('get', value) // 监听
        return value
      },
      set: function reactiveSetter (newVal) {
        observe(newVal) //如果赋值是一个对象,也要递归子属性
        //如果有新值
        if (newVal !== value) {
          console.log('set', newVal) // 监听
          render() //执行渲染逻辑
          value = newVal
        }
      }
    })
  }
}

改变data的属性,会触发set;然后获取data的属性,会触发get

data.location = {
  x: 1000,
  y: 1000
}         //打印=》     set {x: 1000,y: 1000} 染
data.name //打印=》   get 浪里行舟

上面这段代码的主要作用在于:

observe这个函数传入一个 obj(需要被追踪变化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理,给每个属性加上setget方法,以此来达到实现侦测对象变化。值得注意的是,observe 会进行递归调用。

那我们如何侦测Vue中data 中的数据,其实也很简单:

class Vue {
    /* Vue构造类 */
    constructor(options) {
        this._data = options.data;
        observer(this._data);
    }
}

这样我们只要 new 一个 Vue 对象,就会将 data 中的数据进行追踪变化。

但是我们发现一个问题,上面的代码无法检测到对象属性的添加或删除(如data.location.a=1,增加一个a属性)。
这是因为 Vue 通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性。如果是删除属性,我们可以用vm.$delete实现,那如果是新增属性,该怎么办呢?

  1. 可以使用 Vue.set(location, a, 1) 方法向嵌套对象添加响应式属性;
  2. 也可以给这个对象重新赋值,比如data.location = {...data.location,a:1}

Object.defineProperty 不能监听数组的变化,需要进行数组方法的重写

4 观察者 Watcher

4.1 为什么引入Watcher

Vue 中定义一个 Watcher 类来表示观察订阅依赖。至于为啥引入Watcher,《深入浅出vue.js》给出了很好的解释:

属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。

「依赖收集的目的是:」 将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。形成如下所示的这样一个关系(图参考《剖析 Vue.js 内部运行机制》)。

5.2 Watcher的简单实现

class Watcher {
  constructor(obj, key, cb) {
    // 将 Dep.target 指向自己
    // 然后触发属性的 getter 添加监听
    // 最后将 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 获得新值
    this.value = this.obj[this.key]
   // 我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图
    this.cb(this.value)
  }
}

以上就是 Watcher 的简单实现,在执行构造函数的时候将 Dep.target 指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher ,然后执行 update 函数。

「依赖的本质:」
所谓的依赖,其实就是Watcher
至于如何收集依赖,总结起来就一句话:
getter中收集依赖(收集Watch当如Dep中),在setter中触发依赖。先收集依赖,即把用到该数据的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就行了。

具体来说,当外界通过Watcher读取数据时,便会触发getter从而将Watcher添加到依赖中,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。

最后我们对 defineReactive 函数进行改造,在自定义函数中添加依赖收集和派发更新相关的代码,实现了一个简易的数据响应式:

function observe (obj) {
  // 判断类型
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
  function defineReactive (obj, key, value) {
    observe(value)  // 递归子属性
    let dp = new Dep() //新增
    Object.defineProperty(obj, key, {
      enumerable: true, //可枚举(可以遍历)
      configurable: true, //可配置(比如可以删除)
      get: function reactiveGetter () {
        console.log('get', value) // 监听
     // 将 Watcher 添加到订阅
     // target挂载发生在new Watch的时候
       if (Dep.target) {
         // 新增Watch到dep的subs中,就是说new了哪个Watch就要把哪个Watch收集进来
         dp.addSub(Dep.target) 
       }
        return value
      },
      set: function reactiveSetter (newVal) {
        // 如果赋值是一个对象,也要递归子属性
        // 例如上面的location是一个对象,那么这个对象是要继续observe的
        observe(newVal) 
        if (newVal !== value) {
          console.log('set', newVal) // 监听
          render()
          value = newVal
         // 执行 watcher 的 update 方法
          dp.notify() //新增
        }
      }
    })
  }
}

class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data);
        /* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
        new Watcher();
        console.log('模拟视图渲染');
    }
}

当 render function 被渲染的时候,读取所需对象的值,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。之后如果修改对象的值,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图。

「完整流程图:」

1460000013338807.webp

  • 在 new Vue() 后, Vue 会调用_init 函数进行初始化,也就是init 过程,在 这个过程Data通过Observer转换成了getter/setter的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行getter 函数,而在当被赋值的时候会执行 setter函数。
  • 当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。
  • 在修改对象的值的时候,会触发对应的setter, setter通知之前依赖收集得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher就会开始调用 update 来更新视图。

5. 收集依赖

5.1 为什么要收集依赖

我们之所以要观察数据,其目的在于当数据的属性发生变化时,可以通知那些曾经使用了该数据的地方。比如例子中,模板中使用了location 数据,当它发生变化时,要向使用了它的地方发送通知。

let globalData = {
    text: '浪里行舟'
};
let test1 = new Vue({
    template:
        `<div>
            <span>{{text}}</span> 
        <div>`,
    data: globalData
});
let test2 = new Vue({
    template:
        `<div>
            <span>{{text}}</span> 
        <div>`,
    data: globalData
});

如果我们执行下面这条语句:

globalData.text = '前端工匠';

此时我们需要通知 test1 以及 test2 这两个Vue实例进行视图的更新,我们只有通过收集依赖才能知道哪些地方依赖我的数据,以及数据更新时派发更新。那依赖收集是如何实现的?其中的核心思想就是“事件发布订阅模式”。接下来我们先介绍两个重要角色-- 订阅者 Dep观察者 Watcher ,然后阐述收集依赖的如何实现的。

6 Dep,Watcher的关系

Observer负责将数据转换成getter/setter形式; Dep负责管理数据的依赖列表;是一个发布订阅模式,上游对接Observer,下游对接Watcher Watcher是实际上的数据依赖,负责将数据的变化转发到外界(渲染、回调); 首先将data传入Observer转成getter/setter形式;当Watcher实例读取数据时,会触发getter,被收集到Dep仓库中;当数据更新时,触发setter,通知Dep仓库中的所有Watcher实例更新,Watcher实例负责通知外界

  • Dep 负责收集所有相关的的订阅者 Watcher ,具体谁不用管,具体有多少也不用管,只需要根据 target 指向的计算去收集订阅其消息的 Watcher 即可,然后做好消息发布 notify 即可。
  • Watcher 负责订阅 Dep ,并在订阅的时候让 Dep 进行收集,接收到 Dep 发布的消息时,做好其 update 操作即可。

Vue的操作就是加入了发布订阅模式,结合Object.defineProperty的劫持能力,实现了可用性很高的双向绑定。

7. 订阅器 Dep

image.png

「为什么引入 Dep:」
收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖删除依赖向依赖发送消息等。
于是我们先来实现一个订阅者 Dep 类,用于解耦属性的依赖收集和派发更新操作,「说得具体点」:它的主要作用是用来存放 Watcher 观察者对象。我们可以把Watcher理解成一个中介的角色,数据发生变化时通知它,然后它再通知其他地方

「Dep的简单实现:」

class Dep {
    constructor () {
        /* 用来存放Watcher对象的数组 */
        this.subs = [];
    }
    /* 在subs中添加一个Watcher对象 */
    addSub (sub) {
        this.subs.push(sub);
    }
    /* 通知所有Watcher对象更新视图 */
    notify () {
        this.subs.forEach((sub) => {
            sub.update();
        })
    }
}

以上代码主要做两件事情:

  • 用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
  • 用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。 所以当需要依赖收集的时候调用 addSub,当需要派发更新的时候调用 notify。

也就说,所有的Watch最终会存放到Dep的subs中,并且视图更新的流程是,Dep触发了subs中的每个Watch,执行了Watch更新逻辑,Dep的操作是绕不过Watch的,这就验证了上面说的,Watch是一个中介的角色。

调用也很简单:

let dp = new Dep()
dp.addSub(() => {//依赖收集的时候
    console.log('emit here')
})
dp.notify()//派发更新的时候

8.实现

8.1 先实现new Vue的数据劫持

//遍历data,进行数据劫持
function Observer(data) {
  Object.keys(data).forEach((item) => {
    defineReactive(data, item, data[item])
  })
}

function defineReactive(data, key, val) {
  const self = this
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get() {
      return val
    },
    set(newVal) {
      if (newVal === val) {
        return
      }
      val = newVal
    },
  })
}

class Vue {
  constructor(options) {
    if (options && typeof options.data == 'function') {
    //模拟vue源码,挂载data到Vue的_data上
      this._data = options.data.apply(this)
    }
    // 劫持传入的data项
    new Observer(this._data)
  }
}

const vue = new Vue({
  data() {
    return {
      text: 'hello',
    }
  },
})
console.log(vue)//data中属性已经被劫持了

8.2 劫持以后,vue.mount()关联Watch

//遍历data,进行数据拦截
function Observer(data) {
  Object.keys(data).forEach((item) => {
    defineReactive(data, item, data[item])
  })
}

function defineReactive(data, key, val) {
  const self = this
  Object.defineProperty(data, key, {
    enumerable: true,
    // 设置当前描述属性可被修改
    configurable: true,
    get() {
      return val
    },
    set(newVal) {
      if (newVal === val) {
        return
      }
      val = newVal
    },
  })
}

class Watcher {
  //----------4
  constructor(vm, fn) {
    this.vm = vm //----------5
    fn() //----------6
  }
}

class Vue {
  constructor(options) {
    if (options && typeof options.data == 'function') {
      this._data = options.data.apply(this)
    }
    new Observer(this._data)
  }
  mount = () => {
    //--------------2
    new Watcher(this, this.render) //--------------3
  }
  render = () => {
    // 省略一系列的渲染逻辑
    return this._data.text //---------7
  }
}
const vue = new Vue({
  data() {
    return {
      text: 'hello',
    }
  },
})
vue.mount() //------------ 1


Vue mount函数的功能是去读取data中的属性渲染到页面上。这里的关键一步是读数据

那么,读数据最终是要进入数据劫持get方法的。这时我们可以创建一个Watch(观察者)作为中介,让它去进入get。

而mount起到的作用是渲染,自然,在构造Watch的时候需要传入渲染函数。

回到前面我们说的,Watch充当的是中介角色,那么,它是谁和谁之前的中介?实际上它是Observer和我们后面要讲的Dep的中介。

既然是中介,那么它就需要拿到双方的数据。又因为Observer需要观察data,所以这里我们需要将整个Vue构造函数挂载到Watch自身。

整理下流程:

执行vue.mount()渲染页面,需要在Vue定义渲染函数render,然后在mount通过中介Watch去执行定义在Vue的render函数。

创建中介Watch,执行传入的渲染函数,渲染肯定是需要根据页面绑定了哪些data属性去读取data的,这个时候就会触发get方法,

进入get拿到对应的值。

8.3 添加dep后大概结构

  //defineReactive是对Observer的抽离
  const defineReactive = function(obj, key) {
    // 以下代码省略
  }
  
  const Vue = function(options) {
    console.log("Vue",this)
    //打印1  Vue {
                  _data:{
                      text: "123"
                      get text: ƒ get()
                      set text: ƒ set(newVal)
                    },
                  mount: ƒ (),
                  render: ƒ ()
                }
    // 以下代码省略
  }
  
  const Watcher = function(vm, fn) {
    console.log("Watcher",this)
    //打印3 Watcher  this是下面的Dep中subs的对象
    // 以下代码省略
  }
  
  const Dep = function() {
    console.log("Dep",this)
    //打印2  Dep   { 
                    target: null,
                    subs: [
                      {        //是一个Watcher实例
                        subs: Array(1)
                        0: Watcher
                        vm: {    //是一个Vue实例
                            _data:{
                              text: "123",//该属性有了get和set方法
                              get text: ƒ get(),
                              set text: ƒ set(newVal)
                            },
                            mount: ƒ (),
                            render: ƒ ()
                          },

                        addDep: ƒ (dep),
                        update: ƒ (),
                        value: undefined
                      }
                    ],
                    depend: ƒ (),
                    addSub: ƒ (watcher),
                    notify: ƒ ()
                  }

    // 以下代码省略
  }
  
  const vue = new Vue({
    data() {
      return {
        text: 'hello world'
      };
    }
  })
  
  vue.mount(); 
  vue._data.text = '123';

8.4详细代码

参考这个图,会了看的更清晰,注意看执行顺序,在new Vue的时候,就会按顺序,初始化各个构造函数

image.png

/**
 * 
 * 作用:
 * 劫持data的各个属性,挂载set和get方法
 * 在get中将Watch添加到dep的subs中
 * 在set中触发dep.notify更新视图
 */
const Observer = function (data) {
    console.log(1)   //开始4 new Vue的时候就会执行
    // 循环修改为每个属性添加get set
    for (let key in data) {
        defineReactive(data, key);
    }
}

const defineReactive = function (obj, key) {
    console.log(2)    //开始5 new Vue的时候就会执行
    // 局部变量dep,用于get set内部调用
    const dep = new Dep();
    // 获取当前值
    let val = obj[key];
    Object.defineProperty(obj, key, {
        // 设置当前描述属性为可被循环
        enumerable: true,
        // 设置当前描述属性可被修改
        configurable: true,
        get() {
            console.log(3)//开始10  开始19
            console.log('in get');
            // 调用依赖收集器中的addSub,用于收集当前属性与Watcher中的依赖关系
            dep.depend();
            return val;
        },
        set(newVal) {
            console.log(4)//开始15
            if (newVal === val) {
                return;
            }
            val = newVal;
            // 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
            // 这里每个需要更新通过什么断定?dep.subs
            dep.notify();
        }
    });
}

const Vue = function (options) {
    console.log(6)//开始1 new Vue的时候就会执行
    const self = this;
    // 将data赋值给this._data,源码这部分用的Proxy,这里我们用最简单的方式临时实现
    if (options && typeof options.data === 'function') {
        console.log(7)//开始2  options.data是个函数,它返回了一个对象
        this._data = options.data.apply(this);
    }
    // 挂载Dom函数
    this.mount = function () {
        console.log(8)  //开始7  new Vue以后,执行vue.mount()
        new Watcher(self, self.render);
    }
    // 渲染函数
    this.render = function () {
        console.log(9) //开始9 开始18  render函数执行后走到这里
        return self._data.text;  //这里取data值的时候,就会走get方法
    }
    // 开始3, 监听this._data
    //new Vue的时候就会执行,这里执行完,就表示new Vue的过程执行完了
    new Observer(this._data);
}

const Watcher = function (vm, fn) {
    console.log(10)  //开始8  执行vue.mount()以后会走到这里
    const self = this;
    this.vm = vm;
    // 将当前Dep.target指向自己
    // 每次执行new Watch的时候,都会把当前的Watcher挂载到Dep.target
    Dep.target = this;
    // 向Dep方法添加当前Wathcer
    // this.addDep = function (dep) {
    //     console.log(11) //开始13  
    //     dep.addSub(self);
    // }
    // 更新方法,用于触发vm._render
    this.update = function () {
        console.log(12)//开始17
        console.log('in watcher update');
        fn(); //Vue中的render函数
    }
    // 这里会首次调用vm._render,从而触发text的get
    // 从而将当前的Wathcer与Dep关联起来
    this.value = fn();   //开始9  fn是Vue中的render函数,这里fn()在赋值的时候会执行
    // 这里清空了Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,
    // 造成代码死循环
    console.log('Watcher', this)
    Dep.target = null;
}

const Dep = function () {
    //开始6  new Vue》Observer》defineReactive》new Dep()
    console.log(13)
    const self = this;
    // 收集目标,先把它置空
    this.target = null;
    // 存储收集器中需要通知的Watcher
    this.subs = [];
    // 当有目标时,绑定Dep与Wathcer的关系

    this.depend = function () {
        console.log(14)  //开始11   开始20 走了get获取属性后,就要进行依赖收集 
        // targrt挂载发生在new Watch的时候
        if (Dep.target) {
            console.log(15)//开始12  
            self.addSub(Dep.target)
        }
    }
    // 为当前收集器添加Watcher
    this.addSub = function (watcher) {
        console.log(16)//开始14
        self.subs.push(watcher);
    }
    // 通知收集器中所的所有Wathcer,调用其update方法
    this.notify = function () {
        console.log(17) //开始16
        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(); // 挂载dom,渲染页面
vue._data.text = '123'; // 修改属性值,触发set

「解析:」

    1. 一开始new Vue ,会走到46行执行Vue构造函数,打印6,然后判断传入的是否是个函数,从new Vue可以看出,传入的是个data()函数,然后将函数执行后返回的整个data对象挂载到vue的_data属性上(真实的vue也是这么操作的)
    1. 接着依次挂载mountrender函数。
    1. 然后 new Observer(this._data);
    1. new Observer 的时候会走到第一行Observer(关键函数),打印1。我们发现Observer实际就是给data数据都添加上get和set方法,只不过不添加的方法defineReactive给抽离出去了。
    1. 然后走到第9行,执行defineReactive,打印2。 defineReactive的作用:
      • 劫持data的各个属性,挂载setget方法
      • get中将Watch添加到depsubs
      • set中触发dep.notify更新视图
    1. 然后走到12行,new Dep的时候,会走到95行执行Dep,打印13。每次new Dep 的时候,都会置空target,this.target = null,Dep函数剩下的代码都只是定义函数depend和addSub,notify,都不会执行,会跳出Dep函数。然后会到defineReactive函数第13行。
    1. 然后15行给每个属性加上get和set方法。注意:此时只是在挂载,还没有执行,因此不会进入get,set方法内部。也就是说defineReactive剩下的代码中的函数也不会执行,所以会回到Observer,再回到67行new Observer,即new Vue的过程走完了

new Vue执行完毕

开始执行vue.mount()

    1. 然后走到135行的vue.mount(),走到56行,打印8。执行new Watcher,进入Watcher构造函数打印10,然后this.vm = vm; ,将vue实例挂载到Watch的vm属性上了。Dep.target = this,将watch实例挂载到了Dep的target属性上,从而关联起来。然后挂载addDep和update方法,只是定义,没有执行。
    1. 接着89行this.value = fn():fn实际是传进来的render函数(Vue=>new Watcher(self, self.render)=>vm, fn),由于后面有(),所以会立即执行。然后走到60行的render函数,打印9,返回 self._data.text,然后回到Watch中this.value就是返回值data.text。然后,关键的来了:self._data.text这里读取了data中的text,那么,这一步就会触发get方法。
    1. 然后走到21行的get,打印3。
    1. 然后走到25行,执行dep.depend(),再走到104行,打印14。
    1. 这时候判断Dep.target,由于第8步将watch挂载到了Dep.target,这时候为true,所以打印15。然后执行Dep.target.addDep(self),其实就是执行Watch.addDep(self),然后执行self.addSub(Dep.target)
    1. 然后进入this.addSub打印16,完成了依赖收集,subs中就有了Watch了。然后会回到get,执行最后一行,退出get,接着回到this.render,退出render函数,接着回到Watch中的this.value = fn(),继续往后走,Dep.target = null,避免陷入死循环,然后Watch执行完了。

vue.mount()也执行完毕

开始执行修改操作

    1. 然后就是136行赋值操作了vue._data.text = '123',这时候会走到28行的set,打印4,然后设置值为新值
    1. 继续向下走,到36行,dep.notify(),然后走到119行,打印17。
    1. 然后会走到122行,遍历Depsubs数组里面所有的Watch,触发Watch的update方法,走到82行,打印12。
    1. 然后执行Watcher中的fn(),即Vue中的render函数(new Watcher(self, self.render)时传入的render函数),走到60行,打印9。render函数中其实就可以做一些渲染的操作,例如:获取某个节点,将他的内容变成获取的值,就完成了页面的渲染。
    1. 然后走到63行,取data.text,会走get,走到21,打印3。
    1. 然后执行dep.depend(),走到Dep中的this.depend,打印14。但是由于Dep.target为null,15不会打印,也就是说到了这里,所有的流程执行完了。

全部执行完毕

文章>