为什么写这篇vue的分析文章?

对于天资愚钝的前端(我)来说,阅读源码是件不容易的事情,毕竟有时候看源码分析的文章都看不懂。每次看到大佬们用了1~2年的vue就能掌握原理,甚至精通源码,再看看自己用了好几年都还在基本的使用阶段,心中总是羞愧不已。如果一直满足于基本的业务开发,怕是得在初级水平一直待下去了吧。所以希望在学习源码的同时记录知识点,可以让自己的理解和记忆更加深刻,也方便将来查阅。

目录结构

本文以vue的第一次 commit a879ec06 作为分析版本

├── build
│   └── build.js // `rollup` 打包配置
├── dist
│   └── vue.js
├── package.json
├── src // vue源码目录
│   ├── compiler // 将vue-template转化为render函数
│   │   ├── codegen.js // 递归ast提取指令,分类attr,style,class,并生成render函数
│   │   ├── html-parser.js // 通过正则匹配将html字符串转化为ast
│   │   ├── index.js // compile主入口
│   │   └── text-parser.js // 编译{{}}
│   ├── config.js // 对于vue的全局配置文件
│   ├── index.js // 主入口
│   ├── index.umd.js // 未知(应该是umd格式的主入口)
│   ├── instance // vue实例函数
│   │   └── index.js // 包含了vue实例的初始化,compile,data代理,methods代理,watch数据,执行渲染
│   ├── observer // 数据订阅发布的实现
│   │   ├── array.js // 实现array变异方法,$set $remove 实现
│   │   ├── batcher.js // watch执行队列的收集,执行
│   │   ├── dep.js // 订阅中心实现
│   │   ├── index.js // 数据劫持的实现,收集订阅者
│   │   └── watcher.js // watch实现,订阅者
│   ├── util // 工具函数
│   │   ├── component.js
│   │   ├── debug.js
│   │   ├── dom.js
│   │   ├── env.js // nexttick实现
│   │   ├── index.js
│   │   ├── lang.js
│   │   └── options.js
│   └── vdom
│   ├── dom.js // dom操作的封装
│   ├── h.js // 节点数据分析(元素节点,文本节点)
│   ├── index.js // vdom主入口
│   ├── modules // 不同属性处理函数
│   │   ├── attrs.js // 普通attr属性处理
│   │   ├── class.js // class处理
│   │   ├── events.js // event处理
│   │   ├── props.js // props处理
│   │   └── style.js // style处理
│   ├── patch.js // node树的渲染,包括节点的加减更新处理,及对应attr的处理
│   └── vnode.js // 返回最终的节点数据
└── webpack.config.js // webpack配置

从template到html的过程分析

我们的代码是从new Vue()开始的,Vue的构造函数如下:

constructor (options) {
// options就是我们对于vue的配置
this.$options = options
this._data = options.data
// 获取元素html,即template
const el = this._el = document.querySelector(options.el)
// 编译模板 -> render函数
const render = compile(getOuterHTML(el))
this._el.innerHTML = ''
// 实例代理data数据
Object.keys(options.data).forEach(key => this._proxy(key))
// 将method的this指向实例
if (options.methods) {
Object.keys(options.methods).forEach(key => {
this[key] = options.methods[key].bind(this)
})
}
// 数据观察
this._ob = observe(options.data)
this._watchers = []
// watch数据及更新
this._watcher = new Watcher(this, render, this._update)
// 渲染函数
this._update(this._watcher.value)
}

当我们初始化项目的时候,即会执行构造函数,该函数向我们展示了vue初始化的主线:编译template字符串 => 代理data数据/methods的this绑定 => 数据观察 => 建立watch及更新渲染

1. 编译template字符串

const render = compile(getOuterHTML(el))

其中compile的实现如下:

export function compile (html) {
html = html.trim()
// 对编译结果缓存
const hit = cache[html]
// parse函数在parse-html中定义,其作用是把我们获取的html字符串通过正则匹配转化为ast,输出如下 {tag: 'div', attrs: {}, children: []}
return hit || (cache[html] = generate(parse(html)))
}

接下来看看generate函数,ast通过genElement的转化生成了构建节点html的函数,在genElement将对if for 等进行判断并转化( 指令的具体处理将在后面做分析,先关注主流程代码),最后都会执行genData函数

// 生成节点主函数
export function generate (ast) {
const code = genElement(ast)
// 执行code代码,并将this作为code的global对象。所以我们在template中的变量将指向为实例的属性 {{name}} -> this.name
return new Function (`with (this) { return ${code}}`)
} // 解析单个节点 -> genData
function genElement (el, key) {
let exp
// 指令的实现,实际就是在模板编译时实现的
if (exp = getAttr(el, 'v-for')) {
return genFor(el, exp)
} else if (exp = getAttr(el, 'v-if')) {
return genIf(el, exp)
} else if (el.tag === 'template') {
return genChildren(el)
} else {
// 分别为 tag 自身属性 子节点数据
return `__h__('${ el.tag }', ${ genData(el, key) }, ${ genChildren(el) })`
}
}

我们可以看看在genData中都做了什么。上面的parse函数将html字符串转化为ast,而在genData中则将节点的attrs数据进一步处理,例如class -> renderClass style class props attr 分类。在这里可以看到 bind 指令的实现,即通过正则匹配 : 和 bind,如果匹配则把相应的 value值转化为 (value) 的形式,而不匹配的则通过JSON.stringify()转化为字符串('value')。最后输出attrs(key-value),在这里得到的对象是字符串形式的,例如(value)等也仅仅是将变量名,而在generate中通过new Function进一步通过(this.value)得到变量值。

function genData (el, key) {
// 没有属性返回空对象
if (!el.attrs.length) {
return '{}'
}
// key
let data = key ? `{key:${ key },` : `{`
// class处理
if (el.attrsMap[':class'] || el.attrsMap['class']) {
data += `class: _renderClass(${ el.attrsMap[':class'] }, "${ el.attrsMap['class'] || '' }"),`
}
// attrs
let attrs = `attrs:{`
let props = `props:{`
let hasAttrs = false
let hasProps = false
for (let i = 0, l = el.attrs.length; i < l; i++) {
let attr = el.attrs[i]
let name = attr.name
// bind属性
if (bindRE.test(name)) {
name = name.replace(bindRE, '')
if (name === 'class') {
continue
// style处理
} else if (name === 'style') {
data += `style: ${ attr.value },`
// props属性处理
} else if (mustUsePropsRE.test(name)) {
hasProps = true
props += `"${ name }": (${ attr.value }),`
// 其他属性
} else {
hasAttrs = true
attrs += `"${ name }": (${ attr.value }),`
}
// on指令,未实现
} else if (onRE.test(name)) {
name = name.replace(onRE, '')
// 普通属性
} else if (name !== 'class') {
hasAttrs = true
attrs += `"${ name }": (${ JSON.stringify(attr.value) }),`
}
}
if (hasAttrs) {
data += attrs.slice(0, -1) + '},'
}
if (hasProps) {
data += props.slice(0, -1) + '},'
}
return data.replace(/,$/, '') + '}'
}

而对于genChildren,我们可以猜到就是对ast中的children进行遍历调用genElement,实际上在这里还包括了对文本节点的处理。

// 遍历子节点 -> genNode
function genChildren (el) {
if (!el.children.length) {
return 'undefined'
}
// 对children扁平化处理
return '__flatten__([' + el.children.map(genNode).join(',') + '])'
} function genNode (node) {
if (node.tag) {
return genElement(node)
} else {
return genText(node)
}
} // 解析{{}}
function genText (text) {
if (text === ' ') {
return '" "'
} else {
const exp = parseText(text)
if (exp) {
return 'String(' + escapeNewlines(exp) + ')'
} else {
return escapeNewlines(JSON.stringify(text))
}
}
}

genText处理了text及换行,在parseText函数中利用正则解析{{}},输出字符串(value)形式的字符串。

现在我们再看看__h__('${ el.tag }', ${ genData(el, key) }, ${ genChildren(el) })__h__函数

// h 函数利用上面得到的节点数据得到 vNode对象 => 虚拟dom
export default function h (tag, b, c) {
var data = {}, children, text, i
if (arguments.length === 3) {
data = b
if (isArray(c)) { children = c }
else if (isPrimitive(c)) { text = c }
} else if (arguments.length === 2) {
if (isArray(b)) { children = b }
else if (isPrimitive(b)) { text = b }
else { data = b }
}
if (isArray(children)) {
// 子节点递归处理
for (i = 0; i < children.length; ++i) {
if (isPrimitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i])
}
}
// svg处理
if (tag === 'svg') {
addNS(data, children)
}
// 子节点为文本节点
return VNode(tag, data, children, text, undefined)
}

到此为止,我们分析了const render = compile(getOuterHTML(el)),从elhtml字符串到render函数都是怎么处理的。

2. 代理data数据/methods的this绑定

// 实例代理data数据
Object.keys(options.data).forEach(key => this._proxy(key))
// 将method的this指向实例
if (options.methods) {
Object.keys(options.methods).forEach(key => {
this[key] = options.methods[key].bind(this)
})
}

实例代理data数据的实现比较简单,就是利用了对象的setter和getter,读取this数据时返回data数据,在设置this数据时同步设置data数据

_proxy (key) {
if (!isReserved(key)) {
// need to store ref to self here
// because these getter/setters might
// be called by child scopes via
// prototype inheritance.
var self = this
Object.defineProperty(self, key, {
configurable: true,
enumerable: true,
get: function proxyGetter () {
return self._data[key]
},
set: function proxySetter (val) {
self._data[key] = val
}
})
}
}

3. Obaerve的实现

Observe的实现原理在很多地方都有分析,主要是利用了Object.defineProperty()来建立对数据更改的订阅,在很多地方也称之为数据劫持。下面我们来学习从零开始建立这样一个数据的订阅发布体系。

从简单处开始,我们希望有个函数可以帮我们监听数据的改变,每当数据改变时执行特定回调函数

function observe(data, callback) {
if (!data || typeof data !== 'object') {
return
} // 遍历key
Object.keys(data).forEach((key) => {
let value = data[key]; // 递归遍历监听深度变化
observe(value, callback); // 监听单个可以的变化
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
return value;
},
set(val) {
if (val === value) {
return
} value = val; // 监听新的数据
observe(value, callback); // 数据改变的回调
callback();
}
});
});
} // 使用observe函数监听data
const data = {};
observe(data, () => {
console.log('data修改');
})

上面我们实现了一个简单的observe函数,只要我们将编译函数作为callback传入,那么每次数据更改时都会触发回调函数。但是我们现在不能为单独的key设置监听及回调函数,只能监听整个对象的变化执行回调。下面我们对函数进行改进,达到为某个key设置监听及回调。同时建立调度中心,让整个订阅发布模式更加清晰。

// 首先是订阅中心
class Dep {
constructor() {
this.subs = []; // 订阅者数组
} addSub(sub) {
// 添加订阅者
this.subs.push(sub);
} notify() {
// 发布通知
this.subs.forEach((sub) => {
sub.update();
});
}
} // 当前订阅者,在getter中标记
Dep.target = null; // 订阅者
class Watch {
constructor(express, cb) {
this.cb = cb;
if (typeof express === 'function') {
this.expressFn = express;
} else {
this.expressFn = () => {
return new Function(express)();
}
} this.get();
} get() {
// 利用Dep.target存当前订阅者
Dep.target = this;
// 执行表达式 -> 触发getter -> 在getter中添加订阅者
this.expressFn();
// 及时置空
Dep.taget = null;
} update() {
// 更新
this.cb();
} addDep(dep) {
// 添加订阅
dep.addSub(this);
}
} // 观察者 建立观察
class Observe {
constructor(data) {
if (!data || typeof data !== 'object') {
return
} // 遍历key
Object.keys(data).forEach((key) => {
// key => dep 对应
const dep = new Dep();
let value = data[key]; // 递归遍历监听深度变化
const observe = new Observe(value); // 监听单个可以的变化
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
if (Dep.target) {
const watch = Dep.target;
watch.addDep(dep);
}
return value;
},
set(val) {
if (val === value) {
return
} value = val; // 监听新的数据
new Observe(value); // 数据改变的回调
dep.notify();
}
});
});
}
} // 监听数据中某个key的更改
const data = {
name: 'xiaoming',
age: 26
}; const observe = new Observe(data); const watch = new Watch('data.age', () => {
console.log('age update');
}); data.age = 22

现在我们实现了订阅中心订阅者观察者。观察者监测数据的更新,订阅者通过订阅中心订阅数据的更新,当数据更新时,观察者会告诉订阅中心,订阅中心再逐个通知所有的订阅者执行更新函数。到现在为止,我们可以大概猜出vue的实现原理:

  1. 建立观察者观察data数据的更改 (new Observe)

  2. 在编译的时候,当某个代码片段或节点依赖data数据,为该节点建议订阅者,订阅data中某些数据的更新(new Watch)

  3. 当dada数据更新时,通过订阅中心通知数据更新,执行节点更新函数,新建或更新节点(dep.notify())

上面是我们对vue实现原理订阅发布模式的基本实现,及编译到更新过程的猜想,现在我们接着分析vue源码的实现:

在实例的初始化中

// ...
// 为数据建立数据观察
this._ob = observe(options.data)
this._watchers = []
// 添加订阅者 执行render 会触发 getter 订阅者订阅更新,数据改变触发 setter 订阅中心通知订阅者执行 update
this._watcher = new Watcher(this, render, this._update)
// ...

vue中数据观察的实现

// observe函数
export function observe (value, vm) {
if (!value || typeof value !== 'object') {
return
}
if (
hasOwn(value, '__ob__') &&
value.__ob__ instanceof Observer
) {
ob = value.__ob__
} else if (
shouldConvert &&
(isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 为数据建立观察者
ob = new Observer(value)
}
// 存储关联的vm
if (ob && vm) {
ob.addVm(vm)
}
return ob
} // => Observe 函数
export function Observer (value) {
this.value = value
// 在数组变异方法中有用
this.dep = new Dep()
// observer实例存在__ob__中
def(value, '__ob__', this)
if (isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment
// 数组遍历,添加变异的数组方法
augment(value, arrayMethods, arrayKeys)
// 对数组的每个选项调用observe函数
this.observeArray(value)
} else {
// walk -> convert -> defineReactive -> setter/getter
this.walk(value)
}
} // => walk
Observer.prototype.walk = function (obj) {
var keys = Object.keys(obj)
for (var i = 0, l = keys.length; i < l; i++) {
this.convert(keys[i], obj[keys[i]])
}
} // => convert
Observer.prototype.convert = function (key, val) {
defineReactive(this.value, key, val)
} // 重点看看defineReactive
export function defineReactive (obj, key, val) {
// key对应的的订阅中心
var dep = new Dep() var property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
} // 兼容原有setter/getter
// cater for pre-defined getter/setters
var getter = property && property.get
var setter = property && property.set // 实现递归监听属性 val = obj[key]
// 深度优先遍历 先为子属性设置 reactive
var childOb = observe(val)
// 设置 getter/setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val
// Dep.target 为当前 watch 实例
if (Dep.target) {
// dep 为 obj[key] 对应的调度中心 dep.depend 将当前 wtcher 实例添加到调度中心
dep.depend()
if (childOb) {
// childOb.dep 为 obj[key] 值 val 对应的 observer 实例的 dep
// 实现array的变异方法和$set方法订阅
childOb.dep.depend()
} // TODO: 此处作用未知?
if (isArray(value)) {
for (var e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val
// 通过 getter 获取 val 判断是否改变
if (newVal === value) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 为新值设置 reactive
childOb = observe(newVal)
// 通知key对应的订阅中心更新
dep.notify()
}
})
}

订阅中心的实现

let uid = 0

export default function Dep () {
this.id = uid++
// 订阅调度中心的watch数组
this.subs = []
} // 当前watch实例
Dep.target = null // 添加订阅者
Dep.prototype.addSub = function (sub) {
this.subs.push(sub)
} // 移除订阅者
Dep.prototype.removeSub = function (sub) {
this.subs.$remove(sub)
} // 订阅
Dep.prototype.depend = function () {
// Dep.target.addDep(this) => this.addSub(Dep.target) => this.subs.push(Dep.target)
Dep.target.addDep(this)
} // 通知更新
Dep.prototype.notify = function () {
// stablize the subscriber list first
var subs = this.subs.slice()
for (var i = 0, l = subs.length; i < l; i++) {
// subs[i].update() => watch.update()
subs[i].update()
}
}

订阅者的实现

export default function Watcher (vm, expOrFn, cb, options) {
// mix in options
if (options) {
extend(this, options)
}
var isFn = typeof expOrFn === 'function'
this.vm = vm
// vm 的 _watchers 包含了所有 watch
vm._watchers.push(this)
this.expression = expOrFn
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
// deps 一个 watch 实例可以对应多个 dep
this.deps = []
this.newDeps = []
this.depIds = Object.create(null)
this.newDepIds = null
this.prevError = null // for async error stacks
// parse expression for getter/setter
if (isFn) {
this.getter = expOrFn
this.setter = undefined
} else {
warn('vue-lite only supports watching functions.')
}
this.value = this.lazy
? undefined
: this.get()
this.queued = this.shallow = false
} Watcher.prototype.get = function () {
this.beforeGet()
var scope = this.scope || this.vm
var value
try {
// 执行 expOrFn,此时会触发 getter => dep.depend() 将watch实例添加到对应 obj[key] 的 dep
value = this.getter.call(scope, scope)
}
if (this.deep) {
// 深度watch
// 触发每个key的getter watch实例将对应多个dep
traverse(value)
}
// ...
this.afterGet()
return value
} // 触发getter,实现订阅
Watcher.prototype.beforeGet = function () {
Dep.target = this
this.newDepIds = Object.create(null)
this.newDeps.length = 0
} // 添加订阅
Watcher.prototype.addDep = function (dep) {
var id = dep.id
if (!this.newDepIds[id]) {
// 将新出现的dep添加到newDeps中
this.newDepIds[id] = true
this.newDeps.push(dep)
// 如果已在调度中心,不再重复添加
if (!this.depIds[id]) {
// 将watch添加到调度中心的数组中
dep.addSub(this)
}
}
} Watcher.prototype.afterGet = function () {
// 切除key的getter联系
Dep.target = null
var i = this.deps.length
while (i--) {
var dep = this.deps[i]
if (!this.newDepIds[dep.id]) {
// 移除不在expOrFn表达式中关联的dep中watch的订阅
dep.removeSub(this)
}
}
this.depIds = this.newDepIds
var tmp = this.deps
this.deps = this.newDeps
// TODO: 既然newDeps最终会被置空,这边赋值的意义在于?
this.newDeps = tmp
} // 订阅中心通知消息更新
Watcher.prototype.update = function (shallow) {
if (this.lazy) {
this.dirty = true
} else if (this.sync || !config.async) {
this.run()
} else {
// if queued, only overwrite shallow with non-shallow,
// but not the other way around.
this.shallow = this.queued
? shallow
? this.shallow
: false
: !!shallow
this.queued = true
// record before-push error stack in debug mode
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.debug) {
this.prevError = new Error('[vue] async stack trace')
}
// 添加到待执行池
pushWatcher(this)
}
} // 执行更新回调
Watcher.prototype.run = function () {
if (this.active) {
var value = this.get()
if (
((isObject(value) || this.deep) && !this.shallow)
) {
// set new value
var oldValue = this.value
this.value = value
var prevError = this.prevError
// ...
this.cb.call(this.vm, value, oldValue)
}
this.queued = this.shallow = false
}
} Watcher.prototype.depend = function () {
var i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}

wtach回调执行队列

在上面我们可以发现,watch在收到信息更新执行update时。如果非同步情况下会执行pushWatcher(this)将实例推入执行池中,那么在何时会执行回调函数,如何执行呢?我们一起看看pushWatcher的实现。

// batch.js
var queueIndex
var queue = []
var userQueue = []
var has = {}
var circular = {}
var waiting = false
var internalQueueDepleted = false // 重置执行池
function resetBatcherState () {
queue = []
userQueue = []
// has 避免重复
has = {}
circular = {}
waiting = internalQueueDepleted = false
} // 执行执行队列
function flushBatcherQueue () {
runBatcherQueue(queue)
internalQueueDepleted = true
runBatcherQueue(userQueue)
resetBatcherState()
} // 批量执行
function runBatcherQueue (queue) {
for (queueIndex = 0; queueIndex < queue.length; queueIndex++) {
var watcher = queue[queueIndex]
var id = watcher.id
// 执行后置为null
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > config._maxUpdateCount) {
warn(
'You may have an infinite update loop for watcher ' +
'with expression "' + watcher.expression + '"',
watcher.vm
)
break
}
}
}
} // 添加到执行池
export function pushWatcher (watcher) {
var id = watcher.id
if (has[id] == null) {
if (internalQueueDepleted && !watcher.user) {
// an internal watcher triggered by a user watcher...
// let's run it immediately after current user watcher is done.
userQueue.splice(queueIndex + 1, 0, watcher)
} else {
// push watcher into appropriate queue
var q = watcher.user
? userQueue
: queue
has[id] = q.length
q.push(watcher)
// queue the flush
if (!waiting) {
waiting = true
// 在nextick中执行
nextTick(flushBatcherQueue)
}
}
}
}

4. patch实现

上面便是vue中数据驱动的实现原理,下面我们接着回到主流程中,在执行完watch后,便执行this._update(this._watcher.value)开始节点渲染

// _update => createPatchFunction => patch => patchVnode => (dom api)

// vtree是通过compile函数编译的render函数执行的结果,返回了当前表示当前dom结构的对象(虚拟节点树)
_update (vtree) {
if (!this._tree) {
// 第一次渲染
patch(this._el, vtree)
} else {
patch(this._tree, vtree)
}
this._tree = vtree
} // 在处理节点时,需要针对class,props,style,attrs,events做不同处理
// 在这里注入针对不同属性的处理函数
const patch = createPatchFunction([
_class, // makes it easy to toggle classes
props,
style,
attrs,
events
]) // => createPatchFunction返回patch函数,patch函数通过对比虚拟节点的差异,对节点进行增删更新
// 最后调用原生的dom api更新html
return function patch (oldVnode, vnode) {
var i, elm, parent
var insertedVnodeQueue = []
// pre hook
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]() if (isUndef(oldVnode.sel)) {
oldVnode = emptyNodeAt(oldVnode)
} if (sameVnode(oldVnode, vnode)) {
// someNode can patch
patchVnode(oldVnode, vnode, insertedVnodeQueue)
} else {
// 正常的不复用 remove insert
elm = oldVnode.elm
parent = api.parentNode(elm) createElm(vnode, insertedVnodeQueue) if (parent !== null) {
api.insertBefore(parent, vnode.elm, api.nextSibling(elm))
removeVnodes(parent, [oldVnode], 0, 0)
}
} for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i])
} // hook post
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
return vnode
}

结尾

以上分析了vue从template 到节点渲染的大致实现,当然也有某些地方没有全面分析的地方,其中template解析为ast主要通过正则匹配实现,及节点渲染及更新的patch过程主要通过节点操作对比来实现。但是我们对编译template字符串 => 代理data数据/methods的this绑定 => 数据观察 => 建立watch及更新渲染的大致流程有了个比较完整的认知。


欢迎到前端学习打卡群一起学习~516913974

vue的第一个commit分析的更多相关文章

  1. Vue Create 创建一个新项目 命令行创建和视图创建

    Vue Create 创建一个新项目 命令行创建和视图创建 开始之前 你可以先 >>:cd desktop[将安装目录切换到桌面] >>:vue -V :Vue CLI 3.0 ...

  2. git reset到之前的某一个commit或者恢复之前删除的某一个分支

    一.使用了git reset之后,想要找回某一个commit 1.git log -g  这个命令只能显示少部分的commit 推荐使用git reflog 找到想要恢复的那个commit的hash, ...

  3. 关于vue组件的一个小结

    用vue进行开发到目前为止也有将近一年的时间了,在项目技术选型的时候隔壁组选 react的时候我们坚持使用vue作为前端的开发框架.虽然两者思想上的差异不大,但是vue的语法在代码的可读性以及后期的维 ...

  4. [大数据从入门到放弃系列教程]第一个spark分析程序

    [大数据从入门到放弃系列教程]第一个spark分析程序 原文链接:http://www.cnblogs.com/blog5277/p/8580007.html 原文作者:博客园--曲高终和寡 **** ...

  5. vue中封装一个全局的弹窗js

    /** * Created by yx on 2017/12/21. */ export default { /** * 带按钮的弹框 * <!--自定义提示标题,内容,单个按钮事件--> ...

  6. 前端框架之Vue(1)-第一个Vue实例

    vue官方文档 知识储备 es6语法补充 let 使用 var 声明的变量的作用域是全局. { var a = 1; } console.info(a); 例1: var arr = []; for ...

  7. 使用Vue cli3搭建一个用Fetch Api的组件

    系列参考 ,英文原文参考 我的git代码: https://github.com/chentianwei411/Typeahead 目标: 建立一个输入关键字得到相关列表的组件,用Vuejs2和Fet ...

  8. vue.js 作一个用户表添加页面----初级

    使用vue.js 制作一个用户表添加页面,实际上是把原来需要使用js写的部分,改写成vue.js的格式 首先,想象一下,先做思考,我们要添加用户表,设涉及到哪些数据,一个是用户id,一个是用户名,一个 ...

  9. vue + skyline 搭建 一个开发环境

    1.之前用的是ext +  skyline搭建环境 ,正好最近是做前端的事情,有时间用vue + skyline 搭建一个三维场景 2.准备vue 2.x  ,UI 用的是iview 和element ...

随机推荐

  1. 【雕爷学编程】Arduino动手做(62)---1排4键薄膜开关模块

    37款传感器与执行器的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止这37种的.鉴于本人手头积累了一些传感器和执行器模块,依照实践出真知(一定要动手做)的理念,以学习和交流为 ...

  2. Failed to start mongod.service: Unit not found

    其实自己用惯的是MYSQL,然后项目最后一步完善数据读写的部分,本来打算用mysql的,然而在centOS系统上发现安装总是出问题,后来查找一下资料,发现centOS系统上一般用的是Mariadb,这 ...

  3. Maven整合JaCoCo和Sonar,看看你的测试写够了没

    1 简介 单元测试是保证代码质量的重要一环,而如何衡量单元测试写得好不好呢?覆盖率(Coverage)是一个重要指标.而JaCoCo则是专门为Java提供的用于检测测试覆盖率的工具,英文全称为Java ...

  4. 06.drf(django)的权限

    默认配置已经启用权限控制 settings 'django.contrib.auth', 默认 migrate 会给每个模型赋予4个权限,如果 ORM 类不托管给django管理,而是直接在数据库中建 ...

  5. js时间戳转为日期格式的方法

    Date.prototype.Format = function(fmt){ var o = { "M+" : this.getMonth()+1, //月份 "d+&q ...

  6. oracle [精华] 你是否仍迷信rowid分页?

    http://www.itpub.net/thread-1603830-1-1.html

  7. CICD:Jenkins入门和使用

    最近,我们使用的开发服务器被回收了,换了一台新的服务器,CI/CD平台需要重新搭建. 我的运维能力一直薄弱,所以借此机会学习了一番如何使用Jenkins进行持续集成开发和部署,实践并踩了一些坑,在此记 ...

  8. format函数格式化显示的方法

    数字 格式 输出 描述 3.1415926 {:.2f} 3.14 保留小数点后两位 3.1415926 {:+.2f} +3.14 带符号保留小数点后两位 -1 {:+.2f} -1.00 带符号保 ...

  9. 001_C语言中运算符的优先级

    总的来说就是: 1. 最高:单目运算符(() > * 解引用,&取地址,-取相反数,++等自增(或减)运算,!取反运算...); 2. 次之:双目运算符(算数运算符 > 移位运算符 ...

  10. Web前端:2、盒模型的组成

    在HTML中,若想要实心划分区域,则:1.添加标签:2.对标签设置尺寸(宽高) 但只要是添加了一个元素(标签),就会在页面中生成一个盒子,不同元素产生的盒子模型可能不同,这取决于它CSS的displa ...