响应式原理实现(3)vue2和vue3

响应式系统

vue2

watcher,实现回调函数

export default class Watcher {
export default class Watcher {constructor(data, expOrFn, cb, options) {this.data = data;if (typeof expOrFn === "function") {this.getter = expOrFn;} else {this.getter = parsePath(expOrFn);}...this.cb = cb;this.value = this.get(); // 回调函数要用到}...
}
}

initWatch函数

// state.js
import Watcher from "./watcher";
import { pushTarget, popTarget } from "./dep";export function initWatch(data, watch) {for (const key in watch) {const handler = watch[key];createWatcher(data, key, handler);}
}function createWatcher(data, expOrFn, handler) {return $watch(data, expOrFn, handler);
}function $watch(data, expOrFn, handler) {new Watcher(data, expOrFn, handler);
}

immediate

import { observe } from "./reactive";
import { initWatch } from "./state";
const options = {data: {title: "liang",},watch: {title: {handler(newVal, oldVal) {console.log("收到变化", newVal, oldVal);},immediate: true,},},
};
observe(options.data);
initWatch(options.data, options.watch);options.data.title = "changeTitle";
/*** Get the raw type string of a value, e.g., [object Object].*/
const _toString = Object.prototype.toString;/*** Strict object type check. Only returns true* for plain JavaScript objects.*/
export function isPlainObject(obj) {return _toString.call(obj) === "[object Object]";
}function createWatcher(data, expOrFn, handler, options) {// 如果是对象,就将 handler 和 options 分离if (isPlainObject(handler)) {options = handler;handler = handler.handler;}return $watch(data, expOrFn, handler, options);
}
function $watch(data, expOrFn, handler, options) {/******新增 options*************************/const watcher = new Watcher(data, expOrFn, handler, options);/************************************/if (options.immediate) {handler.call(data, watcher.value);}return function unwatchFn() {watcher.teardown();};
}

computed

  1. 惰性的响应式数据
  2. 处理computed的值
  3. computed属性的响应式
    添加lazy属性和dirty属性,dirty为true表示watcher依赖的属性发生了变化,需要重新求值。dirty为false表示依赖的属性没有发生变化,无需重新求值
export default class Watcher {constructor(data, expOrFn, cb, options) {this.data = data;if (typeof expOrFn === "function") {this.getter = expOrFn;} else {this.getter = parsePath(expOrFn);}...// optionsif (options) {this.deep = !!options.deep;this.sync = !!options.sync;this.lazy = !!options.lazy;}this.dirty = this.lazy;this.value = this.lazy ? undefined : this.get();}/*** Evaluate the getter, and re-collect dependencies.*/get() {pushTarget(this); // 保存包装了当前正在执行的函数的 Watcherlet value;try {value = this.getter.call(this.data, this.data);} catch (e) {throw e;} finally {// "touch" every property so they are all tracked as// dependencies for deep watchingif (this.deep) {traverse(value);}popTarget();this.cleanupDeps();}return value;}.../*** Evaluate the value of the watcher.* This only gets called for lazy watchers.*//******新增 *************************/evaluate() {this.value = this.get();this.dirty = false; // dirty 为 false 表示当前值已经是最新}/**********************************/update() {/******新增 *************************/if (this.lazy) {this.dirty = true;/************************************/} else if (this.sync) {this.run();} else {queueWatcher(this);}}}

输出value之前会执行一次evaluate。
处理computed的值

export function noop(a, b, c) {}const computedWatcherOptions = { lazy: true };// computed properties are just getters during SSR
export function initComputed(data, computed) {const watchers = (data._computedWatchers = Object.create(null)); // 保存当前所有的 watcher,并且挂在 data 上供后边使用for (const key in computed) {const userDef = computed[key];const getter = typeof userDef === "function" ? userDef : userDef.get; // 如果是对象就取 get 的值// create internal watcher for the computed property.watchers[key] = new Watcher(data,getter || noop,noop,computedWatcherOptions);// component-defined computed properties are already defined on the// component prototype. We only need to define computed properties defined// at instantiation here.defineComputed(data, key, userDef);}
}

defineComputed 是将computed函数定义为data的属性,就可以和正常属性一样使用computed

const sharedPropertyDefinition = {enumerable: true,configurable: true,get: noop,set: noop,
};
export function defineComputed(target, key, userDef) {// 初始化 get 和 setif (typeof userDef === "function") {sharedPropertyDefinition.get = createComputedGetter(key);sharedPropertyDefinition.set = noop;} else {sharedPropertyDefinition.get = userDef.get? createComputedGetter(key): noop;sharedPropertyDefinition.set = userDef.set || noop;}// 将当前属性挂到 data 上Object.defineProperty(target, key, sharedPropertyDefinition);
}
function createComputedGetter(key) {return function computedGetter() {const watcher = this._computedWatchers && this._computedWatchers[key]; // 拿到相应的 watcherif (watcher) {if (watcher.dirty) {watcher.evaluate();}return watcher.value;}};
}

computed属性的响应式


export default class Watcher {constructor(data, expOrFn, cb, options) {this.data = data;if (typeof expOrFn === "function") {this.getter = expOrFn;} else {this.getter = parsePath(expOrFn);}this.depIds = new Set(); // 拥有 has 函数可以判断是否存在某个 idthis.deps = [];this.newDeps = []; // 记录新一次的依赖this.newDepIds = new Set();...this.dirty = this.lazy;this.value = this.lazy ? undefined : this.get();}/*** Add a dependency to this directive.*/addDep(dep) {const id = dep.id;// 新的依赖已经存在的话,同样不需要继续保存if (!this.newDepIds.has(id)) {this.newDepIds.add(id);this.newDeps.push(dep);if (!this.depIds.has(id)) {dep.addSub(this);}}}/*** Evaluate the value of the watcher.* This only gets called for lazy watchers.*/evaluate() {this.value = this.get();this.dirty = false;}/******新增 *************************//*** Depend on all deps collected by this watcher.*/depend() {let i = this.deps.length;while (i--) {this.deps[i].depend();}}/************************************/
}
function createComputedGetter(key) {return function computedGetter() {const watcher = this._computedWatchers && this._computedWatchers[key];if (watcher) {if (watcher.dirty) {watcher.evaluate();}if (Dep.target) {watcher.depend();}return watcher.value;}};
}

vue3

vue3中的nextick放到后面渲染的时候讲。

export function effect<T = any>(fn: () => T,options?: ReactiveEffectOptions,
): ReactiveEffectRunner {// fn嵌套了effect => effect(() => {effect(fn)})if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {fn = (fn as ReactiveEffectRunner).effect.fn}// 实例化ReactiveEffectconst _effect = new ReactiveEffect(fn, NOOP, () => {if (_effect.dirty) {_effect.run()}})if (options) {// 把option浅拷贝到_effect身上extend(_effect, options)// 记录effect作用域if (options.scope) recordEffectScope(_effect, options.scope)}if (!options || !options.lazy) {// 立即执行effect.run()// 这就是定义一个effect传入的副作用函数会立即执行_effect.run()}// 通过bind改变_effect的this指向,返回函数const runner = _effect.run.bind(_effect) as ReactiveEffectRunner// runnner的effect赋值为_effectrunner.effect = _effectreturn runner
}
export class ReactiveEffect<T = any> {active = true // 标记为激活状态deps: Dep[] = [] // 保存了该reactiveEffect所依赖的所有响应式对象的dep对象computed?: ComputedRefImpl<T>allowRecurse?: booleanonStop?: () => voidonTrack?: (event: DebuggerEvent) => voidonTrigger?: (event: DebuggerEvent) => void_dirtyLevel = DirtyLevels.Dirty_trackId = 0_runnings = 0_shouldSchedule = false_depsLength = 0constructor(public fn: () => T,public trigger: () => void,public scheduler?: EffectScheduler,scope?: EffectScope, // 这个在组件那块用) {// 该副作用所属的作用域recordEffectScope(this, scope)}// run函数: lazy为false直接调的_effect.runrun() {this._dirtyLevel = DirtyLevels.NotDirty// 非激活的情况下只需要执行函数,不需要收集依赖。if (!this.active) {return this.fn()}// shouldTrack为全局变量,当前副作用是否需要被追踪let lastShouldTrack = shouldTracklet lastEffect = activeEffecttry {shouldTrack = trueactiveEffect = thisthis._runnings++preCleanupEffect(this)return this.fn()} finally {postCleanupEffect(this)this._runnings--activeEffect = lastEffectshouldTrack = lastShouldTrack}}stop() {if (this.active) {preCleanupEffect(this)postCleanupEffect(this)this.onStop && this.onStop()this.active = false}}
}

上面的代码是effect的核心。
接下来看watch的实现,首先看一下使用

watch(x, (newValue, oldValue) => {console.log(`x is ${newValue}`)
})
watch(() => {() => x.value + y.value(newValue, oldValue) => {console.log(`sum of x + y is: ${newValue}`)}
})
watch([x, () => y.value], ([newX, newY]) => {console.log(`x is ${newX} and y is ${newY}`)
})

第一个参数: ref(包括计算属性) 响应式对象 getter函数 多个数据源组成的数组
第二个参数: 发生变化的时候的回调函数,接受三个参数,新值,旧值,以及用来注册副作用清理的回调函数
第三个参数: immediate: 创建的时候立即触发回调。deep: 在深层级变更的时候触发回调。flush: 调整回调函数的刷新时机。onTrack/onTrigger:调试侦听器的依赖。
watch本质上是利用了副作用函数重新执行时的可调度性,一个watch本身会创建一个effect,当这个effect依赖的响应式数据发生变化的时候,会执行该effect的调度函数,即scheduler

export function watch<T = any, Immediate extends Readonly<boolean> = false>(source: T | WatchSource<T>,cb: any,options?: WatchOptions<Immediate>,
): WatchStopHandle {if (__DEV__ && !isFunction(cb)) {warn(`\`watch(fn, options?)\` signature has been moved to a separate API. ` +`Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +`supports \`watch(source, cb, options?) signature.`,)}return doWatch(source as any, cb, options)
}
export function watch<T = any, Immediate extends Readonly<boolean> = false>(source: T | WatchSource<T>,cb: any,options?: WatchOptions<Immediate>,
): WatchStopHandle {if (__DEV__ && !isFunction(cb)) {warn(`\`watch(fn, options?)\` signature has been moved to a separate API. ` +`Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +`supports \`watch(source, cb, options?) signature.`,)}return doWatch(source as any, cb, options)
}
function doWatch(source: WatchSource | WatchSource[] | WatchEffect | object,cb: WatchCallback | null,{immediate,deep,flush,once,onTrack,onTrigger,}: WatchOptions = EMPTY_OBJ,
): WatchStopHandle { if (cb && once) {const _cb = cbcb = (...args) => {_cb(...args)unwatch()}}const instance = currentInstanceconst reactiveGetter = (source: object) =>deep === true? source // traverse will happen in wrapped getter below: // for deep: false, only traverse root-level propertiestraverse(source, deep === false ? 1 : undefined)let getter: () => anylet forceTrigger = false // 强制触发副作用函数执行let isMultiSource = false // 侦听的是否为多个源// 如果source是ref对象,则创建source.value的getter函数if (isRef(source)) {getter = () => source.value// 判断数据源是否为浅响应forceTrigger = isShallow(source)} else if (isReactive(source)) {getter = () => reactiveGetter(source)forceTrigger = true} else if (isArray(source)) {isMultiSource = trueforceTrigger = source.some(s => isReactive(s) || isShallow(s))getter = () =>source.map(s => {if (isRef(s)) {return s.value} else if (isReactive(s)) {return reactiveGetter(s)} else if (isFunction(s)) {return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)} else {__DEV__ && warnInvalidSource(s)}})} else if (isFunction(source)) {if (cb) {// 有传入,处理的是watch的情况getter = () =>callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)} else {// 没有传入,处理的是watcheffect的情况getter = () => {if (cleanup) {cleanup()}return callWithAsyncErrorHandling(source,instance,ErrorCodes.WATCH_CALLBACK,[onCleanup],)}}} else {getter = NOOP__DEV__ && warnInvalidSource(source)}// 2.x array mutation watch compatif (__COMPAT__ && cb && !deep) {const baseGetter = gettergetter = () => {const val = baseGetter()if (isArray(val) &&checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)) {traverse(val)}return val}}// 处理的是watch的场景// 递归读取对象的属性值, 相应书数据,需要递归读取数据源中的每个属性if (cb && deep) {const baseGetter = gettergetter = () => traverse(baseGetter())}// 清除富国用函数let cleanup: (() => void) | undefinedlet onCleanup: OnCleanup = (fn: () => void) => {cleanup = effect.onStop = () => {callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)cleanup = effect.onStop = undefined}}let oldValue: any = isMultiSource? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE): INITIAL_WATCHER_VALUEconst job: SchedulerJob = () => {if (!effect.active || !effect.dirty) {return}if (cb) {// watch(source, cb)const newValue = effect.run()// 如果侦听的数据源是响应式数据,需要深度侦听,则deep为true// 如果需要强制触发副作用函数执行,则forceTrigger为true// 如果新旧值发生了变化if (deep ||forceTrigger ||(isMultiSource? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i])): hasChanged(newValue, oldValue)) ||(__COMPAT__ &&isArray(newValue) &&isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))) {// cleanup before running cb againif (cleanup) {cleanup()}// 传入这个函数,在执行完成之后更新新旧值callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [newValue,// pass undefined as the old value when it's changed for the first timeoldValue === INITIAL_WATCHER_VALUE? undefined: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE? []: oldValue,onCleanup,])oldValue = newValue}} else {// watchEffecteffect.run()}}// 让调度任务作为侦听器的回调,这样调度器就知道允许自己派发更新job.allowRecurse = !!cblet scheduler: EffectScheduler// sync 表示同步的watcherif (flush === 'sync') {scheduler = job as any // the scheduler function gets called directly} else if (flush === 'post') {// 放到微任务队列中,等待dom更行结束后执行scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)} else {// 调度器函数默认的执行方式,在组件更新之前执行,如果组件还没有挂载,则在组件挂载之前同步执行回调函数job.pre = trueif (instance) job.id = instance.uidscheduler = () => queueJob(job)}// 初始化getter函数和调度器函数scheduler后, 调用reactiveeffectconst effect = new ReactiveEffect(getter, NOOP, scheduler)const scope = getCurrentScope()const unwatch = () => {effect.stop()if (scope) {remove(scope.effects, effect)}}if (__DEV__) {effect.onTrack = onTrackeffect.onTrigger = onTrigger}// initial runif (cb) {if (immediate) {job()} else {oldValue = effect.run()}} else if (flush === 'post') {queuePostRenderEffect(effect.run.bind(effect),instance && instance.suspense,)} else {effect.run()}if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)return unwatch
}
export function traverse(value: unknown,depth = Infinity,seen?: Set<unknown>,
) {if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {return value}seen = seen || new Set()if (seen.has(value)) {return value}seen.add(value)depth--if (isRef(value)) {traverse(value.value, depth, seen)} else if (isArray(value)) {for (let i = 0; i < value.length; i++) {traverse(value[i], depth, seen)}} else if (isSet(value) || isMap(value)) {value.forEach((v: any) => {traverse(v, depth, seen)})} else if (isPlainObject(value)) {for (const key in value) {traverse(value[key], depth, seen)}for (const key of Object.getOwnPropertySymbols(value)) {if (Object.prototype.propertyIsEnumerable.call(value, key)) {traverse(value[key as any], depth, seen)}}}return value
}

原理类似于

function watch(souce,callBack,options = {}) {let getterif(typeof souce === 'function') {getter =  souce}else {getter = ()=> traverse(souce)}let newVal,oldVal;let work = ()=>{//调用effectFn,得到新的newValnewVal =  effectFn()//当响应式数据变化时候,会执行回调函数callBack()callBack(newVal,oldVal)//新值替换旧值,当作旧值。oldVal = newVal}//我们对传入的对象souce进行循环遍历,把所有属性进行监听const effectFn = effect(()=>getter(),{lazy:true//lazy 是懒执行effectscheduler:work}) if(options.immediate) {work()}else{//我们自己触发点一次effect调用,它肯定优先effect的执行。因为只有当响应式数据变化时候,才会执行effectFn。这是我们手动执行,得到一个初试值oldVal。oldVal =  effectFn()}}
export function triggerEffects(dep: Dep,dirtyLevel: DirtyLevels,debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {pauseScheduling()for (const effect of dep.keys()) {// dep.get(effect) is very expensive, we need to calculate it lazily and reuse the resultlet tracking: boolean | undefinedif (effect._dirtyLevel < dirtyLevel &&(tracking ??= dep.get(effect) === effect._trackId)) {effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirtyeffect._dirtyLevel = dirtyLevel}if (effect._shouldSchedule &&(tracking ??= dep.get(effect) === effect._trackId)) {if (__DEV__) {// eslint-disable-next-line no-restricted-syntaxeffect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))}effect.trigger()if ((!effect._runnings || effect.allowRecurse) &&effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect) {effect._shouldSchedule = falseif (effect.scheduler) {queueEffectSchedulers.push(effect.scheduler)}}}}resetScheduling()
}
export function pauseScheduling() {pauseScheduleStack++
}
export function resetScheduling() {pauseScheduleStack--while (!pauseScheduleStack && queueEffectSchedulers.length) {queueEffectSchedulers.shift()!()}
}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

替换后端国外身份目录服务,宁盾身份域管接管FileNet助力国产化升级

IBM FileNet 是一款优秀的企业内容管理解决方案&#xff0c;为客户提供了领先的文档管理和流程管理集成环境&#xff0c;被大量企业所采用。FileNet 需要使用企业级的目录服务器&#xff08;LDAP&#xff09;作为其用户管理系统&#xff0c;满足其认证和授权的需求。对于 LDAP …

成为git砖家(4): git status 命令简介

1. untracked 和 tracked 状态 Remember that each file in your working directory can be in one of two states: tracked or untracked. Tracked files are files that were in the last snapshot, as well as any newly staged files; they can be unmodified, modified, o…

zabbix使用脚本自定义监控项

1. 在zabbix_agent的配置文件中配置自定义key和脚本位置 vim /etc/zabbix/zabbix_agentd.confUserParametermq_check_log,/etc/zabbix/zabbix_agentd.d/mqlog.shmq_check_log&#xff1a;是这个自定义参数的名称。在Zabbix的监控项&#xff08;item&#xff09;配置中&#xf…

点菜吧——随便点 C#生成套餐

前言 一到食堂发现有多种选择&#xff0c;但是有一个固定的套路&#xff0c;只能是一个荤&#xff0c;二个小荤&#xff0c;菜品数量也不少&#xff0c;任君选择&#xff0c;如果是一个选择困难症&#xff0c;就有点烦了&#xff0c;所以出品这个自动生成套餐软件。各位老板可…

代码随想录算法训练营Day 63| 图论 part03 | 417.太平洋大西洋水流问题、827.最大人工岛、127. 单词接龙

代码随想录算法训练营Day 63| 图论 part03 | 417.太平洋大西洋水流问题、827.最大人工岛、127. 单词接龙 文章目录 代码随想录算法训练营Day 63| 图论 part03 | 417.太平洋大西洋水流问题、827.最大人工岛、127. 单词接龙17.太平洋大西洋水流问题一、DFS二、BFS三、本题总结 82…

在手机查看笔记本电脑上的便签 笔记本电脑和手机共享便签方法

在这个信息时代&#xff0c;笔记本电脑已成为我们工作和学习中不可或缺的工具。我经常在笔记本上记录各种便签&#xff0c;无论是工作中的待办事项&#xff0c;还是生活中的小提醒&#xff0c;都依赖于这些小小的便签。它们轻便、灵活&#xff0c;可以随时随地提醒我接下来要做…

TongHttpServer 简介

1. 概述 随着网络技术的飞速发展,高并发大用户场景越来越普遍,单一应用服务节点已经不能满足并发需求,为了提高整个系统可靠性,扩展性,吞吐率,通常将多个应用服务器通过硬负载/软负载组成集群,负载均衡器根据不同负载算法将请求分发到各个应用服务器节点。 Tong…

花几千上万学习Java,真没必要!(三十六)

1、File类&#xff1a; 测试代码1&#xff1a; package filetest.com; import java.io.File; import java.io.IOException; public class FileOperations { public static void main(String[] args) { // 创建新文件File file new File("example.txt"); tr…

Prometheus+Grafana+Alertmanager监控告警

PrometheusGrafanaAlertmanager告警 Alertmanager开源地址&#xff1a;github.com/prometheus Prometheus是一款基于时序数据库的开源监控告警系统&#xff0c;它是SoundCloud公司开源的&#xff0c;SoundCloud的服务架构是微服务架构&#xff0c;他们开发了很多微服务&#xf…

TCP为什么需要四次挥手?

tcp为什么需要四次挥手&#xff1f; 答案有两个&#xff1a; 1.将发送fin包的权限交给被动断开方的应用层去处理&#xff0c;也就是让程序员处理 2.接第一个答案&#xff0c;应用层有了发送fin的权限&#xff0c;可以在发送fin前继续向对端发送消息 为了搞清楚这个问题&…

前端开发知识-vue

大括号里边放键值对&#xff0c;即是一个对象。 一、vue可以简化前端javascript的操作。 主要特点是可以实现视图、数据的双向绑定。 使用vue主要分为三个步骤&#xff1a; 1.javascript中引入vue.js 可以src中可以是vue的网址&#xff0c;也可以是本地下载。 2.在javasc…

网络爬虫必备工具:代理IP科普指南

文章目录 1. 网络爬虫简介1.1 什么是网络爬虫&#xff1f;1.2 网络爬虫的应用领域1.3 网络爬虫面临的主要挑战 2. 代理IP&#xff1a;爬虫的得力助手2.1 代理IP的定义和工作原理2.2 爬虫使用代理IP的必要性 3. 代理IP的类型及其在爬虫中的应用3.1 动态住宅代理3.2 动态数据中心…

数据挖掘-数据预处理

来自&#x1f96c;&#x1f436;程序员 Truraly | 田园 的博客&#xff0c;最新文章首发于&#xff1a;田园幻想乡 | 原文链接 | github &#xff08;欢迎关注&#xff09; 文章目录 3.3.1 数据的中心趋势平均数和加权平均数众数&#xff0c;中位数和均值描述数据的离散程度 &a…

【MySQL】用户管理连接池原理{数据库权限/连接池/mysql访问逻辑}

文章目录 1.普通用户的引入用户创建用户删除用户修改用户密码 2.数据库的权限给用户授权回收权限实操过程 3.简略介绍mysql连接池3.一个用户注册/登录/使用网页版mysql逻辑 1.普通用户的引入 用户 MySQL中的用户&#xff0c;都存储在系统数据库mysql的user表中 mysql> use…

uniapp微信小程序本地和真机调试文件图片上传成功但体验版不成功

文章目录 导文是因为要添加服务器域名&#xff01; 导文 uniapp微信小程序本地和真机调试文件图片上传成功但体验版不成功 uniapp微信小程序体验版上传图片不成功 微信小程序本地和真机调试文件图片上传成功但体验版不成功 是因为要添加服务器域名&#xff01; 先看一下 你小程…

解决jenkins配置extendreport不展示样式

下载插件&#xff1a;Groovy 、 HTML Publisher plugin 配置&#xff1a; 1&#xff09;Post Steps &#xff1a; 增加 Execute system Groovy script &#xff0c; 内容&#xff1a; System.setProperty("hudson.model.DirectoryBrowserSupport.CSP", "&qu…

C++ | string

前言 本篇博客讲解c中的string类的使用(常用接口) &#x1f493; 个人主页&#xff1a;普通young man-CSDN博客 ⏩ 文章专栏&#xff1a;C_普通young man的博客-CSDN博客 ⏩ 本人giee:普通小青年 (pu-tong-young-man) - Gitee.com 若有问题 评论区见&#x1f4dd; &#x1f389…

Magento2 常用命令以及技巧

1.Magento 命令行工具 Magento2 带有一个命令行工具&#xff0c;在windows下&#xff0c;用管理员权限打开MS-DOS命令提示符&#xff0c;然后cd到Magento根目录&#xff0c;运行下面命令&#xff0c;就可 以看到这个强大的命令行工具的命令清单&#xff1a; php bin/magentoU…

【C++】C++11中R字符串的作用

在 C11 中添加了定义原始字符串的字面量 1.定义和基本使用 定义方式为&#xff1a; R"xxx(原始字符串)xxx"其中 () 两边的字符串可以省略&#xff0c;R只会处理括号中的字符串。 原始字面量 R 可以直接表示字符串的实际含义&#xff0c;而不需要额外对字符串做转义…