Vue学习系列之1、响应式

前言

这是一个Vue源码学习系列。打算开个手写Vue的坑,但愿能在写代码的同时能把其中的细节讲清楚,最终目的是实现一个简版的vue。不知道本身能写到哪一步,总之尽力而为。若是能完成的话,应该是自我超越了和无限的自信了。放个仓库的 传送门javascript

1、观察者模式

场景:最近由于快到暑假了,产品韩梅梅提早一个月在大象拉了一个需求群,把研发李雷、小明拉进了群。她跟两位研发说,“咱们过几天要开发一个新需求,须要两位研发的支持,是关于暑假欢乐谷门票活动的需求,等产品逻辑梳理完,我们就进入开发。”。一个礼拜以后,韩梅梅在群里发出了需求文档,两位研发开始加班加点干活,需求完美上线。html

颇费特~ 好的,观察者模式讲完了。vue

纳尼?等等...等等...,您这是讲了个啥。java

咳咳...很差意思,从新来。react

咱们看一下上面这个场景,它分了几步git

  1. 产品韩梅梅通知两位研发过几天会有个需求让他们进行开发
  2. 几天后,韩梅梅通知研发要开始开发需求了
  3. 研发开始进行开发

总结下来,咱们发现有两个角色,一个发布者(产品韩梅梅),一个观察者(李雷等研发),当发布者的状态更新后,会进行通知观察者,观察者开始执行对应的动做。github

ok,让咱们试着写一下面试

class Dep {
    constructor(state) {
        this.watchers = []
        this.state = state
    }
    // 添加观察者(研发)
    add(watcher) {
  	!this.watchers.includes(watcher) && this.watchers.push(watcher)
    }
    // 移除观察者(研发)
    remove(watcher) {
        let index = this.watcher.indexOf(watcher)
        if (~index) this.watcher.splice(index, 1)
    }
    // 状态更新, 通知所有观察者
    notify() {
        for (let watcher of this.watchers) watcher.update(this)
    }
}

class Watcher {
    constructor(value) {
        this.value = value
    }
    // 更新
    update() {
        console.log('开始开发!')
    }
}


const HanMeiMei = new Dep()
const XiaoMing = new Watcher()
const LiLei = new Watcher()

// 拉群!
HanMeiMei.add(XiaoMing)
HanMeiMei.add(LiLei)

await new Promise(resolve => setTimeout(resolve, 7 * 24 * 60 * 60 * 1000, '一周过去了')))

// 过了一周开始通知研发开发

HanMeiMei.notify()
复制代码

问:那若是换成Vue中的视图数据之间的关系呢?哪一个是个发布者,哪一个是观察者。segmentfault

答:显而易见,数据是发布者,视图是观察者。当数据改变时,会通知视图,视图从新进行渲染。数组

这里有几个问题

  1. What,数据都收集什么样的观察者
  2. How,数据怎么收集的观察者
  3. When,数据何时收集观察者

ok,带着这些问题我们继续往下看

2、Vue中的观察者

一、What

首先,明确一点,Vue实例中的响应数据,几乎所有都来源于data,就是那个Option API中的data。不论是props,computed这些都是基于data的。

其次,Vue中的Watcher分为了三种,

  1. render Watcher,能够简单的理解为template;
  2. computed Watcher,在Vue文档中,说到过缓存这个概念,说白了其实就是计算属性的getter中用到的数据(data) 没有发生过变化,那么这个getter就没必要从新计算,这个我认为是Vue响应式中最绕的,下面的源码重点讲一下
  3. watch Watcher,没错就是那个Option API中的watch,你想一想你数据改了,watch是否是得再执行一遍,那不跟视图是同样的么

因此,what的答案就有了 数据收集了这三种观察者

二、How

说个面试的段子,面试官:vue怎么收集依赖的?

这个其实老生常谈,getter/setter的存储器嘛

诶,那你知道Array是怎么收集的吗?

知道知道,不就是hack的一些原生方法嘛

哦,那为何要hack呢,咋hack的呢,hack了哪些呢,不一样的方法之间又都是怎么处理的数据呢?

......

好了,回去等通知吧(一面挂)

这里信息量太大!关于为何要hack方法,尤大是给出了回答的,主要缘由是由于性能和使用方便之间的取舍,这篇文章有写道:segmentfault.com/a/119000001…

可是你要问我为啥数组附个值还能跟性能扯上关系,咱也不懂,咱也不敢问。

三、When

在vue实例化的时候,在beforeCreate和create之间,会有一个初始化数据的过程,这里会将data、computed所有初始化好,经过getter,哪里用到就在哪里收集观察者。

3、思路

一、先从getter/setter开始

先从转换数据开始,咱们来简单实现一个,很简单就是迭代加递归,两个函数搞定。

// 咱们先来实现第一个函数observe
function observe(data) {
    if (typeof data !== 'object') return
    for (let key of Object.keys(data)) {
        defineReactive(data, key)
    }
    return data
}

// 而后是defineReactive
function defineReactive(data, key) {
    let val = data[key]
    const dep = new Dep()
    observe(val)
    Object.defineProperty(data, key, {
        configurable: false,
        enumerable: true,
        get() {
            dep.depend()
            return val
        },
        set(newVal) {
            if (val === newVal) return
            val = newVal
            observe(val)
            dep.notify()
        }
    })
}
复制代码

这里的逻辑很简单就是经过迭代+递归,将全部值都改成存取器。

这里注意defineReactive方法,我并无直接把data[key] 的这个value经过参数传进去,而是在函数内部取值,之因此为何作,这里先留一个悬念

二、Dep

这里出现了一个class Dep,这里其实Dep就是来收集Watcher的。

好的,咱们继续来实现Dep

class Dep {
  constructor() {
    this.watchers = new Set()
  }
  depend() {
    if (Dep.Target) this.watchers.add(Dep.Target)
  }
  notify() {
    let watchers = this.watchers
    for (let watcher of watchers) {
      watcher.update()
    }
  }
}

复制代码

这里Dep的实现也很简单,就是收集watchers,使用Set确保watcher的惟一。

可是!这里又双叒出现了一个新的东西,Dep.Target。这东西是个啥,其实看代码也能差很少发现,Dep.Target确定是个Watcher实例。

诶~,这么多Watcher实例它究竟是哪一个呢?

好问题!咱们先想一想一个场景,咱们有个数据好比是data,咱们还有个渲染函数,而后呢~这个渲染函数用到了这个data。

用到data了确定就会触发data的getter,从而收集依赖,那咱们要收集的依赖确定就是这个渲染函数了。

相应的Dep.Target的值也就是这个渲染函数


哒嘎! 你觉得这样就结束了吗,No,No,No,嘛哒哒!

要是渲染函数里面还有个渲染函数咋整。

纳尼!还有这种操做吗!

有的,并且不少,当咱们组件里面嵌入了组件的时候就会出现。

我去,那不是很常见吗!那可怎么办。

别慌,咱们只要实现一个栈,有新的函数要执行,咱们就push进来,当函数执行结束,给他pop出去就行了。

ok,那咱们开始实现一下。

Dep.Target = null
const watcherStack = []

// 入栈
function pushTarget(watcher) {
  Dep.Target = watcher
  watcherStack.push(watcher)
}

// 出栈
function popTarget() {
  watcherStack.pop()
  Dep.Target = watcherStack[watcherStack.length - 1]
}
复制代码

完美解决~ 那么最后剩下的的就是watcher的实现了。

三、Watcher

RenderWatcher

上面说过,watcher一共有三种,咱们先实现最简单、最基础的renderWatcher。

class Watcher {
  constructor(getter) {
    this.getter = getter
    this.value = undefined

    this.value = this.get()
  }
  get() {
    pushTarget(this)
    this.getter()
    popTarget()
  }
  update() {
    this.value = this.get()
  }
}
复制代码

这里的逻辑很简单,参数getter就是要执行的函数。对于RenderWatcher来讲getter就是渲染函数。 好的!万事具有,咱们来试着跑个例子。

<body>
  <div id="app"></div>
  <script src="./reactive/reactive.js"></script>
  <script> const data = observe({ age: 12, name: 'Sunyanzhe' }) // 渲染函数 function renderFunction() { document.querySelector('#app').innerHTML = `我叫${data.name}, 我${data.age}岁` } // renderWatcher const renderWatcher = new Watcher(renderFunction) setTimeout(() => { data.age = 25 }, 2000) </script>
</body>
复制代码

renderWatcher.gif

来,咱们捋一下流程:

  1. 第一步固然是数据的处理,先变成getter/setter
  2. new Watcher的时候,这时候Dep.TargetrenderWatcher
  3. 开始执行renderFunction,读取到nameage的属性时,触发getter收集Dep.Target,也就是renderWatcher。此时nameage中的Dep实例都存了renderWatcher
  4. 过了2秒,data.age赋值,触发setter,触发ageDep中保存的watcherupdate方法。此时更新视图。
  5. 这时,renderFunction执行,读取到age时,值为25

别问为啥两秒,一我的就从12变成25了,经历痛苦会让人瞬间成长😂

ComputedWatcher

总听文章里说,computed有什么懒加载,缓存。那是个什么玩意啊

好说,由于computed实际上是一个getter,是函数就要执行嘛。懒加载的意思就是它何时被用到了,它何时执行这个函数。

那缓存又是什么呢?

也很简单,就是computed中用到的值若是没发生改变的话,它的getter函数不进行计算,而是直接用上一次得出的结果。


ok,先到这里,咱们先捋一捋思路

首先,刚刚说到,computed能够被缓存,当它用到的值没有发生改变时,getter不须要执行。

也就是说computed自己也是要有Dep,用来收集数据。

其次,他是lazy的,因此即便数据发生了改变也不用当即执行函数,获取结果。而是能够等到,何时再次用到这个computed的值再去计算。好比在render函数中用到

最后,computed中的数据改变后不能只通知computed的值须要从新更新,还须要通知用到computed的地方也要进行一次更新

总结下来就是,若是一个render函数中有用到computed,那么computed中的数据更新,不只要通知computed的值要改变,还要告诉render函数进行从新执行。而当render函数从新执行的时候,就会再次获取computed。这时computed才会执行他的getter函数

好了,思路捋清了,咱们实现一下。为此咱们要修改一下以前的Dep和Watcher,而且咱们还要实现一个computed方法。

class Dep {
  constructor() {
    this.watchers = new Set()
  }
  // 这里发生了变化
  addSub(watcher) {
    this.watchers.add(watcher)
  }
   // 这里发生了变化
  depend() {
    if (Dep.Target) {
      Dep.Target.addDep(this)
    }
  }
  notify() {
    let watchers = this.watchers
    for (let watcher of watchers) {
      watcher.update()
    }
  }
}

class Watcher {
  constructor( getter, options ) {
    this.getter = getter
    this.deps = new Set()
    this.value = undefined
    this.lazy = undefined
    this.dirty = undefined

    if (options) {
      this.lazy = !!options.lazy
    }
    this.dirty = this.lazy
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  get() {
    pushTarget(this)
    let value = this.getter()
    popTarget()
    return value
  }
  addDep(dep) {
    dep.addSub(this)
    this.deps.add(dep)
  }
  depend() {
    let deps = this.deps
    for (let dep of deps) {
      dep.depend()
    }
    
  }
  evalute() {
    this.value = this.get()
    this.dirty = false
  }
  update() {
    if (this.lazy) {
      this.dirty = true
    } else {
      Promise.resolve().then(() => {
        this.run()
      })
    }
  }
  run() {
    this.value = this.get()
  }
}

function computed(computedGetter) {
  const options = { lazy: true }
  const computedWathcer = new Watcher(computedGetter, options)
  const result = {}
  Object.defineProperty(result, 'value', {
    get() {
      if (computedWathcer.dirty) {
        computedWathcer.evalute()
      }
      if (Dep.Target) {
        computedWathcer.depend()
      }
      return computedWathcer.value
    }
  })
  return result
}
复制代码

看到这里确定很晕,不要紧,我们再举一个🌰,结合🌰来看懂这块逻辑。你们目前只须要关注一点,就是update中咱们用了微任务。

ok,先看例子

<body>
  <div id="app"></div>
  <script src="./reactive/reactive.js"></script>
  <script> const data = observe({ age: 12, name: 'Sunyanzhe' }) function renderFunction() { document.querySelector('#app').innerHTML = `我叫${data.name}, 我${data.age}岁,明年${nextYear.value}` } const nextYear = computed(() => data.age + 1) const renderWatcher = new Watcher(renderFunction) setTimeout(() => { data.age = 25 }, 2000) </script>
</body>

复制代码

computedWatcher.gif

咱们仍是捋一下执行顺序

  1. 首先依旧是observe Data
  2. 而后咱们定义了nextYear这个computed。这时候注意computedWatcher已经生成了,可是因为它是lazy的咱们并无执行get,且这个watcher的dirtytrue。这个很重要!
  3. 开始renderFunction,执行get,收集依赖...这些都没问题,可是你们还记不记得以前说的。此时的栈中有RenderWatcher。
  4. 读取到nextYear,触发nextYear的getter。因为dirty是ture,因此开始计算!此时执行了get方法,中推入了ComputedWatcher。重点来了,RenderWatcher还没执行完!因此目前栈中有两个watcher。computedWatcher的get执行完,出栈,将获得的值赋给value属性。修改dirty属性为false
  5. 另外一个关键,此时的Dep.Target依旧指向的是RenderWatcher,而后computedWatcher执行了depend方法。这个方法的意思,就是要让收集到computedWatcher的dep继续收集那个用到computedWatcher的RenderWatche。(有点绕,多看看代码仔细体会)
  6. 值改变,computed的dirty再次变为true,等待renderWatcher的更新,再次出发computedWatcher的计算。

这里为何使用了微任务,是由于执行顺序的问题,咱们的computed的计算必需要在renderWatcher的更新以后,这样才能收集到对应的依赖。在Vue源码中,有一个执行更新的队列,它会将全部的watcher进行排序,避免报错。

WatchWatcher

其实,watch也很简单,就是加了个callback。

watch比较迷惑的地方其实它的getter是什么,在renderWatcher中,getter是render函数;在computedWatcher中,getter是getter函数;那么watch是什么呢。

其实很简单就是个travers函数,想一想咱们是怎么写watch的

watch: {
  prop1(val) {
    console.log(val)
  }
}

// 转换为
$watch(() => {vm._data.prop1}, console.log)
复制代码

这里面第一个函数是getter,用来收集依赖,第二个就是callback了

那deep呢? deep其实就是深度遍历

废话少说,直接开始实现!

其实很简单,咱们只须要加个callback,找个地方调用一下就行了。

因此咱们就改一下constructor和run这两个

class Watcher {
  constructor( getter, options, cb ) {
    //...
    this.cb = cb
    this.user = undefined

    if (options) {
      this.user = !!options.user
    }
    // ...
  }
  run() {
    let newValue = this.get()
    if (this.user && newValue !== this.value) {
      // 调用回调
      this.cb(newValue, this.value)
      this.value = newValue
    }
  }
}

function watch(watcheGetter, callback) {
  const options = { user: true }
  new Watcher(watcheGetter, options, callback)
}
复制代码

就是如此的简单,比computed简单多了~

最后看一下效果

<body>
  <div id="app"></div>
  <script src="./reactive/reactive.js"></script>
  <script> const data = observe({ age: 12, name: 'Sunyanzhe' }) function renderFunction() { document.querySelector('#app').innerHTML = `我叫${data.name}, 我${data.age}岁,明年${nextYear.value}` } const nextYear = computed(() => data.age + 1) const renderWatcher = new Watcher(renderFunction) watch( () => data.name, (val, oldVal) => { console.log('new---', val) console.log('old---', oldVal) }) setTimeout(() => { data.name = 'yanzhe' }, 1000) setTimeout(() => { data.age = 25 }, 2000) </script>
</body>

复制代码

watch.gif

4、源码以及拓展阅读

在上文中谈到的为何DefineReactive不传value的缘由,在这个issue中:github.com/vuejs/vue/p…,主要缘由是,数据自己就能够是getter/setter

Vue中的源码思路与本文一致,主要多了边界问题的处理,以及数组的hack,有关数组的处理须要你们去看源码去理解

  1. array —— github.com/vuejs/vue/b…
  2. 观察者—— github.com/vuejs/vue/b…
相关文章
相关标签/搜索