From 52701bb66640b00b739d287b1811042740f0e058 Mon Sep 17 00:00:00 2001
From: kaola-blog-bot <32167555+kaola-blog-bot@users.noreply.github.com>
Date: Mon, 9 Apr 2018 22:08:09 -0500
Subject: [PATCH] auto-archiving for issue #259
---
...204patch\345\222\214diff(\344\270\212).md" | 466 ++++++++++++++++++
1 file changed, 466 insertions(+)
create mode 100644 "source/_posts/\346\265\205\346\236\220Vue \344\270\255\347\232\204patch\345\222\214diff(\344\270\212).md"
diff --git "a/source/_posts/\346\265\205\346\236\220Vue \344\270\255\347\232\204patch\345\222\214diff(\344\270\212).md" "b/source/_posts/\346\265\205\346\236\220Vue \344\270\255\347\232\204patch\345\222\214diff(\344\270\212).md"
new file mode 100644
index 0000000..bba7098
--- /dev/null
+++ "b/source/_posts/\346\265\205\346\236\220Vue \344\270\255\347\232\204patch\345\222\214diff(\344\270\212).md"
@@ -0,0 +1,466 @@
+
+
+## 疑问
+
+1.当我修改了属性值时,vdom立即进行diff,重新渲染视图了吗?
+
+2.如果1是对的,那重复修改,性能岂不是很差?如果不是,1是如何实现的?
+
+3.我们的nextTick 具体的实现是怎样的?什么时候需要用到它?
+
+4.vdom diff的过程是怎样的?
+
+## 梗概
+
+关于vue数据更新渲染的几个知识点,先列一下:
+
+* 数据的更新是实时的,但是渲染是异步的。
+
+* 一旦数据变化,会把在同一个事件循环event loop中的观察到的watcher 推入一个队列(相同watcher实例不会重复推入)
+
+* DOM并不是马上更新视图的,2.4版本之后的是利用MicroTask或者MacroTask来批处理DOM更新视图的,那就要了解event loop
+
+* 整个script是一个主任务, setTimeout 是一个macroTask, promise的回调是microtask, 顺序是
+
+ 主script(第一个主任务) —> microTask (全部执行完)—> UI渲染 —> 下一个macroTask
+
+ ```javascript
+ // 先不看结果,想一下你的输出
+ console.log('script start');
+
+ setTimeout(function() {
+ console.log('setTimeout');
+ }, 0);
+
+ Promise.resolve().then(function() {
+ console.log('promise1');
+ }).then(function() {
+ console.log('promise2');
+ });
+
+ console.log('script end');
+ // result:
+ /**
+ * script start
+ * script end
+ * promise1
+ * promise2
+ * setTimeout
+ */
+ ```
+
+
+* 所以dom diff 这个过程是在microtask中去处理的(也有的是强制走macrotask, 本例子走microtask)
+
+## 例子
+接下来,所有的讲解都会围绕下面这个例子
+```javascript
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## 入口
+
+#### 当```vm._update(vm._render(), hydrating)```
+
+经过上一次分享,我们知道通过```vm._render()```方法,我们会获得我们的vdom; 接下去我们进入_update方法;我们看下内部的细节。
+
+```javascript
+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
+
+```javascript
+// 当我们初始进入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}```, 我们下面看下细节
+
+```javascript
+/**
+mouted() {
+ setTimeout(() => {
+ data.a = 2;
+ data.b = 3;
+ this.$nextTick(()=> {
+ console.log(document.querySelector('.f-error'));
+ })
+ }, 500);
+}
+**/
+
+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);
+ }
+};
+
+/**
+ * Push a watcher into the watcher queue.
+ * Jobs with duplicate IDs will be skipped unless it's
+ * pushed when the queue is being flushed.
+ */
+function queueWatcher (watcher) {
+ var 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.
+ 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;
+ // 走到了这里, cb 是flushSchedulerQueue
+ nextTick(flushSchedulerQueue);
+ }
+ }
+}
+
+// 进入了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```方法
+
+```javascript
+ 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]( https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/)更好地了解EventLoop)
+
+```javascript
+// 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, 此时视图已经更新
+// 5
+```
+
+## 拓展
+
+如果很好地理解了micoTask 与 macroTask之间的关系,那么也能很清楚的理解假设我们写成下面这样, 为什么不行了,自己试试喽!下一篇,会细致讲解vdom diff 的过程~
+
+```javascript
+mounted() {
+ setTimeout(() => {
+ data.a = 2;
+ data.b = 3;
+ console.log(document.querySelector('.f-error'));
+ }, 500);
+ }
+```
+
+
+## 参考资料
+
+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)
+6.[Tasks, microtasks, queues and schedules](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/)