手把手带你实现一个最精简的响应式系统来学习Vue的data、computed、watch源码

导读

记得初学Vue源码的时候,在defineReactiveObserverDepWatcher等等内部设计源码之间跳来跳去,发现再也绕不出来了。Vue发展了好久,不少fix和feature的增长让内部源码愈来愈庞大,太多的边界状况和优化设计掩盖了本来精简的代码设计,让新手阅读源码变得愈来愈困难,可是面试的时候,Vue的响应式原理几乎成了Vue技术栈的公司面试中高级前端必问的点之一。前端

这篇文章经过本身实现一个响应式系统,尽可能还原和Vue内部源码一样结构,可是剔除掉和渲染、优化等等相关的代码,来最低成本的学习Vue的响应式原理。vue

预览

源码地址:
github.com/sl1673495/v…react

预览地址:
sl1673495.github.io/vue-reactiv…git

reactive

Vue最经常使用的就是响应式的data了,经过在vue中定义github

new Vue({
    data() {
        return {
            msg: 'Hello World'
        }
    }
})
复制代码

在data发生改变的时候,视图也会更新,在这篇文章里我把对data部分的处理单独提取成一个api:reactive,下面来一块儿实现这个api。面试

要实现的效果:api

const data = reactive({
  msg: 'Hello World',
})

new Watcher(() => {
  document.getElementById('app').innerHTML = `msg is ${data.msg}`
})
复制代码

在data.msg发生改变的时候,咱们须要这个app节点的innerHTML同步更新,这里新增长了一个概念Watcher,这也是Vue源码内部的一个设计,想要实现响应式的系统,这个Watcher是必不可缺的。bash

在实现这两个api以前,咱们先来理清他们之间的关系,reactive这个api定义了一个响应式的数据,其实你们都知道响应式的数据就是在它的某个属性(好比例中的data.msg)被读取的时候,记录下来这时候是谁在读取他,读取他的这个函数确定依赖它。 在本例中,下面这段函数,由于读取了data.msg而且展现在页面上,因此能够说这段渲染函数依赖了data.msg数据结构

// 渲染函数
document.getElementById('app').innerHTML = `msg is ${data.msg}`
复制代码

这也就解释清了,为何咱们须要用new Watcher来传入这段渲染函数,咱们已经能够分析出来Watcher是帮咱们记录下来这段渲染函数依赖的关键。app

在js引擎执行渲染函数的途中,忽然读到了data.msgdata已经被定义成了响应式数据,读取data.msg时所触发的get函数已经被咱们劫持,这个get函数中咱们去记录下data.msg被这个渲染函数所依赖,而后再返回data.msg的值。

这样下次data.msg发生变化的时候,Watcher内部所作的一些逻辑就会通知到渲染函数去从新执行。这不就是响应式的原理嘛。

下面开始实现代码

import Dep from './dep'
import { isObject } from '../utils'

// 将对象定义为响应式
export default function reactive(data) {
  if (isObject(data)) {
    Object.keys(data).forEach(key => {
      defineReactive(data, key)
    })
  }
  return data
}

function defineReactive(data, key) {
  let val = data[key]
  // 收集依赖
  const dep = new Dep()

  Object.defineProperty(data, key, {
    get() {
      dep.depend()
      return val
    },
    set(newVal) {
      val = newVal
      dep.notify()
    }
  })

  if (isObject(val)) {
    reactive(val)
  }
}

复制代码

代码很简单,就是去遍历data的key,在defineReactive函数中对每一个key进行get和set的劫持,Dep是一个新的概念,它主要用来作上面所说的dep.depend()去收集当前正在运行的渲染函数和dep.notify() 触发渲染函数从新执行。

能够把dep当作一个收集依赖的小筐,每当运行渲染函数读取到data的某个key的时候,就把这个渲染函数丢到这个key本身的小筐中,在这个key的值发生改变的时候,去key的筐中找到全部的渲染函数再执行一遍。

Dep

export default class Dep {
  constructor() {
    this.deps = new Set()
  }

  depend() {
    if (Dep.target) {
      this.deps.add(Dep.target)
    }
  }

  notify() {
    this.deps.forEach(watcher => watcher.update())
  }
}

// 正在运行的watcher
Dep.target = null
复制代码

这个类很简单,利用Set去作存储,在depend的时候把Dep.target加入到deps集合里,在notify的时候遍历deps,触发每一个watcher的update。

没错Dep.target这个概念也是Vue中所引入的,它是一个挂在Dep类上的全局变量,js是单线程运行的,因此在渲染函数如:

document.getElementById('app').innerHTML = `msg is ${data.msg}`
复制代码

运行以前,先把全局的Dep.target设置为存储了这个渲染函数的watcher,也就是:

new Watcher(() => {
  document.getElementById('app').innerHTML = `msg is ${data.msg}`
})
复制代码

这样在运行途中data.msg就能够经过Dep.target找到当前是哪一个渲染函数的watcher正在运行,这样也就能够把自身对应的依赖所收集起来了。

这里划重点:Dep.target必定是一个Watcher的实例。

又由于渲染函数能够是嵌套运行的,好比在Vue中每一个组件都会有本身用来存放渲染函数的一个watcher,那么在下面这种组件嵌套组件的状况下:

// Parent组件

<template>
  <div>
    <Son组件 />
  </div>
</template>
复制代码

watcher的运行路径就是: 开始 -> ParentWatcher -> SonWatcher -> ParentWatcher -> 结束。

是否是特别像函数运行中的入栈出栈,没错,Vue内部就是用了栈的数据结构来记录watcher的运行轨迹。

// watcher栈
const targetStack = []

// 将上一个watcher推到栈里,更新Dep.target为传入的_target变量。
export function pushTarget(_target) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

// 取回上一个watcher做为Dep.target,而且栈里要弹出上一个watcher。
export function popTarget() {
  Dep.target = targetStack.pop()
}
复制代码

有了这些辅助的工具,就能够来看看Watcher的具体实现了

import Dep, { pushTarget, popTarget } from './dep'

export default class Watcher {
  constructor(getter) {
    this.getter = getter
    this.get()
  }

  get() {
    pushTarget(this)
    this.value = this.getter()
    popTarget()
    return this.value
  }

  update() {
     this.get()
  }
}

复制代码

回顾一下开头示例中Watcher的使用。

const data = reactive({
  msg: 'Hello World',
})

new Watcher(() => {
  document.getElementById('app').innerHTML = `msg is ${data.msg}`
})
复制代码

传入的getter函数就是

() => {
  document.getElementById('app').innerHTML = `msg is ${data.msg}`
}
复制代码

在构造函数中,记录下getter函数,而且执行了一遍get

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

在这个函数中,this就是这个watcher实例,在执行get的开头先把这个存储了渲染函数的watcher设置为当前的Dep.target,而后执行this.getter()也就是渲染函数

在执行渲染函数的途中读取到了data.msg,就触发了defineReactive函数中劫持的get:

Object.defineProperty(data, key, {
    get() {
      dep.depend()
      return val
    }
  })
复制代码

这时候的dep.depend函数:

depend() {
    if (Dep.target) {
      this.deps.add(Dep.target)
    }
  }

复制代码

所收集到的Dep.target,就是在get函数开头中pushTarget(this)所收集的

new Watcher(() => {
  document.getElementById('app').innerHTML = `msg is ${data.msg}`
})
复制代码

这个watcher实例了。

此时咱们假如执行了这样一段赋值代码:

data.msg = 'ssh'
复制代码

就会运行到劫持的set函数里:

Object.defineProperty(data, key, {
    set(newVal) {
      val = newVal
      dep.notify()
    }
  })
复制代码

此时在控制台中打印出dep这个变量,它内部的deps属性果真存储了一个Watcher的实例。

dep

运行了dep.notify之后,就会触发这个watcher的update方法,也就会再去从新执行一遍渲染函数了,这个时候视图就刷新了。

computed

在实现了reactive这个基础api之后,就要开始实现computed这个api了,这个api的用法是这样:

const data = reactive({
  number: 1
})

const numberPlusOne = computed(() => data.number + 1)

// 渲染函数watcher
new Watcher(() => {
  document.getElementById('app2').innerHTML = ` computed: 1 + number 是 ${numberPlusOne.value} `
})
复制代码

vue内部是把computed属性定义在vm实例上的,这里咱们没有实例,因此就用一个对象来存储computed的返回值,用.value来拿computed的真实值。

这里computed传入的其实仍是一个函数,这里咱们回想一下Watcher的本质,其实就是存储了一个须要在特定时机触发的函数,在Vue内部,每一个computed属性也有本身的一个对应的watcher实例,下文中叫它computedWatcher

先看渲染函数:

// 渲染函数watcher
new Watcher(() => {
  document.getElementById('app2').innerHTML = ` computed: 1 + number 是 ${numberPlusOne.value} `
})
复制代码

这段渲染函数执行过程当中,读取到numberPlusOne的值的时候

首先会把Dep.target设置为numberPlusOne所对应的computedWatcher

computedWatcher的特殊之处在于

  1. 渲染watcher只能做为依赖被收集到其余的dep筐子里,而computedWatcher实例上有属于本身的dep,它能够收集别的watcher做为本身的依赖。
  2. 惰性求值,初始化的时候先不去运行getter。
export default class Watcher {
  constructor(getter, options = {}) {
    const { computed } = options
    this.getter = getter
    this.computed = computed

    if (computed) {
      this.dep = new Dep()
    } else {
      this.get()
    }
  }
}
复制代码

其实computed实现的本质就是,computed在读取value以前,Dep.target确定此时是正在运行的渲染函数的watcher

先把当前正在运行的渲染函数的watcher做为依赖收集到computedWatcher内部的dep筐子里。

把自身computedWatcher设置为 全局Dep.target,而后开始求值:

求值函数会在运行

() => data.number + 1
复制代码

的途中遇到data.number的读取,这时又会触发'number'这个key的劫持get函数,这时全局的Dep.target是computedWatcher,data.number的dep依赖筐子里丢进去了computedWatcher

此时的依赖关系是 data.number的dep筐子里装着computedWatchercomputedWatcher的dep筐子里装着渲染watcher

此时若是更新data.number的话,会一级一级往上触发更新。会触发computedWatcherupdate,咱们确定会对被设置为computed特性的watcher作特殊的处理,这个watcher的筐子里装着渲染watcher,因此只须要触发 this.dep.notify(),就会触发渲染watcher的update方法,从而更新视图。

下面来改造代码:

// Watcher
import Dep, { pushTarget, popTarget } from './dep'

export default class Watcher {
  constructor(getter, options = {}) {
    const { computed } = options
    this.getter = getter
    this.computed = computed

    if (computed) {
      this.dep = new Dep()
    } else {
      this.get()
    }
  }

  get() {
    pushTarget(this)
    this.value = this.getter()
    popTarget()
    return this.value
  }

  // 仅为computed使用
  depend() {
    this.dep.depend()
  }

  update() {
    if (this.computed) {
      this.get()
      this.dep.notify()
    } else {
      this.get()
    }
  }
}
复制代码

computed初始化:

// computed
import Watcher from './watcher'

export default function computed(getter) {
  let def = {}
  const computedWatcher = new Watcher(getter, { computed: true })
  Object.defineProperty(def, 'value', {
    get() {
      // 先让computedWatcher收集渲染watcher做为本身的依赖。
      computedWatcher.depend()
      return computedWatcher.get()
    }
  })
  return def
}
复制代码

这里的逻辑比较绕,若是没理清楚的话能够把代码下载下来一步步断点调试,data.number被劫持的set触发之后,能够看一下number的dep到底存了什么。

dep

watch

watch的使用方式是这样的:

watch(
  () => data.msg,
  (newVal, oldVal) => {
    console.log('newVal: ', newVal)
    console.log('old: ', oldVal)
  }
)
复制代码

传入的第一个参数是个函数,里面须要读取到响应式的属性,确保依赖能被收集到,这样下次这个响应式的属性发生改变后,就会打印出对饮的新值和旧值。

分析一下watch的实现原理,这里依然是利用Watcher类去实现,咱们把用于watch的watcher叫作watchWatcher,传入的getter函数也就是() => data.msgWatcher在执行它以前仍是同样会把自身(也就是watchWatcher)设为Dep.target,这时读到data.msg,就会把watchWatcher丢进data.msg的依赖筐子里。

若是data.msg更新了,则就会触发watchWatcherupdate方法

直接上代码:

// watch
import Watcher from './watcher'

export default function watch(getter, callback) {
  new Watcher(getter, { watch: true, callback })
}

复制代码

没错又是直接用了getter,只是此次传入的选项是{ watch: true, callback },接下来看看Watcher内部进行了什么处理:

export default class Watcher {
  constructor(getter, options = {}) {
    const { computed, watch, callback } = options
    this.getter = getter
    this.computed = computed
    this.watch = watch
    this.callback = callback
    this.value = undefined

    if (computed) {
      this.dep = new Dep()
    } else {
      this.get()
    }
  }
}
复制代码

首先是构造函数中,对watch选项和callback进行了保存,其余没变。

而后在update方法中。

update() {
    if (this.computed) {
     ...
    } else if (this.watch) {
      const oldValue = this.value
      this.get()
      this.callback(oldValue, this.value)
    } else {
      ...
    }
  }
复制代码

在调用this.get去更新值以前,先把旧值保存起来,而后把新值和旧值一块儿经过调用callback函数交给外部,就这么简单。

咱们仅仅是改动寥寥几行代码,就轻松实现了很是重要的api:watch

总结。

有了精妙的Watcher和Dep的设计,Vue内部的响应式api实现的很是简单,不得再也不次感叹一下尤大真是厉害啊!

相关文章
相关标签/搜索