《Vue.js 设计与实现》读书笔记 - 第 4 章、响应系统的作用与实现

第 4 章、响应系统的作用与实现

4.1 响应式数据与副作用

副作用函数就是会对外部造成影响的函数,比如修改了全局变量。

响应式:修改了某个值的时候,某个会读取该值的副作用函数能够自动重新执行。

4.2 响应系统的简单实现

如何实现响应式:

1、副作用读取值的时候,把函数放到值的某个桶里

2、重新给值赋值的时候,执行桶里的函数

在 Vue2 中通过 Object.defineProperty 实现,Vue3 通过 Proxy 实现。

点击查看代码
const data = { text: 'hello world' }
function effect() {
  document.body.innerText = obj.text
}

const bucket = new Set()
const obj = new Proxy(data, {
  get(target, key) {
    bucket.add(effect)
    return target[key]
  },
  set(target, key, newVal) {
    target[key] = newVal
    bucket.forEach(fn => fn())
    // 返回true代表设置操作成功
    return true
  }
})
// 触发读取
effect()
// 修改响应式数据
setTimeout(() => {
  obj.text = 'hello vue3'
}, 1000)

4.3 设计一个完善的响应系统

上面的实现硬编码了函数名,现在在全局维护一个变量来存储这个副作用函数。

点击查看代码
// 全局变量,用于存储被注册的副作用函数
let activeEffect;
// effect 函数用于注册副作用函数
function effect(fn) {
  activeEffect = fn;
  fn();
}

const data = { text: 'hello world' };

const bucket = new Set();
const obj = new Proxy(data, {
  get(target, key) {
    if (activeEffect) {
      bucket.add(activeEffect);
    }
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    bucket.forEach((fn) => fn());
    // 返回true代表设置操作成功
    return true;
  },
});
// 触发读取
effect(
  // 一个匿名的副作用函数
  () => {
    document.body.innerText = obj.text;
  }
);
// 修改响应式数据
setTimeout(() => {
  obj.text = 'hello vue3';
}, 1000);

但是现在 bucket 并没有和 text 字段绑定,也就是说我修改任何值都会触发函数的执行。我们需要重新设计“桶”的数据结构。很简单,把 Set 改成 Map 来存储。

bucket 应该先绑定响应式对象,再绑定字段。

点击查看代码
// 全局变量,用于存储被注册的副作用函数
let activeEffect;
// effect 函数用于注册副作用函数
function effect(fn) {
  activeEffect = fn;
  fn();
  activeEffect = undefined; // 不过书上没有这一句
}
const bucket = new WeakMap();

const data = { text: 'hello world', text1: 'before' };
const obj = new Proxy(data, {
  get(target, key) {
    if (activeEffect) {
      let depsMap = bucket.get(target);
      if (!depsMap) {
        bucket.set(target, (depsMap = new Map()));
      }
      let deps = depsMap.get(key);
      if (!deps) {
        depsMap.set(key, (deps = new Set()));
      }
      deps.add(activeEffect);
    }
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    let depsMap = bucket.get(target);
    if (depsMap) {
      let effects = depsMap.get(key);
      effects && effects.forEach((fn) => fn());
    }
    // 返回true代表设置操作成功
    return true;
  },
});

// 触发读取
effect(
  // 一个匿名的副作用函数
  () => {
    document.body.innerText = obj.text;
  }
);
effect(() => {
  console.log(obj.text1);
});
// 修改响应式数据
setTimeout(() => {
  obj.text = 'hello vue3';
}, 1000);
setTimeout(() => {
  obj.text1 = 'after';
}, 2000);

这里使用了 WeakMap,其对 key 是弱引用,不影响垃圾回收。一旦对象被回收,对象的键和值就无法被访问到,所以再监听就没有意义了。否则一直引用会导致内存溢出。

getset 中的操作分别封装到 tracktrigger

点击查看代码
const data = { text: 'hello world', text1: 'before' };
const obj = new Proxy(data, {
  get(target, key) {
    // 将副作用函数activeEffect加到对应的桶中
    track(target, key);
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key);
    // 返回true代表设置操作成功
    return true;
  },
});

function track(target, key) {
  if (!activeEffect) return;
  let depsMap = bucket.get(target);
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  let deps = depsMap.get(key);
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  deps.add(activeEffect);
}

function trigger(target, key) {
  let depsMap = bucket.get(target);
  if (depsMap) {
    let effects = depsMap.get(key);
    effects && effects.forEach((fn) => fn());
  }
}

4.4 切换分支与 cleanup

如下代码

const data = {  ok: true,text: 'hello world' }
const obj = new proxy(data, {...})

effect(() => {
  document.body.innerText = obj.ok ? obj.text : 'not'
})

effectobj.ok 取不同值的时候,执行代码会发生变化,叫做分支切换。

很明显在 obj.ok = trueeffect 依赖 obj.text,但是当 obj.ok = false 的时候,就和 obj.text 无关,obj.text 再修改时也不应该触发函数重新执行了。

为了实现这个效果,上面的代码需要修改,每次副作用函数重新执行的时候,我们要先把它从所有与之关联的依赖集合中删除。执行后会建立新的关联。

点击查看代码
let activeEffect;
function effect(fn) {
  const effectFn = () => {
    // 执行前先清除依赖
    cleanup(effectFn);
    activeEffect = effectFn;
    fn();
    activeEffect = undefined
  };
  // 用来存储与该副作用函数相关联的依赖集合
  effectFn.deps = [];
  effectFn();
}
function cleanup(effectFn) {
  // 很简单 就是在每个依赖集合中把该函数删除
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}
const bucket = new WeakMap();
// 在 track 中记录 deps
function track(target, key) {
  if (!activeEffect) return;
  let depsMap = bucket.get(target);
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  let deps = depsMap.get(key);
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  deps.add(activeEffect);
  // 当前副作用函数也记录下关联的依赖
  activeEffect.deps.push(deps);
}

function trigger(target, key) {
  let depsMap = bucket.get(target);
  if (depsMap) {
    let effects = depsMap.get(key);
    // 不能直接执行effects 因为执行 effects 会先 cleanup 清除 bucket 中的依赖集合
    // 但是再次执行后会再在集合中添加副作用函数
    // 这样会导致死循环
    const effectsToRun = new Set(effects);
    effectsToRun.forEach((fn) => fn());
  }
}

const data = { ok: true, text: 'hello world' };
const obj = new Proxy(data, {
  get(target, key) {
    // 将副作用函数activeEffect加到对应的桶中
    track(target, key);
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key);
    // 返回true代表设置操作成功
    return true;
  },
});
effect(() => {
  document.body.innerText = obj.ok ? obj.text : 'not';
  console.log('run!');
});
setTimeout(() => {
  obj.ok = false;
  obj.text = 'changed';
}, 1000);

注意其中的 trigger 函数

function trigger(target, key) {
  let depsMap = bucket.get(target);
  if (depsMap) {
    let effects = depsMap.get(key);
    // 不能直接执行effects 因为执行 effects 会先 cleanup 清除 bucket 中的依赖集合
    // 但是再次执行后会再在集合中添加副作用函数
    // 这样会导致死循环
    const effectsToRun = new Set(effects);
    effectsToRun.forEach((fn) => fn());
  }
}

4.3 嵌套的 effect 与 effect 栈

在 Vue 中如果我们使用组件嵌套组件,就会有 effect 嵌套执行。

如果有嵌套的 effect 执行,我们就需要在保存当前 effect 函数的同时,记录之前的 effect 函数,并在当前的函数之前完之后,把上一层的 effect 赋值为 activeEffect。很简单的会想到用栈来实现这个功能。

let activeEffect;
const effectStack = [];
function effect(fn) {
  const effectFn = () => {
    // 执行前先清除依赖
    cleanup(effectFn);
    // 执行前先压入栈中
    activeEffect = effectFn;
    effectStack.push(effect);
    fn();
    // 执行后弹出
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
  };
  // 用来存储与该副作用函数相关联的依赖集合
  effectFn.deps = [];
  effectFn();
}

4.6 避免无限递归循环

const data = { foo: 1 };
const obj = new Proxy(data, {
  // ......
});
effect(() => {
  obj.foo++;
});

上面的代码会导致死循环,因为 obj.foo++; 既有取值又有赋值操作。读取的时候会把该函数添到依赖集合,赋值的时候会导致副作用函数再执行。

所以我们在 trigger 中执行副作用函数的时候,不执行当前正在处理的副作用函数,即 activeEffect

function trigger(target, key) {
  let depsMap = bucket.get(target);
  if (depsMap) {
    let effects = depsMap.get(key);
    // 不能直接执行effects 因为执行 effects 会先 cleanup 清除 bucket 中的依赖集合
    // 但是再次执行后会再在集合中添加副作用函数
    // 这样会导致死循环
    const effectsToRun = new Set();
    effects &&
      effects.forEach((effectFn) => {
        if (effectFn !== activeEffect) {
          effectsToRun.add(effectFn);
        }
      });
    effectsToRun.forEach((fn) => fn());
  }
}

4.7 调度执行

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

effect 函数增加选项,可以指定执行副作用函数的调度器。

点击查看代码
function effect(fn, options = {}) {
  const effectFn = () => {
    // 执行前先清除依赖
    cleanup(effectFn);
    // 执行前先压入栈中
    activeEffect = effectFn;
    effectStack.push(effectFn);
    fn();
    // 执行后弹出
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
  };
  // 把 options 挂在 effectFn 上
  effectFn.options = options;
  // 用来存储与该副作用函数相关联的依赖集合
  effectFn.deps = [];
  effectFn();
}

function trigger(target, key) {
  let depsMap = bucket.get(target);
  if (depsMap) {
    let effects = depsMap.get(key);
    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();
      }
    });
  }
}

具体的使用代码

effect(
  () => {
    console.log('obj.foo', obj.foo);
  },
  {
    scheduler(fn) {
      setTimeout(fn);
    },
  }
);
obj.foo++;
console.log('结束');

// obj.foo 1
// 结束
// obj.foo 2

我们也可以指定副作用函数的执行次数,比如我们对同一个变量连续操作了多次,我们只需要对最终的结果执行副作用函数,中间值可以被忽略。

基于调度器实现这个功能

const jobQueue = new Set();
const p = Promise.resolve();

let isflushing = false;
function flushJob() {
  if (isflushing) return;
  isflushing = true;
  p.then(() => {
    jobQueue.forEach((job) => job());
  }).finally(() => {
    isflushing = false;
  });
}

effect(
  () => {
    console.log('obj.foo', obj.foo);
  },
  {
    scheduler(fn) {
      jobQueue.add(fn);
      flushJob();
    },
  }
);
obj.foo++;
obj.foo++;
obj.foo++;
console.log('结束');

// obj.foo 1
// 结束
// obj.foo 4

通过 Set 实现去重,防止函数执行多次,通过 isflushing 做标记,执行过程中不会再次执行。

4.8 计算属性 computed 和 lazy

如果我们有时希望副作用函数不要立即执行,则需要提供一个选项,lazy 来决定是否立即执行。

function effect(fn, options = {}) {
  const effectFn = () => {
    // 执行前先清除依赖
    cleanup(effectFn);
    // 执行前先压入栈中
    activeEffect = effectFn;
    effectStack.push(effectFn);
    const res = fn();
    // 执行后弹出
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
    // 存储fn的计算结果并返回
    return res;
  };
  // 把 options 挂在 effectFn 上
  effectFn.options = options;
  // 用来存储与该副作用函数相关联的依赖集合
  effectFn.deps = [];
  if (!options.lazy) {
    effectFn();
  }
  // 将副作用函数作为返回值返回
  return effectFn;
}

函数只有在不指定 lazy 的时候才立即执行,同时把函数返回,这样就可以把函数在外部获取并随时手动执行。同时 effectFn 函数返回了函数执行的结果。

现在我们如果把 effect 函数的返回值作为某个属性的 getter 那么我们每次读取这个值的时候,都会触发副作用函数的执行,也会同时收集依赖。

function computed(getter) {
  const effectFn = effect(getter, {
    lazy: true,
  });
  const obj = {
    get value() {
      return effectFn();
    },
  };
  return obj;
}
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value);//3
obj.foo = 2;
console.log(sumRes.value);//4

现在的问题时,每一次读取值,都会触发 getter 的执行,但其实只要依赖不改变,计算属性的值是不会变的。这时我们一方面可以设置一个 flag 标识是否需要重新计算,同时在依赖修改时,只需要更新这个 flag 即可,并不需要重新计算,只需要在读取时计算即可。

function computed(getter) {
  let value;
  // 当前的值是否需要重新计算
  let dirty = true;
  const effectFn = effect(getter, {
    lazy: true,
    // 神来之笔啊!如果依赖修改了 并不需要重新计算 getter 但是需要更新 dirty
    // 只需要在 scheduler 中指定调度方式即可
    scheduler() {
      dirty = true;
    }
  });
  const obj = {
    get value() {
      if (dirty) {
        value = effectFn();
        dirty = false;
      }
      return value;
    },
  };
  return obj;
}
const sumRes = computed(() => {
  console.log('重新计算了getter');
  return obj.foo + obj.bar;
});
console.log(sumRes.value);
// 重新计算了getter
// 3
obj.foo = 2;
obj.foo = 3;
obj.foo = 4;
console.log(sumRes.value);
// 重新计算了getter
// 6
console.log(sumRes.value);
// 6
console.log(sumRes.value);
// 6

现在仍然有一个问题,就是在另一个 effect 中读取计算属性的时候,没有办法让计算属性收集依赖。

点击查看代码
function computed(getter) {
  let value;
  // 当前的值是否需要重新计算
  let dirty = true;
  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      if (!dirty) {
        dirty = true;
        // 因为当 computed 的依赖改变时 computed 的值应该被重新计算
        // 这个时候需要手动触发依赖
        // 如果有依赖的话 就会重新计算 computed 的值了
        trigger(obj, 'value');
      }
    },
  });
  const obj = {
    get value() {
      if (dirty) {
        value = effectFn();
        dirty = false;
      }
      // 读取 value 时手动进行依赖收集
      track(obj, 'value');
      return value;
    },
  };
  return obj;
}
const sumRes = computed(() => {
  return obj.foo + obj.bar;
});
effect(() => {
  console.log('sumRes', sumRes.value); // sumRes 3
});
obj.foo = 2; // sumRes 4
obj.foo = 4; // sumRes 6

在读取的时候做了依赖收集,同时在 computed 的依赖改变时,对收集的依赖触发执行。

4.9 watch 的实现原理

点击查看代码
function watch(source, cb) {
  effect(
    // 调用 traverse 递归地读取
    () => traverse(source),
    {
      scheduler() {
        cb();
      },
    }
  );
}

function traverse(value, seen = new Set()) {
  if (typeof value !== 'object' || value === null || seen.has(value)) return;
  seen.add(value);
  // 暂时只考虑对象 忽略数组等
  for (const k in value) {
    traverse(value[k], seen);
  }
  return value;
}

watch(
  obj,
  () => {
    console.log('foo 的值变了');
  }
);

obj.foo = 1;
obj.foo = 2;

watch 通过 effect 实现,第一个参数遍历传入的对象,读取对象的每一个值,第二个参数是一个回调函数,在 scheduler 中执行,也就是依赖每次更新都会执行。

不过 watch 第一个参数不一定亚奥传入一个值,也会传一个 getter 函数。同时,在 watch 中我们还希望获取改变前后的值。

点击查看代码
function watch(source, cb) {
  let getter;
  if (typeof source === 'function') {
    getter = source;
  } else {
    getter = () => traverse(source);
  }
  let oldValue, newValue;
  const effectFn = effect(() => getter(), {
    lazy: true,
    scheduler() {
      newValue = effectFn();
      cb(newValue, oldValue);
      oldValue = newValue;
    },
  });
  oldValue = effectFn();
}

watch(
  () => obj.foo,
  (newValue, oldValue) => {
    console.log('foo 的值变了', newValue, oldValue);
  }
);

改动1,判断用户传入的参数是否是函数

改动2,在 scheduler 手动调用副作用函数,获取最新的值并缓存,然后在回调时传入。这里使用了 lazy,是为了手动调用第一次副作用函数以获取 oldValue

4.10 立即执行的 watch 与回调执行时机

新增 immediate 选项,让回调函数可以立即执行。

点击查看代码
function watch(source, cb, options = {}) {
  let getter;
  if (typeof source === 'function') {
    getter = source;
  } else {
    getter = () => traverse(source);
  }
  let oldValue, newValue;
  // 把scheduler调度函数提取为job函数
  const job = () => {
    newValue = effectFn();
    cb(newValue, oldValue);
    oldValue = newValue;
  };

  const effectFn = effect(() => getter(), {
    lazy: true,
    scheduler: job,
  });
  if (options.immediate) {
    // 当immediate=true回立即执行回调函数
    job()
  } else {
    oldValue = effectFn();
  }
}

watch(
  () => obj.foo,
  (newValue, oldValue) => {
    console.log('foo 的值变了', newValue, oldValue);
  },
  {
    immediate: true
  }
);

这样,在第一次执行获取初始值之后也会立即执行回调函数,不过第一次的 oldValueundefined

除了立即执行,我们也可以通过其他方式指定回调函数执行时机。

点击查看代码
function watch(source, cb, options = {}) {
  let getter;
  if (typeof source === 'function') {
    getter = source;
  } else {
    getter = () => traverse(source);
  }
  let oldValue, newValue;
  // 把scheduler调度函数提取为job函数
  const job = () => {
    newValue = effectFn();
    cb(newValue, oldValue);
    oldValue = newValue;
  };

  const effectFn = effect(() => getter(), {
    lazy: true,
    scheduler: () => {
      // 设置post就放入微任务队列中执行
      if (options.flush === 'post') {
        const p = Promise.resolve();
        p.then(job);
      } else {
        job(); // 同步执行
      }
    },
  });
  if (options.immediate) {
    // 当immediate=true回立即执行回调函数
    job();
  } else {
    oldValue = effectFn();
  }
}

watch(
  () => obj.foo,
  (newValue, oldValue) => {
    console.log('foo 的值变了', newValue, oldValue);
  },
  {
    flush: 'post', // 'pre'|'post'|'sync'
  }
);

这样的话会导致连续修改数据的时候,执行结果有点问题,毕竟相当是数据全部改变后统一执行回调函数了。

4.11 过期的副作用

竞态问题,连续发送两次请求,后面的先返回,导致先发送的返回结果覆盖了后面的请求。而一般的需求是,保留最后一次请求的结果。

在第二次发送请求的时候,第一次请求已经“过期”,我们应该将其设置无效。

假设在 watch 我们会发送一个异步请求

let finalData
watch(obj, async () => {
  const res = await fetch('/path/to/request')
  finalData = res
})

可以通过在 watch 的回调函数中新增一个 onInvalidate 参数解决。

点击查看代码
function watch(source, cb, options = {}) {
  let getter;
  if (typeof source === 'function') {
    getter = source;
  } else {
    getter = () => traverse(source);
  }
  let oldValue, newValue;
  // cleanup 用于存储用户注册的过期回调
  let cleanup;

  function onInvalidate(fn) {
    cleanup = fn;
  }

  // 把scheduler调度函数提取为job函数
  const job = () => {
    newValue = effectFn();
    // 在执行回调函数之前 先执行过期函数
    // 我们在回调函数中会调用失效函数 会把过期函数绑在cleanup上
    // 我们先调用的回调会先把失效函数绑定
    // 而如果在上一次回调函数执行之前 就触发了下一次的执行 就会调用失效函数
    // 也就是上一次的回调函数对应的失效函数 则上一次的结果会被取消
    if (cleanup) {
      cleanup();
    }
    cb(newValue, oldValue, onInvalidate);
    oldValue = newValue;
  };

  const effectFn = effect(() => getter(), {
    lazy: true,
    scheduler: () => {
      // 设置post就放入微任务队列中执行
      if (options.flush === 'post') {
        const p = Promise.resolve();
        p.then(job);
      } else {
        job(); // 同步执行
      }
    },
  });
  if (options.immediate) {
    // 当immediate=true回立即执行回调函数
    job();
  } else {
    oldValue = effectFn();
  }
}

let t = 0;
const mock = () => {
  return new Promise((resolve) => {
    if (++t <= 2) {
      // 模拟一下 前两次需要1s返回 第3次立即返回
      t = 1000;
    }
    setTimeout(() => {
      resolve(1);
    }, t);
  });
};

watch(
  () => obj.foo,
  async (newValue, oldValue, onInvalidate) => {
    console.log('foo 的值变了', newValue, oldValue);
    let expired = false;
    onInvalidate(() => {
      expired = true;
    });
    const res = await mock(newValue);
    console.log(expired ? '过期了' : '未过期', 'newValue=' + newValue);
    if (!expired) {
      finalData = res;
    }
  }
);

原理有点复杂,就是说第一次每次执行回调的请求之前给 watch 传一个过期函数,然后 watch 把它保存起来,然后在这个过程中如果再次执行 watch 了,就会执行之前保存的过期函数,就会把上次的请求设置为不合法,还挺有趣的=。= 之前面试官还问过我这个问题emmm我都不会。

总结

就是通过 effect 执行副作用函数,把当前执行的函数保存到全局,因为有嵌套执行的情况所以需要用栈来保存。在对象的 get 收集依赖,set 时执行对应依赖,而且每一次执行之前还需要情况上一次执行的依赖。

还有注意事项就是在 get 中如果又 set 当前正在执行的副作用函数,不会触发执行。

computedwatcheffect 上面进行一层封装,加了 lazyschedular 等选项。

还有竞态问题,通过 onInvalidate 解决。

把整章代码全部敲了一遍,感觉确实学到了不少:D

本文转载于网络 如有侵权请联系删除

相关文章

  • 【腾讯二面】5s内建立多少个mysql连接?

    以100每秒的速度向mysql写数据,持续5s,此时我们的程序和mysql建立了多少个tcp连接?从编程的角度来看,一个问题的解答过程,无非是寻求输入输出,这里输出是多少个tcp连接,那么就要锁定输入,也就是参数,有哪些因素会影响这个问题?在牛牛看来只有两点:mysql当前处理能力和连接池配置。参数分析1.mysql处理能力如果负载正常的情况,mysql1s内一定能处理100个请求。如果负载比较高,那1s内就处理不完,为了方便讨论,这里假设1s能处理50个请求。PS:正常实体机的mysql,即使配置差到1核1G,也完全能胜任100/s的单纯插入请求。只有在mysql本身异常,或有其他进程占用系统资源时,才会出现1s处理不过来100个请求的情况。这里的两个分支只是逻辑上的讨论。2.连接池连接池是实现连接复用的手段,和mysql交互时,每次需要建立一个连接,用完就会关掉,这就是短连接。如果在高并发场景,反复建立连接的成本是很高的,所以我们可以使用长连接,即连接用完后先不关闭,放到一个池子里等待复用,这个池子就叫连接池。如图所示,连接池暂存了使用完成之后的mysql连接以待复用,最大空闲连接

  • 还在用Jenkins?试试Gitlab的CI/CD功能吧,贼带劲!

    之前写过一篇文章《再见Jenkins!几行脚本搞定自动化部署,这款神器有点厉害!》,讲的是使用Gogs+Drone来实现自动化部署。最近发现Gitlab的CI/CD功能也能实现自动化部署,用起来也挺简单!如果你使用的是Gitlab作为Git仓库的话,不妨试试它的CI/CD功能。本文还是以SpringBoot的自动化部署为例,实践下Gitlab的CI/DI功能,希望对大家有所帮助!安装通过Gitlab的CI/CD功能实现自动化部署,我们需要安装Gitlab、GitlabRunner、Maven这些服务。安装Gitlab首先我们来安装下Gitlab,对Gitlab安装和使用不了解的朋友可以参考下《10分钟搭建自己的Git仓库》。使用如下命令运行Gitlab服务,这里需要注意的是添加了hostname属性,这样我们就可以通过域名来访问Gitlab了(为了避免一些不必要的麻烦),GITLAB_ROOT_PASSWORD这个环境变量可以直接设置Gitlab中root账号的密码;dockerrun--detach\ --hostnamegit.macrozheng.com\ --publish10

  • Canvas使用beginPath()绘画不同颜色的直线

    Canvas绘画三条平行线<!DOCTYPEhtml> <htmllang="en"> <head> <metacharset="UTF-8"> <title>Title</title> <style> canvas{ border:1pxsolid#cccccc; margin-top:100px; margin-left:100px; } </style> <scripttype="text/javascript"> window.onload=function(){ /*获取元素*/ varmyCanvas=document.querySelector('#myCanvas'); /*获取绘图工具*/ varcontext=myCanvas.getContext('2d'); /*绘制第一条线*/ context.moveTo(100,100.5); context.lin

  • 真正零基础Python入门:手把手教你从变量和赋值语句学起

    导读:在本文中,你会学到如何处理数字、定义与使用变量和常量,以及编写使用这些数据类型执行实际任务的简单程序。作者:凯·霍斯特曼(CayHorstmann),兰斯·尼塞斯(RanceNecaise)如需转载请联系大数据(ID:hzdashuju)当你的程序执行计算时,需要把值存储下来以便后面使用。在Python程序中使用变量来存储值。本文你会学到如何定义和使用变量。为了演示变量的用法,我们会编写一个解决下面问题的程序:在售的软饮料一般分为罐装和瓶装。在商店里,一包6个12盎司的罐装饮料与一个2升的瓶装饮料售价一样,你应该买哪个?(对于液态而言,12盎司约等于0.355升。)▲哪一个包含更多的苏打?6个12盎司的罐装包,还是一个2升的瓶装?在我们的程序中,会定义变量来表示一包中罐的数量和每罐的体积,然后我们会计算一个6罐包的体积(以升为单位),并且输出答案。01定义变量在计算机程序中,变量是一个存储位置,每个变量都有名字并且包含一个值。变量类似于停车场的一个停车位。停车位拥有一个标识符(例如“J053”),并且可以容纳一辆交通工具。变量拥有一个名字(例如cansPerPack),并且可以存

  • 一行代码引发的恐惧

    1 我工作的前5年,都是从事基础系统研发相关的工作。做过后台的接入层,后台的存储系统,RPC框架。说来不怕你笑话,那个时期里面,我对代码一直有一种恐惧感。这种恐惧是怎么来的呢?且让我慢慢说来。 我们所构建的基础系统,都是使用在亿级甚至十亿级用户产品的业务系统之上的。从客户端(前端)到后台业务逻辑层,再到基础架构层,所写的代码是跑在整个调用链路的最后端的。 你可以认为,几乎每个用户的每个请求都会跑到我们写得那部分的代码。 这个对系统带来的影响是:一,代码出问题后,影响的用户范围会很大;二,在这亿级甚至十亿级用户量的情况下,每天所带来的请求可能是千亿级,万亿级的,在如此庞大请求量的情况下,几乎各种奇葩的异常,你都会遇到,代码要极其的健壮,一个小异常没处理好就会带来大麻烦。 这让我想起来,我们故障时候的情形。 几年前,我们每做完一次版本变更,晚上基本都会睡不好,担心变更的代码有问题。对手机的报警短信特别敏感,一有风吹草动,立马就会打开电脑V**看看,即使是在深夜凌晨的时候。 我自己有个习惯,每次变更完,都要间隔几个小时去看看监控曲线和日志,看看有没有异常的苗头,一旦发现有不对劲地方,就会立即

  • 2018年度SD-WAN 优秀应用评选活动结果新鲜出炉

    11月3日,首届国内SD-WAN专场活动2018中国SD-WAN峰会在北京隆重举行,峰会期间举行了2018年度SD-WAN优秀应用评选活动,经过线上评选及专家评审,共计27项应用分别斩获“人气应用奖”、“创新应用奖”、“优秀应用奖”。 SD-WAN全称是Software-DefinedWideAreaNetwork,其主要意思是在一个或者多个物理网络中建立一张虚拟网络,实现转发与控制分离,以简化网络的管理和操作。它是将SDN技术应用到广域网场景中所形成的一种服务。这种服务用于连接广阔地理范围的企业网络、数据中心、互联网应用及云服务,旨在帮助用户降低广域网的开支和提高网络连接灵活性。目前,越来越多的企业意识到需要重新考虑当今云计算的网络架构,所以SD-WAN正在迅速取代传统的WAN。 在此背景之下,由中国通信学会、SDN/NFV技术与产业推进委员会指导、江苏省未来网络创新研究院主办的2018中国SD-WAN峰会在北京盛大开幕,活动邀请了来自中国工程院、电信运营商、互联网、SD-WAN厂商代表等专家学者,围绕SD-WAN现状与趋势、技术难题、应用案例等进行交流研讨。 2018年度SD-WA

  • 震惊,这款控件的速度比姆巴佩还快

    昨日,俄罗斯世界杯决赛在卢日尼基球场打响,法国对阵克罗地亚。最终法国队4-2战胜克罗地亚,继1998年后第二次夺得世界杯冠军!本届杯赛要说冠军法国谁最耀眼,大家一定会说出一个小将的名字,他就是姆巴佩。本届世界杯中,姆巴佩的速度让法国队在防守反击的打法上起到了其他队伍达不到效果。在对阵阿根廷的1/8决赛中,姆巴佩宛若20年前的欧文,千里走单骑,一个人撕毁阿根廷的防线,一首凉凉送给梅西,此役一战成名。世界杯如此,IT开发更是如此,速度已成为了决定成败的关键。说到速度就不得不说葡萄城的Spread表格组件。这款可在应用程序中批量定制和管理Excel文件的.NET组件。无需安装MicrosoftExcel,通过代码,即可快速创建、导入、编辑、转化、导出Excel文件。其最大的特点就是速度快,性能高,给用户提供“怪兽”级的性能,用代码玩转Excel。Spread表格组件可以解决什么问题?使用 Spread表格组件,开发人员可定制每个单元格的数据和格式、处理复杂的公式运算、定制表格、透视表、图表、图片和迷你图、修改条件格式和形状的数据、定制样式外观,并对数据进行增删改查、过滤、排序、分组等操作;还

  • 真相:不会写文案?!可能销售转化率会很差!

    如果您已关注我们的博客一段时间,您可能会知道良好的产品描述有助于产品或者服务的销售。但是我们经常过分强调描述产品本身,而忽略了对产品其他方面的介绍。来自Salsify的一项SurveyMonkey的最新研究表明,如果在网站上找不到有效帮助购买的信息,高达94%的用户将放弃这个网站。88%的购买者认为产品内容的描述在他们的购买决策中扮演着及其重要的角色。那么什么样的内容可以带来销量上的巨大差异呢?您可能会对结果感到惊讶。价格并不是促使用户购买的全部因素从下图中,可以看到:1.价格对用户购买起了作用,但最终决定用户“购买”的是产品内容。2.哪些因素影响用户购买决策呢?答案如下:·首先是详细的产品内容描述;·其次是评分和用户评价;·最后才是价格。价格被理解为决定购买的一大因素,但在用户看来价格并不是最大或最关键的因素。参与过这项研究调查的用户表示,产品特性-特别是亮点描述,图像,视频和评论比价格更有可能说服他们。当被问及根本原因时,用户解释说,这些产品特征是使得他们准确了解将得到什么产品或服务的唯一方式。商品描述更多的满足他们的需求,他们的整体体验感就越好。通常,电子商务网站有很多产品或很多

  • GB28181平台如何接入无人机实现智能巡检?

    ​大家都知道,无人机-巡检系统,有效解决了传统巡查工作空间和时间局限问题,降低人力工作成本,有效替代人工巡检工作模式。智能巡检系统通过人工智能技术和机械智能技术完美结合,在工业等场景下,应用非常广泛。本文旨在讲如何实现无人机(如大疆无人机)数据到GB28181平台(如海康、大华、宇视等国标平台)。本文以Android平台接入大疆无人机为例,首先,无人机可以通过厂商提供的接口,回调编码后的H.264/H.265数据,需要注意的是,由于GB/T28181-2016,官方规范,仅对H.264做过描述,考虑到系统通用性和尽可能的规避转码带来的性能或使用体验问题,一般建议H.264编码。无人机的数据会上来后,可以通过编码后的数据接口,投递到JNI层,把视音频数据封装成PS包,让把PS包以负载的方式封装成RTP包,完成媒体数据的上传即可。本文以转发的模块为例说明,无图无真相:具体实现:APP启动后,我们先点击启动GB28181按钮,完成到国标平台的注册,并通过心跳机制,保持和国标平台端的通信。当国标平台端,需要查看无人机的实时画面时,可以发送Invite,请求无人机画面,Android平台GB28

  • unity3d俄罗斯方块源码教程+源码和程序下载

    小时候,大家都应玩过或听说过《俄罗斯方块》,它是红白机,掌机等一些电子设备中最常见的一款游戏。而随着时代的发展,信息的进步,游戏画面从简单的黑白方块到彩色方块,游戏的玩法机制从最简单的消方块到现在的多人pk等,无一不是在体现它的火爆。在这里,通过这篇文章向大家分享一下自己在制作俄罗斯方块的经验和心得,以及文章最后的源码和pc程序。 首先,看标题都知道这篇文章中所用到的游戏引擎是:unity3d,版本不限,但最好是5.4.3以上的,原因是因为作者自己没有用过5.4.3以下的版本。 准备工具都有:unity3d+VisualStudio2015 素材准备有(密码:m6gz):字体方正粗圆_GBK,以及一些图集 项目分析: 当一切准备就绪后,就可以开始创建我们的俄罗斯方块工程(2d)的。   一、游戏框架的搭建和方块预设物的制作 1.双击unity快捷方式,打开unity界面点击New新建工程,Template类型选择2d,工程名Tetris,点击创建后稍等片刻进入编辑器界面 ​ 2.在Assets文件夹下创建几个常用文件夹用来归类: 创建文件夹方式,在Project面板下右键 ​

  • 微软Hololens学院教程-Hologram 211-Gestures(手势)【微软教程已经更新,本文是老版本】

    这是老版本的教程,为了不耽误大家的时间,请直接看原文,本文仅供参考哦!原文链接:https://developer.microsoft.com/EN-US/WINDOWS/HOLOGRAPHIC/holograms_211 用户操作Hololens的全息对象时需要借助手势,手势的点击类似于我们电脑的鼠标,当我们凝视到某个全息对象时,手势点击即可以得到相应的反馈。这节教程主要讲述如何追踪用户的手势,并对手势操作做出反馈。 这节对手势操作会实现一些新的功能: 1检测什么时候手势被追踪到然后提供反馈 2使用导航手势来旋转全息对象。 3当用户的手移开可检测的范围时提供反馈 4使用操纵事件来移动全息对象。 前提条件: 一台安装好开发工具的Windows10PC toolsinstalled. 一些基础的C#编程能力. 已经完成教程Holograms101. 已经完成教程 Holograms210. 一台开发者版本的HoloLens设备  项目文件 下载项目文件files 勘误表与注释 VS中"仅我的代码"需要被禁用。在工具-》选项-》调试-》可以找到“仅我的

  • 【oracle】ORA-12541:TNS:no listener

    查看监听文件   locatelistener.ora       切换到数据库用户 su-ora11g   查看监听状态 状态显示,监听没有打开   开启监听,start后面加上你需要启动的监听名字(因为listener.ora中可以配置很多监听)。不指明名字的话,默认就是LISTENER的名字 修改完成后连接成功  

  • Django 跨域请求处理

    摘自https://www.cnblogs.com/DI-DIAO/p/8977847.html 使用javascript进行ajax访问的时候,出现如下错误 出错原因:javascript处于安全考虑,不允许跨域访问。下图是对跨域访问的解释:   概念:   这里说的js跨域是指通过js或python在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,或者通过js获取页面中不同域的框架中(Django)的数据。只要协议、域名、端口有任何一个不同,都被当作是不同的域。 解决方法: 1.修改views.py文件 修改views.py中对应API的实现函数,允许其他域通过Ajax请求数据 todo_list=[ {"id":"1","content":"吃饭"}, {"id":"2","content":"吃饭"}, ] classQuery(View): @staticmethod defget(request): response=JsonResponse(todo_list,safe=False) response["Access-Contro

  • 在细雨中呐喊

    人物简介 孙广才(父亲)孙有元(爷爷)孙光平(哥哥)孙光林(作者)孙光明(弟弟)苏宇:南门邻居家的孩子苏杭冯玉青王跃进英花(孙光平媳妇)孙晓明(孙光平儿子)郑玉达:商业局局长罗老头:管仓库王立强李秀丽刘小青国庆 第3章南门 讲述1965年孙光林童年关于南门生活的记忆,被领养后再次回来,全家态度的转变。后来哥哥上学,接着孙光林考上大学,南门被土地征用盖棉花厂,再回故乡与太多留恋。 第4章婚礼 冯玉青被王跃进玷污,独自上医院检查。王跃进结婚,冯玉青闹婚礼。货郎出现,带走冯玉青。 第5章死去 孙光明溺水身亡,孙广才与孙光平幻想政府安排人,春节后幻想破灭,两人前往被救孩子王家索要赔偿,起纠纷后孙广才被警察带走,家里家具当做赔偿拿走。孙广才被放后,勾搭村里寡妇,母亲积怨已久,田野爆发争执,不敌寡妇,孙光平也爬上了寡妇的床。孙光林离家上学。孙光平订婚,孙广才猥亵新娘,婚被辞退。孙光平24岁与英花结婚,有一子孙晓明。孙广才后来又猥亵英花,孙光平割下孙广才耳朵,被警察带走,入狱两年。出狱后,母亲不久离世。孙广才与寡妇在一起,母亲离世后酗酒,某天喝醉后掉入粪坑淹死。 第6章出生 1958年,孙广才进城卖

  • 实现继承的几种方式

    javascript实现继承的原理主要是依靠原型链机制。 第一种方法: //首先创建一个父类对象   functionSuperType(){ this.property=true; }复制   //在该对象的原型上添加了一个getSuperValue方法 SuperType.prototype.getSuperValue=function(){ returnthis.property; }; //创建了一个子类 functionSubType(){ this.subproperty=false; } //继承了SuperType SubType.prototype=newSuperType(); SubType.prototype.getSubValue=function(){ returnthis.subproperty; }; //创建一个子类的实例,但是这个实例已经可以访问父类的方法 varinstance=newSubType(); //这里调用了父类的方法 alert(instance.getSuperValue());//true;复制 优缺点

  • Graphical User Interface (GUI)

    AWTandSwing AbstractWindowToolkit(AWT):   IntroducedinJava1.0Providesclassesandothertoolsforbuildingprogramsthathaveagraphicaluserinterface   Theterm“Abstract”referstotheAWT’sabilitytorunonmultipleplatforms.   BuildingaGUIinvolvescreating“abstract”componentssuchasbuttonsandwindows,whicharethenmappedto“concrete”componentsforaspecificplatform. Swing:IntroducedinJavaSE1.2SwingismorepowerfulandsophisticatedthantheAWT.   SwingisbuiltaroundtheexistingAWT,soithelpstounderstandtheAWTfirst.   ManySwingcl

  • 设计模式(十):享元模式

    优点:   减少了系统中对象的数量,避免了大量细粒度对象给内存带来的压力,实现对细粒度对象的复用。 缺点:   此模式需要维护一个记录了系统已有的所有享元对象的列表,本身就需要耗费资源。此外此模式需要将一些状态外部化,也使得系统及逻辑更加复杂。 适用范围:   一个系统中有大量的对象,同时这些对象耗费大量的内存,这些对象内部状态可提取分组,外部状态可外部化。 客户端:    WebSiteFactoryf=newWebSiteFactory(); //生产的工厂  WebSitewx=f.getWebSiteCategory("博客");  wx.belongToUser(newUser("小菜"));   //专属网站  WebSitemy=f.getWebSiteCategory("专属");//"专属"是区分标识  my.belongToUser(newUser("书生"));  System.out.println("网站分类总数:"+f.getWebSiteCategoryCount()); 一句话概括:   针对不同客户的同种类型的网站的需求,使用同一

  • 开启风螺旋的2.0时代

    2018年,《等距螺旋的原理与计算》成功刊发,在等距(离外扩)螺旋的大家庭里,风螺旋终于和渐开线、阿基米德螺旋攀上了亲戚。为了庆祝这个重要的时刻,我将6月9日作为等距螺旋的生日,到今年那就是3周岁了。     在过去的一年里,风螺旋从理论验证的1.0阶段开始向着产品化的2.0阶段发展。在经过多次尝试之后,终于在CAD平台中成功登陆。     2.0时代的风螺旋可以做什么?首先是将传统飞行程序、PBN飞行程序相关的大部分模板搬迁至CAD平台,实现CAD平台中的一体化应用。     其次,可以在CAD平台中尝试自动化评估的各种可能,开展飞行程序保护区的专题分析。     除此以外,还可以尝试对国产CAD软件平台进行支持,与国产软件共同成长。     在这个“内卷”越来越严重的当下,是否“躺平”成了很多人热议的话题。对风螺旋来说,要走的路还很长,想做的事情还很多,“躺平”是不存在的。 浩辰CAD、中望CAD的风螺旋线插件下载链接在这里,免费提供使用。 ZwSpiral.v2021.d

  • 虚方法、抽象类与抽象方法

    抽象类 何时必须声明一个类为抽象类?(面试题) 当这个类中包含抽象方法时,或是该类并没有完全实现父类的抽象方法时。 abstract修饰符可用于类、方法、属性、索引和事件。在类声明中使用 abstract 修饰符以指示某个类仅旨在作为其他类的基类。标记为abstract的成员,或包含在抽象类中的成员,都必须由派生自抽象类的类来实现。 抽象类的特征: 抽象类由abstract关键字修饰,只能用作基类 抽象类可能包含抽象方法和访问器,同时也可以包含非抽象方法和非抽象访问器(只要有一个抽象方法,该类就必须定义为抽象类) 1abstractclassAbstractClass 2{ 3publicabstractintAge{get;}//抽象访问器 4publicstringName{get{return"张三";}}//非抽象访问器 5 6publicabstractvoidAbstractMethod();//抽象方法 7 8publicvoidMethod()//非抽象方法 9{ 10Console.WriteLine("抽象类中的非抽象方法"); 11} 1

  • 搜索引擎 中 排序学习 的小思考

    问题:   排序是搜索引擎的一个核心问题,早年的排序设计主要是使用排序模型,目前更多的是使用机器学习。排序模型的发展可以分为两个阶段,第一个阶段是基于词频和位置统计的排序模型,如布尔模型、向量空间模型等;第二个阶段是基于链接分析的排序模型,如PageRank模型等。然而排序模型在实际应用过程中存在如下问题: 1.模型参数的调整不方便,当模型需要调整的参数数量很大的时候,传统的排序模型不能很好的处理。 2.模型的整合不方便,每个模型都有各自的优缺点,如何将他们整合成更优秀的排序模型。 3.排序模型的过拟合问题。 这些问题其实是所有建模过程都会碰到的问题,通过机器学习处理这些问题可以更加的方便,这个过程也称为排序学习,排序学习是目前的研究热点之一。在排序学习中,起初人们使用较多的是有监督学习,由于数据集的标注需要耗费大量的时间和人力,那么如何更有效地利用未经过标注的数据成为业界日益关心的问题,已经有很多工作利用半监督学习的方法使用未标注的数据提高排序模型的性能,还有一部分学者在研究利用用户行为特征来调整模型。   我想说的就是后者   小思考: 使用有展现

  • 图解Git命令

        上面的四条命令在工作目录、暂存目录(也叫做索引)和仓库之间复制文件。 ·gitaddfiles把当前文件放入暂存区域。 ·gitcommit给暂存区域生成快照并提交。 ·gitreset--files用来撤销最后一次gitaddfiles,你也可以用gitreset撤销所有暂存区域文件。 ·gitcheckout--files把文件从暂存区域复制到工作目录,用来丢弃本地修改。 你可以用 gitreset-p,gitcheckout-p,origitadd-p进入交互模式。 也可以跳过暂存区域直接从仓库取出文件或者直接提交代码。     gitcommit-a 相当于运行 gitadd 把所有当前目录下的文件加入暂存区域再运行。gitcommit. ·gitcommit files 进行一次包含最后一次提交加上工作目录中文件快照的提交。并且文件被添加到暂存区域。 ·gitcheckoutHEAD-- files 回滚到复制最后一次提交。 约定 后文中

相关推荐

推荐阅读