Vue2响应式原理

Vue响应式原理

利用Object.defineProperty()劫持data数据,里面每个属性都定义了get和set方法 监听属性的变化,

依赖收集:每个属性也都有个数组 保存着谁(视图watcher)依赖了它,当获取属性触发get时,收集了依赖,

依赖更新:当属性变化触发set函数时,通知依赖,通知视图(watcher),视图(watcher)开始更新。

 

一、 Object.defineProperty()

 Object.defineProperty():在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

      get 值是一个函数,当属性被访问时,会触发 get 函数

      set 值同样是一个函数,当属性被赋值时,会触发 set 函数

 

Vue 是怎么知道数据被调用或改变的?

Vue对data对象中的每个属性进行了get、set监听,当属性被调用时触发get方法,当属性被改变时触发set方法,所以Vue 能知道数据被调用或改变。

代码如下:

class KVue {
    constructor(options) {
        this.$options = options;
        if(this.$options == undefined) {
            return;
        }
        this.$data = this.$options.data; //把data赋值给this,在创建的实例中才能获取到data
        this.observe(this.$data);
    }
    // 操作data:如果data是对象,则对属性进行遍历
    observe(value) {
        if(!value || typeof value !== 'object') {
            return;
        }

        Object.keys(value).forEach((key) => {
            this.defineReactive(value, key, value[key]);
        })
    }
    // 操作属性:用Object.defineProperty()监听属性
    defineReactive(obj, key, val) {
        this.observe(val);
        Object.defineProperty(obj, key, {
            get () {
                return val;
            },
            set (newValue) {
                if(val === newValue) {
                    return;
                }
                val = newValue; 
                console.log(val);
            }
        })
    }
}

 

<!DOCTYPE html>
<html>
<head>
    <title></title>
</head>
<body>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="defineProperty.js"></script>
<script type="text/babel">

let kvue = new KVue({
    data: {
        name: 'fndjf',
        list: {
            bar: 'bar'
        }
    }
})
console.log(kvue)
console.log(kvue.$data.name)
kvue.$data.name = '1dgs';
console.log(kvue.$data.name)

console.log('--------------------');

console.log(kvue.$data.list.bar);
kvue.$data.list.bar = 'dfmd';
console.log(kvue.$data.list);

let kvue1 = new KVue();


</script>
</body>
</html>

 

二、依赖收集、更新

依赖收集:data中每个属性都会有一个数组作为依赖收集器,当页面使用某个属性(get)时,页面的watcher就会被放到对应属性的依赖收集器中。

依赖更新:通知属性依赖收集器中的watcher进行更新

代码如下: 

class KVue {
    constructor(options) {
        this.$options = options;
        if(this.$options == undefined) {
            return;
        }
        this.$data = this.$options.data; //把data赋值给this,在创建的实例中才能获取到data
        this.observe(this.$data);

        // 模拟watcher创建
        new Watcher(); //已将实例赋值给Dep.target
        this.$data.name;// 将Dep.target插入name属性中的dep
        new Watcher();//已将实例赋值给Dep.target,覆盖了之前的Dep.target
        this.$data.list.bar; //将Dep.target插入list.bar属性中的dep
    }
    // 操作data
    observe(value) {
        if(!value || typeof value !== 'object') {
            return;
        }

        Object.keys(value).forEach((key) => {
            this.defineReactive(value, key, value[key]);
        })
    }
    // 操作key
    defineReactive(obj, key, val) {
        // 递归监听data属性中的属性
        this.observe(val);

        // 初始化一个属性中的依赖
        const dep = new Dep();

        Object.defineProperty(obj, key, {
            get () {
                // Dep.target是调用该属性的页面的Watcher,添加到该属性的依赖中
                Dep.target && dep.addDep(Dep.target);

                return val;
            },
            set (newValue) {
                if(val === newValue) {
                    return;
                }
                val = newValue; 
                console.log(val);

                // 通知依赖更新
                dep.notify();
            }
        })
    }
}

// Dep:依赖
class Dep {
    constructor() {
        // 存放若干依赖(watcher)
        this.$deps = [];
    }

    // 添加依赖
    addDep(dep) {
        this.$deps.push(dep);
    }

    // 通知依赖数组中的每个watcher更新
    notify() {
        this.$deps.forEach(dep => dep.update(dep));
    }
}

// Watcher:观察者,做具体更新
class Watcher {
    constructor() {
        // 将当前watcher实例赋值给Dep的静态属性,即后一个watcher实例会覆盖前一个watcher实例
        Dep.target = this;
    }

    update(dep) {
        console.log(dep);

        console.log('属性更新了');
    }
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title></title>
</head>
<body>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="defineProperty.js"></script>
<script type="text/babel">

let kvue = new KVue({
    data: {
        name: 'fndjf',
        list: {
            bar: 'bar'
        }
    }
})
console.log(kvue)
console.log(kvue.$data.name)
kvue.$data.name = '1dgs';
console.log(kvue.$data.name)

console.log('--------------------');

console.log(kvue.$data.list.bar);
kvue.$data.list.bar = 'dfmd';
console.log(kvue.$data.list);

</script>
</body>
</html>

 

三、总结

  1. data中每个属性都有个数组作为依赖收集器,收集使用该属性的页面的watcher
  2. Object.defineProperty()  -  get ,用于 依赖收集
  3. Object.defineProperty()  -  set,用于 依赖更新
  4. watcher 可用于视图更新

 

 

四、另一写法

 

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Page Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script>
    // 1.实现整体架构(Vue类、Watcher类),用到了订阅发布者设计模式
    // 2.实现MVVM中的M到V,把数据绑定到视图
    // 3.实现V-M,当文本框输入文本时,触发更新模型中的数据,同时更新相应的视图

    class Vue {
        constructor(options) {
            this.$data = options.data; //获取数据
            this.$el = document.querySelector(options.el); //获取元素#app
            
            this._directive = {}; //存放订阅者信息
            this.Observer(this.$data);
            this.Compile(this.$el);
        }

        // 劫持数据:
        Observer(data) { 
            for(let key in data) {
                this._directive[key] = [];
                let val = data[key];
                let watcher = this._directive[key];
                console.log(watcher);
                Object.defineProperty(data, key, {
                    get() {
                        return val;
                    },
                    set(newVal) {
                        if(val !== newVal) {
                            val = newVal;
                            // 更新视图
                            watcher.forEach(element => {
                                element.update();
                            });
                        }
                    }
                })
            }
            
        }

        // 解析指令:收集依赖、更新视图、订阅
        Compile(el) {
            let node = el.children;
            console.log(node);
            for(let i = 0; i < node.length; i++) {
                // let node = node[i];
                if(node[i].children.length) {
                    this.Compile(node[i]);
                }
                if(node[i].hasAttribute('v-text')) {
                    let attrVal = node[i].getAttribute('v-text');  // myText myBox
                    this._directive[attrVal].push(new Watcher(node[i], this, attrVal, "innerHTML"));
                    console.log(this._directive);
                }
                if(node[i].hasAttribute('v-model')) {
                    let attrVal = node[i].getAttribute('v-model'); // myText myBox
                    this._directive[attrVal].push(new Watcher(node[i], this, attrVal, "innerHTML"));
                    // 文本框出现变化时更新模型
                    node[i].addEventListener('input', ()=>{
                        this.$data[attrVal] = node[i].value;
                    })
                }
            }
        }
    }

    // 订阅者
    class Watcher {  //主要功能是更新视图
        constructor(el, vm, exp, attr) {
            this.el = el;
            this.vm = vm;
            this.exp = exp;
            this.attr = attr;
            this.update();
        }
        update() {
            this.el[this.attr] = this.vm.$data[this.exp];
            // div.innerHTML = vm.$data.myText
        };
    }
    </script>
</head>
<body>
<div id="app">
    <div>
        <div v-text="myText"></div>
        <div v-text="myBox"></div>
        <input type="text" v-model="myText">
        <input type="text" v-model="myBox">
    </div>
</div>
<script>
const app = new Vue({
    el:'#app',
    data: {
        myText: 'textaaaaaa',
        myBox: 'myBoxaaaaa'
    }
})
</script>
</body>
</html>

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章