Appearance
前言
在vue的生态圈下有个重要但是又不是经常会使用的:响应式原理。而vue2和vue3中又使用了不同的api去实现这个数据响应式。听说前端是最卷的,所以为了使自己成为浪潮了一员,我又去偷偷学习了。
废话不多说,直接上
Vue2响应式原理
先看一张来自Vue2官网的一张图:
这张图我是这么理解的:
DOM树被“Touch”,然后getter告诉Watcher去收集depend依赖,通过setter去notify通知Watcher,Watcher再去通过虚拟Dom和diff算法,最后render函数渲染上树。
说人话就是是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。其中有dep类进行了依赖收集和watcher类通过模板编译最后改变真实DOM里面的数据。有什么不对欢迎告知,纯属个人理解。
回忆了一波官方图,回到正轨上
Object.defineProperty()
作用:在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
语法:Object.defineProperty(obj, prop, descriptor)
参数:
- 要添加属性的对象
- 要定义或修改的属性的名称或
[Symbol]
- 要定义或修改的属性描述符
看一个简单的例子
js
let person = {}
let personName = 'lihua'
//在person对象上添加属性namep,值为personName
Object.defineProperty(person, 'namep', {
//但是默认是不可枚举的(for in打印打印不出来),可:enumerable: true
//默认不可以修改,可:wirtable:true
//默认不可以删除,可:configurable:true
get: function () {
console.log('触发了get方法')
return personName
},
set: function (val) {
console.log('触发了set方法')
personName = val
}
})
//当读取person对象的namp属性时,触发get方法
console.log(person.namep)
//当修改personName时,重新访问person.namep发现修改成功
personName = 'liming'
console.log(person.namep)
// 对person.namep进行修改,触发set方法
person.namep = 'huahua'
console.log(person.namep)
let person = {}
let personName = 'lihua'
//在person对象上添加属性namep,值为personName
Object.defineProperty(person, 'namep', {
//但是默认是不可枚举的(for in打印打印不出来),可:enumerable: true
//默认不可以修改,可:wirtable:true
//默认不可以删除,可:configurable:true
get: function () {
console.log('触发了get方法')
return personName
},
set: function (val) {
console.log('触发了set方法')
personName = val
}
})
//当读取person对象的namp属性时,触发get方法
console.log(person.namep)
//当修改personName时,重新访问person.namep发现修改成功
personName = 'liming'
console.log(person.namep)
// 对person.namep进行修改,触发set方法
person.namep = 'huahua'
console.log(person.namep)
监测对象上多个属性
js
var person = {
name:"张三",
age:18
}
function defineProperty(obj,key,val){
Object.defineProperty(obj,key,{
get(){
return val
},
set(newVal){
val = newVal
}
})
}
function Observer(obj){
Object.keys(obj).forEach( (key) =>{
defineProperty(obj,key,obj[key])
})
}
Observer(person)
var person = {
name:"张三",
age:18
}
function defineProperty(obj,key,val){
Object.defineProperty(obj,key,{
get(){
return val
},
set(newVal){
val = newVal
}
})
}
function Observer(obj){
Object.keys(obj).forEach( (key) =>{
defineProperty(obj,key,obj[key])
})
}
Observer(person)
监测多层嵌套对象
上面只是简单的对象,如果是一个层层嵌套的对象呢?答案是递归!
js
//在defineProperty()中对传入的属性进行递归
function defineProperty(obj,key,val){
Object.defineProperty(obj,key,{
get(){
return val
},
set(newVal){
if(newVal === val) return
Observer(newVal)
val = newVal
}
})
}
//在Observer中加一个递归停止的条件
function Observer(obj){
if (typeof obj !== "object" || obj === null) {
return
}
Object.keys(obj).forEach( (key) =>{
defineProperty(obj,key,obj[key])
})
}
//在defineProperty()中对传入的属性进行递归
function defineProperty(obj,key,val){
Object.defineProperty(obj,key,{
get(){
return val
},
set(newVal){
if(newVal === val) return
Observer(newVal)
val = newVal
}
})
}
//在Observer中加一个递归停止的条件
function Observer(obj){
if (typeof obj !== "object" || obj === null) {
return
}
Object.keys(obj).forEach( (key) =>{
defineProperty(obj,key,obj[key])
})
}
在vue2中这里还有些许不完美的地方,那就是无法监听到对象的增删。这里也可以使用vue2中的Vue.$set()
,Vue.$delete()
或者是vm.$set()
,vm.$delete()
。
监听数组
js
let arr = [1,2,3]
let obj = {}
Object.defineProperty(obj,'arr',{
get(){
console.log('触发了get方法')
return arr
},
set(newVal){
console.log("触发了set方法")
arr = newVal
}
})
obj.arr = [] //触发了set方法
obj.arr.push(1) //触发了get方法
obj.arr[0] = 1 //触发了get方法
obj.arr.pop() //触发了get方法
obj.arr = [1,2,3] //触发了set方法
let arr = [1,2,3]
let obj = {}
Object.defineProperty(obj,'arr',{
get(){
console.log('触发了get方法')
return arr
},
set(newVal){
console.log("触发了set方法")
arr = newVal
}
})
obj.arr = [] //触发了set方法
obj.arr.push(1) //触发了get方法
obj.arr[0] = 1 //触发了get方法
obj.arr.pop() //触发了get方法
obj.arr = [1,2,3] //触发了set方法
我们可以看到数组的一些方法例如pop、push为数组添加或者删除元素时,不能触发属性arr的setter,无法对属性arr中元素进行实时监听,为此vue2重写了这些数组添加了响应式。
同时我们注意到的上面的push、pop直接通过索引为数组添加元素时会触发arr的getter方法,这是由于这些操作的返回值。比如使用push为数组添加元素时,push方法返回值总是添加之后数组的新长度,当访问数组新长度时自然会触发getter方法。
首先作为新手来说,除非你一开始看官网文档就很仔细,看了你还得能记住,记住了你还得知道怎么使用,最后能一看就只知道这些问题所在。不然跟我一开始都会遇到的问题:我数据明明更新了为什么我的视图没有更新呢,为什么我的watch监听不到数据变化呢?很奇怪,百思不得其解。我最后用了一个简单的方法去使用:vm.$forceUpdate
强制刷新页面,或者是利用一个computed去计算需要监听的对象JSON.stringify转成字符串的变化,然后watch打印出来,即页面数据更新总是留到下次页面更新的时候更新的,watch更新了所以到下次页面更新了,页面数据也就更新了。
后面我在仔细看文档的时候才放心,其实vue2并不能监听对象和数组的变化,如果通过特定的方法,具体可以看一下vue2 官网的深入响应式解释
Vue3响应式原理
鉴于vue2中监听数据时的一些弊端,vue3监听数据的时候弃用了Object.defineProperty()
,而使用了Proxy()
。
这里打算从源码出发,源码中找到根本所在,然后用之间的关系进行梳理,最后以一个实战的例子去大致实现这个过程,方便自己更好的吸收。
大致的源码的初始化流程这里就不具体去展示了,就直接讲在哪个模块吧。
首先我们vue3中通过creatApp创建实例然后通过mount挂载实例。 在runtime-dom/index.ts中有个creatApp里面有着返回的一个app的实例,这个app 实例使用过ensureRenderer方法中的createApp返回的
接着我们通过ensureRenderer->createRenderer->baseCreateRenderer,这里截止到baseCreateRenderer方法,中间的过程由于篇幅的问题就不做具体展示。baseCreateRenderer()是这个函数重载方法,存在于runtime-core/renderer.ts中有将近2000行的代码,它里面有个初始化组件的方法叫做mountComponent,它的里面有个setupComponent调用,主要是用于初始化组件的data、props、watch、methods、computed、响应式等等,但是具体在哪里呢?
还是要接着往下看。为了防止看的很迷糊,时不时还需要加个图片来展示一下。
上图可以看出这里有initProps和initSlots单看名字就知道什么意思了。
通过setupComponent->setupStatefulComponent->finishComponentSetup,这里有需要截止一下,因为到了重要地方,finishComponentSetup方法中有个applyOptions的调用,这个是干什么的呢?这个就是上面说的初始化内容的地方了,很接近了
这些内容小伙伴们应该很熟悉了吧。但是这些东西从哪里来的并不是今天的主题,接着往下看
在reactive中特也是个函数重载,他里面调用了一个createReactiveObject的函数,返回的是一个由new Proxy的实例对象,但是里面的参数从哪里来呢?
这就需要回到我们的reactive中寻找
通过mutableHandlers->get->createGetter->track->createDep->trackEffects,就是这么个顺序拿到我的mutableHandlers
这里重要的就是track方法中的内容,其中trackEffects就是用来收集activeEffect依赖的。
其中targetMap是个new WeakMap(),往里面set了一个new Map(),而createDep下面中的返回的是一个new Set(),
数据结构是这样的{WeakMap:{Map:{Set}}}
,这样有什么好处呢?
这个就是有关于这三个ES6的新特性内容相关了。WeakMap有着友好的垃圾收回机制,垃圾回收后key自动消失,Map有着很好的键值对的表现形式,Set有着自动去重的功能。一石三鸟,不得不惊讶的赞叹,给大佬们竖个大拇指啊。
可以获取了但是我们会更数据吧,接下来就是mutableHandlers中的set
通过mutableHandlers->set->createSetter->trigger->triggerEffects->triggerEffect,triggerEffect去执行更新机制就可顺利的更新数据了
更新机制在哪里呢?就在一开始的mountComponent方法的中setupComponent下面一点就能看见一个初始化更新机制的setupRenderEffect方法,里面有着初始化更新机制,以便后边更新数据时更新页面
这里就很像React的useEffect一样,清除副作用的效果。
说了这么多,来总结一下内容
- 数据结构会是一个
{WeakMap:{Map:{Set}}}
,有着很好的垃圾回收机制,返回的是一个Proxy实例- 在初始化的时候需要创建一个effect更新机制
- 需要一个track去收集更新机制
- 需要一个trigger执行更新机制
这里的WeakMap、Map、Set就不做详细的介绍,直接进入重要内容。
Proxy()
作用:用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
语法:const p = new Proxy(target, handler)
参数:
target
:要使用 Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler
:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p
的行为。这个的连中有很多的方法可以使用,这里也不做作一一解释了。
Reflect
Proxy还需要Reflect的配合使用,Reflect的方法跟Proxy handler 的方法是一样的。它是一个内置的对象,它提供拦截 JavaScript 操作的方法
简单例子
js
const handler = {
get(target, prop) {
const result = Reflect.get(target, prop)
console.log('拦截了读取数据', prop, result)
return result
},
// 修改属性值或者是添加属性
set(target, prop, value) {
const result = Reflect.set(target, prop, value)
console.log('拦截了修改数据或者是添加属性', prop, value)
return result
},
// 删除某个属性
deleteProperty(target, prop) {
const result = Reflect.deleteProperty(target, prop)
console.log('拦截了删除数据', prop)
return result
}
};
var obj = {}
const obj_1 = new Proxy(obj, handler);
setTimeout(()=>{
obj_1.a = "你好"
},3000)
console.log(obj_1.a)
const handler = {
get(target, prop) {
const result = Reflect.get(target, prop)
console.log('拦截了读取数据', prop, result)
return result
},
// 修改属性值或者是添加属性
set(target, prop, value) {
const result = Reflect.set(target, prop, value)
console.log('拦截了修改数据或者是添加属性', prop, value)
return result
},
// 删除某个属性
deleteProperty(target, prop) {
const result = Reflect.deleteProperty(target, prop)
console.log('拦截了删除数据', prop)
return result
}
};
var obj = {}
const obj_1 = new Proxy(obj, handler);
setTimeout(()=>{
obj_1.a = "你好"
},3000)
console.log(obj_1.a)
当然这里如果是嵌套对象或者是数组的时候就需要递归实现了。
上图中setTimeout目前就相当于更新机制了,前面也说了我们的更新机制并不是通过setTimeout去实现的,而是有个effect, 这个只有一个更新机制还好,要是有很多个呢?每个都写一次effect函数吗?这时候我们就需要封装起来通过track收集effect和trigger执行effect
完整代码
js
const createGetter = () =>{
return function get(target, prop){
const result = Reflect.get(target, prop)
console.log('拦截了读取数据', prop, result,target)
track(target, prop)
return result
}
}
const get = createGetter()
const createSetter = () =>{
return function set(target, prop, value){
const result = Reflect.set(target, prop, value)
console.log('拦截了修改数据或者是添加属性', prop, value)
trigger(target, prop)
return result
}
}
const set = createSetter()
const targetMap = new WeakMap()
let activeEffect = null
function track(target, key) {
// 如果此时activeEffect为null则不执行下面
// 这里判断是为了避免例如console.log(person.name)而触发track
if (!activeEffect) return
console.log("我进来了",target,key)
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, dep = new Set())
}
dep.add(activeEffect) // 把此时的activeEffect添加进去
}
function trigger(target, key) {
let depsMap = targetMap.get(target)
if (depsMap) {
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
}
const handler = {
get,
// 修改属性值或者是添加属性
set,
};
function reactive (target) {
// 判断当前的目标对象是不是object类型(对象/数组)
if (target && typeof target === 'object') {
// 对数组或者是对象中所有的数据进行reactive的递归处理
// 先判断当前的数据是不是数组
if (Array.isArray(target)) {
// 数组的数据要进行遍历操作
target.forEach((item, index) => {
target[index] = reactive(item)
})
} else {
// 再判断当前的数据是不是对象
// 对象的数据也要进行遍历的操作
Object.keys(target).forEach(key => {
target[key] = reactive(target[key])
})
}
return new Proxy(target, handler)
}
// 如果传入的数据是基本类型的数据,那么就直接返回+///-++++
return target
}
function effect(fn) {
activeEffect = fn
activeEffect()
activeEffect = null
}
function ref(target) {
target = reactive(target)
console.log("目标对象",target)
return {
_is_ref: true, // 标识当前的对象是ref对象
// 保存target数据保存起来
_value: target,
get value () {
console.log('劫持到了读取数据')
return this._value
},
set value (val) {
console.log('劫持到了修改数据,准备更新界面', val)
this._value = val
}
}
}
function computed(fn) {
const result = ref()
console.log(result)
effect(() => result.value = fn())
return result
}
var obj = ref({name:"张三",age:20})
let name1 = computed(()=>'你好,'+obj.value.name)
console.log(name1.value,targetMap)
console.log("分割线-------------")
obj.value.name = "李四"
console.log(name1.value,targetMap)
const createGetter = () =>{
return function get(target, prop){
const result = Reflect.get(target, prop)
console.log('拦截了读取数据', prop, result,target)
track(target, prop)
return result
}
}
const get = createGetter()
const createSetter = () =>{
return function set(target, prop, value){
const result = Reflect.set(target, prop, value)
console.log('拦截了修改数据或者是添加属性', prop, value)
trigger(target, prop)
return result
}
}
const set = createSetter()
const targetMap = new WeakMap()
let activeEffect = null
function track(target, key) {
// 如果此时activeEffect为null则不执行下面
// 这里判断是为了避免例如console.log(person.name)而触发track
if (!activeEffect) return
console.log("我进来了",target,key)
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, dep = new Set())
}
dep.add(activeEffect) // 把此时的activeEffect添加进去
}
function trigger(target, key) {
let depsMap = targetMap.get(target)
if (depsMap) {
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
}
const handler = {
get,
// 修改属性值或者是添加属性
set,
};
function reactive (target) {
// 判断当前的目标对象是不是object类型(对象/数组)
if (target && typeof target === 'object') {
// 对数组或者是对象中所有的数据进行reactive的递归处理
// 先判断当前的数据是不是数组
if (Array.isArray(target)) {
// 数组的数据要进行遍历操作
target.forEach((item, index) => {
target[index] = reactive(item)
})
} else {
// 再判断当前的数据是不是对象
// 对象的数据也要进行遍历的操作
Object.keys(target).forEach(key => {
target[key] = reactive(target[key])
})
}
return new Proxy(target, handler)
}
// 如果传入的数据是基本类型的数据,那么就直接返回+///-++++
return target
}
function effect(fn) {
activeEffect = fn
activeEffect()
activeEffect = null
}
function ref(target) {
target = reactive(target)
console.log("目标对象",target)
return {
_is_ref: true, // 标识当前的对象是ref对象
// 保存target数据保存起来
_value: target,
get value () {
console.log('劫持到了读取数据')
return this._value
},
set value (val) {
console.log('劫持到了修改数据,准备更新界面', val)
this._value = val
}
}
}
function computed(fn) {
const result = ref()
console.log(result)
effect(() => result.value = fn())
return result
}
var obj = ref({name:"张三",age:20})
let name1 = computed(()=>'你好,'+obj.value.name)
console.log(name1.value,targetMap)
console.log("分割线-------------")
obj.value.name = "李四"
console.log(name1.value,targetMap)
上面代码中的大部分都是简写的,位置在reactivity这个文件下
结语
本次针对Vue2、3原理的进行了理解阐述,如果有不正确的地方,欢迎小伙伴指出。
如果想更好的学习的,我推荐大家可以看一个阿崔cxr mini-vue