vue源码解析-实现一个基础的MVVM框架
基本介绍
vue.js采用数据劫持结合发布-订阅模式的方式,通过Object.defineProperty()来劫持各个属性的getter,setter,在数据变动时发布消息给订阅者,触发响应的监听回调。

主要功能:
- 实现一个指令解析器Compile
- 实现一个数据监听器Observer
- 实现一个Watcher去更新视图
入口函数和Compile编译类实现
HTML代码示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-text="msg"></div>
<div v-text="person.fav"></div>
<div v-html="htmlStr"></div>
<input type="test" v-model="msg">
<button v-on:click="handleClick">点击我on</button>
<button @click="handleClick">点击我@</button>
<img v-bind:src="imgUrl" />
<img :src="imgUrl" />
</div>
<script src="./MVue.js"></script>
<script>
let vm = new MVue({
el: '#app',
data: {
person: {
name: '编程哥',
age: 18,
fav: '看电影'
},
msg: '学习MVVM实现原理',
htmlStr: '大家学习得怎么样',
imgUrl: './logo.png'
},
methods: {
handleClick () {
console.log(this)
}
}
})
</script>
</body>
</html>
MVue.js代码示例:
const compileUtil = {
getValue(expr, vm) {
return expr.split(".").reduce((pre, cur) => {
return pre[cur];
}, vm.$data);
},
text(node, expr, vm) {
// expr:msg 学习MVVM实现原理 // <div v-text="person.fav"></div>
let value;
if (expr.indexOf("{{") !== -1) {
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm);
});
} else {
value = this.getValue(expr, vm);
}
this.updater.textUpdater(node, value);
},
html(node, expr, vm) {
const value = this.getValue(expr, vm);
this.updater.htmlUpdater(node, value);
},
model(node, expr, vm) {
const value = this.getValue(expr, vm);
this.updater.modelUpdater(node, value);
},
on(node, expr, vm, eventName) {
const fn = vm.$options.methods && vm.$options.methods[expr];
node.addEventListener(eventName, fn.bind(vm), false);
},
bind(node, expr, vm, attrName) {
const value = this.getValue(expr, vm);
this.updater.bindUpdater(node, attrName, value);
},
// 更新的函数
updater: {
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
modelUpdater(node, value) {
node.value = value;
},
bindUpdater(node, attrName, value) {
node.setAttribute(attrName, value);
},
},
};
class Compiler {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 1.获取文档碎片对象,放入内存中会减少页面的回流和重绘
const fragment = this.node2Fragment(this.el);
// 2.编译模板
this.Compiler(fragment);
// 3.追加子元素到根元素
this.el.appendChild(fragment);
}
/*
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-test="msg"></div>
<div v-html="htmlStr"></div>
<input type="test" v-model="msg">
*/
Compiler(fragment) {
// 获取子节点
const childNodes = fragment.childNodes;
[...childNodes].forEach((child) => {
// console.log(child);
if (this.isElementNode(child)) {
// 是元素节点
// 编译元素节点
// console.log("元素节点", child);
this.compilerElement(child);
} else {
// 文本节点
// 编译文本节点
// console.log("文本节点", child);
this.compilerText(child);
}
if (child.childNodes && child.childNodes.length) {
this.Compiler(child);
}
});
}
compilerElement(node) {
const attributes = node.attributes;
[...attributes].forEach((attr) => {
const { name, value } = attr;
if (this.isDirective(name)) {
// 是一个指令 v-text v-html v-model v-on:click v-bind:src
const [, directive] = name.split("-"); // text html model on:click bind:src
const [dirName, eventName] = directive.split(":"); // text html model on bind
// 更新数据,数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName);
// 删除有指令的标签上的属性
node.removeAttribute("v-" + directive);
} else if (this.isEventName(name)) {
// @click="handleClick"
const [, eventName] = name.split("@");
compileUtil["on"](node, value, this.vm, eventName);
// 删除标签上的绑定的事件
node.removeAttribute("@" + eventName);
} else if (this.isAttrName(name)) {
// :src="imgUrl"
const [, attrName] = name.split(":");
compileUtil["bind"](node, value, this.vm, attrName);
// 删除标签上绑定的属性
node.removeAttribute(":" + attrName);
}
});
}
compilerText(node) {
// {{}} v-text
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil["text"](node, content, this.vm);
}
}
isAttrName(attrName) {
return attrName.startsWith(":");
}
isEventName(attrName) {
return attrName.startsWith("@");
}
isDirective(attrName) {
return attrName.startsWith("v-");
}
node2Fragment(el) {
// 创建文档碎片
const f = document.createDocumentFragment();
let firstChild;
while ((firstChild = el.firstChild)) {
f.appendChild(firstChild);
}
return f;
}
isElementNode(node) {
// 判断是否是元素节点对象
return node.nodeType === 1;
}
}
class MVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 1.实现一个数据观察者
// 2.实现一个指令解析器
new Compiler(this.$el, this);
}
}
}
实现Observer劫持并监听所有属性
新建一个Observer.js类,示例代码如下:
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
if (data && typeof data === "object") {
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key]);
});
}
}
defineReactive(obj, key, value) {
// 递归遍历劫持
this.observe(value);
Object.defineProperty(obj, key, {
configurable: false,
enumerable: true,
get() {
// 订阅数据变化时,往Dep中添加观察者
return value;
},
set: (newVal) => {
this.observe(newVal);
if (newVal !== value) {
value = newVal;
}
},
});
}
}
html代码引用:
<script src="./Observer.js"></script>
<script src="./MVue.js"></script>
在Mvue.js类中的调用方式:
class MVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 1.实现一个数据观察者
new Observer(this.$data);
// 2.实现一个指令解析器
new Compiler(this.$el, this);
}
}
}
实现观察者Watcher和依赖收集器Dep
Observer.js代码中新增类Dep(收集观察者与通知观察者)和Watcher:
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先把旧值保存起来
this.oldVal = this.getOldVal();
}
getOldVal() {
Dep.target = this;
let oldVal = compileUtil.getValue(this.expr, this.vm);
Dep.target = null;
return oldVal;
}
update() {
const newVal = compileUtil.getValue(this.expr, this.vm);
if (newVal !== this.oldVal) {
this.cb(newVal);
}
}
}
class Dep {
constructor() {
this.subs = [];
}
// 收集观察者
addSub(watcher) {
this.subs.push(watcher);
}
// 通知观察者去更新
notify() {
console.log("观察者", this.subs);
this.subs.forEach((w) => w.update());
}
}
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
if (data && typeof data === "object") {
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key]);
});
}
}
defineReactive(obj, key, value) {
// 递归遍历劫持
this.observe(value);
const dep = new Dep();
Object.defineProperty(obj, key, {
configurable: false,
enumerable: true,
get() {
// 订阅数据变化时,往Dep中添加观察者
Dep.target && dep.addSub(Dep.target);
return value;
},
set: (newVal) => {
this.observe(newVal);
if (newVal !== value) {
value = newVal;
}
// 告诉Dep通知变化
dep.notify();
}
});
}
}
MVue.js代码:
const compileUtil = {
getValue(expr, vm) {
return expr.split(".").reduce((pre, cur) => {
return pre[cur];
}, vm.$data);
},
getContentValue(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm);
});
},
text(node, expr, vm) {
// expr:msg 学习MVVM实现原理 // <div v-text="person.fav"></div>
let value;
if (expr.indexOf("{{") !== -1) {
// {{person.name}} -- {{person.age}}
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], () => {
this.updater.textUpdater(node, this.getContentValue(expr, vm));
});
return this.getValue(args[1], vm);
});
} else {
new Watcher(vm, expr, (newVal) => {
this.updater.textUpdater(node, newVal);
});
value = this.getValue(expr, vm);
}
this.updater.textUpdater(node, value);
},
html(node, expr, vm) {
const value = this.getValue(expr, vm);
new Watcher(vm, expr, (newVal) => {
this.updater.htmlUpdater(node, newVal);
});
this.updater.htmlUpdater(node, value);
},
model(node, expr, vm) {
const value = this.getValue(expr, vm);
// 绑定更新函数 数据=>视图
new Watcher(vm, expr, (newVal) => {
this.updater.modelUpdater(node, newVal);
});
this.updater.modelUpdater(node, value);
},
on(node, expr, vm, eventName) {
const fn = vm.$options.methods && vm.$options.methods[expr];
node.addEventListener(eventName, fn.bind(vm), false);
},
bind(node, expr, vm, attrName) {
const value = this.getValue(expr, vm);
this.updater.bindUpdater(node, attrName, value);
},
// 更新的函数
updater: {
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
modelUpdater(node, value) {
node.value = value;
},
bindUpdater(node, attrName, value) {
node.setAttribute(attrName, value);
}
}
};
class Compiler {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 1.获取文档碎片对象,放入内存中会减少页面的回流和重绘
const fragment = this.node2Fragment(this.el);
// 2.编译模板
this.Compiler(fragment);
// 3.追加子元素到根元素
this.el.appendChild(fragment);
}
/*
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-test="msg"></div>
<div v-html="htmlStr"></div>
<input type="test" v-model="msg">
*/
Compiler(fragment) {
// 获取子节点
const childNodes = fragment.childNodes;
[...childNodes].forEach((child) => {
// console.log(child);
if (this.isElementNode(child)) {
// 是元素节点
// 编译元素节点
// console.log("元素节点", child);
this.compilerElement(child);
} else {
// 文本节点
// 编译文本节点
// console.log("文本节点", child);
this.compilerText(child);
}
if (child.childNodes && child.childNodes.length) {
this.Compiler(child);
}
});
}
compilerElement(node) {
const attributes = node.attributes;
[...attributes].forEach((attr) => {
const { name, value } = attr;
if (this.isDirective(name)) {
// 是一个指令 v-text v-html v-model v-on:click v-bind:src
const [, directive] = name.split("-"); // text html model on:click bind:src
const [dirName, eventName] = directive.split(":"); // text html model on bind
// 更新数据,数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName);
// 删除有指令的标签上的属性
node.removeAttribute("v-" + directive);
} else if (this.isEventName(name)) {
// @click="handleClick"
const [, eventName] = name.split("@");
compileUtil["on"](node, value, this.vm, eventName);
// 删除标签上的绑定的事件
node.removeAttribute("@" + eventName);
} else if (this.isAttrName(name)) {
// :src="imgUrl"
const [, attrName] = name.split(":");
compileUtil["bind"](node, value, this.vm, attrName);
// 删除标签上绑定的属性
node.removeAttribute(":" + attrName);
}
});
}
compilerText(node) {
// {{}} v-text
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil["text"](node, content, this.vm);
}
}
isAttrName(attrName) {
return attrName.startsWith(":");
}
isEventName(attrName) {
return attrName.startsWith("@");
}
isDirective(attrName) {
return attrName.startsWith("v-");
}
node2Fragment(el) {
// 创建文档碎片
const f = document.createDocumentFragment();
let firstChild;
while ((firstChild = el.firstChild)) {
f.appendChild(firstChild);
}
return f;
}
isElementNode(node) {
// 判断是否是元素节点对象
return node.nodeType === 1;
}
}
class MVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 1.实现一个数据观察者
new Observer(this.$data);
// 2.实现一个指令解析器
new Compiler(this.$el, this);
}
}
}
实现双向的数据绑定和Proxy代理
MVue.js代码:
const compileUtil = {
getValue(expr, vm) {
return expr.split(".").reduce((pre, cur) => {
return pre[cur];
}, vm.$data);
},
setValue(expr, vm, inputVal) {
// 将expr的点语法的字符串转换成数组并设置最有一个值为inputVal
return expr.split(".").reduce((pre, cur, index, arr) => {
if (index === arr.length - 1) {
pre[cur] = inputVal;
}
return pre[cur];
}, vm.$data);
},
getContentValue(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm);
});
},
text(node, expr, vm) {
// expr:msg 学习MVVM实现原理 // <div v-text="person.fav"></div>
let value;
if (expr.indexOf("{{") !== -1) {
// {{person.name}} -- {{person.age}}
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], () => {
this.updater.textUpdater(node, this.getContentValue(expr, vm));
});
return this.getValue(args[1], vm);
});
} else {
new Watcher(vm, expr, (newVal) => {
this.updater.textUpdater(node, newVal);
});
value = this.getValue(expr, vm);
}
this.updater.textUpdater(node, value);
},
html(node, expr, vm) {
const value = this.getValue(expr, vm);
new Watcher(vm, expr, (newVal) => {
this.updater.htmlUpdater(node, newVal);
});
this.updater.htmlUpdater(node, value);
},
model(node, expr, vm) {
const value = this.getValue(expr, vm);
// 绑定更新函数 数据=>视图
new Watcher(vm, expr, (newVal) => {
this.updater.modelUpdater(node, newVal);
});
// 视图=>数据=>视图
node.addEventListener("input", (e) => {
// 设置值
this.setValue(expr, vm, e.target.value);
});
this.updater.modelUpdater(node, value);
},
on(node, expr, vm, eventName) {
const fn = vm.$options.methods && vm.$options.methods[expr];
node.addEventListener(eventName, fn.bind(vm), false);
},
bind(node, expr, vm, attrName) {
const value = this.getValue(expr, vm);
this.updater.bindUpdater(node, attrName, value);
},
// 更新的函数
updater: {
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
modelUpdater(node, value) {
node.value = value;
},
bindUpdater(node, attrName, value) {
node.setAttribute(attrName, value);
}
}
};
class Compiler {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 1.获取文档碎片对象,放入内存中会减少页面的回流和重绘
const fragment = this.node2Fragment(this.el);
// 2.编译模板
this.Compiler(fragment);
// 3.追加子元素到根元素
this.el.appendChild(fragment);
}
/*
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-test="msg"></div>
<div v-html="htmlStr"></div>
<input type="test" v-model="msg">
*/
Compiler(fragment) {
// 获取子节点
const childNodes = fragment.childNodes;
[...childNodes].forEach((child) => {
// console.log(child);
if (this.isElementNode(child)) {
// 是元素节点
// 编译元素节点
// console.log("元素节点", child);
this.compilerElement(child);
} else {
// 文本节点
// 编译文本节点
// console.log("文本节点", child);
this.compilerText(child);
}
if (child.childNodes && child.childNodes.length) {
this.Compiler(child);
}
});
}
compilerElement(node) {
const attributes = node.attributes;
[...attributes].forEach((attr) => {
const { name, value } = attr;
if (this.isDirective(name)) {
// 是一个指令 v-text v-html v-model v-on:click v-bind:src
const [, directive] = name.split("-"); // text html model on:click bind:src
const [dirName, eventName] = directive.split(":"); // text html model on bind
// 更新数据,数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName);
// 删除有指令的标签上的属性
node.removeAttribute("v-" + directive);
} else if (this.isEventName(name)) {
// @click="handleClick"
const [, eventName] = name.split("@");
compileUtil["on"](node, value, this.vm, eventName);
// 删除标签上的绑定的事件
node.removeAttribute("@" + eventName);
} else if (this.isAttrName(name)) {
// :src="imgUrl"
const [, attrName] = name.split(":");
compileUtil["bind"](node, value, this.vm, attrName);
// 删除标签上绑定的属性
node.removeAttribute(":" + attrName);
}
});
}
compilerText(node) {
// {{}} v-text
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil["text"](node, content, this.vm);
}
}
isAttrName(attrName) {
return attrName.startsWith(":");
}
isEventName(attrName) {
return attrName.startsWith("@");
}
isDirective(attrName) {
return attrName.startsWith("v-");
}
node2Fragment(el) {
// 创建文档碎片
const f = document.createDocumentFragment();
let firstChild;
while ((firstChild = el.firstChild)) {
f.appendChild(firstChild);
}
return f;
}
isElementNode(node) {
// 判断是否是元素节点对象
return node.nodeType === 1;
}
}
class MVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 1.实现一个数据观察者
new Observer(this.$data);
// 2.实现一个指令解析器
new Compiler(this.$el, this);
this.proxyData(this.$data);
}
}
proxyData(data) {
for (const key in data) {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newVal) {
data[key] = newVal;
}
});
}
}
}
HTML示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{person.td.msg}}</h3>
<div v-text="msg"></div>
<div v-text="person.fav"></div>
<div v-html="htmlStr"></div>
<input type="test" v-model="person.td.msg">
<button v-on:click="handleClick">点击我on</button>
<button @click="handleClick">点击我@</button>
<img v-bind:src="imgUrl" />
<img :src="imgUrl" />
</div>
<script src="./Observer.js"></script>
<script src="./MVue.js"></script>
<script>
let vm = new MVue({
el: '#app',
data: {
person: {
td: {
msg: '系统td'
},
name: '编程哥',
age: 18,
fav: '看电影'
},
msg: '学习MVVM实现原理',
htmlStr: '大家学习得怎么样',
imgUrl: './logo.png'
},
methods: {
handleClick () {
this.person.name = '学习222';
}
}
})
</script>
</body>
</html>
核心面试题讲解:阐述一下MVVM响应式原理
Vue是采用数据劫持配合发布订阅模式的方式,通过Object.defineProperty()来劫持各个属性的getter和setter,在数据变动时,发布消息给依赖收集器(Dep),去通知观察者(Watcher)做出对应的回调函数,去更新视图。
MVVM作为绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听数据变化,通过Compile来解析编译模板指令,最终利用Watcher建立Observer、Compile之间的桥梁,达到数据变化=>视图更新;视图交互变化=>数据model变更的双向绑定效果
vue源码解析-实现一个基础的MVVM框架的更多相关文章
- 【vuejs深入二】vue源码解析之一,基础源码结构和htmlParse解析器
写在前面 一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残. vuejs是一个优秀的前端mvvm框架,它的易用性和渐进式的理念可以使每一个前端开发人员感到舒服,感到easy.它内 ...
- 【vuejs深入三】vue源码解析之二 htmlParse解析器的实现
写在前面 一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残. 昨天博主分析了一下在vue中,最为基础核心的api,parse函数,它的作用是将vue的模板字符串转换成ast,从而 ...
- Vue源码解析---数据的双向绑定
本文主要抽离Vue源码中数据双向绑定的核心代码,解析Vue是如何实现数据的双向绑定 核心思想是ES5的Object.defineProperty()和发布-订阅模式 整体结构 改造Vue实例中的dat ...
- Vue源码解析之nextTick
Vue源码解析之nextTick 前言 nextTick是Vue的一个核心功能,在Vue内部实现中也经常用到nextTick.但是,很多新手不理解nextTick的原理,甚至不清楚nextTick的作 ...
- 【VUE】Vue 源码解析
Vue 源码解析 Vue 的工作机制 在 new vue() 之后,Vue 会调用进行初始化,会初始化生命周期.事件.props.methods.data.computed和watch等.其中最重要的 ...
- Vue 源码解析:深入响应式原理(上)
原文链接:http://www.imooc.com/article/14466 Vue.js 最显著的功能就是响应式系统,它是一个典型的 MVVM 框架,模型(Model)只是普通的 JavaScri ...
- Vue源码解析(一):入口文件
在学习Vue源码之前,首先要做的一件事情,就是去GitHub上将Vue源码clone下来,目前我这里分析的Vue版本是V2.5.21,下面开始分析: 一.源码的目录结构: Vue的源码都在src目录下 ...
- Vue源码解析之数组变异
力有不逮的对象 众所周知,在 Vue 中,直接修改对象属性的值无法触发响应式.当你直接修改了对象属性的值,你会发现,只有数据改了,但是页面内容并没有改变. 这是什么原因? 原因在于: Vue 的响应式 ...
- Fresco源码解析 - 创建一个ImagePipeline(一)
在Fresco源码解析 - 初始化过程分析章节中, 我们分析了Fresco的初始化过程,两个initialize方法中都用到了 ImagePipelineFactory类. ImagePipeline ...
- vue源码解析之observe
一. vue文档中有"由于 JavaScript 的限制,Vue 不能检测以下数组的变动",是否真是由于JavaScript的限制,还是出于其他原因考虑 当你利用索引直接设置一个数 ...
随机推荐
- 合合TextIn - 大模型加速器
TextIn是合合信息旗下的智能文档处理平台,在智能文字识别领域深耕17年,致力于图像处理.模式识别.神经网络.深度学习.STR.NLP.知识图谱等人工智能领域研究.凭借行业领先的技术实力,为扫描全能 ...
- PHP函数http_build_query使用详解
什么是http_build_query? 使用给出的关联(或下标)数组生成一个经过 URL-encode 的请求字符串.参数 formdata 可以是数组或包含属性的对象.一个 formdata 数组 ...
- oeasy教您玩转vim - 30 - # 屏位行号
屏位行号 回忆上节课内容 上次我们主要讲的翻页 :set scrolloff=2 控制上下留天留地 上下翻页(这个最常用) ctrl + f 向下一屏 ctrl + b 向上一屏 上下移屏一行 c ...
- 如何让 MGR 不从 Primary 节点克隆数据?
问题 MGR 中,新节点在加入时,为了与组内其它节点的数据保持一致,它会首先经历一个分布式恢复阶段.在这个阶段,新节点会随机选择组内一个节点(Donor)来同步差异数据. 在 MySQL 8.0.17 ...
- Apachepoi读写Excel实例
/* * 通过poi创建Excel并写入内容 * */ public static void write() throws IOException { //在内存中创建excel XSSFWorkbo ...
- Hyperledger Fabric 2.x 环境搭建
一.说明 区块链网络的核心是分布式账本,在这个账本中记录了网络中发生的所有交易信息. Hyperledger Fabric是一个是开源的,企业级的,带权限的分布式账本解决方案的平台.Hyperledg ...
- linux中grep的用法详解
linux中grep的用法详解 grep (global search regular expression(RE) and print out the line,全面搜索正则表达式并把行打印出来)是 ...
- 一款基于Fluent设计风格、现代化的WPF UI控件库
前言 今天大姚给大家分享一款基于Fluent设计风格.开源(MIT License).现代化的WPF UI控件库,它提供直观的设计.主题.导航和全新的沉浸式控件,全部都是原生且无缝地集成在一起:WPF ...
- 【RabbitMQ】07 SpringBoot整合RabbitMQ
生产者和消费者的依赖基本一致: 注意工程名称不是一样的 <?xml version="1.0" encoding="UTF-8"?> <pro ...
- 【CentOS】 8版本 Cannot update read-only repo 问题
GUI界面应用市场无法访问 https://blog.csdn.net/hm0406120201/article/details/104553205/