定目标,有了目标才有发力的方向。实现,敲代码或者不敲代码,或者是只分析。这个过程是循环往复的,
目标给实现定了一个方向,而在实现过程中会发现定的目标不是那么清晰,这时会去修正目标。所以这个过程是循环往复的,在不断循环中,我们的认知会不断提高。
从这种循环往复的过程出来,以架构师的视角,分析vue
响应式系统是如何开发的😏
目标:视图与数据的关联
首先,vue
的最大特点就是 视图与数据 的关联。那么就可以设置一个初始的目标,当然这个目标是很粗略的,我们需要在循环过程中不断细化它,在这个过程中反馈回来很多问题,这些问题会让我们去审视我们的目标是否合理。
视图
什么是视图,在程序里表现出来的是什么样的东西?如果使用一个dom
来表现视图,那是视图在计算机里表现出来的就是对象,vue
的虚拟dom也是个对象,那字符串也可以表示视图,看到的就是一串字符串,所以视图,本质上就是数据。那我们的目标就出现偏差了,这么分析,应该是数据与数据
的关联。
那我们在平常工作和生活中,哪里有这种关联呢?Execel
大家都用过,起码在学校做各种文档的时候用过,里面就有了大量的数据与数据的关联。
前三个数据与7
是不是发生关联了?但仔细想想,他们之间有个运算公式,那好像不存在数据与数据之间的关联啊,应该是数据跟某个计算发生了关联。回到我们的 目标 ,尽量把 视图 往 计算方向去靠,那 视图 就应该是和 某个计算 是有关系的,理解起来就是,创建视图的过程 与 数据
发生了关联。当数据发生变化之后, 我们会重新执行创建视图的过程。那创建视图的过程是什么呢?在不同语言里,过程都变现为函数
。
那我们的目标便可以改为 创建视图的函数 与 数据 的关联
。
一定是创建视图的函数和数据的关联吗?比如这个函数是render
,如果能实现这个函数与数据的关联,那通过类比,可不可以通过这种机制 实现任意函数与数据的关联呢?我们看render
函数和普通函数没啥区别,就是通过一些数据,然后返回一个视图。而且在实际开发中,也需要把某些函数与数据发生关联,当某些数据改变的时候,重新拿到新的值。就好像上面Excel
的求和。
这么看来,我们定的目标就有点狭隘了,仅仅局限于视图,那通过这么分析,我们就可以修正目标:函数与数据的关联
。
数据
在语言里有各种各样的数据,那这里的数据是什么数据呢?是定义的所有数据吗?有data1、data2、data3
,当data1
改变的时候,我需要执行函数,得到新的值。那这里说的数据,应该是函数用到的数据。回到render
函数,比如说用到了name
,那就是只有当name
改变时,才去创建视图。 那怎么理解用到
,比如一个判断语句,会走不同的分支,也就是说用到的数据不确定,所以说,一个函数和什么样的数据发生关联,取决于函数的运行过程。
那我们的目标可以进一步修正,目标:函数 与 函数运行过程中用到的数据 的关联
。
好像隐隐约约摸到那个点了啊,再想想,函数运行中用到的数据一定要发生关联吗?就是这个数据发生变化之后,就一定要执行函数吗? 这样的机制是不是不应该固定死?那从设计的角度上来看,应该由用户决定。用户提供数据,提供函数,我们把它们关联起来,另外用户还要告诉我们哪些数据是需要关联的。
那就应该找到一种机制,让用户告诉我们什么样的数据是要发生关联的,我们可以认为需要关联的数据被打上了标记,这个打上标记的过程看做是一个函数。
js
var a = true,
b,
c;
tag(b);//打标记
function fn(){
if(a){
b
}else{
c
}
}
在这个过程中,用到了a
,可能用到b 或 c
,那这些数遍改变,都要关联吗? 这时可以做个标记,说明那个数据变动时需要进行关联。
那对目标进行进一步修正,目标:函数 与 函数运行过程中用到的标记数据
的关联。
有没有,已经和vue
响应式系统搭边了。
实现
好了,目标明确了,那如何去实现这个关系呢?如何建立对应关系呢?来看这个关联,这种关系之所以能够建立,就是函数运行期间,读到了这个数据且打上标记了,那我们就必须去监听数据的读取。还有这个数据发生变化了,还要重新执行函数,js不对自动去做,需要我们手动去执行,因此还需要知道数据什么时候被修改了。另外,当数据被读和被修改的时候,我们还得知道是被哪个函数读了。
因此这里涉及两方面:1.监听数据的读取和修改 2.如何知晓数据对应的函数
在js里,提供了两种方法去监听数据的读取和修改。
defineProperty
它监听范围很窄,只能通过属性描述符去监听已有属性的读取和赋值,不过兼容性不错。
proxy
监听的范围很广。对于对象的操作,在语言层面上比如.a
读取或者赋值等,都是会转变成对象内部方法的调用,这些内部方法的调用就是对象的基本操作。而Proxy
可以拦截全部的操作。
那我们这个标记函数就是去监听数据的各种操作,那因为上面两个方法都需要对象,所以监听的数据必须是对象。
js
function tag(target){
return new Proxy(target,{
get(target,key){
},
set(target,key,value){
}
})
}
那我们在使用的时候,就不能使用原始的对象了,只能使用代理的对象。
js
const proxy = tag(b); //打标记
function fn() {
if (a) {
proxy //代理对象
} else {
c;
}
}
那在vue
里这个标记函数命名为reactive
,而是把这个函数返回的对象称作响应式数据
。
这一过程是非常复杂了,涉及的细节很多,现在只是个雏形,先暂时写点简单的代码。
- 读取
在读取的时候,其中一件事就是把值返回
js
get(target, key) {
return target[key]; //返回对象属性值
},
另外就是哪个函数读取了这个属性,要把它找出来,在vue
里把这个过程称为依赖收集
js
//effect.js
/**
* @description: 依赖收集(建立对应关系)
* @param {* object} target
* @param {* string} key
* @return {*}
*/
export function track(target,key) {}
js
get(target, key) {
track(target, key); //依赖收集
return target[key]; //返回对象属性值
},
- 修改
第一件事就是修改属性值了
js
set(target, key, value) {
Reflect.set(target, key, value); //设置对象的响应属性
},
然后属性发生改变了,那就要重新运行函数,vue
把这一过程称为派发更新
。
js
/**
* @description: 派发更新
* @param {* object} target
* @param {* stirng} key
* @return {*}
*/
export function trigger (target,key){
console.log('派发更新',key);
}
js
set(target, key, value) {
trigger(target, key); //派发更新
return Reflect.set(target, key, value); //设置对象的响应属性
},
看看代码效果
js
import { reactive } from "./reactive.js";
const state = reactive({ a: 1, b: 2 });
function fn() {
state.a;
state.b;//读取
}
fn();
state.a = "gag"; //修改