vue深入响应式原理

445 阅读3分钟

vue深入响应式原理

当一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。

vue存在不能检测数组和对象的变化,

  • 对于对象 vue会在初始化实例时,对 property 执行 getter/setter 转化,所以 property 必须在data 对象上存在
new Vue({
    data:{
        a:1
        b:{}
    }
})
this.a = 2  //响应式
this.b.name = 'Jack' //非响应式

已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用  Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。例如,对于:

this.$set(this.someObject,'b',2)
this.someObject = Object.assign({}, this.someObject ,{a:1,b:2})
  • 数组 vue不能检测一下数组的变动
  1. 利用索引直接设置一个数组项时
    this.items[indexOfItem] = newValue
    
  2. 当你修改数组的长度时
    this.items.length = newLength
    

可用以下方式代替

this.$set(this.items, indexOfItem, newValue)
this.items.splice(indexOfItem, 1 ,newValue)
this.splice(newlength)

用 pop、push、shift、unshift、splice、sort、reverse会触发视图更新,

数据动态绑定简单实现

DOM结构

<div id="app">
    <form>
        <input type="text" v-model="name.age.value" />
        <input type="text" v-model="name.value" />
        <button type="button" v-click="increment">increment</button>
        <button type="button" v-click="alert">alert</button>
    </form>
    <p v-bind="name.age.value"></p>
    <p v-bind="name.value"></p>
</div>

用类似Vue的语法创建一个实例:

window.onload=function(){
        var app=new Example({
            el:"#app",
            data:{
                count:0,
                name:{
                    value:'lowesyang',
                    age:{
                        value:20
                    }
                }
            },
            methods:{
                increment:function(){
                    this.name.age.value++;
                },
                alert:function(){
                    alert(this.name.value)
                }
            }
        })
    }

1. 实例配置、根节点、数据、函数方法名

function Example(options){
    this._init(options);
}
Example.prototype._init=function(options){
        this.$options=options;                              //传入的实例配置
        this.$el=document.querySelector(options.el);        //实例绑定的根节点
        this.$data=options.data;                            //实例的数据
        this.$methods=options.methods;                      //实例的函数
        
        /** 对象深层次属性的取值和修改
         * 直接对Object进行扩展$get和$set
         */
        Object.prototype.$get=function(path){
            var getter=new Function('return this.'+path);
            return getter.call(this);
        };

        Object.prototype.$set=function(path,val){
            var setter=new Function('newVal','this.'+path+' = newVal;');
            setter.call(this,val);
        }


        //与DOM绑定的数据对象集合
        //每个成员属性有一个名为_directives的数组,用于在数据更新时触发更新DOM的各directive
        this._binding={};
        this._parseData(this.$data);

        this._compile(this.$el);                //编译DOM节点
    };

B52B6961-189A-4FE5-A0C7-453E0D8DC972.png

2. data中的数据对象进行遍历调用,使用Object.defineProperty对data中的数据对象进行改造,添加getter/setter函数

//遍历
Example.prototype._parseData=function(obj){
    var OBJECT = 0, DATA =1
    var value;
    path = path || '';
    for(var key in obj){
        //排除原型链上的属性,仅仅遍历对象本身拥有的属性
        if(obj.hasOwnProperty(key)){
            this._binding[path+key]={   //初始化与DOM绑定的数据对象
                _directives:[]
            };
            value=obj[key];
            //值为对象 递归解析
            if(typeof value ==='object'){       
                this.convert(obj,key,value,path+key,OBJECT);
                this._parseData(value,path+key+'.');
            }
            else this.convert(obj,key,value,path+key,DATA);
        }
    }
};

//添加getter setter函数,数组重写原型方法

Example.prototype.convert=function(obj,path,val,absolute,type){
        var binding=this._binding[absolute];
        if(type==1) {    //如果不是深层次对象
            Object.defineProperty(obj, path, {
                get: function () {
                    console.log(`获取${val}`);
                    return val;
                },
                set: function (newVal) {
                    console.log(`更新${newVal}`);
                    if(val!=newVal){
                        val=newVal;
                        binding._directives.forEach(function(item){
                            item.update();
                        })
                    }
                }
            })
        }
        else{   //如果是深层次对象
            var subObj=obj[path]||{};

            // 绑定每个子对象
            Object.defineProperty(obj,path,{
                get:function(){
                    console.log(`获取${subObj}`);
                    return subObj;
                },
                set:function(newVal){
                    console.log(`更新${newVal}`);
                    if(typeof newVal==='object') {
                        //不能直接subObj=newVal,否则将无法触发已有属性的响应式更新
                        for (var kkey in newVal) {
                            subObj[kkey] = newVal[kkey]
                        }
                    }
                    else subObj=newVal;
                    binding._directives.forEach(function(item){
                        item.update();
                    })
                }
            });
        }
    };
// 重写数组原型的方法,Object.defineProperty不具备监听数组的方法
const oldArrayProperty = Array.prototype;
    const arrProto = Object.create(oldArrayProperty);
    ["push","pop","shift","unshift","splice"].forEach(
        methodName => 
        (arrProto[methodName] = function() {
            updateView();
            oldArrayProperty[methodName].call(this, ...arguments);
        })
    )

3. 绑定函数改造, 将参数与函数名分隔开,this.$method调用

//attVal   alert('Hello world')
Example.prototype._parseFunc=function(attrVal){
    var args=/(.*)/.exec(attrVal);   
    if(args) {       //如果函数带参数,将参数字符串转换为参数数组
        args=args[0];      
        //('Hello world')
        attrVal=attrVal.replace(args,"");   
        //'alert'
        args=args.replace(/[\(\)\'\"]/g,'').split(",");   
        //alert
    }
    else args=[];
    return this.$methods[attrVal].bind(this.$data,args);
}; 

4. 实例化一个_binding对象,Directive建立一个DOM节点和对应数据的映射关系,数据初始化与DOM绑定的数据对象, 之后去setter方法中添加directives,这样设置value时,就能引起对应DOM节点更新。

if(obj.hasOwnProperty(key)){
    this._binding[key]={        //初始化与DOM绑定的数据对象
        _directives:[]
    };
function Directive(name,el,vm,exp,attr){
    this.name=name;         //指令名称,例如文本节点,该值设为"text"
    this.el=el;             //指令对应的DOM元素  input
    this.vm=vm;             //指令所属Lue实例  
    this.exp=exp;           //指令对应的值,count
    this.attr=attr;         //绑定的属性值 value

    this.update();          //首次绑定时更新
}


Directive.prototype.update=function(){
    //更新DOM节点的预设属性值
    this.el[this.attr]=this.vm.$data[this.exp];
};

5. 编译DOM节点,初始化时,对DOM进行编译compile,获取子节点,遍历属性(v-click \ v-model),给节点添加相对应的onclick/addEventListener属性,对绑定的函数进行改造

 //解析DOM的指令
    Example.prototype._compile=function(root){
        var _this=this;
        //获取指定作用域下的所有子节点
        var nodes=root.children;
        for(var i=0;i<nodes.length;i++){
            var node=nodes[i];
            //若该元素有子节点,则先递归编译其子节点
            if(node.children.length){
                 this._compile(node);
            }

            if(node.hasAttribute("v-click")) {
                node.onclick = (function () {
                    var attrVal=nodes[i].getAttribute("v-click");
                    // attrVal = alert('Hello world')
                    return _this._parseFunc(attrVal);
                })()
            }

            if(node.hasAttribute("v-model")&& (node.tagName=="INPUT" || node.tagName=="TEXTAREA")){
                //如果是input或textarea标签
                node.addEventListener("input", (function (key) {
                    var attrVal=node.getAttribute("v-model");
                    //将value值的更新指令添加至_directives数组
                    _this._binding[attrVal]._directives.push(new Directive(
                        "input",
                        node,
                        _this,
                        attrVal,
                        "value"
                    ))

                    return function () {
                        //_this.$data[attrVal] = nodes[key].value;
                        _this.$data.$set(attrVal,nodes[key].value);
                    }
                })(i));
            }

            if(node.hasAttribute("v-bind")){
                var attrVal=node.getAttribute("v-bind");
                //将innerHTML的更新指令添加至_directives数组
                _this._binding[attrVal]._directives.push(new Directive(
                        "text",
                        node,
                        _this,
                        attrVal,
                        "innerHTML"
                ))
            }
        }
    }

Object.defineProperty

Object.defineProperty()会在一个对象上定义属性

Object.defineProperty(定义属性的对象, 定义或修改的属性的名称, 定义或修改的属性描述符);
const object = {}
Object.defineProperty(object, 'property', {
    value:42
    writable:false
});
object.property = 77

  • writable writable 属性设置为 false时,不能被重新赋值。
  • enumerable enumerable 定义了对象的属性是否可以在 for...in循环和 Object.keys()中被枚举。
  • 自定义setter和getter