Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue早期源码学习系列之五:批处理更新DOM #88

Open
youngwind opened this issue Sep 7, 2016 · 7 comments
Open

vue早期源码学习系列之五:批处理更新DOM #88

youngwind opened this issue Sep 7, 2016 · 7 comments
Labels

Comments

@youngwind
Copy link
Owner

youngwind commented Sep 7, 2016

前言

在上一篇 #87 中,我们最后谈到,有一个问题还没有解决,我们来看看是什么问题。如下图所示。
bug

我们可以看到,在函数test中,前后两次更改了user.name的值,对应的DOM元素的更新也执行了两次。(注意,这里的DOM元素更新指的是内存中DOM元素的更新,而非浏览器渲染的更新。因为你从视觉上应该也看得出来,虽然DOM元素更新了两次,但是网页上展示的效果却是只修改了一次。此处更多的资料可以参考这里。)

《高性能js》教导我们:“频繁操作DOM是低效率的”,所以,我们是否能做到说,不管user.name变化了多少次,对DOM的更新只进行一次呢?
答案是可以的,方法是批处理

批处理这个词我最早接触它是在大学计算机组成原理的课程上,当时觉得这个东西好晦涩难懂,跟自己没啥关系,所以就不管它。经过这次深入的学习,我发现批处理的应用是非常的广泛的,它代表的是对某一类问题的处理模式,不仅仅存在于计算机组成原理中。

我们来看看vue官方文档怎么描述它如何应用批处理:

Vue.js 默认异步更新 DOM。每当观察到数据变化时,Vue 就开始一个队列,将同一事件循环内所有的数据变化缓存起来。如果一个 watcher 被多次触发,只会推入一次到队列中。等到下一次事件循环,Vue 将清空队列,只进行必要的 DOM 更新。在内部异步队列优先使用 MutationObserver,如果不支持则使用 setTimeout(fn, 0)。

出处:https://vuejs.org.cn/guide/reactivity.html#异步更新队列

批处理原理

在我眼中的批处理是这样的:将一些事件放到一个任务队列中,等到合适的时机再全部拿出来执行。
js这门语言实现批处理的底层原理是“事件循环”,也就是所谓的"Event Loop"。
如果不清楚js事件循环,请务必搞明白这个概念再往下看。可以参考阮一峰的这篇文章

Batcher

好了,接下来我们正式来看看如何实现批处理更新DOM。
我们需要构造这样一个批处理“类”:Batcher

/**
 * 批处理构造函数
 * @constructor
 */
function Batcher() {
    this.reset();
}

/**
 * 批处理重置
 */
Batcher.prototype.reset = function () {
    this.has = {};
    this.queue = [];
    this.waiting = false;
};

/**
 * 将事件添加到队列中
 * @param job {Watcher} watcher事件
 */
Batcher.prototype.push = function (job) {
    if (!this.has[job.id]) {
        this.queue.push(job);
        this.has[job.id] = job;
        if (!this.waiting) {
            this.waiting = true;
            setTimeout(() => {
                this.flush();
            });
        }
    }
};

/**
 * 执行并清空事件队列
 */
Batcher.prototype.flush = function () {
    this.queue.forEach((job) => {
        job.cb.call(job.ctx);
    });
    this.reset();
};

下面我来说明一下几个关键点:

  1. has属性的作用是为了防止已经存在于任务队列中的事件被重复添加,这也是我们为了解决开头的重复修改DOM所必须的要素。
  2. queue是存放事件的任务队列。比方说,就像开头的函数app.test那样,修改了两次name和一次age,所以queue事件队列中就有两个事件,分别对应name和age。(请注意,事件是2个,而非3个)
  3. flush,顾名思义,是“冲洗”的意思。也就是把queue队列里面的事件都拿出来一一执行。
  4. waiting和reset非常关键。我们设想,如果没有这两个东西,随着程序的执行,会有一个name和age的事件被添加到queue中,等主线程空闲了,flush执行它们。然而,当我再次想添加name和age事件的时候却发现没响应?为什么?因为name和age已经存在has对象和queue里面了呀!所以在每次flush完都需要将整个任务队列的状态重置。waiting起的是保护的作用。当flush冲洗函数已经在任务列队时,那么我不再重复将flush函数添加到任务队列中。设想如果没有waiting的保护,在app.test执行完之后,flush函数会被执行两次,这显然不符合我们批处理的初衷。

最后一步,是修改原先watcher的update函数

Watcher.prototype.update = function () {
    //原先是watcher的update直接执行cb
    // this.cb.call(this.ctx, arguments);

   // 现在改成将watcher push到事件队列中,等待主线程空闲再一起执行
    batcher.push(this);
};

实现效果如下图所示:
demo

完整的源码请参考这个版本

React中的批处理

我们在开头提到,批处理是一个广泛应用的思想,在研究vue的批处理的时候,我忽然想起了以前在用react的时候碰到的一个问题, #66 ,里面提到的this.setState是异步的,当时不理解。现在仔细想想,背后的本质也应该是批处理。也就是说,我们在调用setState的时候,并非同步修改了this.state,而是攒到一块再修改this.state。跟我们刚刚把对DOM的修改攒到一块,其实是异曲同工的。

参考资料

  1. Vue源码解读-Watchers(数据订阅者)异步执行队列
  2. https://segmentfault.com/q/1010000005813183
  3. http://jimliu.net/2016/04/29/a-brief-look-at-vue-2-reactivity/

后话

随着对vue早期源码的不断学习,我更加坚信从早期源码开始学习是一条走得通的道路。目前虽然已经初步具备动态数据绑定的能力,但是很多细节还有待打磨和推敲。比如对template的编译,比如构建缓存系统提升性能,我得好好想想接下来该往哪个方向走。

@william-xue
Copy link

this.has = {};
this.queue = [];这里应该说下,用{},可以后者覆盖前者,push到【】里

@LiuMengzhou
Copy link

Batcher.prototype.push = function (job) {
    if (!this.has[job.id]) {
        this.queue.push(job);
        this.has[job.id] = job;
        if (!this.waiting) {
            this.waiting = true;
            setTimeout(() => {
                this.flush();
            });
        }
    }
};

@youngwind 大佬,问一下,这样写的话,app.test中第二次更新的user.name如何覆盖前面的更新。
@william-xue 没看懂你说的,一定是我太笨了。

@hangzho
Copy link

hangzho commented Oct 28, 2017

@LiuMengzhou 我的理解是,在例子中,第一第二次的name更新,都指向同一个job对象。第一次把job加入到queue中以后,第二次更新时,是把第一次的job.cb或者job.ctx给覆盖了。

@cxliustc
Copy link

@LiuMengzhou 最后更新的name是最终的name

@vok123
Copy link

vok123 commented Jan 31, 2018

个人觉得这技巧也是绝了, js对象的无重复key使用, setTimeout事件队列

@lkdghzh
Copy link

lkdghzh commented Jan 31, 2018

mark

@xunan007
Copy link

看Vue的源码一直不明白这个waiting到底有啥用,看到这里总算是懂了,真的是神奇的标志位!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

8 participants