注意attention: 转载学习来源–Vue.js 技术揭秘
注意attention: 转载学习来源–Vue2.1.7源码学习
vue2.5.0学习
从了解一个开源项目入手
要看一个项目的源码,不要一上来就看,先去了解一下项目本身的元数据和依赖,除此之外最好也了解一下 PR 规则,Issue Reporting 规则等等。特别是“前端”开源项目,我们在看源码之前第一个想到的应该是:package.json文件。
在 package.json 文件中,我们最应该关注的就是 scripts 字段和 devDependencies 以及 dependencies 字段,通过 scripts 字段我们可以知道项目中定义的脚本命令,通过 devDependencies 和 dependencies 字段我们可以了解项目的依赖情况。
了解了这些之后,如果有依赖我们就 npm install 安装依赖就ok了。 除了 package.json 之外,我们还要阅读项目的贡献规则文档,了解如何开始,一个好的开源项目肯定会包含这部分内容的,Vue也不例外:https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md,在这个文档里说明了一些行为准则,PR指南,Issue Reporting 指南,Development Setup 以及 项目结构。通过阅读这些内容,我们可以了解项目如何开始,如何开发以及目录的说明
构造函数
vue目录结构: –来源 Vue2.1.7源码学习
vue2.5.0与上面的图片2.1.7有一点不一样,没有entry目录,多了platforms, platforms包含平台入口(即之前的entry),还有与平台相关的一些模块和工具函数。
初始化Vue
1. 2018-12-16-vue源码学习/vue/src/platforms/web/entry-runtime-with-compiler.js
挂载 Vue.prototype.$mount,Vue.compile
2. 2018-12-16-vue源码学习/vue/src/platforms/web/runtime/index.js
挂载 Vue.prototype.__patch__; Vue.prototype.$mount; 执行 Vue.nextTick执行devtools.emit('init', Vue)
3. 2018-12-16-vue源码学习/vue/src/core/index.js
调用 initGlobalAPI(Vue)
4. 2018-12-16-vue源码学习/vue/src/core/global-api/index.js ---挂载全局方法
Vue.set;Vue.delete;Vue.nextTick;Vue.options;Vue.options._base = Vue
initUse(Vue) // 挂载Vue.use 方法
initMixin(Vue) // 挂载Vue.mixin 方法
initExtend(Vue) // 挂载Vue.extend 方法
initAssetRegisters(Vue) // 挂载Vue.directives, Vue.components, Vue.filters 属性,以及 Vue.component Vue.directive Vue.filter 方法
5. 2018-12-16-vue源码学习/vue/src/core/instance/index.js --- 挂载实例方法,以及 定义Vue构造函数
initMixin(Vue) // 挂载 _init方法到原型上
stateMixin(Vue) // 挂载 $data,$props,$set,$delete,$watch 方法
eventsMixin(Vue) // 挂载 $on,$once,$off,$emit
lifecycleMixin(Vue) // 挂载 _update,$forceUpdate,$destroy
renderMixin(Vue) // 挂载 render的相关简单方法,$nextTick,_render
// new Vue 执行 this._init(options) // _init方法在initMixin里面进行了挂载
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
// mergeOptions 用于合并各种属性,比如data,props,watch等
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm) // 事件初始化 vm._events
initRender(vm) // 定义 vm._vnode , vm.$createElement
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) // 初始化数据响应
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 进行dom节点的mount挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
状态管理
总而言之:
- 每一个属性都有一个Dep对象,里面子属性有subs: Array
数组,用于收集所有依赖了此属性的watcher; - 每一个watcher对象都有子属性deps: Array
、newDeps: Array ,用于存放这个watcher所有的属性dep对象(主要是re-collect过程中进行属性dep对象中的watcher移除) - 利用属性的get代理进行属性Dep对象watcher的收集
- 利用属性的set代理进行属性Dep对象watcher的更新,执行watcher里面的回调函数
// 挂载属性 data,prop,method等到实例上 proxy(vm, `_data`, key)
// /vue/src/core/observer/index.js
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
proxy(vm, `_data`, key)
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
// observe 方法
// 返回属性值的Observer对象ob,注意,这个时候,其实对于传入对象生成的ob对象,存在Dep属性,用于动态的$set方法的dep.notify()
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) { // 非对象(原始类型不进行监控) 或者 vnode不进行监控
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
observerState.shouldConvert &&
!isServerRendering() && // ??限制服务器渲染
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
// Observe类
// 构造函数,会将生成的Observer对象(this),挂载到传入对象的__ob__属性上
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this) // 设置__ob__属性 this对象,表示为已观察对象
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment // 设置value的原型为arrayMethods
: copyAugment // 没有原型则进行属性循环设置拦截
augment(value, arrayMethods, arrayKeys) // 修改对象(数组)的原型对象
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
//defineReactive 方法
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep() // 对于每一个属性都会有一个dep,闭包,dep 一直存在于内存当中
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
// 保存原始的get 和 set,这样求值的时候,不会进行依赖分析
const getter = property && property.get
const setter = property && property.set
// __ob__上的dep属性主要用于监控对象属性增加和删减,而闭包所创建的dep用于监控属性值的更新
// 也就是说key的dep是用于闭包内的set,然后value的dep则是用于 $set或者数组等的notify
let childOb = !shallow && observe(val) // 设置属性值的 Observer 对象
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val // 原始求值
if (Dep.target) { // 当前的dep target,整个进程就一个
dep.depend() // 为属性的闭包dep添加watcher
if (childOb) {
childOb.dep.depend() // 也为属性值的dep对象添加watcher
if (Array.isArray(value)) { // 如果是数组,则把数组子项也添加
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal) // childOb作用?
dep.notify()
}
})
}
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
export function set (target: Array<any> | Object, key: any, val: any): any {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (hasOwn(target, key)) { // Object.prototype.hasOwnProperty.call(target, key)
target[key] = val
return val
}
const ob = (target: any).__ob__ // 生成Observer的时候,进行的属性设置
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) { // 根据对象是否有__ob__属性进行对象是否是响应式对象的判断(set方法必须是向响应式对象中添加一个属性)
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify() // 此时利用的是Observer实例对象的dep
return val
}
/**
* Delete a property and trigger change if necessary.
*/
export function del (target: Array<any> | Object, key: any) {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
ob.dep.notify()
}
/**
* Collect dependencies on array elements when the array is touched, since
* we cannot intercept array element access like property getters.
*/
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
// /vue/src/core/observer/array.js --数组处理
// 定义一个以数组原型作为原型的新对象
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto) // 导出已经拦截过方法的对象,为Observer对象中传入的对象数组作为原型
/**
* Intercept mutating methods and emit events
*/
// 拦截方法
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// cache original method
const original = arrayProto[method]
// 定义一个新数组对象的求值方法
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
// 最初始的watcher设置
// /vue/src/core/instance/lifecycle.js
// 精简代码
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// 定义el的watch,updateComponent 作为expOrFn参数,会在构造函数中进行求值执行
// 从而会调用vm._render(),当 vm._render 执行的时候,所依赖的变量就会被求值,并被收集为依赖
vm._watcher = new Watcher(vm, updateComponent, noop)
// /vue/src/core/observer/watcher.js
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: ISet;
newDepIds: ISet;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this) // 定义当前的target watch
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // 对watch的属性或者函数,进行求值,从而通过get方法收集dep
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget() // 把 Dep.target 恢复成上一个状态
this.cleanupDeps() // 依赖清空
}
return value
}
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id) // 当前watch放入id
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this) // 属性dep收集当前watch(this)
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this) // 进行watch的移除,这个我理解也是 watcher里面的deps的作用
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
// 调用时期---属性set的时候调用dep.notify会进行更新
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
// get里面会进行求值
// 同时此处的求值,对于mount render,会进行vnode的 re-render
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* 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()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
关于cleanupDeps
考虑到 Vue 是数据驱动的,所以每次数据变化都会重新 render,那么 vm._render() 方法又会再次执行,并再次触发数据的 getters,所以 Wathcer 在构造函数中会初始化 2 个 Dep 实例数组,newDeps 表示新添加的 Dep 实例数组,而 deps 表示上一次添加的 Dep 实例数组。
在执行 cleanupDeps 函数的时候,会首先遍历 deps,移除对 dep.subs 数组中 Wathcer 的订阅,然后把 newDepIds 和 depIds 交换,newDeps 和 deps 交换,并把 newDepIds 和 newDeps 清空。
那么为什么需要做 deps 订阅的移除呢,在添加 deps 的订阅过程,已经能通过 id 去重避免重复订阅了。
考虑到一种场景,我们的模板会根据 v-if 去渲染不同子模板 a 和 b,当我们满足某种条件的时候渲染 a 的时候,会访问到 a 中的数据,这时候我们对 a 使用的数据添加了 getter,做了依赖收集,那么当我们去修改 a 的数据的时候,理应通知到这些订阅者。那么如果我们一旦改变了条件渲染了 b 模板,又会对 b 使用的数据添加了 getter,如果我们没有依赖移除的过程,那么这时候我去修改 a 模板的数据,会通知 a 数据的订阅的回调,这显然是有浪费的。
因此 Vue 设计了在每次添加完新的订阅,会移除掉旧的订阅,这样就保证了在我们刚才的场景中,如果渲染 b 模板的时候去修改 a 模板的数据,a 数据订阅回调已经被移除了,所以不会有任何浪费,真的是非常赞叹 Vue 对一些细节上的处理。
模板编译
总体来说: el的html模板 —-mount解析,parse模块–> ast —–generate函数的codegen模块—-> render 函数串 —-执行render函数,利用render-help模块–> vnode节点 —-_update方法 , 调用 __patch__方法—> vnode转dom的更新
// 2018-12-16-vue源码学习/vue/src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
// ...
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// ...
// 进行dom节点的mount挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
// /vue/src/platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount // 在/src/platforms/web/runtime/index.js文件中挂载
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el) // document.querySelect
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// render属性生成
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render // 挂载vm.$options.render
options.staticRenderFns = staticRenderFns // 挂载vm.$options.staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
// /vue/src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// /vue/src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
// ...
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// 定义el的watch,updateComponent 作为expOrFn参数,会在构造函数中进行求值执行
// 从而会调用vm._render(),当 vm._render 执行的时候,所依赖的变量就会被求值,并被收集为依赖
vm._watcher = new Watcher(vm, updateComponent, noop)
// ...
return vm
}
// /vue/src/compiler/index.js
// compileToFunctions 方法 --生成 _render函数方法
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options) // 解析模板字符串生成 AST
optimize(ast, options) // 优化语法树
const code = generate(ast, options) // 生成代码
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
// /vue/src/compiler/parser/index.js
// parse 解析
export function createASTElement (
tag: string,
attrs: Array<Attr>,
parent: ASTElement | void
): ASTElement {
return {
type: 1, // AST 元素类型
tag, // 标签名
attrsList: attrs, // 属性列表
attrsMap: makeAttrsMap(attrs), // 属性映射表
parent, // 父的 AST 元素
children: [] // 子 AST 元素集合
}
}
/* 解析HTML */
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag, // 一元标签,即 自闭和标签 <xx/>
canBeLeftOpenTag: options.canBeLeftOpenTag, // 可以不关闭的标签
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldKeepComment: options.comments,
start (tag, attrs, unary) {
// check namespace.
// inherit parent ns if there is one
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs)
}
// 创建ast元素
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true
process.env.NODE_ENV !== 'production' && warn(
'Templates should only be responsible for mapping the state to the ' +
'UI. Avoid placing tags with side-effects in your templates, such as ' +
`<${tag}>` + ', as they will not be parsed.'
)
}
// apply pre-transforms
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
if (!inVPre) {
/*
处理v-pre属性
v-pre元素及其子元素被跳过编译
https://cn.vuejs.org/v2/api/#v-pre
*/
processPre(element)
if (element.pre) {
inVPre = true
}
}
/* 检测是否是<pre>标签*/
if (platformIsPreTag(element.tag)) {
inPre = true
}
/* 如果有v-pre属性,元素及其子元素不会被编译*/
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
/* 匹配v-for属性*/
processFor(element)
/* 匹配if属性,分别处理v-if、v-else以及v-else-if属性*/
processIf(element)
/* 处理v-once属性,https://cn.vuejs.org/v2/api/#v-once*/
processOnce(element)
// element-scope stuff
processElement(element, options)
}
function checkRootConstraints (el) {
if (process.env.NODE_ENV !== 'production') {
if (el.tag === 'slot' || el.tag === 'template') {
warnOnce(
`Cannot use <${el.tag}> as component root element because it may ` +
'contain multiple nodes.'
)
}
if (el.attrsMap.hasOwnProperty('v-for')) {
warnOnce(
'Cannot use v-for on stateful component root element because ' +
'it renders multiple elements.'
)
}
}
}
// tree management
if (!root) {
root = element
checkRootConstraints(root)
} else if (!stack.length) {
// allow root elements with v-if, v-else-if and v-else
if (root.if && (element.elseif || element.else)) {
checkRootConstraints(element)
addIfCondition(root, {
exp: element.elseif,
block: element
})
} else if (process.env.NODE_ENV !== 'production') {
warnOnce(
`Component template should contain exactly one root element. ` +
`If you are using v-if on multiple elements, ` +
`use v-else-if to chain them instead.`
)
}
}
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else if (element.slotScope) { // scoped slot
currentParent.plain = false
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
} else {
currentParent.children.push(element)
element.parent = currentParent
}
}
if (!unary) {
currentParent = element // 如果没有闭合,则设置currentParent为当前元素,为后面children做准备
stack.push(element)
} else {
endPre(element)
}
// apply post-transforms
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
},
end () {
// remove trailing whitespace
const element = stack[stack.length - 1]
const lastNode = element.children[element.children.length - 1]
if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
element.children.pop()
}
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
endPre(element)
},
chars (text: string) {
if (!currentParent) {
if (process.env.NODE_ENV !== 'production') {
if (text === template) {
warnOnce(
'Component template requires a root element, rather than just text.'
)
} else if ((text = text.trim())) {
warnOnce(
`text "${text}" outside root element will be ignored.`
)
}
}
return
}
// IE textarea placeholder bug
/* istanbul ignore if */
if (isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
const children = currentParent.children
text = inPre || text.trim()
? isTextTag(currentParent) ? text : decodeHTMLCached(text)
// only preserve whitespace if its not right after a starting tag
: preserveWhitespace && children.length ? ' ' : ''
if (text) {
let expression
if (!inVPre && text !== ' ' && (expression = parseText(text, delimiters))) {
children.push({
type: 2,
expression,
text
})
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
children.push({
type: 3,
text
})
}
}
},
comment (text: string) {
currentParent.children.push({
type: 3,
text,
isComment: true
})
}
})
// 返回root
return root
// /vue/src/compiler/parser/html-parser.js
// parseHTML方法,进行html标签的读取与解析
// Regular Expressions for parsing tags and attributes
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
// could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
// but for Vue templates we can enforce a simple charset
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!--/
const conditionalComment = /^<!\[/
let IS_REGEX_CAPTURING_BROKEN = false
'x'.replace(/x(.)?/g, function (m, g) {
IS_REGEX_CAPTURING_BROKEN = g === ''
})
// Special Elements (can contain anything)
export const isPlainTextElement = makeMap('script,style,textarea', true)
const reCache = {}
const decodingMap = {
'<': '<',
'>': '>',
'"': '"',
'&': '&',
' ': '\n'
}
const encodedAttr = /&(?:lt|gt|quot|amp);/g
const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#10);/g
// #5992
const isIgnoreNewlineTag = makeMap('pre,textarea', true)
const shouldIgnoreFirstNewline = (tag, html) => tag && isIgnoreNewlineTag(tag) && html[0] === '\n'
function decodeAttr (value, shouldDecodeNewlines) {
const re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr
return value.replace(re, match => decodingMap[match])
}
export function parseHTML (html, options) {
const stack = []
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
let index = 0
let last, lastTag
while (html) {
last = html
// Make sure we're not in a plaintext content element like script/style
/* 保证lastTag不是纯文本标签,比如script、style以及textarea*/
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<')
if (textEnd === 0) {
// Comment:
/* 如果是注释则默认直接去除*/
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd))
}
advance(commentEnd + 3)
continue
}
}
// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2)
continue
}
}
// Doctype:
/* <!DOCTYPE>标签不需要处理直接去除*/
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
// End tag:
// endTag 需注意,是endTag开始的标签,new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
// Start tag:
const startTagMatch = parseStartTag() /* 返回一个开始标签对象 { tagName, attrs, start, end, unarySlash }*/
if (startTagMatch) {
handleStartTag(startTagMatch) // 处理下startTagMatch对象,同时判断是否进入stack栈以及设置lastTag
if (shouldIgnoreFirstNewline(lastTag, html)) {
advance(1)
}
continue
}
}
// 文本处理
let text, rest, next
if (textEnd >= 0) {
rest = html.slice(textEnd)
// 剩余部分的 HTML 不符合标签的格式那肯定就是文本
// 并且还是以 < 开头的文本
// 循环,直到符合标签的格式规则,在进行所有文本的一次性截取
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// < in plain text, be forgiving and treat it as text
next = rest.indexOf('<', 1)
if (next < 0) break
textEnd += next
rest = html.slice(textEnd)
}
text = html.substring(0, textEnd)
advance(textEnd)
}
// textEnd 小于 0 的情况,则说明整个 template 解析完毕了,把剩余的 html 都赋值给了 text
if (textEnd < 0) {
text = html
html = ''
}
if (options.chars && text) {
options.chars(text)
}
} else {
let endTagLength = 0
const stackedTag = lastTag.toLowerCase()
const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
const rest = html.replace(reStackedTag, function (all, text, endTag) {
endTagLength = endTag.length
if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
text = text
.replace(/<!--([\s\S]*?)-->/g, '$1')
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
}
if (shouldIgnoreFirstNewline(stackedTag, text)) {
text = text.slice(1)
}
if (options.chars) {
options.chars(text)
}
return ''
})
index += html.length - rest.length
html = rest
parseEndTag(stackedTag, index - endTagLength, index)
}
if (html === last) {
options.chars && options.chars(html)
if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {
options.warn(`Mal-formatted tag at end of template: "${html}"`)
}
break
}
}
// Clean up any remaining tags
parseEndTag()
/* 为计数index加上n,同时,使html到n个字符以后到位置作为起始位*/
function advance (n) {
index += n
html = html.substring(n)
}
// 解析开始标签
function parseStartTag () {
// match匹配,非全局,则返回第一个完整匹配及其相关的捕获组(Array),同时返回groups: 一个捕获组数组 或 undefined(如果没有定义命名捕获组)。index: 匹配的结果的开始位置。input: 搜索的字符串.
const start = html.match(startTagOpen) // 匹配开始标签
if (start) {
const match = {
tagName: start[1], // tag名称
attrs: [],
start: index
}
advance(start[0].length) // 前进开始便签的长度,包括<字符
let end, attr
// 循环查找属性
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length)
match.attrs.push(attr)
}
if (end) {
match.unarySlash = end[1] // 是否是一元闭合标签
advance(end[0].length)
match.end = index
return match
}
}
}
// 处理解析开始标签后的match对象
function handleStartTag (match) {
const tagName = match.tagName
const unarySlash = match.unarySlash
if (expectHTML) {
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag)
}
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
}
const unary = isUnaryTag(tagName) || !!unarySlash
// 处理属性数组,每个值都是一个match匹配值,需要进行空串处理
const l = match.attrs.length
const attrs = new Array(l)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
// hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) { // 处理浏览器对于没有匹配上的组,match也返回空串bug
if (args[3] === '') { delete args[3] }
if (args[4] === '') { delete args[4] }
if (args[5] === '') { delete args[5] }
}
const value = args[3] || args[4] || args[5] || '' // 取值
attrs[i] = {
name: args[1],
value: decodeAttr(
value,
options.shouldDecodeNewlines
)
}
}
// 非一元标签,则进行开始标签的入栈
if (!unary) {
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
lastTag = tagName
}
// 调用start回调函数
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
// 解析闭合标签
function parseEndTag (tagName, start, end) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index
if (tagName) {
lowerCasedTagName = tagName.toLowerCase()
}
// Find the closest opened tag of the same type
if (tagName) {
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
// If no tag name is provided, clean shop
pos = 0
}
if (pos >= 0) {
// Close all the open elements, up the stack
for (let i = stack.length - 1; i >= pos; i--) {
if (process.env.NODE_ENV !== 'production' &&
(i > pos || !tagName) &&
options.warn
) {
options.warn(
`tag <${stack[i].tag}> has no matching end tag.`
)
}
if (options.end) {
options.end(stack[i].tag, start, end)
}
}
// Remove the open elements from the stack
stack.length = pos
lastTag = pos && stack[pos - 1].tag
} else if (lowerCasedTagName === 'br') {
if (options.start) {
options.start(tagName, [], true, start, end)
}
} else if (lowerCasedTagName === 'p') {
if (options.start) {
options.start(tagName, [], false, start, end)
}
if (options.end) {
options.end(tagName, start, end)
}
}
}
}
// /vue/src/core/instance/lifecycle.js
// _update方法 , 调用 __patch__方法,进行vnode转dom的更新
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
const prevEl = vm.$el
cconst prevVnode = vm._vnode // 最初的_vnode,是在initRender里面定义的
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
// no need for the ref nodes after initial patch
// this prevents keeping a detached DOM tree in memory (#5851)
vm.$options._parentElm = vm.$options._refElm = null
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}