Vue响应式系统的作用与实现(一)

news2024/11/17 13:53:44

响应式系统的作用与实现

0.写在前面:

  • 写了mini-vue之后的疑惑更多了,比如为什么要这样设计?这样做的好处是啥?为什么我想不出来?(我真菜
  • 于是决定去看霍春阳大佬的Vue.js设计与实现。一些参考资料:官方的参考资料,前置知识非常有必要看一下。mini-vue的实现

1.响应式:

Vue2的响应式实现是借助 Object.defineProperty通过重写getter和setter方法来进行的数据劫持,Vue3通过Proxy代理拦截对象中任意属性的变化,通过Reflect反射对源对象的属性进行操作,然后再在get里收集依赖在set里派发更新。

2.副作用函数与响应式数据:

上面中的这句话应该都知道,那我们先来了解一下什么是副作用函数,以及它有什么用?它和响应式数据又有什么关系?

  • 副作用函数:会产生副作用的函数,它的执行会直接或间接影响其他函数的执行。

举例:当一个函数修改了全局变量。

let a = 1;
function effect(){
  a = 2;
}
  • 响应式数据:当值发生变化后,副作用函数会自动重新执行。

那么怎么才能让数据变成响应式数据呢?

  • 我们需要对数据的读取和设置操作进行拦截,把副作用函数存储到一个"桶"中;
  • 即当读取属性时将副作用函数effect添加到桶里,然后返回属性值;
  • 当设置属性值时先更新原始数据,再将副作用函数从桶里取出并重新执行。

3.响应式系统的实现:

上代码前请看两个地方:

  • WeakMap 由 target – > Map构成
    • 这里用WeakMap的原因:它的键是弱引用对象,防止内存泄漏。官网解释的很详细
  • Map 由 key --> Set 构成

上图中Set数据结构所存储的副作用函数集合我们称为 key的依赖集合。

      // 存储副作用函数的桶
      const bucket = new WeakMap();

      // 原始数据
      const data = { text: "hello world" };
      const obj = new Proxy(data, {
        get(target, key) {
          if (!activeEffect) {
            return ;
          }
          // 根据target从桶中取得depsMap,它也是一个Map类型: key -->effects
          let depsMap = bucket.get(target);
          // 如果depsMap不存在,那么新建一个 Map 与 target关联
          if (!depsMap) {
            bucket.set(target, (depsMap = new Map()));
          }
          // 根据key从depsMap中取得 deps(对应着:key --> effects),它是一个Set类型
          let deps = depsMap.get(key);
          // 如果 deps 不存在,同样新建一个 Set 并与 key 关联
          if (!deps) {
            depsMap.set(key, (deps = new Set()));
          }
          // 添加到桶里
          deps.add(activeEffect);
          return target[key];
        },
        set(target, key, newValue) {
          target[key] = newValue;
          // 根据 target 从桶中取得 depsMap,它是Map类型: key --> effects
          const depsMap = bucket.get(target);
          if (!depsMap) return;
          // 根据 key 取出所有副作用函数
          const effects = depsMap.get(key);
          ettects && ettects.forEach((fn) => fn());
        },
      });
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect函数用于注册副作用函数
function effect(fn) {
  // 将副作用函数fn 赋值给 activeEffect
  activeEffect = fn;
  // 执行副作用函数
  fn();
}

4.抽离代码:

上面的get 和 set里面的逻辑太长了,我们把它抽离到track和trigger函数中(学习官方的做法)。

      function track(target, key) {
        if (!activeEffect) {
          return ;
        }
        // 根据target从桶中取得depsMap,它也是一个Map类型: key -->effects
        let depsMap = bucket.get(target);
        // 如果depsMap不存在,那么新建一个 Map 与 target关联
        if (!depsMap) {
          bucket.set(target, (depsMap = new Map()));
        }
        // 根据key从depsMap中取得 deps(对应着:key --> effects),它是一个Set类型
        let deps = depsMap.get(key);
        // 如果 deps 不存在,同样新建一个 Set 并与 key 关联
        if (!deps) {
          depsMap.set(key, (deps = new Set()));
        }
        // 添加到桶里
        deps.add(activeEffect);
      }

      function trigger(target, key) {
        // 根据 target 从桶中取得 depsMap,它是Map类型: key --> effects
        const depsMap = bucket.get(target);
        if (!depsMap) return;
        // 根据 key 取出所有副作用函数
        const effects = depsMap.get(key);
        effects && effects.forEach((fn) => fn());
      }

此时的get和set:

      const obj = new Proxy(data, {
        get(target, key) {
          track(target, key);
          return target[key];
        },
        set(target, key, newValue) {
          target[key] = newValue;
          trigger(target, key);
        },
      });

这时一个稍微完善的响应式系统(破产版)就实现了!下面就对它继续完善。

5.分支切换与cleanup:

分支切换:比如下面的代码,随着obj.ok的值不同而执行不同的分支。

const data = { ok: true, text: 'hello world' }
const obj = new Proxy(data, {/*  */ })
effect(function effectFn() {
  document.body.innerHTML = obj.ok ? obj.text : 'not'
})
  • 它可能会产生遗留的副作用函数。obj.ok== true,读取obj.text的值,所以当副作用函数执行时会触发两个属性的读取操作。

  • 理想状态下,副作用函数只会收集一个依赖放入依赖集合中。

在这里插入图片描述

  • 并且遗留的副作用函数会导致不必要的更新。对应上面中的例子就是,当我们修改obj.ok的值为false后,我们再修改obj.text的值仍然会导致副作用函数重新执行。

解决思路:

  • 每次当副作用函数执行时,先把它从所有与之关联的依赖集合中删除(如下图)。
    在这里插入图片描述

  • 当副作用函数执行完毕后,重新建立联系,新的联系中不包含遗留的副作用函数(对应上面的理想状态)。

  • 那么怎么做呢?要将一个副作用函数从所有和它关联的依赖集合中删除,我们需要明确知道哪些依赖集合中包含它。

重新设计副作用函数:

      // 用一个全局变量存储被注册的副作用函数
      let activeEffect;
      function effect(fn) {
        const effectFn = () => {
          // 当effectFn执行时,将其设置为当前激活的副作用函数
          activeEffect = effectFn;
          fn();
        };
        // 用来存储所有与该副作用函数相关联的依赖集合
        effectFn.deps = [];
        // 执行副作用函数
        effectFn();
      }

在track函数中完成依赖集合的收集:新增一行代码即可

function track(target, key) {
    if (!activeEffect) {
        return;
    }
    // 根据target从桶中取得depsMap,它也是一个Map类型: key -->effects
    let depsMap = bucket.get(target);
    // 如果depsMap不存在,那么新建一个 Map 与 target关联
    if (!depsMap) {
        bucket.set(target, (depsMap = new Map()));
    }
    // 根据key从depsMap中取得 deps(对应着:key --> effects),它是一个Set类型
    let deps = depsMap.get(key);
    // 如果 deps 不存在,同样新建一个 Set 并与 key 关联
    if (!deps) {
        depsMap.set(key, (deps = new Set()));
    }
    // 添加到桶里
    deps.add(activeEffect);
    // deps就是与当前副作用函数存在联系的依赖集合,将其添加到数组中
    activeEffect.deps.push(deps); // 新增
}

完成了对依赖集合的收集,接下来就是将副作用函数从依赖集合中删除:

// 用一个全局变量存储被注册的副作用函数
let activeEffect;
function effect(fn) {
    const effectFn = () => {
        // 调用cleanup 函数完成清除工作
        cleanup(effectFn); // 新增
        // 当effectFn执行时,将其设置为当前激活的副作用函数
        activeEffect = effectFn;
        fn();
    };
    // 用来存储所有与该副作用函数相关联的依赖集合
    effectFn.deps = [];
    // 执行副作用函数
    effectFn();
}

cleanup函数的实现:

function cleanup(effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
        // deps 是依赖集合
        const deps = effectFn.deps[i];
        // 将effectFn从依赖集合中移除
        deps.delete(effectFn);
    }
    // 最后将数组进行重置
    effectFn.deps.length = 0;
}

修改trigger函数避免无限执行:

出现原因:因为在trigger函数内部,遍历effects集合时,它是Set数据结构的,里面存储着副作用函数。当执行时调用cleanup进行清除,但是副作用函数的执行会导致其重新被收集到集合中,而此时遍历仍在进行。

  • 说了那么多,简单来说就是当我们修改时,会触发set,此时不断修改不断触发,一边删除一边往里添加,就会造成无限执行。
function trigger(target, key) {
    // 根据 target 从桶中取得 depsMap,它是Map类型: key --> effects
    const depsMap = bucket.get(target);
    if (!depsMap) return;
    // 根据 key 取出所有副作用函数
    const effects = depsMap.get(key);
    // 新增两行代码
    const effectsToRun = new Set(effects);
    effectsToRun.forEach(effectFn => effectFn());
    // effects && effects.forEach(effectFn => effectFn()) // 删除
}

具体原理可以查看mdn

6.嵌套的effect与effect栈:

Vue.js中什么时候会发生effect嵌套?

举个例子:当组件发生嵌套时,例如Foo组件渲染了Bar组件。

const Bar = {
  render() {/*  */ }
}
// Foo组件渲染了Bar组件
const Foo = {
  render() {
    return <Bar /> // jsx 语法
  }
}

// 等价于
effect(() => {
  Foo.render()
  // 嵌套
  effect(() => {
    Bar.render()
  })
})

这就是effect要设计成可嵌套的原因。但是我们上面设计的很明显不符合这种情况,那么要怎么做呢?

  • 我们可以借助副作用函数栈effectStack来解决,当副作用函数执行时,将当前副作用函数压入栈中,执行完毕后弹出,并始终让activeEffect指向栈顶的副作用函数。
  • 这样就能做到一个响应式数据只会收集直接读取其值的副作用函数,不会出现互相影响的情况。
  • 因为之前我们是通过全局变量activeEffect来存储通过effect函数注册的副作用函数,这意味着着同一时刻 activeEffect 所存储的副作用函数只能有一个。当发生嵌套时,会出现覆盖的情况。

如下代码:

// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// 新增栈
const effectStack = [];
function effect(fn) {
    const effectFn = () => {
        // 调用cleanup 函数完成清除工作
        cleanup(effectFn); // 新增
        // 当effectFn执行时,将其设置为当前激活的副作用函数
        activeEffect = effectFn;
        // 在调用副作用函数之前将当前副作用函数压入栈中
        effectStack.push(effectFn); // 新增
        fn();
        // 执行完毕后,出栈并还原activeEffect之前的值
        effectStack.pop(); //  新增
        activeEffect = effectStack[effectStack.length - 1]; // 新增
    };
    // 用来存储所有与该副作用函数相关联的依赖集合
    effectFn.deps = [];
    // 执行副作用函数
    effectFn();
}

这样一来便实现了响应式数据只会收集直接读取其值的副作用函数为依赖,避免发生混乱。

7.避免无线递归循环:

举个例子:下面这段代码中,既读取了obj.foo的值,又修改了它的值,就造成了无限递归调用自身。

effect(() => obj.foo++);
// 等价于
effect(() => {
    obj.foo = obj.foo + 1;
})

此时的执行流程:

  • 读取值,触发track操作,将当前副作用函数收集到"桶"中。
  • 修改值,触发trigger操作,执行"桶"中的副作用函数。
  • 那么就会造成,副作用函数正在执行中,就要开始下一次执行,于是就造成了上面的结果,产生栈溢出。

解决方法:

  • 读取和设置值在同一个副作用函数内进行,并且要收集和触发的副作用函数都是activeEffect,我们可以在trigger动作发生时增加守卫条件:如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行。

如下代码:

function trigger(target, key) {
    // 根据 target 从桶中取得 depsMap,它是Map类型: key --> effects
    const depsMap = bucket.get(target);
    if (!depsMap) return;
    // 根据 key 取出所有副作用函数
    const effects = depsMap.get(key);

    const effectsToRun = new Set(effects);
    effects && effects.forEach(effectFn => { // 新增
        // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
        if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn);
        }
    })
    effectsToRun.forEach(effectFn => effectFn());
    // effects && effects.forEach(effectFn => effectFn()) // 删除
}

8.调度执行:

  • 可调度性:当trigger动作触发副作用函数重新执行时,能决定副作用函数执行的时机、次数以及方式。

我们可以为effect函数设计一个选项参数 options,允许用户指定调度器,来实现控制函数的执行顺序以及次数。

function effect(fn, options = {}) {
    const effectFn = () => {
        // 调用cleanup 函数完成清除工作
        cleanup(effectFn);
        // 当effectFn执行时,将其设置为当前激活的副作用函数
        activeEffect = effectFn;
        // 在调用副作用函数之前将当前副作用函数压入栈中
        effectStack.push(effectFn);
        fn();
        // 执行完毕后,出栈并还原activeEffect之前的值
        effectStack.pop();
        activeEffect = effectStack[effectStack.length - 1];
    };
    // 将 options 挂载到 effectFn 上
    effectFn.options = options; // 新增
    // 用来存储所有与该副作用函数相关联的依赖集合
    effectFn.deps = [];
    // 执行副作用函数
    effectFn();
}

修改trigger函数,当触发副作用函数重新执行时,如果用户传了调度器,则直接调用。

function trigger(target, key) {
    // 根据 target 从桶中取得 depsMap,它是Map类型: key --> effects
    const depsMap = bucket.get(target);
    if (!depsMap) return;
    // 根据 key 取出所有副作用函数
    const effects = depsMap.get(key);

    const effectsToRun = new Set(effects);
    effects && effects.forEach(effectFn => {
        // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
        if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn);
        }
    })
    effectsToRun.forEach(effectFn => { // 修改这部分
        // 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
        if (effectFn.options.scheduler) {
            effectFn.options.scheduler(effectFn);
        } else {
            effectFn();
        }
    });
}

9.计算属性computed与lazy:

先上结论:计算属性实际上是一个懒执行的副作用函数,我们通过lazy选项使得副作用函数可以懒执行。

懒执行:

  • 有些场景下,我们希望副作用函数不立即执行,而是需要的时候才执行(例如计算属性)。
  • 我们可以像调度执行那样来完成,通过在options中添加lazy属性来达到目的,当options.lazy为true时,不立即执行副作用函数。
  • 并且我们想要在手动执行副作用函数时,能拿到其返回值。
function effect(fn, options = {}) {
    const effectFn = () => {
        cleanup(effectFn);
        activeEffect = effectFn;
        effectStack.push(effectFn);
        const res = fn(); // 新增
        effectStack.pop();
        activeEffect = effectStack[effectStack.length - 1];
        return res; // 新增
    };
    effectFn.options = options;
    effectFn.deps = [];
    // 只有非lazy的时候才执行副作用函数
    if (!options.lazy) {  // 新增
        // 执行副作用函数
        effectFn();
    }
    return effectFn; // 新增
}

接下来就可以实现计算属性了:

function computed(getter) {
    const effectFn = effect(getter, {
        lazy: true
    })
    const obj = {
        get value() {
            return effectFn()
        }
    }
    return obj;
}

测试:

const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, /* 复制之前的即可 */);
const sumRes = computed(() => obj.foo + obj.bar);
console.log(sumRes.value); // 3

现在计算属性能正常工作了,但是还做不到对值进行缓存。(即obj.foo+obj.bar的值没有发生变化,但还是会进行多次计算)

修改后的computed:

  • 添加调度器的原因:当obj.foo或obj.bar的值发生变化,需要重新计算;
  • 并且计算属性发生变化,就要重新进行渲染。(借助track + trigger就可以解决这个问题)
function computed(getter) {
    // value用来缓存上一次计算的值
    let value;
    // dirty标志用来标识是否需要重新计算值,为true则意味"脏",需要重新计算
    let dirty = true;
    const effectFn = effect(getter, {
        lazy: true,
        // 添加调度器,在调度器中将dirty重置为true
        scheduler() {
            if (!dirty) {
                dirty = true;
                // 当计算属性依赖的响应式数据变化时,手动调用trigger函数触发响应式
                trigger(obj, 'value');
            }
        }
    })
    const obj = {
        get value() {
            if (dirty) {
                value = effectFn();
                dirty = false;
            }
            // 当读取value时,手动调用track函数进行追踪
            track(obj, 'value');
            return value;
        }
    }
    return obj;
}

至此,一个稍微完善的computed就实现了。

10.watch的实现原理:

watch本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数。它的本质其实就是利用了effect以及options.sheduler选项。

最简单的watch实现:

function watch(source, cb) {
    effect(
        () => source.foo,
        {
            scheduler(){
                // 当数据变化时,调用回到函数cb
                cb()
            }
        }
    )
}

但是上面其实硬编码了对source.foo的读取,我们可以封装一个通用的读取操作:

function watch(source, cb) {
    effect(
        () => traverse(source),
        {
            scheduler() {
                // 当数据变化时,调用回到函数cb
                cb()
            }
        }
    )
}
function traverse(value, seen = new Set()) {
    // 如果要读取的数据是原始值,或者被读取过了,则什么都不做
    if (typeof value !== 'object' || value === null || seen.has(value)) {
        return;
    }
    // 将数据添加到seen中,代表遍历读取过了,避免循环引用引起死循环
    seen.add(value);
    // 暂时不考虑数组等其他结构
    // 假设value就是一个对象,使用for...in 读取对象的每一个值,并递归调用处理
    for (const k in value) {
        traverse(value[k], seen);
    }
    return value;
}

watcher函数除了可以观测响应式数据之外,还可以接收一个getter函数:

function watch(source, cb) {
    let getter;
    // 如果用户传递进来的是函数,则直接使用
    if (typeof source === 'function') {
        getter = source;
    // 如果不是函数类型,则保留之前的做法,即调用traverse函数递归读取
    } else {
        getter = () => traverse(source);
    }
    effect(
        () => getter(),
        {
            scheduler() {
                cb();
            }
        }
    )
}

其实上面的代码还缺少一个非常重要的能力,即在回调函数中拿不到旧值与新值,我们可以充分利用effect函数的lazy选项来解决这个问题:

function watch(source, cb) {
    let getter;
    // 如果用户传递进来的是函数,则直接使用
    if (typeof source === 'function') {
        getter = source;
        // 如果不是函数类型,则保留之前的做法,即调用traverse函数递归读取
    } else {
        getter = () => traverse(source);
    }
    // 定义新值与旧值
    let oldValue, newValue;
    // 使用effect注册副作用函数时,开启lazy选项,并把返回值存储到effectFn中方便后续手动调用
    const effectFn = effect(
        () => getter(),
        {
            lazy: true,
            // 在scheduler中重新执行副作用函数,得到的是新值
            scheduler() {
                newValue = effectFn();
                cb(newValue, oldValue);
                oldValue = newValue;
            }
        }
    )
    // 手动调用副作用函数,拿到的值就是旧值
    oldValue = effectFn();
}

11.立即执行的watch:

  • 在上面中,我们了解到watch的本质其实是对effect的二次封装,但是watch的特性我们还没有实现完,即:立即执行的回调函数。

在Vue.js中我们有通过选项参数 immediate来指定回调是否需要立即执行。

  • 仔细思考就发现,回调函数的立即执行与后续执行本质上没有差别,所以我们可以把scheduler调度函数封装为一个通用函数,在初始化和变更时执行它。
function watch(source, cb, options = {}) {
    let getter;
    // 如果用户传递进来的是函数,则直接使用
    if (typeof source === 'function') {
        getter = source;
        // 如果不是函数类型,则保留之前的做法,即调用traverse函数递归读取
    } else {
        getter = () => traverse(source);
    }
    // 定义新值与旧值
    let oldValue, newValue;
    // 提取scheduler调度函数为一个独立的 job函数
    const job = () => {
        newValue = effectFn();
        cb(newValue, oldValue);
        oldValue = newValue;
    }
    // 使用effect注册副作用函数时,开启lazy选项,并把返回值存储到effectFn中方便后续手动调用
    const effectFn = effect(
        () => getter(),
        {
            lazy: true,
            // 使用job函数作为调度器函数
            scheduler: job
        }
    )
    if (options.immediate) {
        // 当immediate为true时立即执行job,从而触发执行回调
        job();
    } else {
        oldValue = effectFn();
    }
}

如此一来便实现了watch的立即执行功能。因为此时的回调函数第一次执行时没有oldValue,故此时oldValue的值为undefined。

12.总结:

我们先来回顾一下,

  • 响应式数据的基本实现就是:依赖于对 get和set操作的拦截,从而在副作用函数与响应式数据之间建立联系。
  • 响应系统的根本实现原理:
    • 当进行get操作时,将当前执行的副作用函数存储到"桶"中;
    • 当进行set操作时,再将副作用函数从"桶"中取出并执行。("桶"的数据结构使用WeakMap配合Map构建了新的"桶"结构)

我们还解决了分支切换导致的冗余副作用问题,以及嵌套的effect函数(常发生在父子组件中)、如何避免副作用函数无限递归调用自身,相应系统的调度执行,computed和watch的实现原理等。后面将继续对响应式数据进行完善。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/60824.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

x265 帧间预测

帧间编码入口函数&#xff1a; 从 Analysis::compressCTU 是ctu编码的入口函数&#xff0c;根据 slice 类型判断是 I 还是 BP&#xff0c;如果是BP则执行帧间编码函数 Analysis::compressInterCU_rdx_x&#xff1a;&#xff1a;/*压缩分析CTU过程&#xff1a;1.为当前CTU加载QP…

m基于16QAM的自适应波束形成matlab仿真

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 16QAM全称正交幅度调制是英文Quadrature Amplitude Modulation的缩略语简称&#xff0c;意思是正交幅度调制&#xff0c;是一种数字调制方式。产生的方法有正交调幅法和复合相移法。 波束形成是…

工业级数据分发服务DDS之安全篇

目录引出问题分析问题未授权订阅者未授权发布者截胡篡改跨域攻击解决问题官方标准DDS的安全特性基于域的安全保护域内保护RTI方案RTI安全插件的特性DDS支持的加解密算法用于数据流保护的密码算法用于密钥交换的密码算法用于数字签名的密码算法RTPS-HMAC-Only插件用于数据流保护…

数据结构(10)图的概念、存储

目录 10.1.概念 10.2.存储 10.2.1.邻接矩阵 10.2.2.邻接表 10.1.概念 定义: 图&#xff0c;用来表示多对多的关系&#xff0c;比如地图里城市之间的通路、比如人际关系。 图由顶点和边组成&#xff0c;顶点是图里的每个结点&#xff0c;边是顶点之间的通路&#xff0c;可…

【计算机网络】网络基础(三)

自从计算机、手机被广泛应用于工作、生活、娱乐、学习&#xff0c;那你有没有考虑过QQ的消息、爱奇艺的视频、钉钉的网络会议的数据是如何传输的&#xff1f;这些信息自然是通过网络&#xff08;WIFI、蜂窝网络等&#xff09;传播的&#xff0c;而一个简单的通信网络是由路由器…

513.找树左下角的值

文章目录513.找树左下角的值题目题解 - BFS题解- DFS513.找树左下角的值 题目 给定一个二叉树的 根节点 root&#xff0c;请找出该二叉树的 最底层 最左边 节点的值。 假设二叉树中至少有一个节点。 示例 1: 输入: root [2,1,3] 输出: 1示例 2: 输入: [1,2,3,4,null,5,…

计算机网络学习笔记(II)——应用层(二)

2.4、Email 电子邮件&#xff08;Email&#xff09; 主要由三个部分组成&#xff1a; 用户代理邮件服务器简单邮件传输协议&#xff1a;SMTP 用户代理&#xff08;邮件阅读器&#xff09;&#xff1a; 撰写、编辑和阅读邮件输入和输出邮件保存在服务器上 EMail&#xff1…

UDS入门至精通系列:Service 27

文章目录 前言一、Service 27的功能二、企业规范中怎么定义Service 27三、AUTOSAR关于Service 27实现策略四、关于Service 27集成测试五、用于解锁的dll文件怎样得来?总结前言 本文将近8000字,详细分享了从需求规范提出、功能实现、集成测试等方面对Service 27的介绍! 在引…

PTA编程的一些总结

PTA 1.首先是float单精度浮点数和double双精度浮点数 float占4字节&#xff0c;有效数字7位&#xff0c;double占8字节&#xff0c;有效数字15位 double类型输入16位数字会有误差 老师给的PTA中的练习题中第一题的买U盘需要在结果后面0.000001来弥补double精度 2.判断浮点数…

从ASM看jacoco运行原理

前言 我们在开发中如何保证代码质量&#xff0c;我的回答是做充分的代码测试。Jacoco的出发点是为基于JVM运行的代码提供代码覆盖率统计&#xff0c;期望提供轻量级的、可伸缩的、文档较全的库文件来集成各类构建和开发工具。 ASM介绍 ASM 是一个通用的 Java 字节码操作和分…

架构设计(消息队列)

架构设计&#xff08;消息队列&#xff09; 消息队列 发送者将消息发送到topic&#xff0c;消费者从topic中拉取消息进行消费 发送端消息发送方式 同步发送&#xff1a;消息发送后&#xff0c;需要等待消息发送响应结果&#xff0c;发送失败可重试 异步发送&#xff1a;消息发…

Numpy入门[17]——数组广播机制

Numpy入门[17]——数组广播机制 参考&#xff1a; https://ailearning.apachecn.org/NumPy广播机制 使用Jupyter进行练习 NumPy 中的广播机制&#xff08;Broadcast&#xff09;旨在解决不同形状数组之间的算术运算问题。我们知道&#xff0c;如果进行运算的两个数组形状完全相…

linux网络编程epoll详解

目录epoll原理解析epoll提供的接口epoll的触发模式epoll原理解析 从socket接收网络数据说起&#xff1a; 1、网络传输中&#xff0c;网卡会把接收到的数据写入内存&#xff0c;网卡向 CPU 发出一个中断信号&#xff0c;操作系统便能得知有新数据到来&#xff0c;再通过网卡中断…

第二证券|行业重磅白皮书发布,超高清视频产业规模剑指3万亿

在5G和超高清交融开展的布景下&#xff0c;下流使用需求有望迸发&#xff0c;超高清视频工业前景可观。 超高清工业规模有望突破3万亿 据报道&#xff0c;12月1日&#xff0c;2022国际显现工业大会分论坛——新式显现超高清主题论坛在成都举行。论坛上&#xff0c;中国电子信息…

文本编辑器vi--常用命令查阅版(记得收藏)

一.为何要学习vi   # 所有的UNIX-like系统都会内置vi文本编辑器&#xff0c;其他的文本编辑器则不一定会存在&#xff1b;   # 很多软件的编辑接口都会主动调用vi&#xff1b;   # vim具有程序编辑的能力&#xff0c;可以主动地以字体颜色辨别语法的正确性&#xff0c;方…

双元科技过会:计划募资6.5亿元,比亚迪和蜂巢能源为主要客户

近日&#xff0c;上海证券交易所披露的信息显示&#xff0c;浙江双元科技股份有限公司&#xff08;下称“双元科技”&#xff09;获得科创板上市委会议通过&#xff08;即IPO过会&#xff09;。接下来&#xff0c;双元科技将提交注册。 据贝多财经了解&#xff0c;双元科技于20…

关于天干地支及其计算

以天干地支计算日期是我国悠良的传统文化&#xff0c;最近在看如何计算人的生辰八字&#xff0c;写了个程序&#xff0c;但是只能算年的干支&#xff0c;月、日的干支计算方法太复杂了&#xff0c;望之只能却步&#xff0c;还是乖乖去查万年历比较好。这里记下关于干支的一些东…

[附源码]Python计算机毕业设计Django框架的资产管理系统设计与实现

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

[附源码]Python计算机毕业设计SSM京津冀区域产学研项目管理信息系统(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

50、IO流

*学习的难点&#xff1a;要知道在什么情况&#xff0c;该用什么流 补&#xff1a;ANSI码就是gbk码 一、基本概念&#xff1a; 1、什么是文件&#xff1a; 文件是保存数据的地方 2、文件流&#xff1a; 文件在程序中是以流的形式来操作的 &#xff08;1&#xff09;流&am…