简单实现响应系统

130 阅读6分钟

首先声明本文素材来源于《Vue.js设计与实现》,配合本书阅读代码口味更佳哦。

简单的版本

/**
 * 目前只是一个简单的处理响应式数据的程序。
 */

// 用一个全局变量储存被注册的副作用函数
let activeEffect;
const bucket = new WeakMap();
// effect函数用来注册副作用函数
function effect(fn){
  // 调用effect时把传入的参数fn(副作用函数)赋值给activeEffect
  activeEffect = fn;
  // 函数不能直接执行,必须通过effect函数将副作用函数fn收集起来才能执行
  fn();
}

const obj = new Proxy(data, {
  get(target, key){
    track(target, key);
    // 继续正常返回属性值
    return target[key];
  },
  set(target, key, newVal){
    // 第一步首先是人家设置以后,先让人家设置成功
    target[key] = newVal;
    // 然后将该属性对应的副作用函数都取出来一一执行。
    trigger(target, key);
  }
})

function track(target, key) {
  // fn直接执行,没有通过effect执行,直接return。
  if (!activeEffect) return ;
  // 获取target里面被监听的属性
  depsMap = bucket.get(target);
  // 如果bucket中对应的target不存在说明现在是第一次读取该对象,那就将该对象直接添加进去吧。
  // 添加的操作说明target需要被监听,但是现在还没有确定是target的哪个key需要被监听。
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  // 获取target.key对应的副作用函数,也就是在哪个函数里面读取了target.key
  deps = depsMap.get(key);
  // 如果deps不存在说明target.key是第一次被读取。
  // 有可能是上下文之前在操作target的其他key,有可能是上下文第一次操作target.key。
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  // 将target.key对应的副作用函数记录在set集合中
  deps.add(activeEffect);
}
function trigget(target, key) {
  // 获取监听的targe对象对应的属性
  const depsMap = bucket.get(target);
  if (!depsMap) return ;
  // 根据监听的key获取target.key对应的副作用函数
  const effects = depsMap.get(key);
  // 将副作用函数取出来一一执行
  effects && effects.forEach(fn => fn());
}
// 上面是响应式系统的实现
//-------------------------------------------------//
// 下面是响应式系统的测试
let data = {ok: true, text: 'hello'};
effect(function effectFn(){
  let a = obj.ok ? obj.text : 'not hello';
})

接下来不断打怪升级,一点点修复存在的问题。限于篇幅原因,我直接贴上解决了下面问题的代码。

  • 有时候有些属性的修改是不需要周知所有的副作用函数的,比如分支程序obj.ok ? obj.text : 'not hello';,当obj.ok = false的时候,obj.text怎么改变对程序来讲是没有意义的。
  • 副作用函数的嵌套问题。
  • 允许指定 options 选项,例如使用调度器来控制副作用函数的执行时机和方式。
  • 模拟实现 Vue 中的 watch 函数。
  • 使 watch 函数的 source 不仅能是对象也可以是函数。
  • 使用 watch 记录响应式数据变化前后的值。

/**
 * version10.js
 * 目前只是一个简单的处理响应式数据的程序。
 */
let tmp1, tmp2;
let data = {foo: 1, bar: 2};
export const obj = new Proxy(data, {
  get(target, key){
    track(target, key);
    // print(bucket, target);
    // 继续正常返回属性值
    return target[key];
  },
  set(target, key, newVal){
    // 第一步首先是人家设置以后,先让人家设置成功
    target[key] = newVal;
    // 然后将该属性对应的副作用函数都取出来一一执行。
    trigger(target, key);
    return true;
  }
})

// 用一个全局变量储存被注册的副作用函数
let activeEffect;
const effectStack = [];
const bucket = new WeakMap();
// effect函数用来注册副作用函数
export function effect(fn, options = {}){
  // 在函数作用域中设置函数变量effectFn,这里不能使用var声明符
  // 将effectFn作为副作用函数(通过effectFn调用fn)
  const effectFn = () => {
    // console.log('effectFn is runing!');
    cleanUp(effectFn);
    activeEffect = effectFn;
    effectStack.push(effectFn);
    // 这里的fn是真正的副作用函数,外层的effectFn也好,effect也好都是基于fn封装的副作用函数
    // 使用的是外观模式,好处:在调用副作用函数的时候只需要调用effectFn就行,解耦合。缺点:修改副作用函数的时候需要修改effectFn,违反了开闭原则
    const res = fn();
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
    // 将res作为fn函数的返回值
    return res;
  }
  // 记录用户自定义选项
  effectFn.options = options;
  // 这个数组用来记录和effectFn相关联的target.key对应的集合。
  effectFn.deps = [];
  // 执行effectFn
  if (!options.lazy) {
    effectFn();
  }
  // 返回副作用函数
  return effectFn
}

export function track(target, key) {
  // fn直接执行,没有通过effect执行,直接return。
  if (!activeEffect) return ;
  // 获取target里面被监听的属性
  let depsMap = bucket.get(target);
  // 如果bucket中对应的target不存在说明现在是第一次读取该对象,那就将该对象直接添加进去吧。
  // 添加的操作说明target需要被监听,但是现在还没有确定是target的哪个key需要被监听。
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  // 获取target.key对应的副作用函数,也就是在哪个函数里面读取了target.key
  let deps = depsMap.get(key);
  // 如果deps不存在说明target.key是第一次被读取。
  // 有可能是上下文之前在操作target的其他key,有可能是上下文第一次操作target.key。
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  // 将target.key对应的副作用函数记录在set集合中
  deps.add(activeEffect);
  // 这里将记录与副作用函数相关联的target.key对应的集合
  activeEffect.deps.push(deps);
}
export function trigger(target, key) {
  // 获取监听的targe对象对应的属性
  const depsMap = bucket.get(target);
  // 如果这个对象不需要被监听
  if (!depsMap) return ;
  // 根据监听的key获取target.key对应的副作用函数
  const effects = depsMap.get(key);
  // 将effects集合拷贝下来
  const effectsToRun = new Set();
  effects && effects.forEach(effectFn => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn);
    }
  })
  // 遍历另外一个集合,但是执行本集合下的副作用函数,避免死循环
  effectsToRun.forEach(effectFn => {
    // 如果存在用户自定义函数,调用用户自定义函数。其中用户自定义函数的参数是副作用函数
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn);
    } else {
      effectFn();
    }
  });
  // effects && effects.forEach(fn => fn());
}
// 从effectFn相关联的target.key对应的副作用函数集合中将effectFn删除
export function cleanUp(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  // 上面的操作从每个与之相关的集合中删除了effectFn,但是相关联的集合还在,下面就是重置effectFn数组,将相关的集合置为空
  effectFn.deps.length = 0;
}
// 上面是响应式系统的实现
//-------------------------------------------------//
// 下面是控制用户自定义函数fn执行次数的jobQueue

// 定义一个set记录用户自定义函数fn,利用了set自动去重的特性保证同一个函数只会在set中出现一次。
const jobQueue = new Set();
// 定义一个状态为fullfilled的promise实例。将jibQueue的执行过程当做微任务处理。
const p = Promise.resolve();

// 设置一个标志用来判断是否需要将onFullfilled处理函数压入队列
let isFlushing = false;

let flush = function flushJob(){
  // 如果isFlushing为true,直接返回函数
  if(isFlushing) return;
  // 在这里将isFlushing设置为true,如果obj.foo++执行两次的话(代码参考下面obj.foo++),不会每一次都去设置一个微任务。
  isFlushing = true;
  // 添加微任务
  p.then(() => {
    jobQueue.forEach(job => job());
  }).finally(() => {
    // 当对应的jobQueue函数(也就是对应的微任务)执行完之后,设置为false
    isFlushing = false;
  })

}
// watch.js
// 实现watch函数

import { effect, obj, track, trigger } from './version10.js'

// source 是响应式数据,cb是回调函数
function watch(source, cb) {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  let oldValue, newValue
  // 使用effect注册副作用函数时,开启lazy选项,并把返回值储存到effectFn中以便后续手动调用
  const effectFn = effect(
    () => getter(),
    {
      lazy: true,
      scheduler() {
        // 在scheduler中重新执行副作用函数,得到的是新的值
        // 因为在触发scheduler的时候已经修改了target.key的值,去执行effectFn能够获取到修改后的值,修改后的值是通过traverse来获取到的。
        newValue = effectFn();
        // 这里仍然存在问题,因为obj只有一个,newvalue和oldValue最终都指向obj,这里传入的oldValue和newValue应该是相同的。
        // 将旧的值和新的值作为回调函数的参数
        cb(newValue, oldValue);
        // 更新旧值,不然下一次会得到错误的旧值。
        // 下次响应式数据变化的时候不会再运行watch函数。
        oldValue = newValue
      }
    }
  )
  // 手动调用副作用函数,拿到的值就是旧值
  oldValue = effectFn();
}

// 使用traverse遍历函数

function traverse (value, seen = new Set()) {
  // 如果读取的数据不是对象,或者已经被读取过了,或者值为空,那就什么都不做
  if (typeof value !== 'object' || value === null || seen.has(value)) {
    return ;
  }
  // 将读取到的数据添加到seen中,代表读取过了。
  seen.add(value);
  // 假设value能够通过forin语句遍历
  for (const k in value) {
    traverse(value[k], seen)
  }
  return value
}

/////////////////////////////////////////////

// 使用watch函数
watch(obj, ()=>{
  console.log('数据变化了!');
})

obj.foo ++

上面两段函数的运行过程:

gitee.com/seconp/publ…

等本节结束,我会出一个视频专门讲解上面的实现。

参考

  • [1]霍春阳.Vue.js设计与实现[M].河北:人民邮电出版社,2022:40-75.