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早期源码学习系列之八:列表渲染与diff算法 #91

Open
youngwind opened this issue Sep 22, 2016 · 5 comments
Open

vue早期源码学习系列之八:列表渲染与diff算法 #91

youngwind opened this issue Sep 22, 2016 · 5 comments
Labels

Comments

@youngwind
Copy link
Owner

youngwind commented Sep 22, 2016

前言

继上一篇 #90 实现了条件渲染之后,本篇我们来看看如何实现vue列表渲染功能

问题具象

考虑以下的情况

<div id="app">
    <ul>
        <li b-repeat="item:items">
             {{item.title}}
             {{user.name}}
        </li>
    </ul>
</div>
const app = new Bue({
    el: '#app',
    data: {
        user: {
            name: 'youngwind',
            age: 24
        },
        items: [
            {
                title: 'aaa'
            },
            {
                title: 'bbb'
            },
            {
                title: 'ccc'
            }
        ]
    },
    methods: {
        test: function () {
            //  [a, b, c]  => [b, c, e, f]
            this.items.shift();
            this.items.push({
                title: 'eee'
            });
            this.items.push({
                title: 'fff'
            });
        }
    }
});

我们的目标是:根据数组items循环渲染li标签,并且当items改变的时候能够最小化操作DOM完成重新渲染。

思路

我们先来思考一下,总共有几个难点:

  1. 回想实现条件渲染的时候,把b-if对应的DOM结构看成是一个vue实例。我们这次沿用这个思路,把每个b-repeat对应的li标签都当成一个vue实例来处理。
  2. 作用域问题。在实现条件渲染的时候,因为发现b-if子实例的作用域完全等价于父实例的作用域,所以暂时采取了直接引用父实例的$data和observer的方法,实属权宜之计。然而,在b-repeat里面不能继续采取这种粗糙的方法了。原因:**每一个b-repeat子实例都有自己特有的变量:item。也就是说,每一个子实例都能访问到自己的item和父实例所有的变量,父实例无法访问子实例的item。父子实例的作用域关系是:子实例作用域严格包含父实例作用域,父子实例作用域再也不等价。**有什么办法能够彻底解决作用域这个问题呢?
  3. 当items发生改变的时候,势必会引起对原有DOM列表的插入、删除或者移动,这往往容易触碰到性能问题,有什么办法能够确保最小化地操作DOM,从而提高列表渲染的性能?

父子实例作用域

我们先来解决上面提到的第二个难点:作用域。有没有发现这样一个有趣的现象:父子实例的作用域极度类似于js对象的继承?子实例继承于父实例。比如说,当我们在子实例要找user.name的时候,由于在子实例中找不到user,所以就会沿着原型链跑到父实例上去找,在父实例中找到了user,然后终止查找。当我们在子实例中要找item的时候,因为item是子实例自己的属性,所以在子实例就可以找到,不会跑到父实例上面去找。然而当父实例想访问子实例的item的时候,是无法访问得到的。
综上:通过js的继承,可以解决父子实例的作用域嵌套问题。
具体的实现基本上就是下面这一行代码。

exports._init = function (options) {
    // ....
    this.__proto__ = this.$parent;    // this.$parent就是当前实例的父实例
    //....
};

由此导致的另一个相关的问题:
如果我们用继承来实现作用域嵌套,那么,当父实例的user.name改变的时候,如何将变化传导到各个子实例呢?
答案:把子实例存储到父实例的_children数组中,当父实例的data发生变动,触发父实例的updateBindingAt方法的时候,循环遍历_children,分别触发每个子实例的updateBindingAt方法。

那么,具体如何实现呢?

exports._updateBindingAt = function () {
    this._updateSelfBindingAt(...arguments); // 这个函数就是原来的_updateBindingAt
    this._updateChildrenBindingAt(...arguments);
};

/**
 * 触发本实例所有子实例的updateBindingAt
 */
exports._updateChildrenBindingAt = function () {
    if (!this.$children.length) return;
    this.$children.forEach((child) => {
        child._updateBindingAt(...arguments);
    });
};

diff算法

第三个难点。vue确保列表渲染性能的核心是:diff算法,整个算法分成两部分:初始化更新,如下图所示。
diff算法

初始化

  1. 假设一开始的items=[a,b,c];
  2. 构建vm实例数组Vms=[Vma,Vmb,Vmc];
  3. 依次将实例插入DOM中
  4. 结束初始化

更新

  1. items发生改变,新的值为items=[b,c,e,f]
  2. 实例复用检查。在旧的实例数组OldVms里面发现Vmb和Vmc之前已经有了,给它们打上reused的标签。发现Vme和Vmf之前没有,新建这两个实例,最后构建的新的vm实例数组为Vms=[Vmb,Vmc,Vme,Vmf]。
  3. 循环旧实例数组OldVms,把其中没有打上reused(也就是不可复用)的实例都找出来,然后一一从DOM中删除。
  4. 将实例Vmf插入到DOM列表的最后
  5. 将实例Vme插入到Vmf的前面
  6. 判断Vmc正好处于Vme的前面,所以VMc不用动
  7. 判断Vmb正好处于Vmc的前面,所以Vmb也不用动
  8. 结束更新

说的比较抽象,更为直接的方式是直接去debug源码,我参考的版本vue版本是这个,自己实现的版本在这里,核心的代码在这里,实现的效果如下图所示。
demo

后话

关于列表渲染的性能优化,本篇实现的只是最简单的版本,我感觉还有很多其他的东西没有实现,有待研究,比如track

参考资料

  1. http://jiongks.name/blog/vue-code-review/ (有一部分讲到diff算法)
  2. https://github.com/banama/aboutVue/blob/master/diff.md
@youngwind youngwind added the Vue label Sep 29, 2016
@youngwind youngwind changed the title vue早期源码学习系列之八:如何实现"v-repeat"列表渲染 vue早期源码学习系列之八:列表渲染与diff算法 Oct 8, 2016
@jiandan1989
Copy link

new Vue()

@LiuMengzhou
Copy link

LiuMengzhou commented Aug 17, 2017

更新

  1. items发生改变,新的值为items=[b,c,e,f]
  2. 实例复用检查。在旧的实例数组OldVms里面发现Vma和Vmb之前已经有了,给它们打上reused的标签。发现Vme和Vmf之前没有,新建这两个实例,最后构建的新的vm实例数组为Vms=[Vmb,Vmc,Vme,Vmf]。

应该是Vmb和Vmc吧。Correct me if I am wrong.

@youngwind
Copy link
Owner Author

@LiuMengzhou 是的,此处为笔误,多谢指出,已修正。

@Geocld
Copy link

Geocld commented Nov 3, 2017

diff算法那有一点死活想不明白,实例数组[b,c]生成后,为什么要先插入f再插入e,不能先插入e再插入f

@zhoubhin
Copy link

@Geocld 我也疑惑,关于diff算法,这篇文章写得很详细:aooy/blog#2

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

5 participants