Vue源码分析之实现一个简易版的Vue
目标
参考 https://cn.vuejs.org/v2/guide/reactivity.html
使用 Typescript
编写简易版的 vue 实现数据的响应式和基本的视图渲染,以及双向绑定功能。
测试代码中,编写vue.js是本篇的重点,基本使用方法与常规的Vue一样:
<div id='app'>
<div>{{ person.name }}</div>
<div>{{ count }}</div>
<div v-text='person.name'></div>
<input type='text' v-model='msg' />
<input type='text' v-model='person.name'/>
</div>
<script src='vue.js'></script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: 'Hello vue',
count: 100,
person: { name: 'Tim' },
}
});
vm.msg = 'Hello world';
console.log(vm);
//模拟数据更新
setTimeout(() => { vm.person.name = 'Goooooood'; }, 1000);
<script>
页面渲染结果如下
实现的简易Vue需要完成以下功能
- 可以解析插值表达式,如 {{person.name}}
- 可以解析内置指令,如
v-text
- 可以双向绑定数据,如
v-model
- 数据更新视图随之更新
Vue当中有以下重要的组件
- Vue初始化时需要通过
Object.defineProperty
代理 Vue.data 的数据,以便方便操作, 即访问Vue.personProp
等同于访问Vue.data.personProp
- 通过
Observer
对Vue.data
里所有的数据及其子节点(递归)都进行捕捉,通过getter
setter
实现数据双向绑定 - 初始
Observer
在getter
中收集依赖(watcher观察者)在setter
中发送通知notify
Watcher
中注册依赖Dep
基层Vue
Vue 数据结构,这里只关注下面三个属性
字段 | 说明 |
---|---|
$options | 存放构造时传入的配置参数 |
$data | 存放数据 |
$el | 存放需要渲染的元素 |
实现Vue时,需要完成以下功能:
- 负责接收初始化参数
options
- 负责把data属性注入到vue,并转换成
getter/setter
- 负责调用
observer
监听所有属性变化 - 负责调用
compiler
解析指令和差值表达式
类型接口定以
为保持灵活性,这里直接用any类型
interface VueData {
[key: string]: any,
}
interface VueOptions {
data: VueData;
el: string | Element;
}
interface Vue {
[key: string]: any,
}
Vue实现代码
class Vue {
public $options: VueOptions;
public $data: VueData;
public $el: Element | null;
public constructor(options: VueOptions) {
this.$options = options;
this.$data = options.data || {};
if (typeof options.el == 'string') {
this.$el = document.querySelector(options.el);
} else {
this.$el = options.el;
}
if (!this.$el) {
throw Error(`cannot find element by selector ${options.el}`);
return;
}
this._proxyData(this.$data);
}
//生成代理,通过直接读写vue属性来代理vue.$data的数据,提高便利性
//vue[key] => vue.data[key]
private _proxyData(data: VueData) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key];
},
set(newVal) {
if (newVal == data[key]) {
return;
}
data[key] = newVal;
}
})
})
}
}
- 对于Vue的元数据均以
$
开头表示,因为访问Vue.data
会被代理成Vue.$data.data
,即注入属性与元属性进行区分 - $el 可以为选择器或Dom,但最终需要转成Dom,若不存在Dom抛出错误
- _porxyData,下划线开头为私有属性或方法,此方法可以将 $data 属性注入到vue中
- enumerable 为可枚举, configurable 为可配置,如重定以和删除属性
- setter 中,如果数据没有发生变化则return,发生变化更新 $data
简单测试一下
let vm = new Vue({
el: '#app',
data: {
msg: 'Hello vue',
count: 100,
person: { name: 'Tim' },
}
});
上图中颜色比较幽暗的,表示注入到Vue的属性已成功设置了getter和setter
Observer
- 负责把data选项中的属性转换成响应式数据
- data中某个属性的值也是对象,需要递归转换成响应式
- 数据发生变化时发送通知
Observer 实现代码
class Observer {
constructor(data) {
this.walk(data);
}
walk(data) {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
});
}
defineReactive(obj, key, val) {
//递归处理成响应式
if (typeof val === 'object') {
this.walk(val);
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
//注意:这里val不可改成obj[key],会无限递归直至堆栈溢出
return val;
},
set: (newVal) => {
if (newVal == val) {
return;
}
//注意:这里newVal不可改成obj[key],会触发 getter
val = newVal;
if (typeof newVal == 'object') {
this.walk(newVal);
}
}
});
}
}
walk方法
用于遍历$data属性,传递给defineReactive
做响应式处理defineReactive
如果值为对象则递归调用walk
,如果值为原生数据则设置getter和setter
说明
有的人可能会觉得 defineReactive(data, key, val)
中的形参val多此一举,因为 data[key] == val
也可以获取
其实不然,假设我们只传递了 defineReactive(data, key)
那么在 defineProperty
中 getter 和 setter 要是使用
data[key]
的方式访问值的话,在getter会无限触发get()
, 而在setter会触发一次get()
,因为 data[key]
就是触发getter的方式
另外defineProperty
内部引用了 defineReactive
的参数val,这里会产生闭包空间存储 val的值
defineReactive
Observer 引用
在上面编写的 Vue.constructor
中添加Observer的引用,并传入$data
//Vue.constructor
public constructor(options: VueOptions) {
this.$options = options;
this.$data = options.data || {};
if (typeof options.el == 'string') {
this.$el = document.querySelector(options.el);
} else {
this.$el = options.el;
}
if (!this.$el) {
throw Error(`cannot find element by selector ${options.el}`);
return;
}
this._proxyData(this.$data);
new Observer(this.$data); //新增此行
}
测试
重新打印vm可以看到 $data 里的成员也有getter和setter方法了
Compiler
- 负责编译模板,解析指令
v-xxx
和插值表达式{{var}}
- 负责页面首次渲染
- 当数据发生变化时,重新渲染视图
注意,为简化代码,这里的插值表达式,不处理复杂情况,只处理单一的变量读取
如 {{count + 2}}
=> 不进行处理
如 {{person.name}}
=> 可以处理
Util 辅助工具
为方便操作,我们需要提前编写几个简单的函数功能,并封装到 Util 类中静态方法里
class Util {
static isPrimitive(s: any): s is (string | number) {
return typeof s === 'string' || typeof s === 'number';
}
static isHTMLInputElement(element: Element): element is HTMLInputElement {
return (<HTMLInputElement>element).tagName === 'INPUT';
}
//处理无法引用 vm.$data['person.name'] 情况
static getLeafData(obj: Object, key: string): any {
let textData: Array<any> | Object | String | Number = obj;
if (key.indexOf('.') >= 0) {
let keys = key.split('.');
for (let k of keys) {
textData = textData[k];
}
} else {
textData = obj[key];
}
return textData;
}
static setLeafData(obj: Object, key: string, value: any): void {
if (key.indexOf('.') >= 0) {
let keys = key.split('.');
for (let i = 0; i < keys.length; i++) {
let k = keys[i];
if (i == keys.length - 1) {
obj[k] = value;
} else {
obj = obj[k];
}
}
} else {
if (obj[key]){
obj[key] = value;
}
}
}
}
- isPrimitive
该函数用于判断变量是否为原生类型(string or number)
- isHTMLInputElement
该函数用于判断元素是否为Input元素,用于后面处理 v-model
指令的双向绑定数据,默认:value
@input
- getLeafData
因为key可能为 person.name
, 如果直接中括号访问对象属性如 obj['person.name']
无法等同于 obj.person.name
该函数如果传递的键key中,若不包含点.
,则直接返回 obj[key]。 若包含,则解析处理返回 obj.key1.key2.key3
- setLeafData
同上, key为person.name
时,设置 obj.person.name = value
,否则设置 obj.key = value
Complier 实现代码
class Compiler {
public el: Element | null;
public vm: Vue;
constructor(vm: Vue) {
this.el = vm.$el,
this.vm = vm;
if (this.el) {
this.compile(this.el);
}
}
compile(el: Element) {
let childNodes = el.childNodes;
Array.from(childNodes).forEach((node: Element) => {
if (this.isTextNode(node)) {
this.compileText(node);
} else if (this.isElementNode(node)) {
this.compileElement(node);
}
//递归处理孩子nodes
if (node.childNodes && node.childNodes.length !== 0) {
this.compile(node);
}
})
}
//解析插值表达式 {{text}}
compileText(node: Node) {
let pattern: RegExpExecArray | null;
if (node.textContent && (pattern = /\{\{(.*?)\}\}/.exec(node.textContent))) {
let key = pattern[1].trim();
if (key in this.vm.$data && Util.isPrimitive(this.vm.$data[key])) {
node.textContent = this.vm.$data[key];
}
}
}
//解析 v-attr 指令
compileElement(node: Element) {
Array.from(node.attributes).forEach((attr) => {
if (this.isDirective(attr.name)) {
let directive: string = attr.name.substr(2);
let value = attr.value;
let processer: Function = this[directive + 'Updater'];
if (processer) {
processer.call(this, node, value);
}
}
})
}
//处理 v-model 指令
modelUpdater(node: Element, key: string) {
if (Util.isHTMLInputElement(node)) {
let value = Util.getLeafData(this.vm.$data, key);
if (Util.isPrimitive(value)) {
node.value = value.toString();
}
node.addEventListener('input', () => {
Util.setLeafData(this.vm.$data, key, node.value);
console.log(this.vm.$data);
})
}
}
//处理 v-text 指令
textUpdater(node: Element, key: string) {
let value = Util.getLeafData(this.vm.$data, key);
if (Util.isPrimitive(value)) {
node.textContent = value.toString();
}
}
//属性名包含 v-前缀代表指令
isDirective(attrName: string) {
return attrName.startsWith('v-');
}
//nodeType为3属于文本节点
isTextNode(node: Node) {
return node.nodeType == 3;
}
//nodeType为1属于元素节点
isElementNode(node: Node) {
return node.nodeType == 1;
}
}
- compile
用于首次渲染传入的 div#app
元素, 遍历所有第一层子节点,判断子节点nodeType
属于文本
还是元素
若属于 文本
则调用 compileText
进行处理, 若属于 元素
则调用 compileElement
进行处理。
另外如果子节点的孩子节点 childNodes.length != 0
则递归调用 compile(node)
- compileText
用于渲染插值表达式,使用正则 \{\{(.*?)\}\}
检查是否包含插值表达式,提取括号内变量名
通过工具函数 Utils.getLeafData(vm.$data, key)
尝试读取 vm.$data[key]
和 vm.$data.key1.key2
的值
如果能读取成功,则渲染到视图当中 node.textContent = this.vm.$data[key];
- compileElement
用于处理内置v-指令,通过 node.attributes
获取所有元素指令,Array.from()
可以使NamedNodeMap
转成可遍历的数组
获取属性名,判断是否有 v-
前缀,若存在则进行解析成函数,解析规则如下
- v-text 解析的函数名为
textUpdater()
- v-model 解析函数名为
modelUpdater()
可以通过尝试方法获取,如 this[directive + "Updater"]
若不为 undefined 说明指令处理函数是存在的
最后通过 call 调用,使得 this 指向 Compiler类实例
- textUpdater
与 compileText 类似,尝试读取变量并渲染到Dom中
- modelUpdate
除了尝试读取变量并渲染到Dom中,还需要设置 @input
函数监听视图的变化来更新数据
node.addEventListener('input', () => {
Util.setLeafData(this.vm.$data, key, node.value);
})
Complier 实例化引用
在 Vue.constructor 中引用 Compiler 进行首次页面渲染
//Vue.constructor
public constructor(options: VueOptions) {
this.$options = options;
this.$data = options.data || {};
if (typeof options.el == 'string') {
this.$el = document.querySelector(options.el);
} else {
this.$el = options.el;
}
if (!this.$el) {
throw Error(`cannot find element by selector ${options.el}`);
return;
}
this._proxyData(this.$data);
new Observer(this.$data);
new Compiler(this); //新增此行
}
测试代码
<div id='app'>
<div>{{ person.name }}</div>
<div>{{ count }}</div>
<div v-text='person.name'></div>
<input type='text' v-model='msg' />
<input type='text' v-model='person.name'/>
</div>
<script src='vue.js'></script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: 'Hello vue',
count: 100,
person: { name: 'tim' },
}
})
</scirpt>
渲染结果
至此完成了初始化数据驱动和渲染功能,我们修改 input 表单里的元素内容是会通过 @input
动态更新$data对应绑定v-model
的数据
但是此时我们在控制台中修改 vm.msg = 'Gooooood' ,视图是不会有响应式变化的,因此下面将通过Watcher
和 Dep
观察者模式来实现响应式处理
Watcher 与 Dep
Dep(Dependency)
实现功能:
- 收集依赖,添加观察者(Watcher)
- 通知所有的观察者 (notify)
Dep 实现代码
class Dep {
static target: Watcher | null;
watcherList: Watcher[] = [];
addWatcher(watcher: Watcher) {
this.watcherList.push(watcher);
}
notify() {
this.watcherList.forEach((watcher) => {
watcher.update();
})
}
}
Watcher
实现功能:
- 当变化触发依赖时,Dep通知Watcher进行更新视图
- 当自身实例化时,向Dep中添加自己
Watcher 实现代码
每个观察者Watcher都必须包含 update方法,用于描述数据变动时如何响应式渲染到页面中
class Watcher {
public vm: Vue;
public cb: Function;
public key: string;
public oldValue: any;
constructor(vm: Vue, key: string, cb: Function) {
this.vm = vm;
this.key = key;
this.cb = cb;
//注册依赖
Dep.target = this;
//访问属性触发getter,收集target
this.oldValue = Util.getLeafData(vm.$data, key);
//防止重复添加
Dep.target = null;
}
update() {
let newVal = Util.getLeafData(this.vm.$data, this.key);
if (this.oldValue == newVal) {
return;
}
this.cb(newVal);
}
}
修改 Observer.defineReactive
对于$data
中每一个属性,都对应着一个 Dep,因此我们需要在$data初始化响应式时创建Dep实例,在getter 中收集观察者Dep.addWatcher()
, 在 setter 中通知观察者 Dep.notify()
defineReactive(obj: VueData, key: string, val: any) {
let dep = new Dep(); //新增此行,每个$data中的属性都对应一个Dep实例化
//如果data值的为对象,递归walk
if (typeof val === 'object') {
this.walk(val);
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addWatcher(Dep.target); //检查是否有Watcher,收集依赖的观察者
//此处不能返回 obj[key] 会无限递归触发get
console.log('getter')
return val;
},
set: (newVal) => {
if (newVal == val) {
return;
}
val = newVal;
if (typeof newVal == 'object') {
this.walk(newVal)
}
//发送通知
dep.notify(); //新增此行,$data中属性发送变动时发送通知
}
});
}
修改 Compiler类,下面几个方法均添加实例化Watcher
每个视图对应一个Watcher,以key为关键字触发响应的Dep,并通过getter将Watcher添加至Dep中
class Compiler {
//插值表达式
compileText(node: Node) {
let pattern: RegExpExecArray | null;
if (node.textContent && (pattern = /\{\{(.*?)\}\}/.exec(node.textContent))) {
let key = pattern[1].trim();
let value = Util.getLeafData(this.vm.$data, key);
if (Util.isPrimitive(value)) {
node.textContent = value.toString();
}
new Watcher(this.vm, key, (newVal: string) => { node.textContent = newVal; }); //新增此行
}
}
//v-model
modelUpdater(node: Element, key: string) {
if (Util.isHTMLInputElement(node)) {
let value = Util.getLeafData(this.vm.$data, key);
if (Util.isPrimitive(value)) {
node.value = value.toString();
}
node.addEventListener('input', () => {
Util.setLeafData(this.vm.$data, key, node.value);
console.log(this.vm.$data);
})
new Watcher(this.vm, key, (newVal: string) => { node.value = newVal; }); //新增此行
}
}
//v-text
textUpdater(node: Element, key: string) {
let value = Util.getLeafData(this.vm.$data, key);
if (Util.isPrimitive(value)) {
node.textContent = value.toString();
}
new Watcher(this.vm, key, (newVal: string) => { node.textContent = newVal; }); //新增此行
}
}
至此本篇目的已经完成,实现简易版Vue的响应式数据渲染视图和双向绑定,下面是完整 ts代码和测试代码
实现简易版Vue完整代码
//vue.js
interface VueData {
[key: string]: any,
}
interface VueOptions {
data: VueData;
el: string | Element;
}
interface Vue {
[key: string]: any,
}
class Util {
static isPrimitive(s: any): s is (string | number) {
return typeof s === 'string' || typeof s === 'number';
}
static isHTMLInputElement(element: Element): element is HTMLInputElement {
return (<HTMLInputElement>element).tagName === 'INPUT';
}
//处理无法引用 vm.$data['person.name'] 情况
static getLeafData(obj: Object, key: string): any {
let textData: Array<any> | Object | String | Number = obj;
if (key.indexOf('.') >= 0) {
let keys = key.split('.');
for (let k of keys) {
textData = textData[k];
}
} else {
textData = obj[key];
}
return textData;
}
static setLeafData(obj: Object, key: string, value: any): void {
if (key.indexOf('.') >= 0) {
let keys = key.split('.');
for (let i = 0; i < keys.length; i++) {
let k = keys[i];
if (i == keys.length - 1) {
obj[k] = value;
} else {
obj = obj[k];
}
}
} else {
if (obj[key]){
obj[key] = value;
}
}
}
}
class Vue {
public $options: VueOptions;
public $data: VueData;
public $el: Element | null;
public constructor(options: VueOptions) {
this.$options = options;
this.$data = options.data || {};
if (typeof options.el == 'string') {
this.$el = document.querySelector(options.el);
} else {
this.$el = options.el;
}
if (!this.$el) {
throw Error(`cannot find element by selector ${options.el}`);
return;
}
this._proxyData(this.$data);
new Observer(this.$data);
new Compiler(this);
}
//生成代理,通过直接读写vue属性来代理vue.$data的数据,提高便利性
//vue[key] => vue.data[key]
private _proxyData(data: VueData) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key];
},
set(newVal) {
if (newVal == data[key]) {
return;
}
data[key] = newVal;
}
})
})
}
}
class Observer {
constructor(data: VueData) {
this.walk(data);
}
walk(data: VueData) {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
});
}
//观察vue.data的变化,并同步渲染至视图中
defineReactive(obj: VueData, key: string, val: any) {
let dep = new Dep();
//如果data值的为对象,递归walk
if (typeof val === 'object') {
this.walk(val);
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
//收集依赖
Dep.target && dep.addWatcher(Dep.target);
//此处不能返回 obj[key] 会无限递归触发get
console.log('getter')
return val;
},
set: (newVal) => {
if (newVal == val) {
return;
}
val = newVal;
if (typeof newVal == 'object') {
this.walk(newVal)
}
//发送通知
dep.notify();
}
});
}
}
class Compiler {
public el: Element | null;
public vm: Vue;
constructor(vm: Vue) {
this.el = vm.$el,
this.vm = vm;
if (this.el) {
this.compile(this.el);
}
}
compile(el: Element) {
let childNodes = el.childNodes;
Array.from(childNodes).forEach((node: Element) => {
if (this.isTextNode(node)) {
this.compileText(node);
} else if (this.isElementNode(node)) {
this.compileElement(node);
}
//递归处理孩子nodes
if (node.childNodes && node.childNodes.length !== 0) {
this.compile(node);
}
})
}
// {{text}}
compileText(node: Node) {
let pattern: RegExpExecArray | null;
if (node.textContent && (pattern = /\{\{(.*?)\}\}/.exec(node.textContent))) {
let key = pattern[1].trim();
let value = Util.getLeafData(this.vm.$data, key);
if (Util.isPrimitive(value)) {
node.textContent = value.toString();
}
new Watcher(this.vm, key, (newVal: string) => { node.textContent = newVal; })
}
}
//v-attr
compileElement(node: Element) {
Array.from(node.attributes).forEach((attr) => {
if (this.isDirective(attr.name)) {
let directive: string = attr.name.substr(2);
let value = attr.value;
let processer: Function = this[directive + 'Updater'];
if (processer) {
processer.call(this, node, value);
}
}
})
}
//v-model
modelUpdater(node: Element, key: string) {
if (Util.isHTMLInputElement(node)) {
let value = Util.getLeafData(this.vm.$data, key);
if (Util.isPrimitive(value)) {
node.value = value.toString();
}
node.addEventListener('input', () => {
Util.setLeafData(this.vm.$data, key, node.value);
console.log(this.vm.$data);
})
new Watcher(this.vm, key, (newVal: string) => { node.value = newVal; })
}
}
//v-text
textUpdater(node: Element, key: string) {
let value = Util.getLeafData(this.vm.$data, key);
if (Util.isPrimitive(value)) {
node.textContent = value.toString();
}
new Watcher(this.vm, key, (newVal: string) => {
node.textContent = newVal;
});
}
isDirective(attrName: string) {
return attrName.startsWith('v-');
}
isTextNode(node: Node) {
return node.nodeType == 3;
}
isElementNode(node: Node) {
return node.nodeType == 1;
}
}
class Dep {
static target: Watcher | null;
watcherList: Watcher[] = [];
addWatcher(watcher: Watcher) {
this.watcherList.push(watcher);
}
notify() {
this.watcherList.forEach((watcher) => {
watcher.update();
})
}
}
class Watcher {
public vm: Vue;
public cb: Function;
public key: string;
public oldValue: any;
constructor(vm: Vue, key: string, cb: Function) {
this.vm = vm;
this.key = key;
this.cb = cb;
//注册依赖
Dep.target = this;
//访问属性触发getter,收集target
this.oldValue = Util.getLeafData(vm.$data, key);
//防止重复添加
Dep.target = null;
}
update() {
let newVal = Util.getLeafData(this.vm.$data, this.key);
if (this.oldValue == newVal) {
return;
}
this.cb(newVal);
}
}
测试代码
<div id='app'>
<div>{{ person.name }}</div>
<div>{{ count }}</div>
<div v-text='person.name'></div>
<input type='text' v-model='msg' />
<input type='text' v-model='person.name'/>
</div>
<script src='dist/main.js'></script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: 'Hello vue',
count: 100,
person: { name: 'tim' },
}
})
// vm.msg = 'Hello world';
console.log(vm);
setTimeout(() => { vm.person.name = 'Goooooood' }, 1000);
</scirpt>
Vue源码分析之实现一个简易版的Vue的更多相关文章
- 前端Vue 源码分析-逻辑层
Vue 源码分析-逻辑层 预期的效果: 监听input的输入,input在输入的时候,会触发 watch与computed函数,并且会更新原始的input的数值.所以直接跟input相关的处理就有3处 ...
- [Vue源码分析] v-model实现原理
最近小组有个关于vue源码分析的分享会,提前准备一下… 前言:我们都知道使用v-model可以实现数据的双向绑定,及实现数据的变化驱动dom的更新,dom的更新影响数据的变化.那么v-model是怎么 ...
- Vue源码分析(二) : Vue实例挂载
Vue源码分析(二) : Vue实例挂载 author: @TiffanysBear 实例挂载主要是 $mount 方法的实现,在 src/platforms/web/entry-runtime-wi ...
- Vue源码分析(一) : new Vue() 做了什么
Vue源码分析(一) : new Vue() 做了什么 author: @TiffanysBear 在了解new Vue做了什么之前,我们先对Vue源码做一些基础的了解,如果你已经对基础的源码目录设计 ...
- vue 快速入门 系列 —— 侦测数据的变化 - [vue 源码分析]
其他章节请看: vue 快速入门 系列 侦测数据的变化 - [vue 源码分析] 本文将 vue 中与数据侦测相关的源码摘了出来,配合上文(侦测数据的变化 - [基本实现]) 一起来分析一下 vue ...
- 使用 js 实现一个简易版的 vue 框架
使用 js 实现一个简易版的 vue 框架 具有挑战性的前端面试题 refs https://www.infoq.cn/article/0NUjpxGrqRX6Ss01BLLE xgqfrms 201 ...
- vue源码分析—Vue.js 源码目录设计
Vue.js 的源码都在 src 目录下,其目录结构如下 src ├── compiler # 编译相关 ├── core # 核心代码 ├── platforms # 不同平台的支持 ├── ser ...
- vue源码分析—Vue.js 源码构建
Vue.js 源码是基于 Rollup 构建的,它的构建相关配置都在 scripts 目录下.(Rollup 中文网和英文网) 构建脚本 通常一个基于 NPM 托管的项目都会有一个 package.j ...
- vue源码分析—认识 Flow
认识 Flow Flow 是 facebook 出品的 JavaScript 静态类型检查⼯具.Vue.js 的源码利⽤了 Flow 做了静态类型检查, 所以了解 Flow 有助于我们阅读源码 Flo ...
随机推荐
- nrm安装使用(mac)
在开发工作中时常有需要切换npm源的需求以及更换node版本的情况,这两种情况都有对应的管理器来使用 一.nrm nrm是一个npm源管理工具,使用它可以快速切换npm源. 1.nrm安装(全局安装) ...
- jmeter调试元件Debug Sampler的使用
@@@@@@@@@@@@@@@ 活在当下 今天记录一下jmeter调试工具Debug Sampler的心得,调试对于计算机从业人员来说是家常便饭,jmeter虽然代码不多,但是也需要调试,那么如何进行 ...
- STL入门--sort,lower_bound,upper_bound,binary_search及常见错误
首先,先定义数组 int a[10]; 这是今天的主角. 这四个函数都是在数组上操作的 注意要包含头文件 #include<algorithm> sort: sort(a,a+10) 对十 ...
- Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform!
原文链接:https://blog.csdn.net/u012700515/article/details/56009429 Maven 打包时有标题中警告,需要在pom.xml文件中添加 <p ...
- PHP hypot() 函数
实例 计算不同的直角三角形的斜边长度: <?phpecho hypot(3,4) . "<br>";echo hypot(4,6) . "<br& ...
- 通过缓存Cache记录命中率
import org.apache.juli.logging.Log; /** * 通过此Cache记录命中率 * @author Administrator * */ public class Lo ...
- Spring Boot必备技能之Starter自定义
本文摘自于<Spring Cloud微服务 入门 实战与进阶>一书. 作者:尹吉欢 Spring Boot的方便体现在简化了很多繁琐的配置,对开发人员来说是一个福音,通过引入各种Spri ...
- [NOI2012]随机数生成器【矩阵快速幂】
NOI2012 随机数生成器 题目描述 栋栋最近迷上了随机算法,而随机数是生成随机算法的基础.栋栋准备使用线性同余法(Linear Congruential Method)来生成一个随机数列,这种方法 ...
- Docker入坑指南之EXEC
容器启动之后,如果我们需要进入容器内修改配置,比如mysql修改启动配置 我们启动的附加参数是不是shell,这个时候就可以用docker exec了,docker除了对image参数以外,大部分命令 ...
- Unity目录结构设置
摄像机 Main Camera 跟随主角移动,不看 UI 剧情摄像机 当进入剧情时,可以关闭 main camera,启用剧情摄像机,不看 UI UI 摄像机 看 UI Unity编辑器常用的sett ...