kaola-fed / blog

kaola blog
723 stars 56 forks source link

浅析Vue 中的patch和diff(上) #259

Open AnnVoV opened 6 years ago

AnnVoV commented 6 years ago

疑问

1.当我修改了属性值时,vdom立即进行diff,重新渲染视图了吗?

2.如果1是对的,那重复修改,性能岂不是很差?如果不是,1是如何实现的?

3.我们的nextTick 具体的实现是怎样的?什么时候需要用到它?

4.vdom diff的过程是怎样的?

梗概

关于vue数据更新渲染的几个知识点,先列一下:

例子

接下来,所有的讲解都会围绕下面这个例子

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <style>
        .f-error {
            color: red;
        }
    </style>
</head>
<body>
    <div id="test">
    </div>
    <script type="text/javascript" src="./vue.js"></script>
    <script type="x-template" id="temp">
        <section>
            <div :class="{'f-error': a==2}">{{a+b}}</div>
        </section>
    </script>
    <script type="text/javascript">
        var data = {
            a: 1,
            b: 1
        }
        new Vue({
            el: '#test',
            template: temp,
            data: function() {
                return data;
            },
            mounted() {
                setTimeout(() => {
                    data.a = 2;
                    data.b = 3;
                    this.$nextTick(()=> {
                        console.log(document.querySelector('.f-error'));
                    })
                }, 500);
            }
        });
    </script>
</body>
</html>

入口

vm._update(vm._render(), hydrating)

经过上一次分享,我们知道通过vm._render()方法,我们会获得我们的vdom; 接下去我们进入_update方法;我们看下内部的细节。

Vue.prototype._update = function (vnode, hydrating) {
    var vm = this;
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate');
    }
    ...
    // 初始时,我们是没有prevVnode的, 进入了patch方法
    if (!prevVnode) {
      // initial render
      // 初始化渲染,我们看下细节   
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      );
      // no need for the ref nodes after initial patch
      // this prevents keeping a detached DOM tree in memory (#5851)
      vm.$options._parentElm = vm.$options._refElm = null;
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode);
    }
    activeInstance = prevActiveInstance;
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null;
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm;
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el;
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  };

初始进入patch

// 当我们初始进入patch时,会进入createElm 根据我们的vnode 创建节点
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
      return
    }
    var isInitialPatch = false;
    var insertedVnodeQueue = [];

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true;
      createElm(vnode, insertedVnodeQueue, parentElm, refElm);
    } else {
        ...
    }
}

数据更新时

假设我们的数据是{a:1, b:1}更新为了{a:2, b:3}, 我们下面看下细节

new Vue({ el: '#test', template: temp, data: function() { return data; }, methods: { test() { data.a = 2; data.b = 3; } } });

// 当我们data.a 的值改变时,会进入它的setter set: function reactiveSetter (newVal) { var 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 ("development" !== 'production' && customSetter) { customSetter(); } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); // 当值更新时,dep会通知watcher dep.notify(); }

Dep.prototype.notify = function notify () { // stabilize the subscriber list first var subs = this.subs.slice(); // 我们知道subs里面存放着我们的watcher实例, 进入watcher的update方法
for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } };

Watcher.prototype.update = function update () { / istanbul ignore else / if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { // 一般情况下,没有其他配置会进入这里,将我们的watcher推入队列
queueWatcher(this); } };

/**

// 进入了nextTick 方法,这里涉及到EventLoop相关的内容,后面会简单说一下 function nextTick (cb, ctx) { var _resolve; // 将flushSchedulerQueue塞入cb callbacks.push(function () { if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); if (!pending) { pending = true; // 注意这里,一般情况下使用microTask但某些情境下会强制使用macroTask
if (useMacroTask) { macroTimerFunc(); } else { // 我们的例子会进入这里, microTimerFunc结果是什么呢?往下看 microTimerFunc(); } } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve) { _resolve = resolve; }) } }

// Determine MicroTask defer implementation. / istanbul ignore next, $flow-disable-line / if(typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve(); microTimerFunc = function() { // 重点注意这里是promise的cb, 是一个microTask, 是在主script执行完才会执行的 p.then(flushCallbacks); } }


data.a执行完以后,开始走data.b, 流程都一样,只是当我们遇到watcher的update时有些区别

```javascript
queueWatcher(this);
function queueWatcher (watcher) {
  var id = watcher.id;
  // 判断这个watcher是否已经放入过队列, 当执行到data.b时已经放入过队列了, 所以不会继续往下走了(这个也很好理解)
  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.
      var 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);
    }
  }
}

然后进入this.$nextTick方法

 Vue.prototype.$nextTick = function (fn) {
    return nextTick(fn, this)
  };

function nextTick (cb, ctx) {
  var _resolve;
  // 将我们的cb塞入callbacks      
  callbacks.push(function () {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, 'nextTick');
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  // 因为上一次pending 已经置为true,所以此时不符合条件
  if (!pending) {
    pending = true;
    if (useMacroTask) {
      macroTimerFunc();
    } else {
      microTimerFunc();
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(function (resolve) {
      _resolve = resolve;
    })
  }
}

接下去,就到了我们之前讲到的,主script执行完了开始执行microTask, 进入flushSchedulerQueue方法。(tips: 推荐阅读Tasks, microtasks, queues and schedules更好地了解EventLoop)

//  p.then(flushCallbacks);
function flushCallbacks () {
  pending = false;
  var copies = callbacks.slice(0);
  callbacks.length = 0;
  for (var i = 0; i < copies.length; i++) {
    copies[i]();
  }
}
// 开始遍历callbacks 执行其中的cb
// 第一个cb 是 flushSchedulerQueue
// 第二个cb 是 我们的 console.log(document.querySelector('.f-error')); 

// 第一个cb里面,watcher.run 最终会进入vdom的diff, 下一篇具体讲细节
function flushSchedulerQueue () {
  flushing = true;
  var watcher, 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];
    id = watcher.id;
    has[id] = null;
    watcher.run();
    // in dev build, check and stop circular updates.
    ...
  }

  // keep copies of post queues before resetting state
  var activatedQueue = activatedChildren.slice();
  var 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');
  }
}
// 然后执行了我们的cb, 此时视图已经更新
// <div class='f-error'>5</div>    

总结

这一篇blog主要是为了让大家清楚

拓展

思考1:不用nextTick

如果很好地理解了micoTask 与 macroTask之间的关系,那么也能很清楚的理解假设我们写成下面这样, 为什么不行了,自己试试喽!下一篇,会细致讲解vdom diff 的过程~

mounted() {
                setTimeout(() => {
                    data.a = 2;
                    data.b = 3;
                    console.log(document.querySelector('.f-error'));
                }, 500);
            }

思考2: 如果都用MicroTask有什么问题?

看下这个issue, @click would trigger event other vnode @click event. #6566

贴一下代码,vue的版本是2.4.2,在此版本下当你点击了‘expand is true’以后,expand click 和 off click都打印出来了,countA与countB都变成了1,文案还是expand is true

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>JS Bin</title>
    <!--<script src="https://unpkg.com/vue@2.4.2/dist/vue.js"></script>-->
    <script src="./vue2.4.js"></script>
</head>
<body>
<div class="panel" id="app">
    <div class="header" v-if="expand">
        <i @click="expandClick">Expand is True</i>
    </div>
    <!-- 注意这里@click 绑定到了外层 -->
    <div class="expand" v-if="!expand" @click="offClick">
        <i>Expand is False</i>
    </div>
    <div>
        countA: {{countA}}
    </div>
    <div>
        countB: {{countB}}
    </div>
    <div>
        expand: {{expand}}
    </div>
    Please Click `Expand is Ture`.
</div>
</body>

<script>
    debugger;
    new Vue({
        el: '#app',
        data: {
            expand: true,
            countA: 0,
            countB: 0,
        },
        methods: {
            expandClick() {
                this.expand = false;
                this.countA++;
                console.log('expand click');
            },
            offClick() {
                this.expand = true;
                this.countB++;
                console.log('off click');
            }
        }
    })
</script>
</html>

尤大在这个issue下面给了回答,引一下:

大致原因是:<i>标签的点击动作触发了第一次nextTick(microTask), 然后我们得到了新的vdom并进行了渲染;microTask先于冒泡这个task,在microTask生成新dom的过程中,外层div添加了listener; 渲染完成后,冒泡触发了新的listener,所以又进入了新的cb。所以在Vue2.5版本中你会看到event handler使用了macroTask进行包裹

/** Vue.js v2.5.13 **/
function add$1 (
  event,
  handler,
  once$$1,
  capture,
  passive
) {
  // 看这里          
  handler = withMacroTask(handler);
  if (once$$1) { handler = createOnceHandler(handler, event, capture); }
  target$1.addEventListener(
    event,
    handler,
    supportsPassive
      ? { capture: capture, passive: passive }
      : capture
  );
}

/**
 * Wrap a function so that if any code inside triggers state change,
 * the changes are queued using a Task instead of a MicroTask.
 */
function withMacroTask (fn) {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true;
    var res = fn.apply(null, arguments);
    useMacroTask = false;
    return res
  })
}

参考资料

1.vue2.0 正确理解Vue.nextTick()的用途 http://www.cnblogs.com/minigrasshopper/p/7879545.html 2.从event loop规范探究javaScript异步及浏览器更新渲染时机 https://github.com/aooy/blog/issues/5 3.Promise的队列与setTimeout的队列有何关联?https://www.zhihu.com/question/36972010/answer/71338002 4.JavaScript 运行机制详解:再谈Event Loop http://www.ruanyifeng.com/blog/2014/10/event-loop.html 5.Vue源码详解之nextTick:MutationObserver只是浮云,microtask才是核心!https://github.com/Ma63d/vue-analysis/issues/6 https://chuckliu.me/#!/posts/58bd08a2b5187d2fb51c04f9 6.Tasks, microtasks, queues and schedules https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/ 7.@click would trigger event other vnode @click event. #6566 https://github.com/vuejs/vue/issues/6566

lizhongzhen11 commented 6 years ago

学习了,多谢