nextick原理

我丢,我这个居然忘了写,要不是重新看我的面经的话差点就忘了。啧啧啧,补上。(我发现我之前理解的nextick有点毛病,重新去看了一下技术博客和书总结了一下。
5VmvOP.jpg

什么是异步渲染


对于上面的代码,首先假如是直接去渲染的话肯定是要渲染两次:第一次渲染,然后变成第二次渲染。这样的话就会造成性能浪费,因为我们实际上只需要渲染最后一次就行了。所以vue就使用了异步渲染,去达到只渲染一次这种效果。也就是说它将第一次赋值和第二次赋值带来的响应进行了合并,然后对最后的datas进行了一次页面渲染。

为什么需要异步渲染

从用户体验,你看第二次重新改变的话肯定会闪烁一下,那肯定用户就不得舒服了晒。然后就是性能,渲染一次肯定要比渲染两次的性能消耗要低一点。

怎么实现vue异步渲染呢?

在Vue中的异步渲染实际上是在数据每次变化时,将其所要引起页面变化的部分都放到一个异步API的回调函数里,直到同步代码执行完之后,异步回调开始执行,最终将同步代码里所有需要渲染变化的部分合并起来,最终执行一次渲染操作。比如上面的例子,首先当datas第一次赋值,页面渲染文字,但是这个渲染会暂存,datas第二次赋值时,再次暂存将要引起的变化,这些变化会被丢到异步API,promise.then的回调函数中(微任务队列),等到所有同步代码执行完之后,then函数的回调函数得到执行,然后将遍历存储着数据变化的全局数组,将数组里的所有数据确定先后优先级,最终合并成一套需要展示到页面上的数据,执行页面渲染操作。
那么在遍历数组的时候进行了什么操作呢?就是会对数组里面的数据进行一些筛查操作,将重复操作国的数据进行处理,实际就是先复制的丢弃不渲染,然后按照优先级组合成一套数据渲染。这样就大概是原理的全部内容,接下来从原理入手分析。

异步渲染原理

首先当我们给datas赋值的时候this.datas = '第一次渲染',datas所绑定的Object.defineProperty的setter函数触发,然后触发所订阅的notify函数。(vue响应式原理)

defineReactive() {
  ...
  set: function reactiveSetter(newVal) {
    dep.notify()
  }
}

在notify中,会将收集的所有订阅组件watcher中的update方法执行一遍。

Dep.prototype.notify = function notify() {
  ...
  let subs = this.subs.slice();
  for(let i = 0; i < subs.length; i++) {
    subs[i].update()
  }
}

update函数得到执行后,默认情况下lazy是fasle,sync也是false,直接进入把所有响应变化存储进全局数组queueWatcher函数下。

Watcher.prototype.update = function update() {
  ...
  if(this.lazy) { //如果是懒加载
    this.dirty = true;
  } else if (this.sync) { //如果是同步,立即响应
    this.run()
  } else {
    queueWatcher(this)
  }
}

queueWatcher函数里,会先将组件的watcher存进全局数组变量queue里。默认情况下config.async是true,直接进入nextTick的函数执行,nextTick是一个浏览器异步API实现的方法,它的回调函数是flushSchedulerQueue函数。

function queueWatcher(watcher) {
  ...
  queue.push(watcher) //在全局队列里存储将要响应的变化update函数
  if(!config.async) { //如果页面更新是同步
    flushSchedulerQueue()
    return
  } 
  //将页面更新函数放进异步API里执行,同步代码执行完开始执行更新页面函数
  nextTick(flushSchedulerQueue);
}

nextTick函数执行后,传入的flushSchedulerQueue又一次被push进callbacks全局数组中,pending在初始情况下是false,这时候将触发timerFunc。

function nextTick(cb, ctx) {
  let _resolve;
  callbacks.push(function() {
    if(cb) {
      try {
        ca.call(ctx)
      } catch(e) {handleError(e, ctx, 'nextTick)}
    } else if(_resolve){
      _resolve(ctx)
    }
  })

  if(!pending) {
    pending = true
    timerFunc();
  }
  if(!cb && typeof Promise !== 'undefined) {
    return new Promise(resolve => {_resolve = resolve})
  }
}

timerFunc函数是由浏览器的Promise、MutationObserve、setImmediate、setTimeout这些异步API实现的,异步API的回调函数是flushCallbacks函数。(为了简便这里只写Promsie)

    let timerFunc;
    if(typeof Promise !== 'undefined' && isNative(Promise)) {
        let p = Promise.resolve();
        timerFunc = function() {
        p.then(flushCallbacks);
        if(isIos) {setTimeout(noop)}   
        }
        ifUsingMicroTask = true
    }...

flushCallbacks函数中将遍历执行nextTick里push的callback全局数组,全局callback数组中实际上是是之前push的flushScheduleQueue的执行函数

//将nextTick里push进去的flushScheduleQueue函数依次调用
function flushCallbacks() {
  pending = false;
  let copies = callbacks.slice(0) //复制
  callbacks.length = 0
  for( let i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

callback遍历执行的flushScheduleQueue函数中,flushScheduleQueue里先按照id进行了优先级排序,接下来将之前存储的watcher对象全局queue遍历执行,触发渲染函数watcher.run

function flushScheduleQueue() {
  let watcher, id;
  //按照id从小到大开始排序,越小的越前触发
  updatequeue.sort((a, b) => a.id - b.id)
  //queue是全局数组,它在queueWatcher函数里,每次updates触发的时候将当时的watcher给push进去
  for(let i = 0; i < queue.length; i++) {
    ...
    watcher.run()
  }
 //渲染
}

watcher.run的实现在watcher原型链上,初始状态下active属性为true,直接执行watcher原型链的set方法。

Watcher.prototype.run = function run() {
  if(this.active) {
    let value = this.get()
...
  }
}

get函数中,将实例watcher对象push到全局数组中,开始调用实例的getter方法,执行完毕后,将watcher对象从全局数组弹出,并且清除已经渲染过的依赖实例。

Watcher.prototype.get = function get() {
  pushTarget(this);
  //将实例push到全局数组targetStack
  let VM = this.vm
  value = this.getter.call(vm, vm);
  ...
}

实例的getter方法实际是在实例化的时候传入的函数,也激素下面vm的真正更新函数_update。

function () {
  vm._update(vm.render(), hydrating)
}

实例的_update函数执行后,将会把两次的虚拟节点传入vm的patch方法执行渲染操作

Vue.prototype._update = function (vnode, hydrating) {
  let vm = this
  ...
  let prevVnode = vm._node
  vm._vnode = vnode
  if(!prevVnode) {
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
  } else {
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
 
}

所以上面就是所有的源码了。然后提几点注意的,首先nextTick既有可能是微任务,也有可能是宏任务,从优先去Promise和MutationObserve可以看出nextTIck优先微任务,其次是setImmediate和setTimeout宏任务。(我们只写了promsie)
同时,vue也能进行同步渲染,只需要将设置Vue.config.async = false就行了。
5Vof3t.md.jpg

posted @ 2021-10-11 14:06  卿六  阅读(358)  评论(0编辑  收藏  举报