4

vue 源码自问自答-响应式原理

最近看了 Vue 源码和源码分析类的文章,感觉明白了很多,但是仔细想想却说不出个所以然。

所以打算把自己掌握的知识,试着组织成自己的语言表达出来

不打算平铺直叙的写清楚 vue 源码的前因后果和全部细节,而是以自问自答的形式,回答我自己之前的疑惑,

如果有错误的地方,欢迎指正哈~

Vue 的双向数据绑定原理

Vue 实现响应式的核心 API 是 ES5 的 Object.defineProperty(obj,key,descriptor),Vue 的「响应式」和「依赖收集」都依靠这个 API

它接受 3 个参数,分别是 obj / key / 描述符,返回的是一个包装后的对象

它的作用就是,用这个 API 包装过后的对象可以拥有 getter 和 setter 函数。

getter 会在对象的这个 key 被获取时触发,setter 会在这个对象的 key 被修改时触发。

一个 Vue 项目的开始, 通常是从 Vue 构造函数的实例化开始的。

new Vue()的时候会执行一个_init()方法,会初始化属性,比如 props/event/生命周期钩子,也包括 data 对象的初始化。

Vue 在初始化时,将 data 对象上的所有 key,都包装成拥有 getter 和 setter 的属性。

  • 渲染页面时,会执行 render function(无论是用 template 还是 render 最终都会生成 render 函数)
  • 执行 render function 会获取 data 对象上的属性,所以会触发对应属性的 getter 函数
  • getter 触发时,主要就做 2 个事情。 1.把值返回 2.依赖收集,也就是讲 watcher 存放到 Dep 实例的一个队列里。
  • 当修改对象的值触发 setter,setter 同样是做 2 个事情。1.把 newVal 设置好 2.用一个循环通知之前存放在 dep 实例中 watcher 对象们,watcher 对象调用各自的 update 方法来更新视图

实现双向数据绑定的demo1 - 忽略「收集依赖」的版本

function cb() {
    console.log("更新视图");
}

function defineReactve(obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: () => {
            console.log("触发了getter");
            return val;
        },
        set: newVal => {
            console.log("触发了setter");
            if (newVal === val) return;
            val = newVal;
            cb()
        }
    });
}

function observe(data) {
    function walk(data) {
        Object.keys(data).forEach(key => {
            if (typeof data[key] === "object") {
                walk(data[key]);
            } else {
                defineReactve(data, key, data[key]);
            }
        });
    }
    walk(data);
}

class Vue {
    constructor(options) {
        this._data = options.data;
        observe(this._data);
    }
}

var vm = new Vue({
    data: {
        msg: "test",
        person: {
            name: "ziwei",
            age: 18
        }
    }
});

vm._data.person.name = 'hello'

// 触发setter和cb函数,从而视图更新

实现双向数据绑定的demo2 - 「收集依赖」的版本

        function defineReactve( obj, key, val ) {
            const dep = new Dep()
            Object.defineProperty( obj, key, {
                enumerable: true,
                configurable: true,
                get: () => {
                    console.log( "触发了getter" );
                    dep.addSub(Dep.target)
                    return val;
                },
                set: newVal => {
                    console.log( "触发了setter" );
                    if ( newVal === val ) return;
                    val = newVal;
                    dep.notify()  // 通知队列的wather去update视图
                }
            } );
        }

        function observe( data ) {
            function walk( data ) {
                Object.keys( data ).forEach( key => {
                    if ( typeof data[ key ] === "object" ) {
                        walk( data[ key ] );
                    } else {
                        defineReactve( data, key, data[ key ] );
                    }
                } );
            }
            walk( data );
        }

        class Dep{
            constructor(){
                this.subs = []
            }

            addSub(){
                this.subs.push(Dep.target)
            }

            notify(){
                this.subs.forEach(sub => {
                    sub.update()
                })
            }
        }

        Dep.target = null

        class Watcher{
            constructor(){
                Dep.target = this
            }

            update(){
                console.log('update更新视图啦~')
            }
        }

        class Vue {
            constructor( options ) {
                this._data = options.data;
                observe( this._data );

                new Watcher()           // 模拟页面渲染,触发getter,依赖收集的效果
                this._data.person.name
            }
        }

        var vm = new Vue( {
            data: {
                msg: "test",
                person: {
                    name: "ziwei",
                    age: 18
                }
            }
        } );

        vm._data.person.name = 'hello'

一些省略掉的环节

这样就是 Vue 响应式的一个基本原理,不过我描述的过程中,也省略了很多环节,比如

  • Vue 是如何实现给 data 对象上的属性都拥有 getter 和 setter 的
  • 为什么要进行「依赖收集」,
  • 如何避免重复「收集依赖」
  • watcher 调用 update,也并不是直接更新视图。实现上中间还有 patch 的过程以及使用队列来异步更新的策略。
Vue 是如何实现给 data 对象上的属性都拥有 getter 和 setter 的
通过循环data对象,给对象的每一个key,用Object.defineProperty包装

遍历时,如果发现data[key]也是对象的话,需要用递归
为什么要进行「依赖收集」?

举2个场景的栗子🌰

  • 1.如果我们不进行依赖收集,那页面里有2处使用了data上的数据,当这个数据发生变化时,会触发setter,然后setter当中进行视图更新。

但问题在于,你并不知道谁依赖我?我应该更新哪几个地方

  • 2.如果没有依赖收集,我们修改了data中的msg,按说我们要触发setter更新视图,但是如果视图里并没有用到data.msg的话,实际上不应该更新的。
如何避免重复「收集依赖」

在往Dep中放wather对象是,实际上wather的update方法时,会把其放入queue队列中,会通过watch.id判断是否重复了,重复的wather就不会被推入队列

watcher 调用 update,也并不是直接更新视图。实现上中间还有 patch 的过程以及使用队列来异步更新的策略。

异步更新的策略,类似于setTimeout(fn,0) ,目的就是为了避免频繁的更新dom,让页面的渲染的性能更好。


Ziwei
4.1k 声望420 粉丝

前端程序员