Blog

响应式系统

time: 2019.11.21
author: heyunjiang

目录
1 现象分析
2 initState
3 observe 与 Observer
4 defineReactive
5 Dep 6 watcher
7 dom diff

分析前的问题

  1. 如何监听数据变化?defineProperty
  2. 监听哪些数据变化?首先声明的数据,多层都会被监听
  3. 数据变化如何缓存?queueWatcher
  4. 什么时机触发更新?dep, nextTick, observer
  5. 如何只更新对应的元素节点?diff key 按最小粒度更新
  6. props, computed 数据是响应式的吗?

补充一点:双向数据绑定 v-model 使用的是 value prop + change 事件来实现的,属于语法糖,简化编码步骤,统一不同类型表单的数据绑定。后续更新 dom 还是响应式系统来实现的

1 现象分析

先分析现象:通过修改 state 数据,会触发组件重新渲染,this.hello = 'world'

看源码前分析:

  1. 这里是直接修改的数据,vue 没有什么特别的能力,必然依赖 javascript 原生语言的能力,来监听数据的变化
  2. 在 vue 组件 init 的时候,会有 initState(vm) 这个阶段

2 initState

源码分析:initState

// initData
function initData (vm: Component) {
  let data = vm.$options.data
  // 设置 vm._data 属性
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // proxy data on instance
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    const key = keys[i]
    proxy(vm, `_data`, key)
  }
  // observe data
  observe(data, true /* asRootData */)
}

// getData 内部执行来 data 方法
export function getData (data: Function, vm: Component): any {
  // pushTarget 和 popTarget 是定义在 dep.js 内部方法,此处作用是 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}

// proxy
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

从上面 initState 可以看到

  1. 定义在 data 方法内部的数据,是保存在 vm._data 上的
  2. vm.hello 是通过 proxy 方法到 vm._data.hello 数据
  3. proxy 是通过 Object.defineProperty 设置 vm 对象的 get、set 描述属性,此刻只是方法到 vm._data 数据,并没有涉及到数据监听(为什么说不涉及数据监听呢?)

接下来分析 observe() 方法

3 observe 与 Observer

// observe 定义在 '/observer/index'
export function observe (value: any, asRootData: ?boolean): Observer | void {
  let ob = new Observer(value)
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

可以看到

  1. 每次调用 observe({}) 一个数据对象 plain object 时,会实例化一个 Observer 对象
// Observer
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    this.walk(value)
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}

从上面的 Observer 方法我们可以看到,Observer 有如下作用

  1. 设置 object 的 ob 属性,指向 Observer 实例
  2. 遍历每个属性,并 通过 defineReactive 方法设置为响应式的;如果 observe 的是一个简单数据类型,则 walk 方法内部会执行中止

每个组件一个 data 对象,一个根 Observer 对象,每个子 plain object 对应一个 Observer 对象,一个根 Dep 对象,多个子 Dep 对象,一个 watcher 对象

4 defineReactive

// defineReactive
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

从上面的 defineReactive 方法可以看到

  1. 每个属性,都对应一个 Dep 对象的实例 dep
  2. 如果属性的值是一个对象,则再次调用 observe 实例化一个 Observer 对象 !shallow && observe(val),并且为该对象的每个属性再次调用一次 defineReactive 方法, 形成一个递归
  3. 每个属性,都设置 Object.defineProperty,在 get 的时候,属性对应的 dep.depend(),在 set 的时候执行 dep.notify()。并且如果当前属性的新值为一个 object,则再次 observe 这个属性
  4. 任何组件访问我这个属性,对应组件的 watcher 对象都会被所访问属性的 dep 对象收集到;在响应式数据属性更改的时候,它会让自身 dep 对象所收集的 watcher 对象都执行一次更新

问题1 如何监听数据变化?
答: 为对象的每个属性通过 Object.defineProperty 修改 get、set 描述属性,并且通过实例化一个 Dep 对象来收集及通知变更

问题2 监听哪些数据变化?
答:1. data 的顶层数据 2. 类型为 object 的数据也会递归监听 3. 数组项为 object 类型的
注意一下情况不会被监听到:1. 数组项为非 object 的,如果通过下标更新,或者修改数组长度 2. 为 object 新增或者删除属性

前面看到每个属性对应一个 dep 实例,从 new Dep 到 dep.depend, dep.notify,也没有看到与当前属性哪里挂勾了,那么这个 dep 是干啥的呢?

5 Dep

time: 2019.12.10

在分析 Dep 之前,有几个问题

  1. 每一个 object 对应一个 observer 实例, 每一个 observer 实例对应一个 dep 实例,每个 object 的属性也对应一个 dep 实例,这些 dep 是用来干嘛的?dep 是用于收集依赖,watcher 缓存管理,派发更新等
  2. dep 与属性是怎么关联的呢?属性闭包中包含 dep,属性变化时,通过 dep.notify 更新通知 watcher
  3. 属性的 dep 与 observer 的 dep 有什么关系?独立关系,一个 object 可能被整体替换,也可能只更新属性
  4. observer 和 dep 是怎么更新 vnode 的呢?vm._render
  5. 为什么要每个组件对应一个 watcher, 每个 object 对应一个 observer,每个属性对应一个 dep?这个是 vue 实现的细粒度依赖收集与派发更新
import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

从 dep 源码可以看到

  1. Dep 类的 target 属性,是只有主动调用 pushTarget 添加和 popTarget 释放
  2. 收集依赖:每一个属性的 dep 实例,是在 get 时,通过 depend 保存在当前 Dep.taget 上的;也就是说,每一个 watcher 保存了多个 dep 实例
  3. 缓存更新:每一个属性在 set 时,通过 dep.notify 通知 当前 dep 对应的 watcher 实例缓存当前更新,是通过 watcher.update() 将待更新的数据加入缓存队列
  4. dep id:每个 dep 实例都有一个 id,也就是说每个属性都能被 watcher 通过 dep 准确追踪到属性值变更
  5. 挂载在当前活跃的 watcher 上:dep.depend 时会将当前 dep 挂载在当前活跃的 watcher 上,不用担心当前 watcher 是谁(其实通常是本组件对应的 watcher)

问题

  1. 什么时机会调用 pushTarget 更新当前 target?在 new Watcher() 的时候
  2. 什么时机会调用 addSub 将对应组件的 watcher 添加到当前 dep 实例保存的 watcher 队列中?在 dep.depend 的时候

6 watcher

vue 为每个组件实例化一个 watcher;也会为每一个 computed 和 watch 属性都实例化一个 watcher

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true)

在实例化 watcher 的时候,会调用 this.get 方法,内部包含了 pushTarget(this) ,会将当前 watcher 压入 Dep 类的 watcher 实例栈中。

在 dep.depend 的时候,会执行 Dep.target.addDep(this),以及在 dep.notify 的时候,会执行 subs[i].update(),这2个方法做了什么操作呢?也就是说,当数据变化的时候,会通过 dep.notify 调用 watcher 的 update 方法,实现更新缓存和更新

在 watcher 内部,这2个方法的实现如下

  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

可以看出

  1. 在将 dep 实例添加到当前活跃 watcher 的时候,也会把当前 watcher 添加到 dep 实例的 watcher 队列中
  2. 在调用 watcher.update 时,一般会走 queueWatcher 方法,也就是将当前 watcher 添加到一个缓冲队列中,然后在 nextTick 中执行 wacher 队列
  3. 如果没有上个队列在处理,则添加进队列;如果上个队列正在处理,则把它添加到当前更新队列中,会根据 watcher id 来按顺序处理。

问题3 数据变化如何缓存?queueWatcher
答:通过 queueWatcher 将其缓存在一个数组队列中
问题4 什么时机触发更新?
答:上个 nextTick 结束了,然后马上开启下一个 nextTick 来 flush 队列

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  queue.sort((a, b) => a.id - b.id)

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
  }
}

run () {
  if (this.active) {
    const value = this.get()
    if (
      value !== this.value ||
      isObject(value) ||
      this.deep
    ) {
      const oldValue = this.value
      this.value = value
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

可以看出

  1. 是通过 watcher.run 来执行 watcher 内部更新,run 方法本质是是调用 watcher.get 方法,而 get 也是调用了 watcher.getter 方法,watcher.getter 是在构造函数中定义的,也就是我们传入的 updateComponent = () => {vm._update(vm._render(), hydrating)} 方法。
  2. 绕来绕去,本质也是执行的 updateComponent 方法,而我们通过 this.hello = 'world' 更新当前组件的数据,也会在最后通过 vm._render 再次更新 vnode ,从而渲染更新真实 dom。
  3. 在初始化 watcher 的时候,会执行 value = this.getter.call(vm, vm),也就是执行了 vm._render,在 render 过程中会读取 vm.$data 上的数据,会触发属性的 getter,此刻 Dep.target 正式当前 watcher ,而此刻 getter 就会执行 dep.depend 将当前 watcher 压入当前 dep 实例的 watcher 队列中,就实现了一次组件的响应式系统

现在的问题

  1. 如何只更新需要的 dom? dom diff

7 dom diff

这里简单总结一下 dom diff 的算法

在组件更新的时候,是通过对比前后虚拟 dom 来派发更新。预想一些问题

  1. 是怎么判断是同一个节点
  2. key 如何提升 diff 性能
  3. 如果一个组件变化,在重新 render 时,它的 children 会重新 render 吗?

通过 vm._render() 调用 createComponent 生成新的 vnode,如果已经创建过了,则会在 render 时继续使用上次的 key

const { render, _parentVnode } = vm.$options
vnode = render.call(vm._renderProxy, vm.$createElement)

然后会通过 vm._update() 来更新 vnode,vnode 是挂载在 vm 实例上的

// vnode 保存在 vm 对象上
const vm: Component = this
const prevVnode = vm._vnode
vm._vnode = vnode

vm.$el = vm.__patch__(prevVnode, vnode)
// patch.js 通过 sameVnode 比较是否是同一个 vnode
if (!isRealElement && sameVnode(oldVnode, vnode)) {
  patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}

// sameVnode 通过比较前后 key 是否相等
function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
function patchVnode (
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  // 1 如果新旧 vnode 节点不变,则不更新
  if (oldVnode === vnode) {
    return
  }

  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  const elm = vnode.elm = oldVnode.elm

  let i
  const data = vnode.data
  // 2. 调用 prepatch 钩子
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }

  const oldCh = oldVnode.children
  const ch = vnode.children

  if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {
      if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(ch)
      }
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
      removeVnodes(oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {
    nodeOps.setTextContent(elm, vnode.text)
  }
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  }
}
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm

  // removeOnly is a special flag used only by <transition-group>
  // to ensure removed elements stay in correct relative positions
  // during leaving transitions
  const canMove = !removeOnly

  if (process.env.NODE_ENV !== 'production') {
    checkDuplicateKeys(newCh)
  }

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      if (isUndef(idxInOld)) { // New element
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
      } else {
        vnodeToMove = oldCh[idxInOld]
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          // same key but different element. treat as new element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        }
      }
      newStartVnode = newCh[++newStartIdx]
    }
  }
  if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(oldCh, oldStartIdx, oldEndIdx)
  }
}

从上面可以看到

  1. 如果新旧 dom 的 key 不同,则直接替换旧的 dom
  2. 如果新旧 dom 的 key 相同,则分节点变化或者没有变化,没有变化则直接 return
  3. 如果组件数据有变化,在调用 updateChildren 更新组件时,diff 算法则在 updateChildren 方法里面了

在 Vnode 的 constructor 中,有 key 的声明 this.key = data && data.key ,这个是用户主动传入的,有如下作用

  1. 判断更新前后是否是同一个 vnode,体现在 sameVnode 方法中
  2. 在 normalizeArrayChildren 时,如果有 key,则标记为 c.key = `__vlist${nestedIndex}_${i}__`
  3. 在处理通过 v-for 生成的列表时,会将 key 值没有变化的节点做渲染优化

参考

vue 官网
vue 技术揭秘