Vue原理篇
1.Vue2响应式原理
相信大家都经常听说Vue2的双向数据绑定是通过发布订阅者模式结合数据劫持实现的。要理解这句话,需要3步,一是了解什么是发布订阅者模式,二是了解什么数据劫持,三是如何将二者结合实现响应式。
1.1 理解发布订阅者模式
两种理解方式:一种比较偏向于生活化,一种比较偏向于技术化。
(1)第一种理解方式——生活化:
先不要管代码,想象一下我们在淘宝上买东西,暂时没货了,所以我们先把商品(需要监听的数据)加入到了购物车中,此时购物车就会知道我们需要该商品的消息,而一旦该商品发生了变化(比如降价了),就会发布消息告知我们。
所以按照这个想法,我们要制作一个订阅器模型,里面有3个东西:
第一个是容器(对象),用于存放用户的各种消息函数(数组)。这里你可以把容器理解成购物车,key是商品唯一标识,value就是该商品的各种消息。
第二个是添加订阅的方法(两个参数,一个是商品唯一标识key,一个是要添加的方法),就是往容器中的对应key值添加订阅的消息。理解容器后,这个就简单了,如果购物车中没有商品,那我们就先初始化商品,然后再添加需要知道的消息,比如是降价了,还是变贵了。
第三个是发布的方法,告诉用户数据发生了变化,并触发订阅的方法。对应来说,就是那个商品发生了改变就要发出该商品的所有消息。
(2)第二种理解方式——技术化:
订阅发布者模式主要理解订阅器模型Dep。分为3个部分:
1.clientList:{target:[fn1,fn2…]},容器,用于存放每个target/tag(通常一个target对应一个变量)的事件数组
2.listen(key,fn),订阅者,用于往clientList容器中订阅/添加指定target的事件
3.trigger(tag,val) 发布者,用于触发clientList容器中定义的指定target的事件
// 订阅器模型
// 订阅发布者模式主要订阅器模型Dep。分为3个部分:
// 1.clientList:{target:[fn1,fn2...]},容器,用于存放每个target/tag(通常一个target对应一个变量)的事件数组
// 2.listen(key,fn),订阅者,用于往clientList容器中订阅/添加指定target的事件
// 3.trigger(tag,val) 发布者,用于触发clientList容器中定义的指定target的事件
let Dep = {
clientList:{},// 容器
// 添加订阅
listen:function(key,fn){
// if(!this.clientList[key]){
// this.clientList[key] = []
// }
// this.clientList[key].push(fn)
// 短路表达式 fn:附送消息
(this.clientList[key] || (this.clientList[key] = [])).push(fn)
},
// 发布
trigger:function(){
let key = Array.prototype.shift.call(arguments), // tag
fns = this.clientList[key]
if(!fns || fns.length === 0){
return false
}
for(let i = 0,fn;fn = fns[i++];){
// console.log('this:',this ) // Dep
// console.log('arguments:',arguments) // 这是视图1/这是视图2
fn.apply(this,arguments)
}
}
}
1.2 理解数据劫持
其实数据劫持也没什么好理解的,它就是Object暴露出来的一个方法Object.defineProperty,用于监听指定对象中指定key的value的获取和设置(变化)。get方法可以获取对象(vue实例中的data返回值)属性的值,set方法可以监听属性的变化。
let value = ''
const obj = {}
Object.defineProperty(obj,'age',{
get:function(){
console.log('取值')
return value
},
set:function(val){
console.log('设置值')
value = val
}
})
1.3 发布订阅者模式结合数据劫持
(1)设计一个函数,传入要劫持的数据obj(相当于vue的data),目标元素target(相当于{{}}包裹的部分),目标元素dataKey(相当于data中的key),选择器selector(比如元素的class)
(2)这个函数每次使用,都会使用订阅器中的liste发布者,给容器client的对应target中添加一个事件(比如el.innerTExt = text)
(3)然后使用数据劫持Object.defineProperty,劫持传入的data的dateKey属性(就是vue中的data中的属性),每次发生改变,就使用订阅器中的订阅者trigger,传入target和value,从而触发对应target的事件,并使用传入的value作为参数
(4)每个虚拟dom都会成为target,并且使用该函数
index.js:
// 订阅器模型
let Dep = {
clientList:{},// 容器
// 添加订阅
listen:function(key,fn){
// if(!this.clientList[key]){
// this.clientList[key] = []
// }
// this.clientList[key].push(fn)
// 短路表达式 fn:附送消息
(this.clientList[key] || (this.clientList[key] = [])).push(fn)
},
// 发布
trigger:function(){
let key = Array.prototype.shift.call(arguments), // tag
fns = this.clientList[key]
// console.log('key:',key)
// console.log('fns:',fns)
if(!fns || fns.length === 0){
return false
}
for(let i = 0,fn;fn = fns[i++];){
// console.log('this:',this ) // Dep
// console.log('arguments:',arguments) // 这是视图1/这是视图2
fn.apply(this,arguments)
}
}
}
// 数据劫持
// data:要劫持的数据
// tag:具体的目标元素
// dataKey:目标元素的key
// selector:选择器
let dataHi = function({data,tag,dataKey,selector}){
let value = '',
el = document.querySelector(selector)
Object.defineProperty(data,dataKey,{
// 取值
get:function(){
console.log('取值')
return value
},
set:function(val){
console.log('设置值')
value = val
// 发布——如果只有虚拟dom,没有初始化或者改变的话,不会触发set中的发布
Dep.trigger(tag,val)
}
})
Dep.listen(tag,function(text){
el.innerText = text
})
}
// 描述一下:
// 发布订阅者模式结合数据劫持
// 1.了解数据劫持Object.defineProperty,get方法可以获取对象(vue实例中的data返回值)属性的值,set方法可以监听属性的变化
// 2.了解发布订阅者模式,其实我觉得叫订阅发布者模式更恰当,因为总是先订阅listen,才能发布trigger
// 订阅发布者模式主要理解订阅器模型Dep。分为3个部分:
// 1.clientList:{target:[fn1,fn2...]},容器,用于存放每个target/tag(通常一个target对应一个变量)的事件数组
// 2.listen(key,fn),订阅者,用于往clientList容器中订阅/添加指定target的事件
// 3.trigger(tag,val) 发布者,用于触发clientList容器中定义的指定target的事件
// 3.了解数据劫持结合发布订阅者模式实现双向绑定/响应式原理:
// 1.设计一个函数,传入要劫持的数据obj(相当于vue的data),目标元素target(相当于{{}}包裹的部分),目标元素dataKey(相当于data中的key),选择器selector(比如元素的class)
// 2.这个函数每次使用,都会使用订阅器中的liste发布者,给容器client的对应target中添加一个事件(比如el.innerTExt = text)
// 3.然后使用数据劫持Object.defineProperty,劫持传入的data的dateKey属性(就是vue中的data中的属性),每次发生改变,就使用订阅器中的订阅者trigger,传入target和value,从而触发对应target的事件,并使用传入的value作为参数
// 4.每个虚拟dom都会成为target,并且使用该函数
// 4.初始化或者更新data中的数据时都会触发订阅器中的trigger,从而完成响应式的操作
1.4 初始化或者更新data中的数据时都会触发订阅器中的trigger,从而完成响应式的操作
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
订阅视图-1:<span class="box-1"></span>
订阅视图-2:<span class="box-2"></span>
</div>
<script src="index.js"></script>
<script>
let obj = {}
dataHi({
data:obj,
tag:'view-1',
dataKey:'one',
selector:'.box-1'
})
dataHi({
data:obj,
tag:'view-2',
dataKey:'two',
selector:'.box-2'
})
// 1.初始化赋值
obj.one = '这是视图1'
obj.two = '这是视图2'
// 2.劫持数据,更新者负责重新渲染 N次
// 响应式
// 1.数据联动(双向绑定)
// 2.需要捕获到修改
</script>
</body>
</html>
初始化:
更新:
2.Vue3响应式原理
Vue3和Vue2完全不同,根本没用Object.defineProperty,而是使用了Proxy。
Proxy 可以对目标对象的读取、函数调用等操作进行拦截,然后进行操作处理。它不直接操作对象,而是像代理模式,通过对象的代理对象进行操作,在进行这些操作时,可以添加一些需要的额外操作。
一个 Proxy 对象由两个部分组成: target 、 handler 。在通过 Proxy 构造函数生成实例对象时,需要提供这两个参数。 target 即目标对象, handler 是一个对象,声明了代理 target 的指定行为。
const target = {
name:'sheldon',
age:10
}
let proxy = new Proxy(target,{
get:function(target,key){
return target[key]
},
set:function(target,key,value){
console.log(`修改了${key}`)
target[key] = value
}
})
console.log(proxy.name)
proxy.age = 26