vue响应式源码解析(二)

依赖派发

先前的文章主要提到的是响应式数据和依赖收集这部分逻辑,下面会花一些时间来讲述一下,依赖触发这部分的逻辑。

处理逻辑

依赖派发这部分逻辑针对对象和数组有着不同的处理方式,对象类型的依赖触发是在defineReactive中定义,而数组的依赖触发是在拦截器中定义,下面先来看一下对象类型的这部分逻辑处理:

defineReactive定义

相关源码:

defineReactive(
 obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
){
    //...
    setter时触发当前逻辑,进行依赖触发
      set: function reactiveSetter (newVal) {
      如果对象本身就要getter方法,则先调用getter方法
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // 值相同或者都为NAN,返回即可
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // 如果setter存在,调用setter方法
      if (setter) {
        setter.call(obj, newVal)
      } else {
      // 否则直接赋值
        val = newVal
      }
      // shallow为false,即不传的情况下,会将value变成响应式的
      childOb = !shallow && observe(newVal)
      // 触发依赖
      dep.notify()
    }
}

可以看到上面实际上核心流程就一句,setter时调用dep.notify方法,通知到相关的dep

dep.notify定义

相关代码:

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

notify方法逻辑也很简单,循环遍历subs中每一项,调用它的update方法,subs中存储的就是相关的watcher,所以下来看一下update方法实现。

update方法实现:

相关代码:

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   * 依赖项更新时触发当前逻辑
   */
  update() {
    /* istanbul ignore else */
    // 计算属性处理
    if (this.computed) {
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true;
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() => {
          this.dep.notify();
        });
      }
      // 同步直接调用run方法,立即执行
    } else if (this.sync) {
      this.run();
    } else {
      // 这块不看后面代码,猜也能知道肯定跟任务队列有关
      queueWatcher(this);
    }
  }

上面核心流程也就是最后那个queueWatcher方法,传递的参数为watcher实例,下面我们看一下它的实现:

queueWatcher方法实现

相关代码:

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  // 这个id就是watcher中定义的uid,dep中也有类似实现
  const id = watcher.id
  // 这几步目的就是剔除掉任务队列中重复的watcher,避免重复渲染
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      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 + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

这部分主要功能就是将相应的watcher push进一个队列中,去除重复的watcher,在nextTick后调用flushSchedulerQueue进行异步更

新。

flushSchedulerQueue 方法实现

相关代码:

/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  // 修改先前的标记位,这样下一次就可以走到else的逻辑
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  // 尤大在这里的注释非常到位,赞一个
  // 1. 组件是从parent到child这个顺序开始更新的,因为父组件总是比子组件先创建
  // 2. 一个组件的用户watcher会在渲染watcher前执行,因为用户定义的watcher是在渲染watcher之前创建的
  // 3. 如果一个组件在父组件watcher 运行期间被销毁了,那它的watcher就会被跳过
  
  // 给任务队列拍个序,按照id自增的顺序排队,那肯定用户定义的比较早,id在前面,执行先执行
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  // 不对队列的长度做缓存,因为在watcher运行期间,可能会有更多的watcher被push进来
  for (index = 0; index < queue.length; index++) {
    // 注意一点:index不是在当前函数内部定义的,所以在执行过程中,函数外部就可以知道执行到队列中那个位置了
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // 在执行run方法时,可能会产生新的watcher,也就会再次调用queueWatcher方法
    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
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  // 这里的逻辑是为了chrome的调试工具准备的
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

在上面这段代码执行过程中,执行run方法时,可能会产生新的watcher,也就会再次调用queueWatcher方法,这时由于标志位flushing被修改为true,所以就会走到else的逻辑:

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
      // 现在就会走到else逻辑
    } 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
      // 因为index是在外部定义的,所以就可以知道执行到什么位置
      // 找出当前执行到的watcher刚好小于插入watcherid的位置,执行插入操作
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

先前流程我在注释中有详细解释,有疑问参考注释即可,当执行完毕后,queue队列长度发生改变,这也就是为什么之前不会去缓存队列的长度。

之后会执行一个resetSchedulerState的函数,现在查看它的定义:

resetSchedulerState定义

相关代码:

export const MAX_UPDATE_COUNT = 100

const queue: Array<Watcher> = []
const activatedChildren: Array<Component> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0

/**
 * Reset the scheduler's state.
 */
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}

该方法作用就是清空当前的任务队列,恢复默认值,等待下一次的执行。

现在来看一下watcher.run方法的实现

run方法定义

相关代码

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run() {
    if (this.active) {
      this.getAndInvoke(this.cb);
    }
  }

  getAndInvoke(cb: Function) {
   	// 执行get方法,获取新值
    const value = this.get();
    // 新值与旧值不同、值是对象类型、使用了deep,满足任一条件,都执行回调函数
    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) {
        // 执行回调函数的时候,会把新值和旧值都传回去,这就是使用watch时拿到的函数参数
        try {
          cb.call(this.vm, value, oldValue);
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`);
        }
      } else {
        cb.call(this.vm, value, oldValue);
      }
    }
  }

如果是执行渲染watcher,会触发依赖收集逻辑,从而将watcher收集到dep中。

总结

依赖派发就是在setter时触发相关的依赖,调用dep中的notify函数,通知到subs中相应的watcher,watcher调用内部的run方法或者添加到一个异步队列当中,在nextTick中执行,在这里做了一定的优化,从而避免了watcher的重复触发。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值