# 1.响应式数据
# 1.1.Object.defineProperty
# 1.1.1.基础语法
定义一个对象可以使用构造函数或者字面量的形式,例如:
let obj = new Object()
obj.name = "xiaoqiang"
// 或者 使用字面量形式
let obj2 = {}
obj2.name = "xiaowang"
除了上面定义属性的方式外,还可以使用Object.defineProperty这种方式来定义或修改属性
语法:
Object.defineProperty(obj, prop, descriptor)
参数:
obj要在其上定义属性的对象。
prop要定义或修改的属性的名称。
descriptor将被定义或修改的属性描述符。
返回值:
被传递给函数的对象
# 1.1.2.描述详解
value: 设置属性的值
writable: 值是否可以重写。true | false
enumerable: 目标属性是否可以被枚举。true | false
configurable: 目标属性是否可以被删除或是否可以再次修改特性 true | false
举例:
var obj = {
name:"nodeing"
}
//对象已有的属性添加特性描述
Object.defineProperty(obj,"name",{
configurable:true | false,
enumerable:true | false,
value:任意类型的值,
writable:true | false
});
//对象新添加的属性的特性描述
Object.defineProperty(obj,"age",{
configurable:true | false,
enumerable:true | false,
value:任意类型的值,
writable:true | false
});
下面详细解释描述符:
# value
value: 只是用来设置当前需要添加或者修改属性的值,它可以是任意的类型
举例:
let obj = new Object()
Object.defineProperty(obj, "name", {
value: "nodeing",
})
console.log(obj.name) // nodeing 如果不设置value值,默认为undefined
# writable
writeable: 这个用来控制属性的值是否可以被重写。设置为true可以被重写;设置为false,不能被重写。默认为false。
举例:
let obj = new Object()
Object.defineProperty(obj, "name", {
value:"nodeing",
writable: false
})
obj.name = "nodeing.com"
// 当writeable默认为false的时候,修改name的值是无效的 依旧会输出nodeing
console.log(obj.name)
你可以将上面案例中的 writable的值改成true,再来运行看看结果
# enumerable
enumerable: 作用是控制属性是否可以被枚举,即是否可以被(for in)或者 (Object.keys())这些方法遍历出来,默认
为false,设置为true以后,才可以被遍历出来
举例:
let obj = new Object()
Object.defineProperty(obj, "name", {
value:"nodeing",
enumerable: true
})
console.log(obj)
console.log(Object.keys(obj))
# configurable
configurable: 作用有两个,1.是否可以使用delete删除目标属性,2.除 value 和 writable 特性外的其他特性是否可以被修改
举例:
let obj = new Object()
Object.defineProperty(obj, "name", {
value:"nodeing",
enumerable: true,
configurable: false
})
delete obj.name
console.log(obj.name) // 依然可以输出value值:nodeing
上面代码中,你可以将configurable的值改成true,再运行试试看
举例2:
let obj = new Object()
Object.defineProperty(obj, "name", {
value:"nodeing",
enumerable: true,
configurable: false
})
Object.defineProperty(obj, "name", {
value:"nodeing",
enumerable: true,
configurable: true
})
console.log(obj.name) // TypeError: Cannot redefine property: name
上面代码中,你可以将第一次定义name属性的时候,给的configurable值改为true,再来运行看看输出结果
TIP
注意1: configurable 如果默认为false的时候,要修改writable的值,必须是从true改为false,如果是从false改为true,就会抛出异常
TIP
注意2:
如果不设置属性的特性,configurable、enumerable、writable这些值都默认为false
Object.defineProperty(obj,'name',{});
这种情况,configurable、enumerable、writable这些值都默认为false,value为undefined
# 1.1.3.存取器描述
前面我们学习了 writable和value这两种描述选项,它们是和设置属性值相关的,我们还可以使用存取器getter和setter来获取或者设置值
var obj = {};
Object.defineProperty(obj,"name",{
get:function (){} | undefined, //getter 是一种获得属性值的方法
set:function (value){} | undefined // setter是一种设置属性值的方法。
configurable: true | false
enumerable: true | false
});
TIP
注意:使用了getter和setter后,writable和value就不能使用了
举例:
let obj = new Object()
Object.defineProperty(obj, "name", {
get: function(){
console.log("你正在获取name的值")
return "nodeing"
},
enumerable: true,
configurable: true
})
console.log(obj.name) // 触发get函数, 当前name属性的值为get函数返回的值
举例2:
let obj = new Object()
let initValue = "nodeing"
Object.defineProperty(obj, "name", {
get: function(){
console.log("你正在获取name的值")
return initValue
},
set: function(value){
console.log("你正在设置name的值")
// 如果使用 obj.name = "nodeing.com"来重新设置值,此时的值 nodeing.com,会传给set函数里的第一个参数,
// 意味着 value = "nodeing.com"
initValue = value
console.log("设置完毕")
},
enumerable: true,
configurable: true
})
obj.name = "nodeing.com"
console.log(obj.name)
TIP
从上面两个例子中可以看出,get和set两个函数并不是一定要同时成对出现的
# 1.2.实现Vue中的响应式数据
# 1.2.1.回顾Vue中的数据响应式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h1>{{msg}}</h1>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
let vm = new Vue({
el: "#app",
data: {
msg: "nodeing"
}
})
// 当修改msg值的时候,视图会跟着变化
vm.msg = "nodeing.com"
</script>
</body>
</html>
# 1.2.2.实现Vue中的数据响应式
我们的需求是,当数据发生变化,视图跟着变化,下面我们通过Object.defineProperty来实现
首先,我们来实现最简单的数据劫持
// 监听的数据对象
let data = {
msg: "nodeing"
}
// 把data渲染到视图
function render(){
console.log("模拟msg被渲染")
}
// 观察对象
function observer(obj){
// 在这里面,需要把每个属性劫持到geter和setter
// 首先判断obj是不是一个对象
if(Object.prototype.toString.call(obj) === "[object Object]"){
// 循环obj的属性,分别修改成getter和setter的模式
for(let key in obj){
toRective(obj,key,obj[key])
}
}
}
function toRective(obj,key,initValue) {
Object.defineProperty(obj,key,{
get:function() {
return initValue
},
set: function (value) {
initValue = value
render()
}
})
}
// 调用观察者方法 观察data变化
observer(data)
上面就是最简单的数据劫持原理,但是,上面代码只是实现了对象第一层属性的监听,当数据变成下面这样:
let data = {
msg: "nodeing",
goods: {
name:"手机",
price: 100
}
}
我们没办法监听到上面更深层次的对象,因此,还需要修改代码
// 监听的数据对象
let data = {
msg: "nodeing",
goods: {
name:"手机",
price: 100
}
}
// 把data渲染到视图
function render(){
console.log("模拟msg被渲染")
}
// 观察对象
function observer(obj){
// 在这里面,需要把每个属性劫持到geter和setter
// 首先判断obj是不是一个对象
if(Object.prototype.toString.call(obj) === "[object Object]"){
// 循环obj的属性,分别修改成getter和setter的模式
for(let key in obj){
toRective(obj,key,obj[key])
}
}
}
function toRective(obj,key,initValue) {
if (Object.prototype.toString.call(initValue) === "[object Object]"){
observer(initValue)
}
Object.defineProperty(obj,key,{
get:function() {
return initValue
},
set: function (value) {
initValue = value
render()
}
})
}
// 调用观察者方法 观察data变化
observer(data)
data.goods.price = 20
当给data对象的属性设置一个新的对象的时候,这个新对象也是需要被监听的
data.goods = {
a:"手机",
b: 200
}
data.goods.a = 20
如果设置data属性变成上面这样,我们需要修改toRective方法
function toRective(obj,key,initValue) {
if (isObjcet(obj)){
observer(initValue)
}
Object.defineProperty(obj,key,{
get:function() {
return initValue
},
set: function (value) {
// 当新设置的值和旧的值一样的时候,就不渲染
if(initValue === value) return
// 当新设置的value值也是一个对象的时候,也需要observer操作
if(isObjcet(value)){
observer(value)
}
initValue = value
render()
}
})
}
// 定义一个判断对象的方法
function isObjcet(obj) {
return Object.prototype.toString.call(obj) === "[object Object]"
}
# 1.2.3.set方法和变更方法
前面的代码,我们已经大致实现了数据响应式变化,但还有一些问题需要解决,比如,当新增对象属性的时候,无法实现响应式,实际上,在vue中,对于已经创建的实例,Vue不允许动态添加根级别的响应式property的,查看文档 (opens new window)
例如:
let data = {
msg: "nodeing",
goods: {
name:"手机",
price: 100
}
}
// 新增一个a属性 这个a属性不是响应式的
data.a = 200
在vue中,实现了一个Vue.set方法来对新增属性进行响应式监听,它有一个别名vm.$set
Vue.set(vm.someObject, 'b', 2)
基于前面的代码,我们也可以去实现这个set方法
function $set(obj, key, value) {
toRective(obj, key, value)
}
Object.defineProperty是不支持数组的,Vue文档中,包裹了一些数组的方法,它们是支持响应式的,这些方法有:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
我们也需要继续完善代码,以支持数组的更新检测
// 监听的数据对象
let data = {
msg: "nodeing",
goods: {
name:"手机",
price: 100
}
}
// 把data渲染到视图
function render(){
console.log("模拟msg被渲染")
}
// 需要支持更新检测的方法
let arrayMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
// 实现思路: 1 这些方法都是在数组原型上的 2 我们重新包裹这些原型上的方法,在调用了这些原型方法后,调用render方法更新视图
// 第一步,先把原型复制一份出来,以免对数组原型产生破坏
let newPrototype = Object.create(Array.prototype)
arrayMethods.forEach((method)=>{
newPrototype[method] = function () {
Array.prototype[method].call(this, ...arguments)
render()
}
})
// 观察对象
function observer(obj){
// 先判断是不是数组
if(Array.isArray(obj)){
obj.__proto__ = newPrototype
return
}
// 在这里面,需要把每个属性劫持到geter和setter
// 首先判断obj是不是一个对象
if(Object.prototype.toString.call(obj) === "[object Object]"){
// 循环obj的属性,分别修改成getter和setter的模式
for(let key in obj){
toRective(obj,key,obj[key])
}
}
}
function toRective(obj,key,initValue) {
if (isObjcet(obj)){
observer(initValue)
}
Object.defineProperty(obj,key,{
get:function() {
return initValue
},
set: function (value) {
// 当新设置的值和旧的值一样的时候,就不渲染
if(initValue === value) return
if(isObjcet(value)){
observer(value)
}
initValue = value
render()
}
})
}
function isObjcet(obj) {
return Object.prototype.toString.call(obj) === "[object Object]"
}
function $set(obj, key, value) {
toRective(obj, key, value)
}
let arr = [1,2,3]
// 调用观察者方法 观察data变化
observer(arr)
arr.push(4)
我们自定义的$set方法也需要支持数组
function $set(obj, key, value){
if(Array.isArray(obj)){
return obj.splice(key,1,value)
}
toReactive(obj,key,value)
}
# 1.3.proxy基础用法
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写
如果要访问一个目标对象,在中间加一层拦截器,可以对外界的访问进行过滤和修改,例如:
let obj = {
a: 1
}
let proxy = new Proxy(obj, {
get(target, prop ){
console.log(target, prop)
return Reflect.get(target, prop)
},
set(target, key, value){
console.log("xxx")
return Reflect.set(target, key, value)
}
})
proxy.a
# 1.4.使用proxy来实现响应式数据
let data = {
msg: "hello nodeing",
goods: {
name: "手机",
price: 200,
},
arr: [1,2,3]
};
function isObjcet(obj) {
return Object.prototype.toString.call(obj) === "[object Object]";
}
function render() {
console.log("模拟视图更新")
}
let handler = {
get(target, prop){
if (Array.isArray(target[prop]) || isObjcet(target[prop])){
return new Proxy(target[prop], handler)
}
return Reflect.get(target, prop)
},
set(target,key, value){
if (key ==="length") return true
Reflect.set(target, key, value)
render()
return true
}
}
let proxy = new Proxy(data, handler)
// proxy.msg = "20"
proxy.arr.push(1)
// proxy.goods.price = 3000
proxy.goods.price = 200