Vue批量异步更新策略

1. 事件循环简介

在一次更新周期内,可能有好多组件都需要更新,最高效的方式就是把这些组件集中批量一起更新,全部更新完后浏览器再一下子刷新页面

浏览器同一时间只执行一个宏任务,宏任务执行完之后就要刷新一下页面(如果还有微任务则微任务也要执行完才刷新页面)

把刷新任务放在微任务当中,同步任务执行完,批量执行微更新dom,然后浏览器刷新一次界面

dom刷新问题,dom何时刷新的

假设执行如下代码,能立刻看到弹窗吗,答案是不能,需要等两秒,为什么

scss 复制代码
function work(){  
     showTip(); // 这里对dom进行了更新,show出提示信息或弹出模态窗口之类 
     calculate();  //计算逻辑,很复杂,需要2s
     hideTip();  // 把提示信息隐藏起来,同样有对dom的改动  
} 

答:浏览器内核是多线程的,有一个常驻线程叫JavaScript引擎线程,负责执行js代码还有一个常驻线程叫GUI渲染线程,负责执行页面渲染。js线程和GUI渲染线程是互斥的,当执行js的时候,GUI线程会被挂起。所以,我们可以理解为什么dom更新总是不能被立刻执行。就我们上面代码来说,弹窗的操作dom被浏览器记录下来并放在了GUI线程的任务队列中,并没有立刻执行,当js线程的当前事件循环执行完毕(宏任务和微任务都执行完毕),此时会走一次GUI线程渲染(页面更新),此时才看到结果,如果js线程计算量特别大卡住了,GUI线程自然也就必须等待直到js执行完毕。

以下代码能在浏览器上看到实时的数值变化吗

ini 复制代码
var lis = document.getElementsByTagName("li");  
for (var i=0; i<lis.length; i++) { // yes this could be more efficient, don't care  
  // do something here to lis[i]  
  div.innerHTML = "processing list item " + i; // fail 
}; 

答:不能,div.innerHTML这行代码是实时执行的,并不是异步,但是,执行完以后,页面并没有立刻刷新。刷新是另外一个GUI线程做的事情,要等当前时间循环执行完毕才会渲染,所以只能看到最后一次变化,可以修改成异步递归执行,每次改一次值然后就等下一次时间循环再改一次,这个样就可以做到每个数值都被刷新

ini 复制代码
var lis = document.getElementsByTagName("li");  
var counter = 0;  
function doWork() {  
  // do something here to lis[i]  
  counter += 1;  
  div.innerHTML = "processing list item " + counter;  
  if (counter < lis.length) {  
    setTimeout(doWork, 1);  
  }  
};  
setTimeout(doWork, 1);

2. 依赖收集流程

总体步骤汇总

前置条件

已经完成了数据响应式的初始化。在数据响应式初始化的时候defineReactive,此时并没有触发getter,getter(依赖收集)是在挂在$mount的渲染的步骤,读了数据的时候才触发的(在render里面),此时dep.target是有值的,执行完渲染之后target会立即清空。

代码流程

  1. 挂载函数里的渲染函数render里访问数据,触发getter

  2. Dep.target有值,执行dep.depend()

  3. Dep.target.addDep(this)

  4. 执行watcher中的addDep (dep: Dep)

  5. dep.addSub(this)

  6. 把watcher收集进了dep里面的subs容器,完毕

详细代码步骤

1. 渲染函数render里访问数据,触发getter

_render函数里的这一行代码触发

ini 复制代码
vnode = render.call(vm._renderProxy, vm.$createElement)
2. Dep.target有值,执行dep.depend()

Dep.target的值是在执行watcher的get函数的时候触发的,watcher是在挂载渲染的时候,初始化new的会执行一次get

如果是数组,则把数组循环递归执行dep.depend,depend的都是同一个watcher

scss 复制代码
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 这个dep是一个类,主要作用是建立起数据和watch之间的一个桥梁
      // target就是watcher,同一时间只有一个watch能被计算
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },


function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}
3. Dep.target.addDep(this),addDep (dep: Dep)

执行watcher中的addDep方法(此时this指向watcher),然后执行dep中的addSub方法,把当前watcher push进subs里面。依赖收集完毕,defineReactive返回值return value

kotlin 复制代码
  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)
      }
    }
  }

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

3. 派发更新流程

总体步骤汇总

前置条件:1.数据响应式已经初始化完毕,每个数据都有了唯一dep;2.各个watcher都已经创建完毕,已经被收集仅getter(依赖收集)。

初始化数据,响应式defineReactive监听数据,此时已经响应式,但是target为null

初始化watcher(initWatcher),此时创建一次watcher实例,创建同时执行一次get,get这个函数,先把watcher作为target退给dep,使得dep.target有值,然后立刻第一次真正的渲染value = this.getter.call(vm, vm),此时触发getter函数,这个watcher就被收集进dep里面了,这个getter里面走了update逻辑真正的渲染,走完就能在界面中看到这个组件的渲染了。全程并没有触发setter,以后再次触发getter也不会再次push,因为不是在watcher初始化里面触发的。

代码流程

  1. 触发defineReactive里面定义的setter函数

  2. dep.notify():notify里面for循环批量执行watcher的update函数

  3. subs[i].update():执行watcher的update函数

  4. queueWatcher:queueWatcher干的事情就是把订阅者push到queue里面,等到下一个nextTick的时候批量执行

  5. nextTick:在p.then这个微任务里面执行flushSchedulerQueue函数。flushSchedulerQueue里面装了某个dep里面的所有watcher,nextTick里面会for循环批量执行,而callbacks里面又是装了所有的flushSchedulerQueue,意思是一次异步更新里面会有多个dep(多个数据发生改变),每个dep里面又有多个watcher要执行

  6. flushSchedulerQueue:先按照先父后子的顺序排序,然后for循环执行

  7. watcher.run():run函数里面会执行一次watch.get()实例方法

  8. watch.get():内部执行value = this.getter.call(vm, vm),真正开始干活的函数vm._update(vm._render(), hydrating)

详细代码步骤

1. 触发defineReactive里面定义的setter函数

有多少次赋值,就会触发多少次setter函数,for循环一万次则触发一万次setter。如果值一致,则会直接return,不往下走

objectivec 复制代码
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // 如果值是相同的则什么都不做
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 如果新值是一个对象,那么重新把它变成响应式对象
      childOb = !shallow && observe(newVal)
      // 这个函数就是派发更新的过程
      dep.notify()
    }
2. dep.notify()

subs里面装的是watcher,for循环执行subs里面的每个watcher的update函数。从这里可以看出dep就是管理watcher实例的,因为dep就是一个装watcher的容器,增删用watcher

css 复制代码
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
3. subs[i].update()

走到watcher里面的update方法,lazy和computed相关。如果sync为true,则同步更新立即更新,不会push到队列中,不会等到下一个刷新节点

kotlin 复制代码
  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
4. queueWatcher

1) 如果有了这个watcher(通过id判断)重复,则不再进行重复推进队列,这种情况出现在例如for循环里面重复修改值的情况。此处优化和setter那处的优化不同,setter那处是值相同,会触发setter,但不触发setter里面的dep.update,不会往下走,而此处是值不同,但是走的是同一个watcher,只执行一次就可以。

2) 在nextTick函数内执行,时间节点是本次宏任务执行完后,执行微任务时候执行。然后浏览器刷新,然后接着执行下一个任务队列的宏任务(非vue知识)

arduino 复制代码
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  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

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}
5. nextTick

在nextTick里面执行flushSchedulerQueue函数,nextTick原理详见nextTick原理文档

6. flushSchedulerQueue

1) watcher执行之前会先排序,用户自定义的先执行,,根据id从大到小来排。组件的更新时父到子的(watcher执行先父后子)

2) 有watcher.before会先执行watcher.before,渲染函数才有才可能有这个,用户自定义没有

3) for循环执行watcher.run,真正干活的函数

4) 执行完所有的watcher.run之后,清空队列queue,并且触发更新的钩子callUpdatedHooks(updatedQueue)

scss 复制代码
/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  flushing = true
  let watcher, id

  // 为什么要从小到大排序,
  // 组件更新是父到子的,为了保证父的在前面,子的在后面
  // 用户定义的watch要保证在渲染watch之前,也就是假设在用户watch里面定义一个alert,alert完以后页面的相应数据才会变化
  // 当我们的子组件在在父组件的watch中销毁了,那么子组件的watch就不用执行了
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    // TODO..
  }

  // 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 */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}
7. watcher.run

watcher.run里面执行this.get函数,this.get函数里面执行value = this.getter.call(vm, vm),这个函数是在依赖收集过程中收集进来的,真正渲染的函数,从此走到渲染逻辑里面vm._update(vm._render(), hydrating),此函数在创建watcher的时候传入。从此watcher的逻辑走完

kotlin 复制代码
  run () {
    if (this.active) {
      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
        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
  }
8. 往下走渲染逻辑,详情见渲染文档

4. 遗留问题

1. 为什么要清空deps,this.cleanupDeps(),这个是啥,里面装了啥数据,为什么执行完更新之后要清空(猜想:清空的是当前微任务一共要执行的dep,而不是清空某个dep里面装的watcher)

思路:不是额任何情况都清空的,有条件if (!this.newDepIds.has(dep.id)),要理解watcher的newDepIds和deps都是做什么用的

知识清空所有dep里面的当前这个watcher,不是把dep里面所有的watcher都清空

2. 为什么用户watcher是在渲染watcher之前执行的,体现在代码的哪里,用户watcher的逻辑流程呢

1)用户渲染watcher在合并配置的时候已经被合并到options上了,但是是怎么添加到对应的dep里面的?

在于一下这两行代码

ini 复制代码
this.getter = parsePath(expOrFn)

value = this.getter.call(vm, vm)

渲染函数typeof expOrFn === 'function'为true,不会走parsePath逻辑,但是用户函数,expOrFn确实一个key,watcher的中的函数的函数名字符串,parsePath解析出来会返回一个函数,赋值给getter,而正常渲染函数,getter就是渲染函数,会执行会访问data里面的数据。但是这个用户watcher,在执行的时候也会访问一次对应的data中的相应数据,是为了访问而访问的,没什么别的用处。由此而触发依赖收集

kotlin 复制代码
    function parsePath (path) {
      if (bailRE.test(path)) {
        return
      }
      var segments = path.split('.');
      return function (obj) {
        for (var i = 0; i < segments.length; i++) {
          if (!obj) { return }
          obj = obj[segments[i]]; // obj是vm,segments[i]就是键名,访问触发代理,进而触发了依赖收集
        }
        return obj
      }
    }
2)再解答为什么用户watcher会优先与渲染watcher执行

因为用户watcher更优先创建,在initWatch里面就创建了,而渲染watcher在挂载的时候才创建,所以用户watcher的id更加小。而当派发更新需要执行watcher的时候,会先利用id排序一下,保证用户id先执行

3. 初始化watch和初始化computed做了什么,computed的流程,和watcher的异同,体现在代码的哪里

相同点:

在是在初始化的时候就都是new 一个Watcher。

不同点:

computed多设置了了dirty为true的参数,watcher没有,computed会把自己这个回调函数被依赖的数据收集进dep中,以至于被依赖的数据派发更新的时候,也会执行computed,然后computed还会判断前后两次的值是否一直,不一致才会是dirty变更为true,然后重新计算。

kotlin 复制代码
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }

4. dep和watcher是多对多的关系,什么情况才是多对多,举例子

一个组件一个watcher,一个组件里面用到了好多data的值,就是watcher一对多。

一个数据除了管理了渲染watcher,还管理了用户watcher,用户computed,此时是dep一对多。

相关推荐
css趣多多13 分钟前
案例自定义tabBar
前端
林的快手2 小时前
CSS列表属性
前端·javascript·css·ajax·firefox·html5·safari
匹马夕阳2 小时前
ECharts极简入门
前端·信息可视化·echarts
API_technology2 小时前
电商API安全防护:JWT令牌与XSS防御实战
前端·安全·xss
yqcoder3 小时前
Express + MongoDB 实现在筛选时间段中用户名的模糊查询
java·前端·javascript
十八朵郁金香3 小时前
通俗易懂的DOM1级标准介绍
开发语言·前端·javascript
计算机-秋大田3 小时前
基于Spring Boot的兴顺物流管理系统设计与实现(LW+源码+讲解)
java·vue.js·spring boot·后端·spring·课程设计
m0_528723814 小时前
HTML中,title和h1标签的区别是什么?
前端·html
Dark_programmer4 小时前
html - - - - - modal弹窗出现时,页面怎么能限制滚动
前端·html
GDAL4 小时前
HTML Canvas clip 深入全面讲解
前端·javascript·canvas