Vue——响应式原理

Vue的MVVM思想中,主要是靠VM 视图-模型完成响应,充当数据与视图之间的桥梁,数据更新响应视图、视图文本数据更新响应数据。

  • 数据劫持
  • 发布订阅

  数据劫持指的是vue利用ES5的Object.defineProperty属性对data选项中的数据进行getter和setter设置;
  发布订阅指 的是vue通过自定义事件将data的变化反应到视图上去,vue通过observe观察者对象反应数据的变化,然后通知vue生成新的vdom,进而渲染视图。

一、如何实现Object.defineProperty

  Vue通过设定对象属性的getter/setter 方法来监听数据的变化。通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。

function observe(value, cb) {
    Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb))
}

function defineReactive (obj, key, val, cb) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: ()=>{
            /*....依赖收集等....*/
            return val
        },
        set:newVal=> {
            val = newVal;
            cb();/*订阅者收到消息的回调*/
        }
    })
}

class Vue {
    constructor(options) {
        this._data = options.data;
        observe(this._data, options.render) //循环遍历每一个data的数据、进行依赖收集和观察
    }
}

let app = new Vue({
    el: '#app',
    data: {
        text: 'text',
        text2: 'text2'
    },
    render(){
        console.log("render");
    }
})

 

 二、依赖收集(为什么不是二维数组?)

先看下这段代码:

new Vue({
    template: 
        `<div>
            <span>text1:</span> {{text1}}
            <span>text2:</span> {{text2}}
        <div>`,
    data: {
        text1: 'text1',
        text2: 'text2',
        text3: 'text3'
    }
});

按照响应式原理,数据text3被修改的时候也会触发setter导致出现渲染,这显然是不正确的。(因为没有数据依赖)。

1.先说说Dep,订阅者列表

  当对data上的对象进行修改值的时候会触发它的setter,那么取值的时候自然就会触发getter事件,所以我们只要在最开始进行一次render,那么所有被渲染所依赖的data中的数据就会被getter收集到Dep的subs中去。在对data中的数据进行修改的时候setter只会触发Dep的subs的函数。

定义一个依赖收集类Dep。(subs:二维数组,键值对,每个键名都有对应的一张订阅者表。)

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

    addSub (sub: Watcher) {
        this.subs.push(sub)
    }

    removeSub (sub: Watcher) {
        remove(this.subs, sub)
    }
    notify () {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update()
        }
    }
}
function remove (arr, item) {
    if (arr.length) {
        const index = arr.indexOf(item)
        if (index > -1) {
            return arr.splice(index, 1)
        }
    }
}

 

2.订阅者 watcher

  当依赖收集的时候会addSub到sub中,在修改data中数据的时候会触发dep对象的notify,通知所有Watcher对象去修改对应视图。

class Watcher {
    constructor (vm, expOrFn, cb, options) {
        this.cb = cb;
        this.vm = vm;
/*在这里将观察者本身赋值给全局的target,只有被target标记过的才会进行依赖收集*/ Dep.target = this; /*Github:https://github.com/answershuto*/ /*触发渲染操作进行依赖收集*/ this.cb.call(this.vm); } update () { this.cb.call(this.vm); } }

 

3.开始收集依赖

class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data, options.render);
        let watcher = new Watcher(this, );
    }
}

function defineReactive (obj, key, val, cb) {
    /*在闭包内存储一个Dep对象*/
    const dep = new Dep();

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: ()=>{
            if (Dep.target) {
                /*Watcher对象存在全局的Dep.target中*/
                dep.addSub(Dep.target);
            }
        },
        set:newVal=> {
            /*只有之前addSub中的函数才会触发*/
            dep.notify();
        }
    })
}
Dep.target = null;

  将观察者Watcher实例赋值给全局的Dep.target,然后触发render操作只有被Dep.target标记过的才会进行依赖收集。有Dep.target的对象会将Watcher的实例push到subs中,在对象被修改触发setter操作的时候dep会调用subs中的Watcher实例的update方法进行渲染。

 

 

三、总结

 

 

 

 

 

 

 

 

 

 

  首先说一下template解析成AST以及render function的过程:主要是通过正则解析模板成为AST树,然后会将AST树编译成render function,其中运用来缓存等优化方法都是值得一读的。

  接着从Data开始看,当数据Data发生变化时,会触发setter,setter会触发闭包中的Dep通知所有对该数据进行观察的观察者对象Watcher,Watcher会调用_update来更新视图,_update的第一个参数是一个render函数,然后返回一个VNode节点。

  新的VNode节点会与之前的VNode节点进行一个patch的过程,比较得出最小单位的修改。最后将这些修改渲染到真实DOM上。

 

posted @ 2021-03-15 22:06  Jiox  阅读(114)  评论(0编辑  收藏  举报