Loading...
墨滴

jouryjc

2021/07/11  阅读:51  主题:橙心

[咖聊] 你瞅啥?瞅 reactive


highlight: androidstudio

端一杯 82 年的加浓美式 ☕️,瞅这 reactive,咋地?

第一杯美式 ☕️ ,我们了解真个 Vue 的执行过程

第二杯美式 ☕️,我们深究从 template 到 render 函数的过程

这第三杯加浓美式 ☕️,我们来聊聊这 reactive。上栗子 🌰:

<div id="app">
    <div>{{ message }}</div>
    <div v-for="item of relationship" :key="item.name">
        <span>name:{{ item.name }}</span>
        <span>relation:{{ item.relation }}</span>
    </div>
    <div>
        <button @click="addRelatives">添加亲戚</button>
    </div>
    <p>
        Province: {{ address.children.name }}
    </p>
</div>
new Vue({
    el'#app',

    data () {
        return {

            // 基础类型
            message'this is a message',

            // array
            relationship: [{
                name'myFatherName',
                relation'Father and son'
            }, {
                name'myBrotherName',
                relation'brothers'
            }],

            // object
            address: {
                type'country',
                name'China',
                children: {
                    type'province',
                    name'GuangDong',
                    children: {
                        type'city',
                        name'ShenZhen',
                    }
                }
            }
        };
    },

    methods: {
        addRelatives () {
            this.userInfo.relationship.push({
                name'myMomName',
                relation'Mom and son'
            });
        },

    }
})

栗子 🌰 中构造了基础类型、数组、对象的响应式情况。

observe

提几个问题:

  • observe 是对全部数据做观察吗?
  • 响应式数据有什么特征?可以让我们快速地判断一个数据是不是响应式的。🧐

带着问题把代码溜起来,先看到入口:

export function initState (vm: Component{
  // 初始化一个 _wathcers 数组,作用我们后面看
  vm._watchers = []
  const opts = vm.$options

  // 初始化props
  if (opts.props) initProps(vm, opts.props)

  // 初始化函数
  if (opts.methods) initMethods(vm, opts.methods)

  // 初始化data
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }

  // 初始化computed
  if (opts.computed) initComputed(vm, opts.computed)

  // 初始化watch
  // 这个 nativeWatch 定义在 src/core/util/env.js 中
  // nativeWatch = ({}).watch,注释说的是 Firefox 中对象原型有这么一个方法,但是我没有打印出来,囧
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

propscomputedwatch 那些我们放到后面再深究,先从主流程看起。聚焦到 data 部分:

function initData (vm: Component{
  // 获取合并 data 的处理函数,在mergeField时会合并,栗子 🌰 中的 parentVal 是 undefined,所以这里的 data 就是我们看到的 data
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  
  // ... 省略一大段报警信息 🚔

    // 判断key的首字符是不是 $ 或 _
    // 不是将属性代理到 vue 实例中
    } else if (!isReserved(key)) {

      // 代理到实例下面
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  // 调用 observe 方法观测整个 data 的变化,把 data 变成响应式
  observe(data, true /* asRootData */)
}

定义数据的首字符不是 $_ 作为第一个字符的话,会调用 proxy 代理到 Vue 实例上。这个过程在 [咖聊] Vue 执行过程 有举例说明,忘记的童鞋可以回去看看。最后调用 observedata 数据变成响应式:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void

  // 如果有 __ob__ 属性并且是 Observer 的实例,说明已经是个响应式对象,就直接返回
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__

  // 没有的话实例一个ob对象
  } else if (
    shouldObserve &&  // 定义的变量,这里是true,在initProps会改变该值,后面分析 props 的过程再看
    !isServerRendering() && // 服务端渲染
    (Array.isArray(value) || isPlainObject(value)) && // 数组或纯对象
    Object.isExtensible(value) && // 可扩展
    !value._isVue // 不是 vue 实例
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

observe 先判断数据是不是响应式的,如果是直接返回;不是的话判断几个条件(栗子 🌰 中都满足),然后把数据对象当作参数传到 Observer 构造函数,实例化一个 ob

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value

    // 实例化一个 Dep 对象
    this.dep = new Dep()
    this.vmCount = 0

    // 把自身实例添加到数据对象 value 的 __ob__ 属性上
    // 定义在 src/core/util/lang.js
    def(value, '__ob__'this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */

  // 将每个属性转换成 getter/setter,注意函数参数是对象
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */

  observeArray (items: Array<any>) {

    // 遍历数组再次调用 observe
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

Observer 构造函数中把参数赋值给 value 私有属性,然后调用 def 函数在对象上挂上 __ob__ 属性,最后判断值如果是数组,循环调用 observe,如果是对象,那依次对每个 key 执行 defineReactive。回到栗子 🌰 中,执行完 def之后,我们的 value 变成下图所示:

__ob__.jpg
__ob__.jpg

我们的 value 是对象,会执行到 walk 函数,我们先来看下 observe 的重点——defineReactive:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
{

  /*在闭包内存储一个Dep对象*/
  const dep = new Dep()

  // 拿到 obj 的属性描述符
  const property = Object.getOwnPropertyDescriptor(obj, key)

  // 判断,如果是不能配置的属性
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  // 如果你之前就定义了 getter 和 setter,这里获取定义值
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  // 对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerabletrue,
    configurabletrue,
    getfunction reactiveGetter ({
      // ...
    },
    setfunction reactiveSetter (newVal{
   // ...
    }
  })
}

第一个重点,获取属性的描述符,如果对象是不可配置的,那么我们就不会设置 gettersetter。这也是为什么我们在写代码的时候使用 Object.freeze 包装常量的原因,再举个栗子 🌰 补充说明一下:

Object.freeze.jpg
Object.freeze.jpg

回到本文的栗子 🌰 中

第一个属性 { message: 'this is a message' }。直接给 message 挂上 getsetover

第二个属性,数组的情况:

relationship: [{
    name'myFatherName',
    relation'Father and son'
}, {
    name'myBrotherName',
    relation'brothers'
}],

数组会对原型上的部分函数做拦截,然后重新定义方法:

// 后面代码多次访问该原型,所以提前缓存起来,避免多次访问属性。编码小技巧,你get 到了吗?
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method{
  // 缓存原来的方法
  const original = arrayProto[method]
  // 重新定义数组相关的API
  def(arrayMethods, method, function mutator (...args{
    // 先按照原来定义执行一遍
    const result = original.apply(this, args)
    // 附加的其他逻辑,这部分代码后面派发更新时会详细分析到
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 调用 observe 把插入的项也变成响应式数据
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

飘远了,回到 defineReactive 中来,数组值执行到下面这行代码时:

let childOb = !shallow && observe(val)

执行到 observe,然后会进入下面这部分逻辑:

if (Array.isArray(value)) {
    
    // hasProto 是判断是否存在 __proto__ 属性
    // 存在直接 value.__proto__ = arrayMethods
    // 否则通过 def 函数重新定义数组原型上的方法
    const augment = hasProto
    ? protoAugment
    : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
}

然后通过 observeArray 给数组每一项都调用 observe,栗子 🌰 中的数组项是对象,处理过程跟第三个属性一毛一样,所以我们看第三个属性的处理方式:

address: {
    type'country',
    name'China',
    children: {
        type'province',
        name'GuangDong',
        children: {
            type'city',
            name'ShenZhen',
        }
    }   
}

很简单,既然是对象,就回到了一开始 data 的逻辑循环中,defineReactive -> observe -> walk -> defineReactive ,就这样给每个属性都标记上了 __ob__ 属性。最终处理完的对象如下图所示:

observe-result.jpg
observe-result.jpg

小结

响应式第一步工作给 data 里的每一个属性都通过 Object.defineProperty 挂上 getset,小节开头的两个问题也就迎刃而解了。上面 defineReactive 代码逻辑中,将 getset 的逻辑去掉了,接下来我们就来补充这部分的逻辑。

依赖收集

再提几个问题:

  • 什么时候会访问到上面这部分数据,触发数据 get 呢?
  • 依赖收集具体含义是什么?依赖是指什么?收集到哪里?

observe 好了数据之后,程序主流程会进入到挂载阶段——$mount。这时栗子 🌰 就会经过上一节我们分析过的生成 render 函数过程,不了解的童鞋可以前往 [咖聊] “模板编译”真经 了解模板过程。

有了 render 函数,就会进入 mountComponent

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component 
{
  vm.$el = el
  // ...
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    // ...
  } else {
    updateComponent = () => {

      // vm._render()生成虚拟节点
      // 调用 vm._update 更新 DOM
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined

  // 实例化 watcher
  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before () {
        if (vm._isMounted) {
          callHook(vm, 'beforeUpdate')
        }
      }
    },
    true /* isRenderWatcher */)

  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook

  // vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

逻辑一大段,我们先不管,直接进到 Watcher 类里面看看:

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  computed: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  dep: Dep;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
      
    // 第5个参数标志是不是渲染watcher,整个Vue有3种watcher:renderWatcher、computedWatcher、userWatcher,在执行mountComponent时,这里是true的
    if (isRenderWatcher) {
        
      // 把渲染 watcher 挂在实例的 _watcher 上
      vm._watcher = this
    }
      
    // vm._watchers 在前面 initState 时初始化了
    vm._watchers.push(this)
    // 这里的 options 是上面说到的 3 种 watcher 对应的一些属性
    // 对于渲染watcher过程,这里有 before 函数,其他都是false
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.computed = !!options.computed
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.computed // for computed watchers

    // 跟dep相关的一些属性
    // 表示 Watcher 实例持有的 Dep 实例的数组
    this.deps = []
    this.newDeps = []

    // 代表 this.deps 和 this.newDeps 的 id Set
    this.depIds = new Set()
    this.newDepIds = new Set()

    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {

      // 这里的 getter 是 updateComponent 函数
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function ({}
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }

    // computed watcher并不会立刻求值
    if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } else {

      // 执行watch的get函数
      this.value = this.get()
    }
  }

  // ...
}

Watcher 类定义很长,我们先只看构造函数,就是给实例上挂各种属性。作用后面遇到了再看。这里最终会执行到:

this.value = this.get();

看下 get 函数定义:

get () {
    // 把当前的 watcher 对象 push 到 targetStack 中
    // 然后赋值给 Dep.target,这个作用后面会讲到
    pushTarget(this)
    let value
    const vm = this.vm
    try {

        // this.getter 对应就是 updateComponent 函数
        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
        // deep watcher
        if (this.deep) {
            traverse(value)
        }
        popTarget()
        this.cleanupDeps()
    }
    return value
}

pushTargetpopTarget 的定义在 src\core\observer\dep.js:

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
const targetStack = []

export function pushTarget (_target: ?Watcher{

  // Dep.target 赋值为当前的渲染 watcher 并压栈(为了恢复用)
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget ({
  Dep.target = targetStack.pop()
}

get 函数对应的 value = this.getter.call(vm, vm) 最终会执行到 updateComponent,回到 mountComponent 中查看这部分逻辑:

updateComponent = () => {
    // vm._render()生成虚拟节点
    // 调用 vm._update 更新 DOM
    vm._update(vm._render(), hydrating)
}

vm._render() 会执行 render 函数,生成 VNode

Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    // ...
    let vnode
    try {

        // 写 render 函数的第一个参数 h,就是 vm.$createElement
        // vm._renderProxy 就是 vm,在 init 阶段定义了
        vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
        // ...
    }
    // ...
    // set parent
    vnode.parent = _parentVnode
    return vnode

}

栗子 🌰 生成的 render 函数:

    with (this) {
        return _c('div', {
            attrs: {
                "id""app"
            }
        }, [_c('div', [_v(_s(message))]), _v(" "), _l((relationship), function(item{
            return _c('div', {
                key: item.name
            }, [_c('span', [_v("name:" + _s(item.name))]), _v(" "), _c('span', [_v("relation:" + _s(item.relation))])])
        }), _v(" "), _c('div', [_c('button', {
            on: {
                "click": addRelatives
            }
        }, [_v("添加亲戚")])]), _v(" "), _c('p', [_v("\n      Province: " + _s(address.children.name) + "\n    ")])], 2)
    }
}

生成的 render 函数有一堆小矮人,_v_l_s,这些小矮人定义在 src\core\instance\render-helpers\index.js

import { toNumber, toString, looseEqual, looseIndexOf } from 'shared/util'
import { createTextVNode, createEmptyVNode } from 'core/vdom/vnode'
import { renderList } from './render-list'
import { renderSlot } from './render-slot'
import { resolveFilter } from './resolve-filter'
import { checkKeyCodes } from './check-keycodes'
import { bindObjectProps } from './bind-object-props'
import { renderStatic, markOnce } from './render-static'
import { bindObjectListeners } from './bind-object-listeners'
import { resolveScopedSlots } from './resolve-slots'

export function installRenderHelpers (target: any{
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
}

这部分函数都是封装的 VNode 工具函数。具体函数的执行这里不讨论,放在后面 VNode的生成章节。但是我们可以明确的是,在生成 VNode 的过程中,一定是会访问到我们的数据(this.messagethis.relationship等等),这时就会触发数据 getter,此时就把上一小节省掉的 get 函数拉回来:

get: function reactiveGetter () {
    
    // 当前值,比如 message 就是 this is a message
    const value = getter ? getter.call(obj) : val
    
    // 还记得大明河畔的夏雨荷吗?
    // 在执行 Watcher 的 get 时,执行了 pushTarget,当时给 Dep.target 赋值了渲染 Watcher
    if (Dep.target) {

        // 依赖收集
        dep.depend()
        
        // 如果有子ob,就一起收集了
        if (childOb) {
            childOb.dep.depend()
            if (Array.isArray(value)) {
                dependArray(value)
            }
        }
    }
    return value
},

万水千山,终于到本节的重点了。在 observe 过程中,实例化了一个依赖实例

var dep = new Dep();

调用 dep.depend 收集依赖,这会我们看到上一小节忽略掉的 Dep 类:

export default class Dep {
  // 静态属性 target,这是一个在任何时间都是全局唯一 Watcher
  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 指渲染 watcher
      Dep.target.addDep(this)
    }
  }
  
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Dep.target.addDep(this) 回到 Watcher 中执行 addDep 逻辑:

addDep (dep: Dep) {
    const id = dep.id
    
    // 防止重复添加,去重
    if (!this.newDepIds.has(id)) {

        // 把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中
        // 这个目的是为后续数据变化时候能通知到哪些 subs 做准备
        this.newDepIds.add(id)
        this.newDeps.push(dep)
        if (!this.depIds.has(id)) {
            
            // 把当前渲染 watcher 添加到依赖实例的 subs 数组中
            dep.addSub(this)
        }
    }
}

然后再把 渲染watcher 加到 depsubs 数组中,作为订阅项收集。

render 函数执行完了之后,还有一小部分逻辑:

// 处理 deep 的 user watcher
if (this.deep) {
    traverse(value)
}
// 恢复成上一个状态,当前vm的依赖收集完了,对应的渲染也要更新
// 在栗子 🌰 中把 Dep.target 变成 undefined
popTarget()
this.cleanupDeps()

最后这个 cleanupDeps 做了哪些动作呢?来🧐🧐

cleanupDeps () {
    let i = this.deps.length
    while (i--) {
        const dep = this.deps[i]
        if (!this.newDepIds.has(dep.id)) {
            // 移除对Watcher的订阅
            dep.removeSub(this)
        }
    }
    
    // 交换 newDepIds 和 depIds
    let 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
}

因为每次修改数据都会重新触发 render 函数的执行,也就是会重新做依赖收集的动作。因此设计新老 depIds 是为了做最小化的更新。

小结

依赖收集会发生在 render 函数生成 VNode 的过程中。因为会访问到具体的数据,从而触发数据的 get ,然后当前的渲染 watcher 会把数据当作依赖添加到实例相关属性上,dep 也会把渲染 watcher 存到订阅者(subs)数组中。当我们修改数据时,就会用收集的结果去触发渲染 watcher 更新,下面我们就来看下派发更新的详细过程。

派发更新

栗子 🌰 中点击“添加亲戚”按钮,就会 push 一项数据到 relationship 中。我们来看一下这个过程。前面我们讲过,就是数组的操作会重写原型方法来附加派发更新的逻辑。当我们执行代码:

this.relationship.push({
    name'myMomName',
    relation'Mom and son'
});

就会触发拦截之后的数组方法:

// 重新定义数组相关的API
def(arrayMethods, method, function mutator (...args{
    // 先按照原来定义执行一遍
    const result = original.apply(this, args)
    // 获取当前实例 this.relationship 的 ob 对象
    const ob = this.__ob__
    let inserted
    switch (method) {
        case 'push':
        case 'unshift':
            inserted = args
            break
        case 'splice':
            inserted = args.slice(2)
            break
    }
    // 调用 observe 把插入的项也变成响应式数据
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
})

栗子 🌰 中的是 push 一个数组项,拿到当前属性的 __ob__ 对象,对插入的项做响应式处理(ob.observeArray)。最终触发 ob.dep.notify()

notify () {
    const subs = this.subs.slice()
    // 依次触发渲染 watcher 的 update
    for (let i = 0, l = subs.length; i < l; i++) {
        subs[i].update()
    }
}

上面的 update 会触发位于 src\core\observer\watcher.jsupdate 方法:

update () {
    // ...
    // 这里省略掉 computed watcher 和同步 user watcher 的逻辑,直接看栗子 🌰会执行到的 queueWatcher
    queueWatcher(this)
}

queueWatchersrc\core\observer\scheduler.js 中定义:

const queue: Array<Watcher> = []
let waiting = false
let flushing = false
let index = 0

export function queueWatcher (watcher: Watcher{
  const id = watcher.id

  // 确保一个 watcher 只添加一次
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      // 把当前 watcher 加入到队列中
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 10, watcher)
    }
    // queue the flush

    // 保证只执行一次nextTick
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

queueWatcher 可以看到是把渲染 watcher 加入到队列中,并不是每次更新都实时执行。然后执行到 nextTick(flushSchedulerQueue)

export function withMacroTask (fn: Function): Function {
  return fn._withTask || (fn._withTask = function ({
    useMacroTask = true
    const res = fn.apply(nullarguments)
    useMacroTask = false
    return res
  })
}

export function nextTick (cb?: Function, ctx?: Object{
  let _resolve

  // 把传入的函数压入callbacks数组,需要callbacks而不是直接执行cb是因为多次执行nextTick时,能用同步顺序在下一个tick中执行,而不需要开启多个宏/微任务
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    // 对于手动触发的一些数据更新,比如栗子 🌰 中的 click 事件,强行走宏任务 
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // $flow-disable-line
  // 当nextTick不传函数时,提供一个promise化的调用

  // 不传cb直接返回一个promise的调用
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

nextTick 之前,我们先看宏任务 macroTimerFunc 和微任务 microTimerFunc 的实现:

// 宏任务的实现:先看是否支持 setImmediate,然后判断是否支持 MessageChannel,最终 setTimeout 兜底
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
else {
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
// 微任务实现:先判断是否支持Promise,否则直接指向 macroTimerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
    // in problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
else {
  // fallback to macro
  microTimerFunc = macroTimerFunc
}

回到 nextTick, 把回调函数存到 callbacks,通过 macroTimerFunc 触发 flushCallbacks,然后依次调用 callbacks 中的函数——flushSchedulerQueue

function flushSchedulerQueue ({
  flushing = true
  let watcher, id

  /**
   * 1.组件的更新由父到子;因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。
   * 2.用户的自定义 watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的。
   * 3.如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。
   */

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

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  // 这里不缓存队列长度的原因是在 watcher.run() 的时候,很可能用户会再次添加新的 watcher
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    // 执行 before 函数,也就是 beforeUpdated 钩子
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  // 重置调度队列的状态
  resetSchedulerState()

  // call component updated and activated hooks
  // 执行一些钩子函数,比如keep-alive的actived和组件的updated钩子
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

先对 watcher 做了排序,然后依次执行组件的 beforeUpated 的钩子函数。依次调用 wathcer.run 触发更新:

run () {
    if (this.active) {
        this.getAndInvoke(this.cb)
    }
}

getAndInvoke (cb: Function) {
    // 对于渲染 watcher 而言,调用 get 会重新触发 updateComponent,就又会重新执行依赖收集,不一样的是更新时是有老的VNode,会做对比再重新patch
    const value = this.get()
    if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
    ) {
        // set new value
        const oldValue = this.value
        this.value = value
        this.dirty = false
        if (this.user) {
            try {

                // 这就是为什么我们写自定义watcher时能拿到新值和旧值
                cb.call(this.vm, value, oldValue)
            } catch (e) {
                handleError(e, this.vm, `callback for watcher "${this.expression}"`)
            }
        } else {
            cb.call(this.vm, value, oldValue)
        }
    }
}

然后就会执行 getAndInvoke 函数,其实也就是执行 this.get() 获取当前的值,如果满足新老值不相等、新值是对象、deep 模式下的任一条件,就执行回调 cb。回调中的参数是新老值,这也是为什么我们写自定义 watcher 有新老值参数的原因。对于栗子 🌰 而言,这里 this.get 重新触发 updateComponent

小结

在数据更新时,会触发订阅数组中 watcherupdate,但并不是每次改变都会实时更新,而是通过队列维护更新列表。最后经过队列排序、队列遍历分别触发 watcherrun,从而触发 VNode 的更新,页面的刷新。flushSchedulerQueue 最后还做了数据重置的工作。

总结

最后再看到官方文档响应式原理图:

data.png
data.png

根据上图做一个总结:在 render 阶段,会读取到响应式数据,触发数据的 getter,然后会通过 Deprender watcher 的收集。当修改响应式数据时,会触发数据的 setter,从而触发 Depnotify。让所有的 render watcher 异步地更新,从而重新渲染页面!我们知道,重新渲染页面会做 VNodediff,然后高效地渲染页面。这部分内容我们留在下一小节分析。

jouryjc

2021/07/11  阅读:51  主题:橙心

作者介绍

jouryjc