目录
vue2响应式
什么是响应式
首先我们先来了解一下什么是响应式,我们先来看一下官方的解释
当你把一个普通的 JavaScript 对象传入 Vue 实例作为
data
选项,Vue 将遍历此对象所有的 property,并使用Object.defineProperty
把这些 property 全部转为 getter/setter。Object.defineProperty
是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
ok,我们通过上面那句话得知,他是使用了Object.defineProperty
来完成响应式处理的,那他究竟是怎么使用的呢?
怎么实现的响应式
废话不多说,我们点这里 Object.defineProperty
进入到MDN看一下里面的例子
var bValue = 38;
Object.defineProperty(o, "b", {
// 使用了方法名称缩写(ES2015 特性)
// 下面两个缩写等价于:
// get : function() { return bValue; },
// set : function(newValue) { bValue = newValue; },
get() { return bValue; },
set(newValue) { bValue = newValue; },
enumerable : true,
configurable : true
});
我们看到该方法接收三个参数 Object.defineProperty(obj, prop, descriptor)
obj
要定义属性的对象。
prop
要定义或修改的属性的名称或 Symbol
。(该对象的key)
descriptor
要定义或修改的属性描述符。(你要更改该对象的哪些方法)
看上去很简单,我们只需要传入一个对象,再传入一个key值,再定义一些方法,就可以实现简单的 响应式了,那我们接下来自己实现一下吧
Object的响应式处理
<!DOCTYPE html>
<html lang="en">
<body>
<h1 id="name"></h1>
<input id="input" type="text">
</body>
<script>
const Input = document.getElementById("input");
const Name = document.getElementById("name");
const Data = {name: 'jackliu'}
Name.innerHTML = Data.name
Input.oninput = (e) => {
Data.name = e.target.value
}
function watchKey(obj, key, val) {
Object.defineProperty(obj, key, {
get: () => {
return val
},
set: (newVal) => {
if (newVal !== val) {
console.log('渲染视图')
val = newVal
Name.innerHTML = val
}
}
});
}
watchKey(Data, 'name', Data.name)
</script>
</html>
这样我们就能进行单个值的监听了,那我们如何进行多个值的监听呢?我们只需要对对象进行循环就好了
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<h1 id="show"></h1>
<input id="Name" type="text">
<input id="Id" type="text">
</body>
<script>
const Name = document.getElementById("Name");
const Id = document.getElementById("Id");
const Show = document.getElementById("show");
const Data = {
name: 'jackliu',
id: 0
}
Show.innerHTML = Data.Show
Name.oninput = (e) => {
Data.name = e.target.value
}
Id.oninput = (e) => {
Data.id = e.target.value
}
// 判断类型
function checkObjOrArr(obj) {
const type = Object.prototype.toString.call(obj)
if (['[object Object]', '[object Array]'].includes(type)) {
return true
} else {
return false
}
}
// 给对象的所有key添加响应式
function watchAll(obj) {
if (!checkObjOrArr(obj)) {
return obj
}
for (let key in obj) {
watchKey(obj, key, obj[key])
}
}
function watchKey(obj, key, val) {
Object.defineProperty(obj, key, {
get: () => {
return val
},
set: (newVal) => {
if (newVal !== val) {
console.log('渲染视图')
val = newVal
changeShow()
}
}
});
}
watchAll(Data)
function changeShow() {
Show.innerHTML = `name: ${Data.name} id: ${Data.id}`
}
changeShow()
</script>
</html>
如此一来我们就能监听一个对象上所有的key,但是这里有一个大问题,我们现在监听的都只是值类型,
const Data = {
name: 'jackliu',
id: 0
}
如果我们的key中的类型是对象或数组,会怎么样呢?
const Data = {
name: 'jackliu',
ids: {
id: 0
}
}
我们可以看到,值类型的name是有响应式的,但是对象类型的ids下面的id失去了响应式
那我们该如何解决这个问题呢?
// 单个key添加响应式
function watchKey(obj, key, val) {
watchAll(val)
Object.defineProperty(obj, key, {
get: () => {
return val
},
set: (newVal) => {
if (newVal !== val) {
console.log('渲染视图')
val = newVal
changeShow()
return
}
watchAll(val)
}
});
}
很简单,只需要在添加单个key的响应式时,再执行一次 watchAll 就可以了,如果当前key的值是对象,那么就对其所有的key进行绑定,如果是值类型,就直接返回,不做处理,走到这一步,对对象的监听其实已经算是完成的差不多了,但是,还有一个很严重的问题,大家不妨先思考一下,是什么问题
没错,就是值类型,一旦被改为object类型,那我们的新值就会失去响应式,这可咋整?
// 单个key添加响应式
function watchKey(obj, key, val) {
watchAll(val)
Object.defineProperty(obj, key, {
get: () => {
return val
},
set: (newVal) => {
if (newVal !== val) {
console.log('渲染视图')
val = newVal
watchAll(val)
changeShow()
}
}
});
}
是的,聪明的你一定想到了,我们只需要在值变更的时候再执行一次 watchAll 对新值绑定响应式即可,该方法中我们已经做了判断,如果是值类型,我们直接返回不做处理,如果是对象或数组,我们就会绑定响应式,到此为止,对象和值类型的响应式,我们已经搞明白了,那数组类型该如何处理呢?
Array的响应式处理
由于Object.defineProperty
只能对object实现响应式,当我们的数据是数组时,上面的代码就无能为力了,怎么办呢,我们需要对数组,做一下处理
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<h1 id="show"></h1>
<input id="Name" type="text" />
<input id="Id" type="text" />
<input id="Add" type="button" value="Add" />
</body>
<script>
const Name = document.getElementById("Name");
const Id = document.getElementById("Id");
const Show = document.getElementById("show");
const Data = {
name: 'jackliu',
ids: {
id: 0
},
money: [1]
}
Show.innerHTML = Data.Show
Name.oninput = (e) => {
Data.name = e.target.value
}
Id.oninput = (e) => {
Data.ids.id = e.target.value
}
Add.onclick = () => {
Data.money.push(0)
}
// 判断类型
function checkObjOrArr(obj) {
const type = Object.prototype.toString.call(obj)
if (['[object Object]', '[object Array]'].includes(type)) {
return true
} else {
return false
}
}
// 获取数组的显式原型
const arrayProto = Array.prototype;
// 通过Object.create 得到转换后的具有数组显式原型的对象
const newProto = Object.create(arrayProto);
// 定制想要实现监听的原型属性
['push', 'pop', 'shift', 'unshift'].forEach((name) => {
newProto[name] = function(...args) {
arrayProto[name].call(this, ...args)
console.log('视图渲染', this)
changeShow()
}
})
// 给对象的所有key添加响应式
function watchAll(obj) {
if (!checkObjOrArr(obj)) {
return obj
}
// 修改数组的隐式原型
if (Array.isArray(obj)) {
obj.__proto__ = newProto;
}
for (let key in obj) {
watchKey(obj, key, obj[key])
}
}
// 单个key添加响应式
function watchKey(obj, key, val) {
watchAll(val)
Object.defineProperty(obj, key, {
get: () => {
return val
},
set: (newVal) => {
if (newVal !== val) {
console.log('渲染视图')
val = newVal
watchAll(val)
changeShow()
}
}
});
}
watchAll(Data)
// 修改dom内容,此处应为虚拟dom,但是我们本章不讲,所以直接操作了原生dom
function changeShow() {
Show.innerHTML = `name: ${Data.name} id: ${Data.ids.id} 存款: ${Data.money.join('')}`
}
changeShow()
</script>
</html>
优点
基于es5实现,支持绝大部分浏览器
缺点
由于是递归实现监听,所以如果数据层级过深,会导致初始化的时间过长,而且原生不支持监听数组,需要进行处理,对象中新增的key无法获取响应性,通过数组的下标改变数据时,也无法触发响应式
vue3响应式
值类型响应式
vue中声明响应式的方式可以简单分为两种,一种是值类型响应式,例如字符串,布尔值,数字,一种是复杂类型响应式,例如对象,数组,map,set
我们先来看一下值类型是如何实现响应式的
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<h1 id="Show"></h1>
<input id="input" type="text">
</body>
<script>
const Input = document.getElementById("input");
const Show = document.getElementById("Show");
const Data = refFn('jackliu')
Show.innerHTML = Data.Show
Input.oninput = (e) => {
Data.value = e.target.value
}
function refFn(_val) {
return {
get value() {
return _val
},
set value(newVal) {
_val = newVal
// vue触发数据变化的操作,我们不展开,统一改成修改dom
changeShow()
}
}
}
function changeShow() {
Show.innerHTML = Data.value
}
changeShow()
</script>
</html>
去掉注释,10行代码搞定,只能说 牛啊牛啊
我们从上边的代码可以看出,ref方法返回了一个对象,该对象有个被设置了set和get方法的value,这个value,通过在set方法中触发我们自定义的事件,来实现响应式
复杂类型响应式
vue3是使用了 Proxy 和 Reflect 来完成的响应式处理,我们需要先了解这两个是什么东西,究竟是怎么用的
Proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。简而言之,我们可以使用proxy来代理一个对象,被我们代理之后的对象,访问其方法都会被我们设置的捕捉器所拦截。
Reflect
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。Reflect
不是一个函数对象,因此它是不可构造的。该方法拥有Object对象的所有方法,不过存在一些细微差别。
如何使用
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<h1 id="Show"></h1>
<h1 id="Id"></h1>
<input id="input" type="text">
</body>
<script>
const Input = document.getElementById("input");
const Id = document.getElementById("Id");
const Show = document.getElementById("Show");
const Data = { name: 'jackliu' }
Show.innerHTML = Data.Show
Input.oninput = (e) => {
newData.name = e.target.value
}
const ProxyData = (data) => {
if (!data || typeof data !== 'object') {
return data;
}
const config = {
/**
* 属性读取操作的捕捉器。
* @target 目标对象。
* @key 被获取的属性名。
* @receiver Proxy或者继承Proxy的对象
*/
get: (target, key, receiver) => {
// Reflect.ownKeys 返回对象的非原型属性 类似 Object.keys
const keys = Reflect.ownKeys(target)
// 判断当前获取的key是否已存在
if (keys.includes(key)) {
console.log("get", target, key, receiver)
}
console.log('执行代理')
// Reflect.get 如果未获取到对应的值,会返回false
const result = Reflect.get(target, key, receiver)
// 此步返回了一个ProxyData方法,解释一下为什么要返回它,如果result为值类型,那么则会直接返回当前值,如果非值类型,那么,就对其进行代理
return ProxyData(result)
},
/**
* 属性读取操作的捕捉器。
* @target 目标对象。
* @key 被获取的属性名。
* @value 新属性值。
* @receiver 最初被调用的对象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是 proxy 本身)。
*/
set: (target, key, value, receiver) => {
const keys = Reflect.ownKeys(target)
// 如果当前值已存在,那就是修改之前的值,否则就是新增
if (keys.includes(key)) {
// 判断值是否相同,如果相同,直接return
if (value === Reflect.get(target, key, receiver)) {
console.log("重复修改", target, key, receiver)
return true
}
} else {
console.log("新增", target, key, receiver)
}
const result = Reflect.set(target, key, value, receiver)
// 执行我们的方法
changeShow()
return result
},
}
return new Proxy(data, config)
}
const newData = ProxyData({ ...Data })
window.newData = newData
function changeShow() {
Show.innerHTML = newData.name
Id.innerHTML = newData.id
}
changeShow()
</script>
</html>
大致的原理和vue2的类似,都是更改了对象原始的get,set,只不过proxy原生支持数组,不用另外特殊处理,且通过下标更改值,依然能触发响应式
优点
速度快,只有get数据时才会添加响应式,不用初始化时深层次递归,可以检测到代理对象属性的动态添加和删除,可以监测到数组的下标和length属性的变更
缺点
ES6的proxy语法对于低版本浏览器不支持,IE11
有错误的或有遗漏的地方希望大家可以进行指正和补充,感谢!