架构的思考(1)

定目标,有了目标才有发力的方向。实现,敲代码或者不敲代码,或者是只分析。这个过程是循环往复的,

架构的思考(1)目标给实现定了一个方向,而在实现过程中会发现定的目标不是那么清晰,这时会去修正目标。所以这个过程是循环往复的,在不断循环中,我们的认知会不断提高。

从这种循环往复的过程出来,以架构师的视角,分析vue响应式系统是如何开发的😏

目标:视图与数据的关联

首先,vue的最大特点就是 视图与数据 的关联。那么就可以设置一个初始的目标,当然这个目标是很粗略的,我们需要在循环过程中不断细化它,在这个过程中反馈回来很多问题,这些问题会让我们去审视我们的目标是否合理。

视图

什么是视图,在程序里表现出来的是什么样的东西?如果使用一个dom来表现视图,那是视图在计算机里表现出来的就是对象,vue的虚拟dom也是个对象,那字符串也可以表示视图,看到的就是一串字符串,所以视图,本质上就是数据。那我们的目标就出现偏差了,这么分析,应该是数据与数据的关联。

那我们在平常工作和生活中,哪里有这种关联呢?Execel大家都用过,起码在学校做各种文档的时候用过,里面就有了大量的数据与数据的关联。

架构的思考(1)
前三个数据与7是不是发生关联了?但仔细想想,他们之间有个运算公式,那好像不存在数据与数据之间的关联啊,应该是数据跟某个计算发生了关联。回到我们的 目标 ,尽量把 视图 往 计算方向去靠,那 视图 就应该是和 某个计算 是有关系的,理解起来就是,创建视图的过程 与 数据发生了关联。当数据发生变化之后, 我们会重新执行创建视图的过程。那创建视图的过程是什么呢?在不同语言里,过程都变现为函数

那我们的目标便可以改为 创建视图的函数 与 数据 的关联

一定是创建视图的函数和数据的关联吗?比如这个函数是render,如果能实现这个函数与数据的关联,那通过类比,可不可以通过这种机制 实现任意函数与数据的关联呢?我们看render函数和普通函数没啥区别,就是通过一些数据,然后返回一个视图。而且在实际开发中,也需要把某些函数与数据发生关联,当某些数据改变的时候,重新拿到新的值。就好像上面Excel的求和。

这么看来,我们定的目标就有点狭隘了,仅仅局限于视图,那通过这么分析,我们就可以修正目标:函数与数据的关联

数据

在语言里有各种各样的数据,那这里的数据是什么数据呢?是定义的所有数据吗?有data1、data2、data3,当data1改变的时候,我需要执行函数,得到新的值。那这里说的数据,应该是函数用到的数据。回到render函数,比如说用到了name,那就是只有当name改变时,才去创建视图。 那怎么理解用到,比如一个判断语句,会走不同的分支,也就是说用到的数据不确定,所以说,一个函数和什么样的数据发生关联,取决于函数的运行过程。

那我们的目标可以进一步修正,目标:函数 与 函数运行过程中用到的数据 的关联

好像隐隐约约摸到那个点了啊,再想想,函数运行中用到的数据一定要发生关联吗?就是这个数据发生变化之后,就一定要执行函数吗? 这样的机制是不是不应该固定死?那从设计的角度上来看,应该由用户决定。用户提供数据,提供函数,我们把它们关联起来,另外用户还要告诉我们哪些数据是需要关联的。

那就应该找到一种机制,让用户告诉我们什么样的数据是要发生关联的,我们可以认为需要关联的数据被打上了标记,这个打上标记的过程看做是一个函数。

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可以拦截全部的操作。

那我们这个标记函数就是去监听数据的各种操作,那因为上面两个方法都需要对象,所以监听的数据必须是对象。

  function tag(target){
    return new Proxy(target,{
      get(target,key){

      },
      set(target,key,value){

      }
    })
  }

那我们在使用的时候,就不能使用原始的对象了,只能使用代理的对象。

const proxy = tag(b); //打标记

function fn() {
  if (a) {
    proxy //代理对象
  } else {
    c;
  }
}

那在vue里这个标记函数命名为reactive,而是把这个函数返回的对象称作响应式数据

这一过程是非常复杂了,涉及的细节很多,现在只是个雏形,先暂时写点简单的代码。

  • 读取

在读取的时候,其中一件事就是把值返回

 get(target, key) {
      return target[key]; //返回对象属性值
    },

另外就是哪个函数读取了这个属性,要把它找出来,在vue里把这个过程称为依赖收集

//effect.js

/**
 * @description: 依赖收集(建立对应关系)
 * @param {* object} target
 * @param {* string} key
 * @return {*}
 */
export function track(target,key) {}
 get(target, key) {
      track(target, key); //依赖收集
      return target[key]; //返回对象属性值
    },
  • 修改

第一件事就是修改属性值了

 set(target, key, value) {
      Reflect.set(target, key, value); //设置对象的响应属性
    },

然后属性发生改变了,那就要重新运行函数,vue把这一过程称为派发更新

/**
 * @description: 派发更新
 * @param {* object} target
 * @param {* stirng} key
 * @return {*}
 */
export function trigger (target,key){
  console.log('派发更新',key);
}
 set(target, key, value) {
      trigger(target, key); //派发更新
      return Reflect.set(target, key, value); //设置对象的响应属性
    },

看看代码效果

import { reactive } from "./reactive.js";

const state = reactive({ a: 1, b: 2 });

function fn() {
  state.a;
  state.b;//读取
}
fn();

state.a = "gag"; //修改

架构的思考(1)

原文链接:https://juejin.cn/post/7346729977897812007 作者:初出茅庐的

(0)
上一篇 2024年3月17日 下午4:46
下一篇 2024年3月17日 下午4:57

相关推荐

发表回复

登录后才能评论