首页 ›  文章

Vue数据响应式原理代码剖析

2019-12-03

数据响应式即数据双向绑定,就是把Model绑定到view,当我们用Javascript代码更新model时,view就会自动更新,如果用户更新了view,model的数据也自动更新了,这种情况就是双向绑定

vue2.0原理

vue2.0实现数据响应式的原理就是利用了Object.defineProperty()这个方法重新定义了对象获取属性值(get)和设置属性值(set)的操作来实现的。

什么是defineProperty

defineProperty其实是定义对象的属性

defineProperty其实并不是核心的一个对象做数据双向绑定,而是去给对象做属性标签,只不过属性里的set和get实现了响应式

属性名 默认值
vale undefined
get undefined
set undefined
writeable false
enumerable false
configurable false

查看自有属性

var a = {
    b: 123,
    c: 456
}

console.log(Object.getOwnPropertyDescriptor(a,'b'))

// 执行结果
{
    configurable: true
    enumerable: true
    value: 123
    writable: true
}

defineProperty get和set实现方式

var a = {
    b: 123,
    c: 456
}

var _value = a.b;
Object.defineProperty(a,'b',{
    get: function(){
        console.log('you get b')
        return _value
    },
    set: function(newval){
        console.log('this newvalue is' + newval)
        _value = newval
    }
})

console.log(Object.getOwnPropertyDescriptor(a,'b'))
a.b  // 123
a.c = 567
a.c  // 567

vue从改变一个数据到发生变化的过程

vue改变数据的过程.png

实现简单的对对象变化的监听

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <div id="app"></div>

    <script>
        function vue() {
            this.$data = {
                a: 1
            }
            this.el = document.getElementById('app');
            this._html = '';
            this.observe(this.$data);
            this.render();
        }

        vue.prototype.observe = function(obj) {
            var value;
            var self = this;
            for (var key in obj) {
                value = obj[key];
                if (typeof value === 'object') {
                    this.observe(obj)
                } else {
                    Object.defineProperty(this.$data, key, {
                        // get部分做依赖收集
                        get: function() {
                            return value
                        },
                        // set触发依赖更新视图
                        set: function(newvalue) {
                            value = newvalue;
                            self.render();
                        }
                    })
                }
            }
        }

        vue.prototype.render = function() {
            this._html = 'i am ' + this.$data.a;
            this.el.innerHTML = this._html;
        }

        var vm = new vue();
        setTimeout(function() {
            console.log('changes')
            console.log(vm.$data);
            vm.$data.a = 123
        }, 2000)
    </script>
</body>

</html>

实现简单的对数组变化的监听

var arraypro = Array.prototype;
var arrayob = Object.create(arraypro); // 深拷贝,断开引用
var arr = ['push', 'pop', 'shift'];

// 目标:arr里的方法,既能保持原有方法,又能触发更新
// 装饰者模式
arr.forEach(function(method, index) {
    arrayob[method] = function() {
        var ret = arraypro[method].apply(this, arguments);
        return ret;
    }
})

var arr = [];
arr._proto_ = arrayob;
arr.push(123);

arr // 查看
arr.push(345)
arr // 查看

什么是vue的依赖收集

new Dep()

  • 收集依赖 -- 当前这个变量,有哪些地方依赖了的(dep.depend())
  • 更新依赖部分(dep.notify)触发虚拟dom上的ast

什么是虚拟DOM

虚拟dom其实就是一个json对象

{
    'component1': observer("ast"),  // ast抽象语法术
    'component2': observer("ast"),  // ast抽象语法术
}

vue3原理

vue3.0原理采用ES6的Proxy对象来实现。Proxy对象用于定义基本操作的自定义行为

Proxy用于修改某些操作的默认行为,Proxy意思为“代理”,即在访问对象之前建立一道“拦截”,任何访问该对象的操作之前都会通过这道“拦截”

var ob = {
    a: 1,
    b: 2
}

// 使用时不可以再操作原对象了,所以直接对原对象进行重写
ob = new Proxy(ob, {
    get: function(target,key,receive){
        console.log(target,key)
        return target[key]
    },
    set: function(target,key,newvalue,receive){
        console.log(target,key,newvalue)
        target[key] = newvalue;
    }
})

为什么改用proxy

  • defineProperty只能监听某个属性,不能对全对象监听
  • 可以省去for in提升效率
  • 可以监听数组,不用再去单独的对数组做特异性操作
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <div id="app"></div>

    <script>
        function vue() {
            this.$data = {
                a: 1
            }
            this.el = document.getElementById('app');
            this._html = '';
            this.observe(this.$data);
            this.render();
        }

        vue.prototype.observe = function(obj) {
            var self = this;
            this.$data = new Proxy(this.$data, {
                get: function(target, key) {
                    return target[key]
                },
                set: function(target, key, newvalue) {
                    target[key] = newvalue;
                    self.render();
                }
            })
        }

        vue.prototype.render = function() {
            this._html = 'i am ' + this.$data.a;
            this.el.innerHTML = this._html;
        }

        var vm = new vue();
        setTimeout(function() {
            console.log('changes')
            console.log(vm.$data);
            vm.$data.a = 123
        }, 2000)
    </script>
</body>

</html>

我们还可以利用proxy做什么?

function createValidator(target, validator) {
    return new Proxy(target, {
        _validator: validator,
        set(target, key, value, proxy) {
            if (target.hasOwnProperty(key)) {
                var validator = this._validator[key];
                if (validator(value)) {
                    return Reflect.set(target, key, value, proxy);
                } else {
                    throw Error('type error');
                }
            }
        }
    })
}

var personValidator = {
    name(val) {
        return typeof val === 'string'
    },
    age(val) {
        return typeof val === 'number' && val > 18
    }
}

class person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
        return createValidator(this, personValidator);
    }
}

var pj = new person('潘潘', 18)
function isProtected(key, action) {
    if (key.slice(0, 1) === '_') { //规定:如果成员变量名以"_"开头,就视为私有的,抛出异常阻止操作
        throw new Error(`Invalid attempt to ${action} private "${key}" property`);
    }
}

var yourObj = {
    _a: 0,
    b: 3
}
var myObj = new Proxy(yourObj, {
    get: function(target, key) { //target为目标对象, key为成员变量
        isProtected(key, 'get');
        return target[key];
    },
    set: function(target, key, value) {
        isProtected(key, 'set');
        target[key] = value;
        return true;
    }
});

console.log(myObj._a)

使用proxy实现数据响应(具体案例)

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <div id="app">
        <h3 id="paragraph"></h3>
        <input type="text" id="input">
    </div>

    <script>
        // 获取段落的节点
        const paragraph = document.getElementById('paragraph');
        // 获取输入框节点
        const input = document.getElementById('input');

        // 需要代理的数据对象
        const data = {
            text: 'hello world'
        }

        const handler = {
            // 监控data中的text属性变化
            set: function(target, prop, value) {
                // 更新值
                target[prop] = value;
                // 更新视图
                paragraph.innerHTML = value;
                input.value = value;
                return true;
            }
        }

        // 构造proxy对象
        const myText = new Proxy(data, handler);

        // 添加input监听事件
        input.addEventListener('input', function(e) {
            myText.text = e.target.value; // 更新myText的值
        }, false)

        // 初始化值
        myText.text = data.text;
    </script>
</body>

</html>

使用defineProperty实现数据响应式(具体案例)

vue数据响应式实现.png

  • 实现一个整体的架构(包括MVVM类或者VUE类、Watcher类),这里用到一个订阅发布设计模式
  • 然后实现MVVM中的由M到V,把模型里面的数据绑定到视图
  • 最后实现V-M,当文本框输入文本的时候,由文本事件触发更新模型中的数据,同时也更新相对应的视图

``` <!DOCTYPE html>

<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title>
<style>
    input {
        border: 1px solid #000;
    }
</style>

<script>
    // 发布者
    class Vue {
        constructor(options) {
            this.$data = options.data; // 获取数据
            this.$el = document.querySelector(options.el); // 获取元素对象

            // 容器--保存订阅者信息,实例对象
            this._directive = {};
            // {myText: [订阅者1,订阅者2],myBox: [订阅者1,订阅者2]}

            this.Observer(this.$data);
            this.Complie(this.$el);
        }

        // 劫持数据
        Observer(data) {
            // 准备好数据澡盆
            for (let key in data) {
                this._directive[key] = [];

                // 为什么要进行数据劫持?
                let val = data[key];
                let watcher = this._directive[key];

                Object.defineProperty(this.$data, key, {
                    get: function() {
                        return val;
                    },
                    set: function(newVal) {
                        if (newVal !== val) {
                            val = newVal;
                            // element代表着订阅者的实例对象
                            watcher.forEach(element => {
                                element.update();
                            })
                        }
                    }
                })
            }
        }

        // 解析指令
        // 为什么要解析指令--依赖收集--更新视图--订阅
        Complie(el) {
            let nodes = el.children; // 获取appdiv对象下所有的子对象
            for (let i = 0; i < nodes.length; i++) {
                let node = nodes[i];
                // 递归算法 继续往树形结构查找
                if (node.children.length) {
                    this.Complie(node);
                }
                if (node.hasAttribute('v-text')) {
                    // 订阅
                    let attVal = node.getAttribute('v-text');
                    // push什么? -- 订阅者 -- 订阅者是谁?
                    this._directive[attVal].push(new Watcher(node, this, attVal, 'innerHTML'));
                }

                if (node.hasAttribute('v-model')) {
                    // 订阅
                    let attVal = node.getAttribute('v-model');
                    // push什么? -- 订阅者 -- 订阅者是谁?
                    this._directive[attVal].push(new Watcher(node, this, attVal, 'value'));

                    // this问题解决
                    // 方式一:
                    let self = this;
                    node.addEventListener('input', function() {
                        // 当文本框发生数据变化,更新模型
                        self.$data[attVal] = node.value;
                    })

                    // 方式二:
                    // node.addEventListener('input', () => {
                    //     // 当文本框发生数据变化,更新模型
                    //     this.$data[attVal] = node.value;
                    // })

                    // 方式三:
                    // node.addEventListener('input', function() {
                    //     // 当文本框发生数据变化,更新模型
                    //     this.$data[attVal] = node.value;
                    // }.bind(this))
                }
            }
        }
    }

    // 订阅者 负责更新本身的状态
    class Watcher { // 主要是功能 更新视图
        constructor(el, vm, exp, attr) {
            this.el = el;
            this.vm = vm;
            this.exp = exp;
            this.attr = attr;

            this.update();
        }
        update() {
            // div.innerHTML = this.vue.$data['myText'];
            // input[value] = this.vue.$data['myText'];
            this.el[this.attr] = this.vm.$data[this.exp]
        }
    }
</script>
</head> <body>

数据响应式

<input type="text" v-model="myTest"> <input type="text" v-model="myBox">
<script> const app = new Vue({ el: '#app', data: { myTest: '晚上好', myBox: '我是个盒子' } }) </script> </body>
原文链接:segmentfault.com

上一篇:Promise 使用心得
下一篇:React生命周期更新阶段
相关文章

首次访问,人机识别验证

扫描下方二维码回复 1024 获取验证码,验证完毕后 永久 无须验证

操作步骤:[打开微信]->[扫描上侧二维码]->[关注 FedJavaScript 的微信] 输入 1024 获取验证码

验证码有误,请重新输入