vue模拟双向数据绑定原理

162 阅读5分钟

前言:

vue是一套渐进式框架(mvvm),用于写前端的用户界面。

用vue开发有很明显的感觉就是声明式开发,开发者能写什么,在哪里写,怎么写都被vue给事先确定了。作为一个vue使用级的选手,学习vue就是学习框架的这些规则。并通过这些规则实现项目的开发。 在学习这类框架之前,前端开发者往往都学过jQuery和了解过Lodash这类库。为此我去了解和感受了一下库和框架的区别。

库:它不实现具体的某个功能,但是它提供了许多项目中常用得方法。它内部得方法需要开发者自己去根据需求自 行调用。

框架:框架内部也有大量的方法,开发者使用框架的话,必须按照框架规定的写法,标准和规则。 同时它内部也有许多方法并不是由开发者自己去调用的,开发者只需要在规定的地方准备特定的业务处理程序,之后由框架自行去调用了。

简单理解就是:是你用它还是它用你的区别。

关于渐进式:

一般框架或者库的开发者会对每个功能板块要实现的东西都单独封装为一个个的js文件模块,使用时,按照功能模块按需引入自己的项目中使用。 对于不同开发者在使用框架的时候,能用到的功能模块不同,所以框架作者将这个大框架分为几个不同功能模块的小模块,开发者可以选择多个模块的多种组合去进行开发。

vue的渐进式体现

vue.js (基础核心模块):内部有基础语法、核心实现、组件开发、指令等(核心库只关注视图层)

vue-router (路由模块):构建SPA应用使用的前端路由模块

vuex (公共状态管理模块):组件间数据共享管理的模块

vue-cli(脚手架):快速搭建vue项目的工具模块

components(组件):第三方UI库,如:element-ui、iview、vux等

使用vue就是创建一个vue实例,由实例(vm监听器)去管理html部分和data数据部分。底层就是一个类,使用类构造实例,new Vue(options)

MVVM

开发思想:

  • 传统操作DOM模式,常常引发回流,重绘
  • MVC:model view controller
  • MVVM:model view viewModel

模拟实现双向数据绑定原理

1.0版本:
function oberver(obj){
    if( typeof obj !== 'object' || obj ===null){
        return
    }
    for(let key in obj){
        defineReactive(obj,key,obj[key])
    }
}
function defineReactive(obj,key,value) {
    Object.defineProperty(obj,key,{
        get(){
            return value  //这里形成了闭包,get函数引用了外层函数defineReactive的value值。
        },
        set(newValue){
            value = newValue  //这里set函数引用了外层函数defineReactive的value值,并修改了该value值
            console.log('视图更新')
        }
    })
}
let data1 = { name:'小白' }
let data2 = { name:{age:17} }
oberver(data)
data.name='abc'
data.age=17  //这里是后续添加的属性,可以看出它没有set与get,所以没有响应式能力
let data2 = { name:{age:17} }
observe(data2)
data2.name.age=17  
不足:
没有初始化的对象属性不具备响应式的能力
对于多层的数据结构无法实现内层数据的响应式能力
如果设置 data.name={gender:'male'},那新增加的  {gender:'male'} 将没有响应式的能力
2.0版本:
function oberver(obj){
    if( typeof obj !== 'object' || obj ===null){
        return
    }
    for(let key in obj){
        defineReactive(obj,key,obj[key])
    }
}
function defineReactive(obj,key,value) {
	oberver(value)    // +++++++++++++++++++++  递归实现内层响应式数据,比较耗性能
    Object.defineProperty(obj,key,{
        get(){
            return value  //这里形成了闭包,get函数引用了外层函数defineReactive的value值。
        },
        set(newValue){
           if(value !== newValue){
            observe(newValue)  //++++++++++++++++++++++    newvalue可能是一个对象类型的数据
            value = newValue  //这里set函数引用了外层函数defineReactive的value值,并修改了该value值
            console.log('视图更新')
          }
        }
    })
}
let data1 = { name:'小白' }
let data2 = { name:{age:17} }
oberver(data)
data.name.age=17  
不足:
没有考虑data1是数组的情况,同时也没有考虑data对象的某个属性的属性值是数组的情况,同时data对象的某个属性的属性值是数组,而该数组的某个元素项是一个对象的情况也没有考虑。
3.0版本:
function oberver(obj){
    if( typeof obj !== 'object' || obj ===null){
        return
    }
    if(Array.isArray(obj)){//这里处理的是数组类型
        for(let i=0;i<obj.length;i++){
            let item = obj[i]
            observer(item)   //如果数组中的某项是对象,那也将该对象的属性设为响应式的数据
        }
    }else{//这步else处理的是对象类型
        for(let key in obj){
        defineReactive(obj,key,obj[key])
    	}
    }
}

function defineReactive(obj,key,value) {
	oberver(value)    // +++++++++++++++++++++  递归实现内层响应式数据,比较耗性能
    Object.defineProperty(obj,key,{
        get(){
            return value  //这里形成了闭包,get函数引用了外层函数defineReactive的value值。
        },
        set(newValue){
           if(value !== newValue){
            observe(newValue)  //++++++++++++++++++++++    newvalue可能是一个对象类型的数据
            value = newValue  //这里set函数引用了外层函数defineReactive的value值,并修改了该value值
            console.log('视图更新')
          }
        }
    })
}
let data1 = { name:'小白' }
let data2 = { name:{age:17} }
oberver(data)
data.name.age=17  
不足:
缺少常见数组方法操作数据后仍然具有响应式的能力,如数组的push,splice,unshift
4.0版本:
let arrayProto = Array.prototype
let proto = Object.create(arrayProto);
['push','unshift','splice'].forEach(method=>{
    proto[method] = function(...args){  //这里的args数据可能也是一个对象类型的数据,也要考虑监视一下
        let inserted
        switch(method){
                case:'push':
                case:'unshift':
                	inserted = args
                	break;
            	case 'splice': //只有传递三个参数才表示追加效果
                	inserted = args.splice(2)
                	break;
            	default:
                	break;
        }
       console.log('试图更新')
       arrayObserve(inserted)
       arrayProto[method].call(this,...args)
    }
})

function arrayObserve(obj){
    for(let i=0;i<obj.length;i++){
            let item = obj[i]
            observer(item)   //如果数组中的某项是对象,那也将该对象的属性设为响应式的数据
    }
}


function oberver(obj){
    if( typeof obj !== 'object' || obj ===null){
        return
    }
    
    if(Array.isArray(obj)){//这里处理的是数组类型
        Object.setPrototypeOf(obj.proto)  //+++++++++++++++++  通过设置原型,对数组方法进行重写
        arrayObserve(obj)
    }else{//这步else处理的是对象类型
        for(let key in obj){
        defineReactive(obj,key,obj[key])  
    	}
    }
}

function defineReactive(obj,key,value) {   
	oberver(value)  
    Object.defineProperty(obj,key,{  
        get(){
            return value 
        },
        set(newValue){
           if(value !== newValue){
            observe(newValue) 
            value = newValue  
            console.log('视图更新')
          }
        }
    })
}
let data1 = { name:'小白' }
let data2 = { name:{age:17} }
let data3 = { d:[1,2,3] }
oberver(data1)
data1.name.age=17  
oberver(data3)
data3.d.push={ name:'jack'}

总结,从上面的各个版本来看,可以得出:

  • data对应的数据类型必须是对象,或者函数返回对象

  • vue框架中,如果需要data中的数据实现响应式的能力,那必须考虑初始化数据

  • 对于data中的属性值是数组时,那数组中的基本数据类型没有响应式能力,但是数组中的对象类型元素可以有响应式能力

  • vue的底层中重新写了一些数组的方法

  • 实现响应式数据的关键方法: Object.defineProperty Object.setPrototypeOf(obj.proto) let proto = Object.create(arrayProto)

  • 对于数组的响应式,vue是通过重写数组的方法实现的

  • 通过数组的索引去修改元素项,是无法实现响应式的

  • 要理解什么样的数据操作不会引发视图更新