Repository: answershuto/learnVue Branch: master Commit: 1c0371909bbc Files: 213 Total size: 619.7 KB Directory structure: gitextract_wq0j9njv/ ├── README.md ├── docs/ │ ├── VNode节点.MarkDown │ ├── VirtualDOM与diff(Vue实现).MarkDown │ ├── Vue.js异步更新DOM策略及nextTick.MarkDown │ ├── Vuex源码解析.MarkDown │ ├── Vue事件机制.MarkDown │ ├── Vue组件间通信.MarkDown │ ├── 从template到DOM(Vue.js源码角度看内部运行机制).MarkDown │ ├── 从源码角度再看数据绑定.MarkDown │ ├── 依赖收集.MarkDown │ ├── 响应式原理.MarkDown │ ├── 聊聊Vue的template编译.MarkDown │ ├── 聊聊keep-alive组件的使用及其实现原理.MarkDown │ └── 说说element组件库broadcast与dispatch.MarkDown ├── images/ │ ├── VNode.sketch │ ├── VNode2.sketch │ ├── diff1.sketch │ ├── diff10.sketch │ ├── diff2.sketch │ ├── diff3.sketch │ ├── diff4.sketch │ ├── diff5.sketch │ ├── diff6.sketch │ ├── diff7.sketch │ ├── diff8.sketch │ └── diff9.sketch ├── vue-router-src/ │ ├── components/ │ │ ├── link.js │ │ └── view.js │ ├── create-matcher.js │ ├── create-route-map.js │ ├── history/ │ │ ├── abstract.js │ │ ├── base.js │ │ ├── hash.js │ │ └── html5.js │ ├── index.js │ ├── install.js │ └── util/ │ ├── async.js │ ├── dom.js │ ├── location.js │ ├── params.js │ ├── path.js │ ├── push-state.js │ ├── query.js │ ├── resolve-components.js │ ├── route.js │ ├── scroll.js │ └── warn.js ├── vue-src/ │ ├── compiler/ │ │ ├── codegen/ │ │ │ ├── events.js │ │ │ └── index.js │ │ ├── directives/ │ │ │ ├── bind.js │ │ │ ├── index.js │ │ │ └── model.js │ │ ├── error-detector.js │ │ ├── helpers.js │ │ ├── index.js │ │ ├── optimizer.js │ │ └── parser/ │ │ ├── entity-decoder.js │ │ ├── filter-parser.js │ │ ├── html-parser.js │ │ ├── index.js │ │ └── text-parser.js │ ├── core/ │ │ ├── components/ │ │ │ ├── index.js │ │ │ └── keep-alive.js │ │ ├── config.js │ │ ├── global-api/ │ │ │ ├── assets.js │ │ │ ├── extend.js │ │ │ ├── index.js │ │ │ ├── mixin.js │ │ │ └── use.js │ │ ├── index.js │ │ ├── instance/ │ │ │ ├── events.js │ │ │ ├── index.js │ │ │ ├── init.js │ │ │ ├── inject.js │ │ │ ├── lifecycle.js │ │ │ ├── proxy.js │ │ │ ├── render-helpers/ │ │ │ │ ├── bind-object-props.js │ │ │ │ ├── check-keycodes.js │ │ │ │ ├── render-list.js │ │ │ │ ├── render-slot.js │ │ │ │ ├── render-static.js │ │ │ │ ├── resolve-filter.js │ │ │ │ └── resolve-slots.js │ │ │ ├── render.js │ │ │ └── state.js │ │ ├── observer/ │ │ │ ├── array.js │ │ │ ├── dep.js │ │ │ ├── index.js │ │ │ ├── scheduler.js │ │ │ └── watcher.js │ │ ├── util/ │ │ │ ├── debug.js │ │ │ ├── env.js │ │ │ ├── error.js │ │ │ ├── index.js │ │ │ ├── lang.js │ │ │ ├── options.js │ │ │ ├── perf.js │ │ │ └── props.js │ │ └── vdom/ │ │ ├── create-component.js │ │ ├── create-element.js │ │ ├── create-functional-component.js │ │ ├── helpers/ │ │ │ ├── extract-props.js │ │ │ ├── get-first-component-child.js │ │ │ ├── index.js │ │ │ ├── merge-hook.js │ │ │ ├── normalize-children.js │ │ │ ├── resolve-async-component.js │ │ │ └── update-listeners.js │ │ ├── modules/ │ │ │ ├── directives.js │ │ │ ├── index.js │ │ │ └── ref.js │ │ ├── patch.js │ │ └── vnode.js │ ├── platforms/ │ │ ├── web/ │ │ │ ├── compiler/ │ │ │ │ ├── directives/ │ │ │ │ │ ├── html.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── model.js │ │ │ │ │ └── text.js │ │ │ │ ├── index.js │ │ │ │ ├── modules/ │ │ │ │ │ ├── class.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── style.js │ │ │ │ └── util.js │ │ │ ├── compiler.js │ │ │ ├── runtime/ │ │ │ │ ├── class-util.js │ │ │ │ ├── components/ │ │ │ │ │ ├── index.js │ │ │ │ │ ├── transition-group.js │ │ │ │ │ └── transition.js │ │ │ │ ├── directives/ │ │ │ │ │ ├── index.js │ │ │ │ │ ├── model.js │ │ │ │ │ └── show.js │ │ │ │ ├── index.js │ │ │ │ ├── modules/ │ │ │ │ │ ├── attrs.js │ │ │ │ │ ├── class.js │ │ │ │ │ ├── dom-props.js │ │ │ │ │ ├── events.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── style.js │ │ │ │ │ └── transition.js │ │ │ │ ├── node-ops.js │ │ │ │ ├── patch.js │ │ │ │ └── transition-util.js │ │ │ ├── runtime-with-compiler.js │ │ │ ├── runtime.js │ │ │ ├── server/ │ │ │ │ ├── directives/ │ │ │ │ │ ├── index.js │ │ │ │ │ └── show.js │ │ │ │ ├── modules/ │ │ │ │ │ ├── attrs.js │ │ │ │ │ ├── class.js │ │ │ │ │ ├── dom-props.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── style.js │ │ │ │ └── util.js │ │ │ ├── server-renderer.js │ │ │ └── util/ │ │ │ ├── attrs.js │ │ │ ├── class.js │ │ │ ├── compat.js │ │ │ ├── element.js │ │ │ ├── index.js │ │ │ └── style.js │ │ └── weex/ │ │ ├── compiler/ │ │ │ ├── directives/ │ │ │ │ ├── index.js │ │ │ │ └── model.js │ │ │ ├── index.js │ │ │ └── modules/ │ │ │ ├── append.js │ │ │ ├── class.js │ │ │ ├── index.js │ │ │ ├── props.js │ │ │ └── style.js │ │ ├── compiler.js │ │ ├── framework.js │ │ ├── runtime/ │ │ │ ├── components/ │ │ │ │ ├── index.js │ │ │ │ ├── transition-group.js │ │ │ │ └── transition.js │ │ │ ├── directives/ │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ ├── modules/ │ │ │ │ ├── attrs.js │ │ │ │ ├── class.js │ │ │ │ ├── events.js │ │ │ │ ├── index.js │ │ │ │ ├── style.js │ │ │ │ └── transition.js │ │ │ ├── node-ops.js │ │ │ ├── patch.js │ │ │ └── text-node.js │ │ ├── runtime-factory.js │ │ └── util/ │ │ └── index.js │ ├── server/ │ │ ├── bundle-renderer/ │ │ │ ├── create-bundle-renderer.js │ │ │ ├── create-bundle-runner.js │ │ │ └── source-map-support.js │ │ ├── create-renderer.js │ │ ├── render-context.js │ │ ├── render-stream.js │ │ ├── render.js │ │ ├── template-renderer/ │ │ │ ├── create-async-file-mapper.js │ │ │ ├── index.js │ │ │ ├── parse-template.js │ │ │ └── template-stream.js │ │ ├── util.js │ │ ├── webpack-plugin/ │ │ │ ├── client.js │ │ │ ├── server.js │ │ │ └── util.js │ │ └── write.js │ ├── sfc/ │ │ └── parser.js │ └── shared/ │ ├── constants.js │ └── util.js └── vuex-src/ ├── helpers.js ├── index.esm.js ├── index.js ├── mixin.js ├── module/ │ ├── module-collection.js │ └── module.js ├── plugins/ │ ├── devtool.js │ └── logger.js ├── store.js └── util.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: README.md ================================================ # learnVue ## 介绍 Vue.js源码分析,记录了个人学习Vue.js源码的过程中的一些心得以及收获。以及对于Vue框架,周边库的一些个人见解。 在学习的过程中我为Vue.js(2.3.0)、Vuex(2.4.0)、Vue-router(3.0.1)加上了注释,分别在文件夹[vue-src](./vue-src)、[vuex-src](./vuex-src)以及[vue-router-src](./vue-router-src)中,希望可以帮助有需要的同学更好地学习理解Vue.js及周边库的源码。 感谢[尤大](https://github.com/yyx990803)提高生产力。 本项目希望对Vue.js做更进一步的探索与学习,Vue.js基础内容请参考Vue.js官网,[https://cn.vuejs.org/v2/guide/](https://cn.vuejs.org/v2/guide/)。 可能会有理解存在偏差的地方,欢迎提issue指出,共同学习,共同进步。 --- ## 目录 ### 源码相关 [Vue.js响应式原理](./docs/响应式原理.MarkDown) [Vue.js依赖收集](./docs/依赖收集.MarkDown) [从Vue.js源码角度再看数据绑定](./docs/从源码角度再看数据绑定.MarkDown) [Vue.js事件机制](./docs/Vue事件机制.MarkDown) [VNode节点(Vue.js实现)](./docs/VNode节点.MarkDown) [Virtual DOM与diff(Vue.js实现)](./docs/VirtualDOM与diff(Vue实现).MarkDown) [聊聊Vue.js的template编译](./docs/聊聊Vue的template编译.MarkDown) [Vue.js异步更新DOM策略及nextTick](./docs/Vue.js异步更新DOM策略及nextTick.MarkDown) [从template到DOM(Vue.js源码角度看内部运行机制)](./docs/从template到DOM(Vue.js源码角度看内部运行机制).MarkDown) [Vuex源码解析](./docs/Vuex源码解析.MarkDown) [聊聊keep-alive组件的使用及其实现原理](./docs/聊聊keep-alive组件的使用及其实现原理.MarkDown) ### 随笔杂谈 [Vue组件间通信](./docs/Vue组件间通信.MarkDown) [说说element组件库broadcast与dispatch](./docs/说说element组件库broadcast与dispatch.MarkDown) --- ## 对于新手同学 由于以上内容都是针对 Vue.js 源码进行讲解了,可能有一些不太熟悉源码的同学读起来感觉晦涩难懂。 笔者撰写的[《剖析 Vue.js 内部运行机制》](https://juejin.im/book/5a36661851882538e2259c0f)或许可以帮到你。 ## 关于作者 作者: 染陌 Email:answershuto@gmail.com Github: [https://github.com/answershuto](https://github.com/answershuto) 知乎:[https://www.zhihu.com/people/cao-yang-49/activities](https://www.zhihu.com/people/cao-yang-49/activities) 掘金:[https://juejin.im/user/58f87ae844d9040069ca7507](https://juejin.im/user/58f87ae844d9040069ca7507) 对内容有任何疑问,欢迎联系我。 ================================================ FILE: docs/VNode节点.MarkDown ================================================ ## 抽象DOM树 在刀耕火种的年代,我们需要在各个事件方法中直接操作DOM来达到修改视图的目的。但是当应用一大就会变得难以维护。 那我们是不是可以把真实DOM树抽象成一棵以JavaScript对象构成的抽象树,在修改抽象树数据后将抽象树转化成真实DOM重绘到页面上呢?于是虚拟DOM出现了,它是真实DOM的一层抽象,用属性描述真实DOM的各个特性。当它发生变化的时候,就会去修改视图。 可以想象,最简单粗暴的方法就是将整个DOM结构用innerHTML修改到页面上,但是这样进行重绘整个视图层是相当消耗性能的,我们是不是可以每次只更新它的修改呢?所以Vue.js将DOM抽象成一个以JavaScript对象为节点的虚拟DOM树,以VNode节点模拟真实DOM,可以对这颗抽象树进行创建节点、删除节点以及修改节点等操作,在这过程中都不需要操作真实DOM,只需要操作JavaScript对象后只对差异修改,相对于整块的innerHTML的粗暴式修改,大大提升了性能。修改以后经过diff算法得出一些需要修改的最小单位,再将这些小单位的视图进行更新。这样做减少了很多不需要的DOM操作,大大提高了性能。 Vue就使用了这样的抽象节点VNode,它是对真实DOM的一层抽象,而不依赖某个平台,它可以是浏览器平台,也可以是weex,甚至是node平台也可以对这样一棵抽象DOM树进行创建删除修改等操作,这也为前后端同构提供了可能。 ## VNode基类 先来看一下Vue.js源码中对VNode类的定义。 ```javascript export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope functionalContext: Component | void; // only for functional component root nodes key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? constructor ( tag?: string, data?: VNodeData, children?: ?Array, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions ) { /*当前节点的标签名*/ this.tag = tag /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/ this.data = data /*当前节点的子节点,是一个数组*/ this.children = children /*当前节点的文本*/ this.text = text /*当前虚拟节点对应的真实dom节点*/ this.elm = elm /*当前节点的名字空间*/ this.ns = undefined /*编译作用域*/ this.context = context /*函数化组件作用域*/ this.functionalContext = undefined /*节点的key属性,被当作节点的标志,用以优化*/ this.key = data && data.key /*组件的option选项*/ this.componentOptions = componentOptions /*当前节点对应的组件的实例*/ this.componentInstance = undefined /*当前节点的父节点*/ this.parent = undefined /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/ this.raw = false /*静态节点标志*/ this.isStatic = false /*是否作为根节点插入*/ this.isRootInsert = true /*是否为注释节点*/ this.isComment = false /*是否为克隆节点*/ this.isCloned = false /*是否有v-once指令*/ this.isOnce = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } } ``` 这是一个最基础的VNode节点,作为其他派生VNode类的基类,里面定义了下面这些数据。 tag: 当前节点的标签名 data: 当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息 children: 当前节点的子节点,是一个数组 text: 当前节点的文本 elm: 当前虚拟节点对应的真实dom节点 ns: 当前节点的名字空间 context: 当前节点的编译作用域 functionalContext: 函数化组件作用域 key: 节点的key属性,被当作节点的标志,用以优化 componentOptions: 组件的option选项 componentInstance: 当前节点对应的组件的实例 parent: 当前节点的父节点 raw: 简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false isStatic: 是否为静态节点 isRootInsert: 是否作为跟节点插入 isComment: 是否为注释节点 isCloned: 是否为克隆节点 isOnce: 是否有v-once指令 --- 打个比方,比如说我现在有这么一个VNode树 ```JavaScript { tag: 'div' data: { class: 'test' }, children: [ { tag: 'span', data: { class: 'demo' } text: 'hello,VNode' } ] } ``` 渲染之后的结果就是这样的 ```html
hello,VNode
``` ## 生成一个新的VNode的方法 下面这些方法都是一些常用的构造VNode的方法。 ### createEmptyVNode 创建一个空VNode节点 ```javascript /*创建一个空VNode节点*/ export const createEmptyVNode = () => { const node = new VNode() node.text = '' node.isComment = true return node } ``` ### createTextVNode 创建一个文本节点 ```javascript /*创建一个文本节点*/ export function createTextVNode (val: string | number) { return new VNode(undefined, undefined, undefined, String(val)) } ``` ### createComponent 创建一个组件节点 ```javascript // plain options object: turn it into a constructor if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor) } // if at this stage it's not a constructor or an async component factory, // reject. /*Github:https://github.com/answershuto*/ /*如果在该阶段Ctor依然不是一个构造函数或者是一个异步组件工厂则直接返回*/ if (typeof Ctor !== 'function') { if (process.env.NODE_ENV !== 'production') { warn(`Invalid Component definition: ${String(Ctor)}`, context) } return } // async component /*处理异步组件*/ if (isUndef(Ctor.cid)) { Ctor = resolveAsyncComponent(Ctor, baseCtor, context) if (Ctor === undefined) { // return nothing if this is indeed an async component // wait for the callback to trigger parent update. /*如果这是一个异步组件则会不会返回任何东西(undifiened),直接return掉,等待回调函数去触发父组件更新。s*/ return } } // resolve constructor options in case global mixins are applied after // component constructor creation resolveConstructorOptions(Ctor) data = data || {} // transform component v-model data into props & events if (isDef(data.model)) { transformModel(Ctor.options, data) } // extract props const propsData = extractPropsFromVNodeData(data, Ctor, tag) // functional component if (isTrue(Ctor.options.functional)) { return createFunctionalComponent(Ctor, propsData, data, context, children) } // extract listeners, since these needs to be treated as // child component listeners instead of DOM listeners const listeners = data.on // replace with listeners with .native modifier data.on = data.nativeOn if (isTrue(Ctor.options.abstract)) { // abstract components do not keep anything // other than props & listeners data = {} } // merge component management hooks onto the placeholder node mergeHooks(data) // return a placeholder vnode const name = Ctor.options.name || tag const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children } ) return vnode } ``` ### cloneVNode 克隆一个VNode节点 ```javascript export function cloneVNode (vnode: VNode): VNode { const cloned = new VNode( vnode.tag, vnode.data, vnode.children, vnode.text, vnode.elm, vnode.context, vnode.componentOptions ) cloned.ns = vnode.ns cloned.isStatic = vnode.isStatic cloned.key = vnode.key cloned.isCloned = true return cloned } ``` ## createElement ```javascript // wrapper function for providing a more flexible interface // without getting yelled at by flow export function createElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean ): VNode { /*兼容不传data的情况*/ if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined } /*如果alwaysNormalize为true,则normalizationType标记为ALWAYS_NORMALIZE*/ if (isTrue(alwaysNormalize)) { normalizationType = ALWAYS_NORMALIZE } /*Github:https://github.com/answershuto*/ /*创建虚拟节点*/ return _createElement(context, tag, data, children, normalizationType) } /*创建虚拟节点*/ export function _createElement ( context: Component, tag?: string | Class | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode { /*    如果传递data参数且data的__ob__已经定义(代表已经被observed,上面绑定了Oberver对象), https://cn.vuejs.org/v2/guide/render-function.html#约束 那么创建一个空节点 */ if (isDef(data) && isDef((data: any).__ob__)) { process.env.NODE_ENV !== 'production' && warn( `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` + 'Always create fresh vnode data objects in each render!', context ) return createEmptyVNode() } /*如果tag不存在也是创建一个空节点*/ if (!tag) { // in case of component :is set to falsy value return createEmptyVNode() } // support single function children as default scoped slot /*默认默认作用域插槽*/ if (Array.isArray(children) && typeof children[0] === 'function') { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } let vnode, ns if (typeof tag === 'string') { let Ctor /*获取tag的名字空间*/ ns = config.getTagNamespace(tag) /*判断是否是保留的标签*/ if (config.isReservedTag(tag)) { // platform built-in elements /*如果是保留的标签则创建一个相应节点*/ vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component /*从vm实例的option的components中寻找该tag,存在则就是一个组件,创建相应节点,Ctor为组件的构造类*/ vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children /*未知的元素,在运行时检查,因为父组件可能在序列化子组件的时候分配一个名字空间*/ vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor /*tag不是字符串的时候则是组件的构造类*/ vnode = createComponent(tag, data, context, children) } if (isDef(vnode)) { /*如果有名字空间,则递归所有子节点应用该名字空间*/ if (ns) applyNS(vnode, ns) return vnode } else { /*如果vnode没有成功创建则创建空节点*/ return createEmptyVNode() } } ``` createElement用来创建一个虚拟节点。当data上已经绑定__ob__的时候,代表该对象已经被Oberver过了,所以创建一个空节点。tag不存在的时候同样创建一个空节点。当tag不是一个String类型的时候代表tag是一个组件的构造类,直接用new VNode创建。当tag是String类型的时候,如果是保留标签,则用new VNode创建一个VNode实例,如果在vm的option的components找得到该tag,代表这是一个组件,否则统一用new VNode创建。 ================================================ FILE: docs/VirtualDOM与diff(Vue实现).MarkDown ================================================ ## VNode 在刀耕火种的年代,我们需要在各个事件方法中直接操作DOM来达到修改视图的目的。但是当应用一大就会变得难以维护。 那我们是不是可以把真实DOM树抽象成一棵以JavaScript对象构成的抽象树,在修改抽象树数据后将抽象树转化成真实DOM重绘到页面上呢?于是虚拟DOM出现了,它是真实DOM的一层抽象,用属性描述真实DOM的各个特性。当它发生变化的时候,就会去修改视图。 可以想象,最简单粗暴的方法就是将整个DOM结构用innerHTML修改到页面上,但是这样进行重绘整个视图层是相当消耗性能的,我们是不是可以每次只更新它的修改呢?所以Vue.js将DOM抽象成一个以JavaScript对象为节点的虚拟DOM树,以VNode节点模拟真实DOM,可以对这颗抽象树进行创建节点、删除节点以及修改节点等操作,在这过程中都不需要操作真实DOM,只需要操作JavaScript对象后只对差异修改,相对于整块的innerHTML的粗暴式修改,大大提升了性能。修改以后经过diff算法得出一些需要修改的最小单位,再将这些小单位的视图进行更新。这样做减少了很多不需要的DOM操作,大大提高了性能。 Vue就使用了这样的抽象节点VNode,它是对真实DOM的一层抽象,而不依赖某个平台,它可以是浏览器平台,也可以是weex,甚至是node平台也可以对这样一棵抽象DOM树进行创建删除修改等操作,这也为前后端同构提供了可能。 具体VNode的细节可以看[VNode节点](https://github.com/answershuto/learnVue/blob/master/docs/VNode%E8%8A%82%E7%82%B9.MarkDown)。 ## 修改视图 众所周知,Vue通过数据绑定来修改视图,当某个数据被修改的时候,set方法会让闭包中的Dep调用notify通知所有订阅者Watcher,Watcher通过get方法执行vm._update(vm._render(), hydrating)。 这里看一下_update方法 ```JavaScript Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this /*如果已经该组件已经挂载过了则代表进入这个步骤是个更新的过程,触发beforeUpdate钩子*/ if (vm._isMounted) { callHook(vm, 'beforeUpdate') } const prevEl = vm.$el const prevVnode = vm._vnode const prevActiveInstance = activeInstance activeInstance = vm vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. /*基于后端渲染Vue.prototype.__patch__被用来作为一个入口*/ if (!prevVnode) { // initial render vm.$el = vm.__patch__( vm.$el, vnode, hydrating, false /* removeOnly */, vm.$options._parentElm, vm.$options._refElm ) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } activeInstance = prevActiveInstance // update __vue__ reference /*更新新的实例对象的__vue__*/ 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. } ``` _update方法的第一个参数是一个VNode对象,在内部会将该VNode对象与之前旧的VNode对象进行__patch__。 什么是__patch__呢? ## __patch__ patch将新老VNode节点进行比对,然后将根据两者的比较结果进行最小单位地修改视图,而不是将整个视图根据新的VNode重绘。patch的核心在于diff算法,这套算法可以高效地比较virtual DOM的变更,得出变化以修改视图。 那么patch如何工作的呢? 首先说一下patch的核心diff算法,diff算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有O(n),是一种相当高效的算法。 ![img](https://i.loli.net/2017/08/27/59a23cfca50f3.png) ![img](https://i.loli.net/2017/08/27/59a2419a3c617.png) 这两张图代表旧的VNode与新VNode进行patch的过程,他们只是在同层级的VNode之间进行比较得到变化(第二张图中相同颜色的方块代表互相进行比较的VNode节点),然后修改变化的视图,所以十分高效。 让我们看一下patch的代码。 ```JavaScript /*createPatchFunction的返回值,一个patch函数*/ return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { /*vnode不存在则直接调用销毁钩子*/ if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element /*oldVnode未定义的时候,其实也就是root节点,创建一个新的节点*/ isInitialPatch = true createElm(vnode, insertedVnodeQueue, parentElm, refElm) } else { /*标记旧的VNode是否有nodeType*/ /*Github:https://github.com/answershuto*/ const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node /*是同一个节点的时候直接修改现有的节点*/ patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) } else { if (isRealElement) { // mounting to a real element // check if this is server-rendered content and if we can perform // a successful hydration. if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { /*当旧的VNode是服务端渲染的元素,hydrating记为true*/ oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { /*需要合并到真实DOM上*/ if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { /*调用insert钩子*/ invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } else if (process.env.NODE_ENV !== 'production') { warn( 'The client-side rendered virtual DOM tree is not matching ' + 'server-rendered content. This is likely caused by incorrect ' + 'HTML markup, for example nesting block-level elements inside ' + '

, or missing . Bailing hydration and performing ' + 'full client-side render.' ) } } // either not server-rendered, or hydration failed. // create an empty node and replace it /*如果不是服务端渲染或者合并到真实DOM失败,则创建一个空的VNode节点替换它*/ oldVnode = emptyNodeAt(oldVnode) } // replacing existing element /*取代现有元素*/ const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) if (isDef(vnode.parent)) { // component root element replaced. // update parent placeholder node element, recursively /*组件根节点被替换,遍历更新父节点element*/ let ancestor = vnode.parent while (ancestor) { ancestor.elm = vnode.elm ancestor = ancestor.parent } if (isPatchable(vnode)) { /*调用create回调*/ for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode.parent) } } } if (isDef(parentElm)) { /*移除老节点*/ removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { /*Github:https://github.com/answershuto*/ /*调用destroy钩子*/ invokeDestroyHook(oldVnode) } } } /*调用insert钩子*/ invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm } ``` 从代码中不难发现,当oldVnode与vnode在sameVnode的时候才会进行patchVnode,也就是新旧VNode节点判定为同一节点的时候才会进行patchVnode这个过程,否则就是创建新的DOM,移除旧的DOM。 怎么样的节点算sameVnode呢? ## sameVnode 我们来看一下sameVnode的实现。 ```JavaScript /* 判断两个VNode节点是否是同一个节点,需要满足以下条件 key相同 tag(当前节点的标签名)相同 isComment(是否为注释节点)相同 是否data(当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息)都有定义 当标签是的时候,type必须相同 */ function sameVnode (a, b) { return ( a.key === b.key && a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) } // Some browsers do not support dynamically changing type for // so they need to be treated as different nodes /* 判断当标签是的时候,type是否相同 某些浏览器不支持动态修改类型,所以他们被视为不同节点 */ function sameInputType (a, b) { if (a.tag !== 'input') return true let i const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type return typeA === typeB } ``` 当两个VNode的tag、key、isComment都相同,并且同时定义或未定义data的时候,且如果标签为input则type必须相同。这时候这两个VNode则算sameVnode,可以直接进行patchVnode操作。 ## patchVnode 还是先来看一下patchVnode的代码。 ```JavaScript /*patch VNode节点*/ function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { /*两个VNode节点相同则直接返回*/ if (oldVnode === vnode) { return } // reuse element for static trees. // note we only do this if the vnode is cloned - // if the new node is not cloned it means the render functions have been // reset by the hot-reload-api and we need to do a proper re-render. /* 如果新旧VNode都是静态的,同时它们的key相同(代表同一节点), 并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次), 那么只需要替换elm以及componentInstance即可。 */ if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) { vnode.elm = oldVnode.elm vnode.componentInstance = oldVnode.componentInstance return } let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { /*i = data.hook.prepatch,如果存在的话,见"./create-component componentVNodeHooks"。*/ i(oldVnode, vnode) } const elm = vnode.elm = oldVnode.elm const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { /*调用update回调以及update钩子*/ for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } /*如果这个VNode节点没有text文本时*/ if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { /*新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren*/ if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { /*如果老节点没有子节点而新节点存在子节点,先清空elm的文本内容,然后为当前节点加入子节点*/ if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { /*当新节点没有子节点而老节点有子节点的时候,则移除所有ele的子节点*/ removeVnodes(elm, oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { /*当新老节点都无子节点的时候,只是文本的替换,因为这个逻辑中新节点text不存在,所以直接去除ele的文本*/ nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { /*当新老节点text不一样时,直接替换这段文本*/ nodeOps.setTextContent(elm, vnode.text) } /*调用postpatch钩子*/ if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } } ``` patchVnode的规则是这样的: 1.如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),那么只需要替换elm以及componentInstance即可。 2.新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren,这个updateChildren也是diff的核心。 3.如果老节点没有子节点而新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节点加入子节点。 4.当新节点没有子节点而老节点有子节点的时候,则移除该DOM节点的所有子节点。 5.当新老节点都无子节点的时候,只是文本的替换。 ## updateChildren ```JavaScript function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, elmToMove, refElm // removeOnly is a special flag used only by // to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { /*前四种情况其实是指定key的时候,判定为同一个VNode,则直接patchVnode即可,分别比较oldCh以及newCh的两头节点2*2=4种情况*/ patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { /* 生成一个key与旧VNode的key对应的哈希表(只有第一次进来undefined的时候会生成,也为后面检测重复的key值做铺垫) 比如childre是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}] beginIdx = 0 endIdx = 2 结果生成{key0: 0, key1: 1, key2: 2} */ if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) /*如果newStartVnode新的VNode节点存在key并且这个key在oldVnode中能找到则返回这个节点的idxInOld(即第几个节点,下标)*/ idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null if (isUndef(idxInOld)) { // New element /*newStartVnode没有key或者是该key没有在老节点中找到则创建一个新的节点*/ createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { /*获取同key的老节点*/ elmToMove = oldCh[idxInOld] /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !elmToMove) { /*如果elmToMove不存在说明之前已经有新节点放入过这个key的DOM中,提示可能存在重复的key,确保v-for的时候item有唯一的key值*/ warn( 'It seems there are duplicate keys that is causing an update error. ' + 'Make sure each v-for item has a unique key.' ) } if (sameVnode(elmToMove, newStartVnode)) { /*Github:https://github.com/answershuto*/ /*如果新VNode与得到的有相同key的节点是同一个VNode则进行patchVnode*/ patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) /*因为已经patchVnode进去了,所以将这个老节点赋值undefined,之后如果还有新节点与该节点key相同可以检测出来提示已有重复的key*/ oldCh[idxInOld] = undefined /*当有标识位canMove实可以直接插入oldStartVnode对应的真实DOM节点前面*/ canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { // same key but different element. treat as new element /*当新的VNode与找到的同样key的VNode不是sameVNode的时候(比如说tag不一样或者是有不一样type的input标签),创建一个新的节点*/ createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } } } } if (oldStartIdx > oldEndIdx) { /*全部比较完成以后,发现oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多,所以这时候多出来的新节点需要一个一个创建出来加入到真实DOM中*/ refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { /*如果全部比较完成以后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,老节点多余新节点,这个时候需要将多余的老节点从真实DOM中移除*/ removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } } ``` 直接看源码可能比较难以捋清其中的关系,我们通过图来看一下。 ![img](https://i.loli.net/2017/08/28/59a4015bb2765.png) 首先,在新老两个VNode节点的左右头尾两侧都有一个变量标记,在遍历过程中这几个变量都会向中间靠拢。当oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时结束循环。 索引与VNode节点的对应关系: oldStartIdx => oldStartVnode oldEndIdx => oldEndVnode newStartIdx => newStartVnode newEndIdx => newEndVnode 在遍历中,如果存在key,并且满足sameVnode,会将该DOM节点进行复用,否则则会创建一个新的DOM节点。 首先,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode两两比较一共有2*2=4种比较方法。 当新老VNode节点的start或者end满足sameVnode时,也就是sameVnode(oldStartVnode, newStartVnode)或者sameVnode(oldEndVnode, newEndVnode),直接将该VNode节点进行patchVnode即可。 ![img](https://i.loli.net/2017/08/28/59a40c12c1655.png) 如果oldStartVnode与newEndVnode满足sameVnode,即sameVnode(oldStartVnode, newEndVnode)。 这时候说明oldStartVnode已经跑到了oldEndVnode后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。 ![img](https://ooo.0o0.ooo/2017/08/28/59a4214784979.png) 如果oldEndVnode与newStartVnode满足sameVnode,即sameVnode(oldEndVnode, newStartVnode)。 这说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时真实的DOM节点移动到了oldStartVnode的前面。 ![img](https://i.loli.net/2017/08/29/59a4c70685d12.png) 如果以上情况均不符合,则通过createKeyToOldIdx会得到一个oldKeyToIdx,里面存放了一个key为旧的VNode,value为对应index序列的哈希表。从这个哈希表中可以找到是否有与newStartVnode一致key的旧的VNode节点,如果同时满足sameVnode,patchVnode的同时会将这个真实DOM(elmToMove)移动到oldStartVnode对应的真实DOM的前面。 ![img](https://i.loli.net/2017/08/29/59a4d7552d299.png) 当然也有可能newStartVnode在旧的VNode节点找不到一致的key,或者是即便key相同却不是sameVnode,这个时候会调用createElm创建一个新的DOM节点。 ![img](https://i.loli.net/2017/08/29/59a4de0fa4dba.png) 到这里循环已经结束了,那么剩下我们还需要处理多余或者不够的真实DOM节点。 1.当结束时oldStartIdx > oldEndIdx,这个时候老的VNode节点已经遍历完了,但是新的节点还没有。说明了新的VNode节点实际上比老的VNode节点多,也就是比真实DOM多,需要将剩下的(也就是新增的)VNode节点插入到真实DOM节点中去,此时调用addVnodes(批量调用createElm的接口将这些节点加入到真实DOM中去)。 ![img](https://i.loli.net/2017/08/29/59a509f0d1788.png) 2。同理,当newStartIdx > newEndIdx时,新的VNode节点已经遍历完了,但是老的节点还有剩余,说明真实DOM节点多余了,需要从文档中删除,这时候调用removeVnodes将这些多余的真实DOM删除。 ![img](https://i.loli.net/2017/08/29/59a4f389b98cb.png) ## DOM操作 由于Vue使用了虚拟DOM,所以虚拟DOM可以在任何支持JavaScript语言的平台上操作,譬如说目前Vue支持的浏览器平台或是weex,在虚拟DOM的实现上是一致的。那么最后虚拟DOM如何映射到真实的DOM节点上呢? Vue为平台做了一层适配层,浏览器平台见[/platforms/web/runtime/node-ops.js](https://github.com/answershuto/learnVue/blob/master/vue-src/platforms/web/runtime/node-ops.js)以及weex平台见[/platforms/weex/runtime/node-ops.js](https://github.com/answershuto/learnVue/blob/master/vue-src/platforms/weex/runtime/node-ops.js)。不同平台之间通过适配层对外提供相同的接口,虚拟DOM进行操作真实DOM节点的时候,只需要调用这些适配层的接口即可,而内部实现则不需要关心,它会根据平台的改变而改变。 现在又出现了一个问题,我们只是将虚拟DOM映射成了真实的DOM。那如何给这些DOM加入attr、class、style等DOM属性呢? 这要依赖于虚拟DOM的生命钩子。虚拟DOM提供了如下的钩子函数,分别在不同的时期会进行调用。 ```JavaScript const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] /*构建cbs回调函数,web平台上见/platforms/web/runtime/modules*/ for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { cbs[hooks[i]].push(modules[j][hooks[i]]) } } } ``` 同理,也会根据不同平台有自己不同的实现,我们这里以Web平台为例。Web平台的钩子函数见[/platforms/web/runtime/modules](https://github.com/answershuto/learnVue/tree/master/vue-src/platforms/web/runtime/modules)。里面有对attr、class、props、events、style以及transition(过渡状态)的DOM属性进行操作。 以attr为例,代码很简单。 ```JavaScript /* @flow */ import { isIE9 } from 'core/util/env' import { extend, isDef, isUndef } from 'shared/util' import { isXlink, xlinkNS, getXlinkProp, isBooleanAttr, isEnumeratedAttr, isFalsyAttrValue } from 'web/util/index' /*更新attr*/ function updateAttrs (oldVnode: VNodeWithData, vnode: VNodeWithData) { /*如果旧的以及新的VNode节点均没有attr属性,则直接返回*/ if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) { return } let key, cur, old /*VNode节点对应的Dom实例*/ const elm = vnode.elm /*旧VNode节点的attr*/ const oldAttrs = oldVnode.data.attrs || {} /*新VNode节点的attr*/ let attrs: any = vnode.data.attrs || {} // clone observed objects, as the user probably wants to mutate it /*如果新的VNode的attr已经有__ob__(代表已经被Observe处理过了), 进行深拷贝*/ if (isDef(attrs.__ob__)) { attrs = vnode.data.attrs = extend({}, attrs) } /*遍历attr,不一致则替换*/ for (key in attrs) { cur = attrs[key] old = oldAttrs[key] if (old !== cur) { setAttr(elm, key, cur) } } // #4391: in IE9, setting type can reset value for input[type=radio] /* istanbul ignore if */ if (isIE9 && attrs.value !== oldAttrs.value) { setAttr(elm, 'value', attrs.value) } for (key in oldAttrs) { if (isUndef(attrs[key])) { if (isXlink(key)) { elm.removeAttributeNS(xlinkNS, getXlinkProp(key)) } else if (!isEnumeratedAttr(key)) { elm.removeAttribute(key) } } } } /*设置attr*/ function setAttr (el: Element, key: string, value: any) { if (isBooleanAttr(key)) { // set attribute for blank value // e.g. if (isFalsyAttrValue(value)) { el.removeAttribute(key) } else { el.setAttribute(key, key) } } else if (isEnumeratedAttr(key)) { el.setAttribute(key, isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true') } else if (isXlink(key)) { if (isFalsyAttrValue(value)) { el.removeAttributeNS(xlinkNS, getXlinkProp(key)) } else { el.setAttributeNS(xlinkNS, key, value) } } else { if (isFalsyAttrValue(value)) { el.removeAttribute(key) } else { el.setAttribute(key, value) } } } export default { create: updateAttrs, update: updateAttrs } ``` attr只需要在create以及update钩子被调用时更新DOM的attr属性即可。 ================================================ FILE: docs/Vue.js异步更新DOM策略及nextTick.MarkDown ================================================ ## 操作DOM 在使用vue.js的时候,有时候因为一些特定的业务场景,不得不去操作DOM,比如这样: ```html ``` ```javascript export default { data () { return { test: 'begin' }; }, methods () { handleClick () { this.test = 'end'; console.log(this.$refs.test.innerText);//打印“begin” } } } ``` 打印的结果是begin,为什么我们明明已经将test设置成了“end”,获取真实DOM节点的innerText却没有得到我们预期中的“end”,而是得到之前的值“begin”呢? ## Watcher队列 带着疑问,我们找到了Vue.js源码的Watch实现。当某个响应式数据发生变化的时候,它的setter函数会通知闭包中的Dep,Dep则会调用它管理的所有Watch对象。触发Watch对象的update实现。我们来看一下update的实现。 ```javascript update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { /*同步则执行run直接渲染视图*/ this.run() } else { /*异步推送到观察者队列中,下一个tick时调用。*/ queueWatcher(this) } } ``` 我们发现Vue.js默认是使用[异步执行DOM更新](https://cn.vuejs.org/v2/guide/reactivity.html#异步更新队列)。 当异步执行update的时候,会调用queueWatcher函数。 ```javascript /*将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送*/ export function queueWatcher (watcher: Watcher) { /*获取watcher的id*/ const id = watcher.id /*检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验*/ if (has[id] == null) { has[id] = true if (!flushing) { /*如果没有flush掉,直接push到队列中即可*/ queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i >= 0 && queue[i].id > watcher.id) { i-- } queue.splice(Math.max(i, index) + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true nextTick(flushSchedulerQueue) } } } ``` 查看queueWatcher的源码我们发现,Watch对象并不是立即更新视图,而是被push进了一个队列queue,此时状态处于waiting的状态,这时候会继续会有Watch对象被push进这个队列queue,等到下一个tick运行时,这些Watch对象才会被遍历取出,更新视图。同时,id重复的Watcher不会被多次加入到queue中去,因为在最终渲染时,我们只需要关心数据的最终结果。 那么,什么是下一个tick? ## nextTick vue.js提供了一个[nextTick](https://cn.vuejs.org/v2/api/#Vue-nextTick)函数,其实也就是上面调用的nextTick。 nextTick的实现比较简单,执行的目的是在microtask或者task中推入一个function,在当前栈执行完毕(也许还会有一些排在前面的需要执行的任务)以后执行nextTick传入的function,看一下源码: ```javascript /** * Defer a task to execute it asynchronously. */ /* 延迟一个任务使其异步执行,在下一个tick时执行,一个立即执行函数,返回一个function 这个函数的作用是在task或者microtask中推入一个timerFunc,在当前调用栈执行完以后以此执行直到执行到timerFunc 目的是延迟到当前调用栈执行完以后执行 */ export const nextTick = (function () { /*存放异步执行的回调*/ const callbacks = [] /*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/ let pending = false /*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/ let timerFunc /*下一个tick时的回调*/ function nextTickHandler () { /*一个标记位,标记等待状态(即函数已经被推入任务队列或者主线程,已经在等待当前栈执行完毕去执行),这样就不需要在push多个回调到callbacks时将timerFunc多次推入任务队列或者主线程*/ pending = false /*执行所有callback*/ const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } // the nextTick behavior leverages the microtask queue, which can be accessed // via either native Promise.then or MutationObserver. // MutationObserver has wider support, however it is seriously bugged in // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It // completely stops working after triggering a few times... so, if native // Promise is available, we will use it: /* istanbul ignore if */ /* 这里解释一下,一共有Promise、MutationObserver以及setTimeout三种尝试得到timerFunc的方法 优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法都会在microtask中执行,会比setTimeout更早执行,所以优先使用。 如果上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。 参考:https://www.zhihu.com/question/55364497 */ if (typeof Promise !== 'undefined' && isNative(Promise)) { /*使用Promise*/ var p = Promise.resolve() var logError = err => { console.error(err) } timerFunc = () => { p.then(nextTickHandler).catch(logError) // in problematic UIWebViews, Promise.then doesn't completely break, but // it can get stuck in a weird state where callbacks are pushed into the // microtask queue but the queue isn't being flushed, until the browser // needs to do some other work, e.g. handle a timer. Therefore we can // "force" the microtask queue to be flushed by adding an empty timer. if (isIOS) setTimeout(noop) } } else if (typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // use MutationObserver where native Promise is not available, // e.g. PhantomJS IE11, iOS7, Android 4.4 /*新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行),即textNode.data = String(counter)时便会触发回调*/ var counter = 1 var observer = new MutationObserver(nextTickHandler) var textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } } else { // fallback to setTimeout /* istanbul ignore next */ /*使用setTimeout将回调推入任务队列尾部*/ timerFunc = () => { setTimeout(nextTickHandler, 0) } } /* 推送到队列中下一个tick时执行 cb 回调函数 ctx 上下文 */ return function queueNextTick (cb?: Function, ctx?: Object) { let _resolve /*cb存到callbacks中*/ callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise((resolve, reject) => { _resolve = resolve }) } } })() ``` 它是一个立即执行函数,返回一个queueNextTick接口。 传入的cb会被push进callbacks中存放起来,然后执行timerFunc(pending是一个状态标记,保证timerFunc在下一个tick之前只执行一次)。 timerFunc是什么? 看了源码发现timerFunc会检测当前环境而不同实现,其实就是按照Promise,MutationObserver,setTimeout优先级,哪个存在使用哪个,最不济的环境下使用setTimeout。 这里解释一下,一共有Promise、MutationObserver以及setTimeout三种尝试得到timerFunc的方法。 优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法的回调函数都会在microtask中执行,它们会比setTimeout更早执行,所以优先使用。 如果上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。 为什么要优先使用microtask?我在顾轶灵在知乎的回答中学习到: ``` JS 的 event loop 执行时会区分 task 和 microtask,引擎在每个 task 执行完毕,从队列中取下一个 task 来执行之前,会先执行完所有 microtask 队列中的 microtask。 setTimeout 回调会被分配到一个新的 task 中执行,而 Promise 的 resolver、MutationObserver 的回调都会被安排到一个新的 microtask 中执行,会比 setTimeout 产生的 task 先执行。 要创建一个新的 microtask,优先使用 Promise,如果浏览器不支持,再尝试 MutationObserver。 实在不行,只能用 setTimeout 创建 task 了。 为啥要用 microtask? 根据 HTML Standard,在每个 task 运行完以后,UI 都会重渲染,那么在 microtask 中就完成数据更新,当前 task 结束就可以得到最新的 UI 了。 反之如果新建一个 task 来做数据更新,那么渲染就会进行两次。 参考顾轶灵知乎的回答:https://www.zhihu.com/question/55364497/answer/144215284 ``` 首先是Promise,Promise.resolve().then()可以在microtask中加入它的回调, MutationObserver新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入microtask,即textNode.data = String(counter)时便会加入该回调。 setTimeout是最后的一种备选方案,它会将回调函数加入task中,等到执行。 综上,nextTick的目的就是产生一个回调函数加入task或者microtask中,当前栈执行完以后(可能中间还有别的排在前面的函数)调用该回调函数,起到了异步触发(即下一个tick时触发)的目的。 ## flushSchedulerQueue ```javascript /*Github:https://github.com/answershuto*/ /** * Flush both queues and run the watchers. */ /*nextTick的回调函数,在下一个tick时flush掉两个队列同时运行watchers*/ function flushSchedulerQueue () { flushing = true let watcher, id // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child) // 2. A component's user watchers are run before its render watcher (because // user watchers are created before the render watcher) // 3. If a component is destroyed during a parent component's watcher run, // its watchers can be skipped. /* 给queue排序,这样做可以保证: 1.组件更新的顺序是从父组件到子组件的顺序,因为父组件总是比子组件先创建。 2.一个组件的user watchers比render watcher先运行,因为user watchers往往比render watcher更早创建 3.如果一个组件在父组件watcher运行期间被销毁,它的watcher执行将被跳过。 */ queue.sort((a, b) => a.id - b.id) // do not cache length because more watchers might be pushed // as we run existing watchers /*这里不用index = queue.length;index > 0; index--的方式写是因为不要将length进行缓存,因为在执行处理现有watcher对象期间,更多的watcher对象可能会被push进queue*/ for (index = 0; index < queue.length; index++) { watcher = queue[index] id = watcher.id /*将has的标记删除*/ has[id] = null /*执行watcher*/ watcher.run() // in dev build, check and stop circular updates. /* 在测试环境中,检测watch是否在死循环中 比如这样一种情况 watch: { test () { this.test++; } } 持续执行了一百次watch代表可能存在死循环 */ if (process.env.NODE_ENV !== 'production' && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] > MAX_UPDATE_COUNT) { warn( 'You may have an infinite update loop ' + ( watcher.user ? `in watcher with expression "${watcher.expression}"` : `in a component render function.` ), watcher.vm ) break } } } // keep copies of post queues before resetting state /**/ /*得到队列的拷贝*/ const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() /*重置调度者的状态*/ resetSchedulerState() // call component updated and activated hooks /*使子组件状态都改编成active同时调用activated钩子*/ callActivatedHooks(activatedQueue) /*调用updated钩子*/ callUpdateHooks(updatedQueue) // devtool hook /* istanbul ignore if */ if (devtools && config.devtools) { devtools.emit('flush') } } ``` flushSchedulerQueue是下一个tick时的回调函数,主要目的是执行Watcher的run函数,用来更新视图 ## 为什么要异步更新视图 来看一下下面这一段代码 ```html ``` ```javascript export default { data () { return { test: 0 }; }, mounted () { for(let i = 0; i < 1000; i++) { this.test++; } } } ``` 现在有这样的一种情况,mounted的时候test的值会被++循环执行1000次。 每次++时,都会根据响应式触发setter->Dep->Watcher->update->patch。 如果这时候没有异步更新视图,那么每次++都会直接操作DOM更新视图,这是非常消耗性能的。 所以Vue.js实现了一个queue队列,在下一个tick的时候会统一执行queue中Watcher的run。同时,拥有相同id的Watcher不会被重复加入到该queue中去,所以不会执行1000次Watcher的run。最终更新视图只会直接将test对应的DOM的0变成1000。 保证更新视图操作DOM的动作是在当前栈执行完以后下一个tick的时候调用,大大优化了性能。 ## 访问真实DOM节点更新后的数据 所以我们需要在修改data中的数据后访问真实的DOM节点更新后的数据,只需要这样,我们把文章第一个例子进行修改。 ```html ``` ```javascript export default { data () { return { test: 'begin' }; }, methods () { handleClick () { this.test = 'end'; this.$nextTick(() => { console.log(this.$refs.test.innerText);//打印"end" }); console.log(this.$refs.test.innerText);//打印“begin” } } } ``` 使用Vue.js的global API的$nextTick方法,即可在回调中获取已经更新好的DOM实例了。 ================================================ FILE: docs/Vuex源码解析.MarkDown ================================================ ## Vuex 我们在使用Vue.js开发复杂的应用时,经常会遇到多个组件共享同一个状态,亦或是多个组件会去更新同一个状态,在应用代码量较少的时候,我们可以组件间通信去维护修改数据,或者是通过事件总线来进行数据的传递以及修改。但是当应用逐渐庞大以后,代码就会变得难以维护,从父组件开始通过prop传递多层嵌套的数据由于层级过深而显得异常脆弱,而事件总线也会因为组件的增多、代码量的增大而显得交互错综复杂,难以捋清其中的传递关系。 那么为什么我们不能将数据层与组件层抽离开来呢?把数据层放到全局形成一个单一的Store,组件层变得更薄,专门用来进行数据的展示及操作。所有数据的变更都需要经过全局的Store来进行,形成一个单向数据流,使数据变化变得“可预测”。 Vuex是一个专门为Vue.js框架设计的、用于对Vue.js应用程序进行状态管理的库,它借鉴了Flux、redux的基本思想,将共享的数据抽离到全局,以一个单例存放,同时利用Vue.js的响应式机制来进行高效的状态管理与更新。正是因为Vuex使用了Vue.js内部的“响应式机制”,所以Vuex是一个专门为Vue.js设计并与之高度契合的框架(优点是更加简洁高效,缺点是只能跟Vue.js搭配使用)。具体使用方法及API可以参考[Vuex的官网](https://vuex.vuejs.org/zh-cn/intro.html)。 先来看一下这张Vuex的数据流程图,熟悉Vuex使用的同学应该已经有所了解。 ![](https://vuex.vuejs.org/vuex.png) Vuex实现了一个单向数据流,在全局拥有一个State存放数据,所有修改State的操作必须通过Mutation进行,Mutation的同时提供了订阅者模式供外部插件调用获取State数据的更新。所有异步接口需要走Action,常见于调用后端接口异步获取更新数据,而Action也是无法直接修改State的,还是需要通过Mutation来修改State的数据。最后,根据State的变化,渲染到视图上。Vuex运行依赖Vue内部数据双向绑定机制,需要new一个Vue对象来实现“响应式化”,所以Vuex是一个专门为Vue.js设计的状态管理库。 ## 安装 使用过Vuex的朋友一定知道,Vuex的安装十分简单,只需要提供一个store,然后执行下面两句代码即完成的Vuex的引入。 ```javascript Vue.use(Vuex); /*将store放入Vue创建时的option中*/ new Vue({ el: '#app', store }); ``` 那么问题来了,Vuex是怎样把store注入到Vue实例中去的呢? Vue.js提供了[Vue.use](https://cn.vuejs.org/v2/api/#Vue-use)方法用来给Vue.js安装插件,内部通过调用插件的install方法(当插件是一个对象的时候)来进行插件的安装。 我们来看一下Vuex的install实现。 ```javascript /*暴露给外部的插件install方法,供Vue.use调用安装插件*/ export function install (_Vue) { if (Vue) { /*避免重复安装(Vue.use内部也会检测一次是否重复安装同一个插件)*/ if (process.env.NODE_ENV !== 'production') { console.error( '[vuex] already installed. Vue.use(Vuex) should be called only once.' ) } return } /*保存Vue,同时用于检测是否重复安装*/ Vue = _Vue /*将vuexInit混淆进Vue的beforeCreate(Vue2.0)或_init方法(Vue1.0)*/ applyMixin(Vue) } ``` 这段install代码做了两件事情,一件是防止Vuex被重复安装,另一件是执行applyMixin,目的是执行vuexInit方法初始化Vuex。Vuex针对Vue1.0与2.0分别进行了不同的处理,如果是Vue1.0,Vuex会将vuexInit方法放入Vue的_init方法中,而对于Vue2.0,则会将vuexinit混淆进Vue的beforeCreate钩子中。来看一下vuexInit的代码。 ```javascript /*Vuex的init钩子,会存入每一个Vue实例等钩子列表*/ function vuexInit () { const options = this.$options // store injection if (options.store) { /*存在store其实代表的就是Root节点,直接执行store(function时)或者使用store(非function)*/ this.$store = typeof options.store === 'function' ? options.store() : options.store } else if (options.parent && options.parent.$store) { /*子组件直接从父组件中获取$store,这样就保证了所有组件都公用了全局的同一份store*/ this.$store = options.parent.$store } } ``` vuexInit会尝试从options中获取store,如果当前组件是根组件(Root节点),则options中会存在store,直接获取赋值给$store即可。如果当前组件非根组件,则通过options中的parent获取父组件的$store引用。这样一来,所有的组件都获取到了同一份内存地址的Store实例,于是我们可以在每一个组件中通过this.$store愉快地访问全局的Store实例了。 那么,什么是Store实例? ## Store 我们传入到根组件的store,就是Store实例,用Vuex提供的Store方法构造。 ```javascript export default new Vuex.Store({ strict: true, modules: { moduleA, moduleB } }); ``` 我们来看一下Store的实现。首先是构造函数。 ```javascript constructor (options = {}) { // Auto install if it is not done yet and `window` has `Vue`. // To allow users to avoid auto-installation in some cases, // this code should be placed here. See #731 /* 在浏览器环境下,如果插件还未安装(!Vue即判断是否未安装),则它会自动安装。 它允许用户在某些情况下避免自动安装。 */ if (!Vue && typeof window !== 'undefined' && window.Vue) { install(window.Vue) } if (process.env.NODE_ENV !== 'production') { assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`) assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`) assert(this instanceof Store, `Store must be called with the new operator.`) } const { /*一个数组,包含应用在 store 上的插件方法。这些插件直接接收 store 作为唯一参数,可以监听 mutation(用于外部地数据持久化、记录或调试)或者提交 mutation (用于内部数据,例如 websocket 或 某些观察者)*/ plugins = [], /*使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。*/ strict = false } = options /*从option中取出state,如果state是function则执行,最终得到一个对象*/ let { state = {} } = options if (typeof state === 'function') { state = state() } // store internal state /* 用来判断严格模式下是否是用mutation修改state的 */ this._committing = false /* 存放action */ this._actions = Object.create(null) /* 存放mutation */ this._mutations = Object.create(null) /* 存放getter */ this._wrappedGetters = Object.create(null) /* module收集器 */ this._modules = new ModuleCollection(options) /* 根据namespace存放module */ this._modulesNamespaceMap = Object.create(null) /* 存放订阅者 */ this._subscribers = [] /* 用以实现Watch的Vue实例 */ this._watcherVM = new Vue() // bind commit and dispatch to self /*将dispatch与commit调用的this绑定为store对象本身,否则在组件内部this.dispatch时的this会指向组件的vm*/ const store = this const { dispatch, commit } = this /* 为dispatch与commit绑定this(Store实例本身) */ this.dispatch = function boundDispatch (type, payload) { return dispatch.call(store, type, payload) } this.commit = function boundCommit (type, payload, options) { return commit.call(store, type, payload, options) } // strict mode /*严格模式(使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误)*/ this.strict = strict // init root module. // this also recursively registers all sub-modules // and collects all module getters inside this._wrappedGetters /*初始化根module,这也同时递归注册了所有子module,收集所有module的getter到_wrappedGetters中去,this._modules.root代表根module才独有保存的Module对象*/ installModule(this, state, [], this._modules.root) // initialize the store vm, which is responsible for the reactivity // (also registers _wrappedGetters as computed properties) /* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */ resetStoreVM(this, state) // apply plugins /* 调用插件 */ plugins.forEach(plugin => plugin(this)) /* devtool插件 */ if (Vue.config.devtools) { devtoolPlugin(this) } } ``` Store的构造类除了初始化一些内部变量以外,主要执行了installModule(初始化module)以及resetStoreVM(通过VM使store“响应式”)。 ### installModule installModule的作用主要是为module加上namespace名字空间(如果有)后,注册mutation、action以及getter,同时递归安装所有子module。 ```javascript /*初始化module*/ function installModule (store, rootState, path, module, hot) { /* 是否是根module */ const isRoot = !path.length /* 获取module的namespace */ const namespace = store._modules.getNamespace(path) // register in namespace map /* 如果有namespace则在_modulesNamespaceMap中注册 */ if (module.namespaced) { store._modulesNamespaceMap[namespace] = module } // set state if (!isRoot && !hot) { /* 获取父级的state */ const parentState = getNestedState(rootState, path.slice(0, -1)) /* module的name */ const moduleName = path[path.length - 1] store.`_withCommit`(() => { /* 将子module设成响应式的 */ Vue.set(parentState, moduleName, module.state) }) } const local = module.context = makeLocalContext(store, namespace, path) /* 遍历注册mutation */ module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(store, namespacedType, mutation, local) }) /* 遍历注册action */ module.forEachAction((action, key) => { const namespacedType = namespace + key registerAction(store, namespacedType, action, local) }) /* 遍历注册getter */ module.forEachGetter((getter, key) => { const namespacedType = namespace + key registerGetter(store, namespacedType, getter, local) }) /* 递归安装mudule */ module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) }) } ``` ### resetStoreVM 在说resetStoreVM之前,先来看一个小demo。 ```javascript let globalData = { d: 'hello world' }; new Vue({ data () { return { $$state: { globalData } } } }); /* modify */ setTimeout(() => { globalData.d = 'hi~'; }, 1000); Vue.prototype.globalData = globalData; /* 任意模板中 */

{{globalData.d}}
``` 上述代码在全局有一个globalData,它被传入一个Vue对象的data中,之后在任意Vue模板中对该变量进行展示,因为此时globalData已经在Vue的prototype上了所以直接通过this.prototype访问,也就是在模板中的{{prototype.d}}。此时,setTimeout在1s之后将globalData.d进行修改,我们发现模板中的globalData.d发生了变化。其实上述部分就是Vuex依赖Vue核心实现数据的“响应式化”。 不熟悉Vue.js响应式原理的同学可以通过笔者另一篇文章[响应式原理](https://github.com/answershuto/learnVue/blob/master/docs/%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86.MarkDown)了解Vue.js是如何进行数据双向绑定的。 接着来看代码。 ```javascript /* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */ function resetStoreVM (store, state, hot) { /* 存放之前的vm对象 */ const oldVm = store._vm // bind store public getters store.getters = {} const wrappedGetters = store._wrappedGetters const computed = {} /* 通过Object.defineProperty为每一个getter方法设置get方法,比如获取this.$store.getters.test的时候获取的是store._vm.test,也就是Vue对象的computed属性 */ forEachValue(wrappedGetters, (fn, key) => { // use computed to leverage its lazy-caching mechanism computed[key] = () => fn(store) Object.defineProperty(store.getters, key, { get: () => store._vm[key], enumerable: true // for local getters }) }) // use a Vue instance to store the state tree // suppress warnings just in case the user has added // some funky global mixins const silent = Vue.config.silent /* Vue.config.silent暂时设置为true的目的是在new一个Vue实例的过程中不会报出一切警告 */ Vue.config.silent = true /* 这里new了一个Vue对象,运用Vue内部的响应式实现注册state以及computed*/ store._vm = new Vue({ data: { $$state: state }, computed }) Vue.config.silent = silent // enable strict mode for new vm /* 使能严格模式,保证修改store只能通过mutation */ if (store.strict) { enableStrictMode(store) } if (oldVm) { /* 解除旧vm的state的引用,以及销毁旧的Vue对象 */ if (hot) { // dispatch changes in all subscribed watchers // to force getter re-evaluation for hot reloading. store._withCommit(() => { oldVm._data.$$state = null }) } Vue.nextTick(() => oldVm.$destroy()) } } ``` resetStoreVM首先会遍历wrappedGetters,使用Object.defineProperty方法为每一个getter绑定上get方法,这样我们就可以在组件里访问this.$store.getters.test就等同于访问store._vm.test。 ```javascript forEachValue(wrappedGetters, (fn, key) => { // use computed to leverage its lazy-caching mechanism computed[key] = () => fn(store) Object.defineProperty(store.getters, key, { get: () => store._vm[key], enumerable: true // for local getters }) }) ``` 之后Vuex采用了new一个Vue对象来实现数据的“响应式化”,运用Vue.js内部提供的数据双向绑定功能来实现store的数据与视图的同步更新。 ```javascript store._vm = new Vue({ data: { $$state: state }, computed }) ``` 这时候我们访问store._vm.test也就访问了Vue实例中的属性。 这两步执行完以后,我们就可以通过this.$store.getter.test访问vm中的test属性了。 ### 严格模式 Vuex的Store构造类的option有一个strict的参数,可以控制Vuex执行严格模式,严格模式下,所有修改state的操作必须通过mutation实现,否则会抛出错误。 ```javascript /* 使能严格模式 */ function enableStrictMode (store) { store._vm.$watch(function () { return this._data.$$state }, () => { if (process.env.NODE_ENV !== 'production') { /* 检测store中的_committing的值,如果是false代表不是通过mutation的方法修改的 */ assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`) } }, { deep: true, sync: true }) } ``` 首先,在严格模式下,Vuex会利用vm的$watch方法来观察$$state,也就是Store的state,在它被修改的时候进入回调。我们发现,回调中只有一句话,用assert断言来检测store._committing,当store._committing为false的时候会触发断言,抛出异常。 我们发现,Store的commit方法中,执行mutation的语句是这样的。 ```javascript this._withCommit(() => { entry.forEach(function commitIterator (handler) { handler(payload) }) }) ``` 再来看看_withCommit的实现。 ```javascript _withCommit (fn) { /* 调用withCommit修改state的值时会将store的committing值置为true,内部会有断言检查该值,在严格模式下只允许使用mutation来修改store中的值,而不允许直接修改store的数值 */ const committing = this._committing this._committing = true fn() this._committing = committing } ``` 我们发现,通过commit(mutation)修改state数据的时候,会在调用mutation方法之前将committing置为true,接下来再通过mutation函数修改state中的数据,这时候触发$watch中的回调断言committing是不会抛出异常的(此时committing为true)。而当我们直接修改state的数据时,触发$watch的回调执行断言,这时committing为false,则会抛出异常。这就是Vuex的严格模式的实现。 接下来我们来看看Store提供的一些API。 ### commit([mutation](https://vuex.vuejs.org/zh-cn/mutations.html)) ```javascript /* 调用mutation的commit方法 */ commit (_type, _payload, _options) { // check object-style commit /* 校验参数 */ const { type, payload, options } = unifyObjectStyle(_type, _payload, _options) const mutation = { type, payload } /* 取出type对应的mutation的方法 */ const entry = this._mutations[type] if (!entry) { if (process.env.NODE_ENV !== 'production') { console.error(`[vuex] unknown mutation type: ${type}`) } return } /* 执行mutation中的所有方法 */ this._withCommit(() => { entry.forEach(function commitIterator (handler) { handler(payload) }) }) /* 通知所有订阅者 */ this._subscribers.forEach(sub => sub(mutation, this.state)) if ( process.env.NODE_ENV !== 'production' && options && options.silent ) { console.warn( `[vuex] mutation type: ${type}. Silent option has been removed. ` + 'Use the filter functionality in the vue-devtools' ) } } ``` commit方法会根据type找到并调用_mutations中的所有type对应的mutation方法,所以当没有namespace的时候,commit方法会触发所有module中的mutation方法。再执行完所有的mutation之后会执行_subscribers中的所有订阅者。我们来看一下_subscribers是什么。 Store给外部提供了一个subscribe方法,用以注册一个订阅函数,会push到Store实例的_subscribers中,同时返回一个从_subscribers中注销该订阅者的方法。 ```javascript /* 注册一个订阅函数,返回取消订阅的函数 */ subscribe (fn) { const subs = this._subscribers if (subs.indexOf(fn) < 0) { subs.push(fn) } return () => { const i = subs.indexOf(fn) if (i > -1) { subs.splice(i, 1) } } } ``` 在commit结束以后则会调用这些_subscribers中的订阅者,这个订阅者模式提供给外部一个监视state变化的可能。state通过mutation改变时,可以有效补获这些变化。 ### dispatch([action](https://vuex.vuejs.org/zh-cn/actions.html)) 来看一下dispatch的实现。 ```javascript /* 调用action的dispatch方法 */ dispatch (_type, _payload) { // check object-style dispatch const { type, payload } = unifyObjectStyle(_type, _payload) /* actions中取出type对应的action */ const entry = this._actions[type] if (!entry) { if (process.env.NODE_ENV !== 'production') { console.error(`[vuex] unknown action type: ${type}`) } return } /* 是数组则包装Promise形成一个新的Promise,只有一个则直接返回第0个 */ return entry.length > 1 ? Promise.all(entry.map(handler => handler(payload))) : entry[0](payload) } ``` 以及registerAction时候做的事情。 ```javascript /* 遍历注册action */ function registerAction (store, type, handler, local) { /* 取出type对应的action */ const entry = store._actions[type] || (store._actions[type] = []) entry.push(function wrappedActionHandler (payload, cb) { let res = handler.call(store, { dispatch: local.dispatch, commit: local.commit, getters: local.getters, state: local.state, rootGetters: store.getters, rootState: store.state }, payload, cb) /* 判断是否是Promise */ if (!isPromise(res)) { /* 不是Promise对象的时候转化称Promise对象 */ res = Promise.resolve(res) } if (store._devtoolHook) { /* 存在devtool插件的时候触发vuex的error给devtool */ return res.catch(err => { store._devtoolHook.emit('vuex:error', err) throw err }) } else { return res } }) } ``` 因为registerAction的时候将push进_actions的action进行了一层封装(wrappedActionHandler),所以我们在进行dispatch的第一个参数中获取state、commit等方法。之后,执行结果res会被进行判断是否是Promise,不是则会进行一层封装,将其转化成Promise对象。dispatch时则从_actions中取出,只有一个的时候直接返回,否则用Promise.all处理再返回。 ### watch ```javascript /* 观察一个getter方法 */ watch (getter, cb, options) { if (process.env.NODE_ENV !== 'production') { assert(typeof getter === 'function', `store.watch only accepts a function.`) } return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options) } ``` 熟悉Vue的朋友应该很熟悉watch这个方法。这里采用了比较巧妙的设计,_watcherVM是一个Vue的实例,所以watch就可以直接采用了Vue内部的watch特性提供了一种观察数据getter变动的方法。 ### registerModule ```javascript /* 注册一个动态module,当业务进行异步加载的时候,可以通过该接口进行注册动态module */ registerModule (path, rawModule) { /* 转化称Array */ if (typeof path === 'string') path = [path] if (process.env.NODE_ENV !== 'production') { assert(Array.isArray(path), `module path must be a string or an Array.`) assert(path.length > 0, 'cannot register the root module by using registerModule.') } /*注册*/ this._modules.register(path, rawModule) /*初始化module*/ installModule(this, this.state, path, this._modules.get(path)) // reset store to update getters... /* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */ resetStoreVM(this, this.state) } ``` registerModule用以注册一个动态模块,也就是在store创建以后再注册模块的时候用该接口。内部实现实际上也只有installModule与resetStoreVM两个步骤,前面已经讲过,这里不再累述。 ### unregisterModule ```javascript /* 注销一个动态module */ unregisterModule (path) { /* 转化称Array */ if (typeof path === 'string') path = [path] if (process.env.NODE_ENV !== 'production') { assert(Array.isArray(path), `module path must be a string or an Array.`) } /*注销*/ this._modules.unregister(path) this._withCommit(() => { /* 获取父级的state */ const parentState = getNestedState(this.state, path.slice(0, -1)) /* 从父级中删除 */ Vue.delete(parentState, path[path.length - 1]) }) /* 重制store */ resetStore(this) } ``` 同样,与registerModule对应的方法unregisterModule,动态注销模块。实现方法是先从state中删除模块,然后用resetStore来重制store。 ### resetStore ```javascript /* 重制store */ function resetStore (store, hot) { store._actions = Object.create(null) store._mutations = Object.create(null) store._wrappedGetters = Object.create(null) store._modulesNamespaceMap = Object.create(null) const state = store.state // init all modules installModule(store, state, [], store._modules.root, true) // reset vm resetStoreVM(store, state, hot) } ``` 这里的resetStore其实也就是将store中的_actions等进行初始化以后,重新执行installModule与resetStoreVM来初始化module以及用Vue特性使其“响应式化”,这跟构造函数中的是一致的。 ## 插件 Vue提供了一个非常好用的插件[Vue.js devtools](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) ```javascript /* 从window对象的__VUE_DEVTOOLS_GLOBAL_HOOK__中获取devtool插件 */ const devtoolHook = typeof window !== 'undefined' && window.__VUE_DEVTOOLS_GLOBAL_HOOK__ export default function devtoolPlugin (store) { if (!devtoolHook) return /* devtoll插件实例存储在store的_devtoolHook上 */ store._devtoolHook = devtoolHook /* 出发vuex的初始化事件,并将store的引用地址传给deltool插件,使插件获取store的实例 */ devtoolHook.emit('vuex:init', store) /* 监听travel-to-state事件 */ devtoolHook.on('vuex:travel-to-state', targetState => { /* 重制state */ store.replaceState(targetState) }) /* 订阅store的变化 */ store.subscribe((mutation, state) => { devtoolHook.emit('vuex:mutation', mutation, state) }) } ``` 如果已经安装了该插件,则会在windows对象上暴露一个__VUE_DEVTOOLS_GLOBAL_HOOK__。devtoolHook用在初始化的时候会触发“vuex:init”事件通知插件,然后通过on方法监听“vuex:travel-to-state”事件来重置state。最后通过Store的subscribe方法来添加一个订阅者,在触发commit方法修改mutation数据以后,该订阅者会被通知,从而触发“vuex:mutation”事件。 ## 最后 Vuex是一个非常优秀的库,代码量不多且结构清晰,非常适合研究学习其内部实现。最近的一系列源码阅读也使我自己受益匪浅,写这篇文章也希望可以帮助到更多想要学习探索Vuex内部实现原理的同学。 ================================================ FILE: docs/Vue事件机制.MarkDown ================================================ ## Vue事件API 众所周知,Vue.js为我们提供了四个事件API,分别是[$on](https://cn.vuejs.org/v2/api/#vm-on-event-callback),[$once](https://cn.vuejs.org/v2/api/#vm-once-event-callback),[$off](https://cn.vuejs.org/v2/api/#vm-off-event-callback),[$emit](https://cn.vuejs.org/v2/api/#vm-emit-event-…args)。 ## 初始化事件 初始化事件在vm上创建一个_events对象,用来存放事件。_events的内容如下: ```javascript { eventName: [func1, func2, func3] } ``` 存放事件名以及对应执行方法。 ```javascript /*初始化事件*/ export function initEvents (vm: Component) { /*在vm上创建一个_events对象,用来存放事件。*/ vm._events = Object.create(null) /*这个bool标志位来表明是否存在钩子,而不需要通过哈希表的方法来查找是否有钩子,这样做可以减少不必要的开销,优化性能。*/ vm._hasHookEvent = false // init parent attached events /*初始化父组件attach的事件*/ const listeners = vm.$options._parentListeners if (listeners) { updateComponentListeners(vm, listeners) } } ``` ## $on $on方法用来在vm实例上监听一个自定义事件,该事件可用$emit触发。 ```javascript Vue.prototype.$on = function (event: string | Array, fn: Function): Component { const vm: Component = this /*如果是数组的时候,则递归$on,为每一个成员都绑定上方法*/ if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { this.$on(event[i], fn) } } else { (vm._events[event] || (vm._events[event] = [])).push(fn) // optimize hook:event cost by using a boolean flag marked at registration // instead of a hash lookup /*这里在注册事件的时候标记bool值也就是个标志位来表明存在钩子,而不需要通过哈希表的方法来查找是否有钩子,这样做可以减少不必要的开销,优化性能。*/ if (hookRE.test(event)) { vm._hasHookEvent = true } } return vm } ``` ## $once $once监听一个只能触发一次的事件,在触发以后会自动移除该事件。 ```javascript Vue.prototype.$once = function (event: string, fn: Function): Component { const vm: Component = this function on () { /*在第一次执行的时候将该事件销毁*/ vm.$off(event, on) /*执行注册的方法*/ fn.apply(vm, arguments) } on.fn = fn vm.$on(event, on) return vm } ``` ## $off $off用来移除自定义事件 ```javascript Vue.prototype.$off = function (event?: string | Array, fn?: Function): Component { const vm: Component = this // all /*如果不传参数则注销所有事件*/ if (!arguments.length) { vm._events = Object.create(null) return vm } // array of events /*如果event是数组则递归注销事件*/ if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { this.$off(event[i], fn) } return vm } // specific event const cbs = vm._events[event] /*Github:https://github.com/answershuto*/ /*本身不存在该事件则直接返回*/ if (!cbs) { return vm } /*如果只传了event参数则注销该event方法下的所有方法*/ if (arguments.length === 1) { vm._events[event] = null return vm } // specific handler /*遍历寻找对应方法并删除*/ let cb let i = cbs.length while (i--) { cb = cbs[i] if (cb === fn || cb.fn === fn) { cbs.splice(i, 1) break } } return vm } ``` ## $emit $emit用来触发指定的自定义事件。 ```javascript Vue.prototype.$emit = function (event: string): Component { const vm: Component = this if (process.env.NODE_ENV !== 'production') { const lowerCaseEvent = event.toLowerCase() if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) { tip( `Event "${lowerCaseEvent}" is emitted in component ` + `${formatComponentName(vm)} but the handler is registered for "${event}". ` + `Note that HTML attributes are case-insensitive and you cannot use ` + `v-on to listen to camelCase events when using in-DOM templates. ` + `You should probably use "${hyphenate(event)}" instead of "${event}".` ) } } let cbs = vm._events[event] if (cbs) { /*将类数组的对象转换成数组*/ cbs = cbs.length > 1 ? toArray(cbs) : cbs const args = toArray(arguments, 1) /*遍历执行*/ for (let i = 0, l = cbs.length; i < l; i++) { cbs[i].apply(vm, args) } } return vm } ``` ================================================ FILE: docs/Vue组件间通信.MarkDown ================================================ ## 什么是Vue组件? [组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以是原生 HTML 元素的形式,以 is 特性扩展。](https://cn.vuejs.org/v2/guide/components.html)

## Vue组件间通信 ### 父组件向子组件通信 #### 方法一:props 使用[props](https://cn.vuejs.org/v2/guide/components.html#Prop),父组件可以使用props向子组件传递数据。 父组件vue模板father.vue ``` ``` 子组件vue模板child.vue ``` ```
#### 方法二 使用$children 使用[$children](https://cn.vuejs.org/v2/api/#vm-children)可以在父组件中访问子组件。

### 子组件向父组件通信
#### 方法一:使用[vue事件](https://cn.vuejs.org/v2/guide/components.html#使用-v-on-绑定自定义事件) 父组件向子组件传递事件方法,子组件通过$emit触发事件,回调给父组件。 父组件vue模板father.vue ``` ``` 子组件vue模板child.vue ``` ```
#### 方法二: 通过修改父组件传递的props来修改父组件数据 这种方法只能在父组件传递一个引用变量时可以使用,字面变量无法达到相应效果。因为引用变量最终无论是父组件中的数据还是子组件得到的props中的数据都是指向同一块内存地址,所以修改了子组件中props的数据即修改了父组件的数据。 但是并不推荐这么做,并不建议直接修改props的值,如果数据是用于显示修改的,在实际开发中我经常会将其放入data中,在需要回传给父组件的时候再用事件回传数据。这样做保持了组件独立以及解耦,不会因为使用同一份数据而导致数据流异常混乱,只通过特定的接口传递数据来达到修改数据的目的,而内部数据状态由专门的data负责管理。
#### 方法三:使用$parent 使用[$parent](https://cn.vuejs.org/v2/api/#vm-parent)可以访问父组件的数据。

### 非父子组件、兄弟组件之间的数据传递 非父子组件通信,Vue官方推荐[使用一个Vue实例作为中央事件总线](https://cn.vuejs.org/v2/guide/components.html#非父子组件通信)。 Vue内部有一个事件机制,可以参考[源码](https://github.com/vuejs/vue/blob/dev/src/core/instance/events.js)。 $on方法用来监听一个事件。 $emit用来触发一个事件。 ```javascript /*新建一个Vue实例作为中央事件总线*/ let event = new Vue(); /*监听事件*/ event.$on('eventName', (val) => { //......do something }); /*触发事件*/ event.$emit('eventName', 'this is a message.'); ```

### 多层级父子组件通信: 在Vue1.0中实现了$broadcast与$dispatch两个方法用来向子组件(或父组件)广播(或派发),当子组件(或父组件)上监听了事件并返回true的时候会向爷孙级组件继续广播(或派发)事件。但是这个方法在Vue2.0里面已经被移除了。 之前在学习饿了么的开源组件库[element](https://github.com/ElemeFE/element)的时候发现他们重新实现了broadcast以及dispatch的方法,以mixin的方式引入,具体可以参考[《说说element组件库broadcast与dispatch》](https://github.com/answershuto/learnVue/blob/master/docs/%E8%AF%B4%E8%AF%B4element%E7%BB%84%E4%BB%B6%E5%BA%93broadcast%E4%B8%8Edispatch.MarkDown)。但是跟Vue1.0的两个方法实现有略微的不同。这两个方法实现了向子孙组件事件广播以及向多层级父组件事件派发的功能。但是并非广义上的事件广播,它需要指定一个commentName进行向指定组件名组件定向广播(派发)事件。 其实这两个方法内部实现还是用到的还是$parent以及$children,用以遍历子节点或是逐级向上查询父节点,访问到指定组件名的时候,调用$emit触发指定事件。

### 复杂的单页应用数据管理 当应用足够复杂情况下,请使用[vuex](https://cn.vuejs.org/v2/guide/state-management.html)进行数据管理。 ================================================ FILE: docs/从template到DOM(Vue.js源码角度看内部运行机制).MarkDown ================================================ ## 从new一个Vue对象开始 ```javascript let vm = new Vue({ el: '#app', /*some options*/ }); ``` 很多同学好奇,在new一个Vue对象的时候,内部究竟发生了什么? 究竟Vue.js是如何将data中的数据渲染到真实的宿主环境中的? 又是如何通过“响应式”修改数据的? template是如何被编译成真实环境中可用的HTML的? Vue指令又是如何执行的? 带着这些疑问,我们从Vue的构造类开始看起。 ## Vue构造类 ```javascript function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) { warn('Vue is a constructor and should be called with the `new` keyword') } /*初始化*/ this._init(options) } ``` Vue的构造类只做了一件事情,就是调用_init函数进行初始化 来看一下init的代码 ```javascript 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-init:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // a flag to avoid this being observed /*一个防止vm实例自身被观察的标志位*/ 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 { 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) /*初始化render*/ initRender(vm) /*调用beforeCreate钩子函数并且触发beforeCreate钩子事件*/ callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props /*初始化props、methods、data、computed与watch*/ initState(vm) initProvide(vm) // resolve provide after data/props /*调用created钩子函数并且触发created钩子事件*/ callHook(vm, 'created') /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { /*格式化组件名*/ vm._name = formatComponentName(vm, false) mark(endTag) measure(`${vm._name} init`, startTag, endTag) } if (vm.$options.el) { /*挂载组件*/ vm.$mount(vm.$options.el) } } ``` _init主要做了这两件事: 1.初始化(包括生命周期、事件、render函数、state等)。 2.$mount组件。 在生命钩子beforeCreate与created之间会初始化state,在此过程中,会依次初始化props、methods、data、computed与watch,这也就是Vue.js对options中的数据进行“响应式化”(即双向绑定)的过程。对于Vue.js响应式原理不了解的同学可以先看一下笔者的另一片文章[《Vue.js响应式原理》](https://github.com/answershuto/learnVue/blob/master/docs/%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86.MarkDown)。 ```javascript /*初始化props、methods、data、computed与watch*/ export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options /*初始化props*/ if (opts.props) initProps(vm, opts.props) /*初始化方法*/ if (opts.methods) initMethods(vm, opts.methods) /*初始化data*/ if (opts.data) { initData(vm) } else { /*该组件没有data的时候绑定一个空对象*/ observe(vm._data = {}, true /* asRootData */) } /*初始化computed*/ if (opts.computed) initComputed(vm, opts.computed) /*初始化watchers*/ if (opts.watch) initWatch(vm, opts.watch) } ``` ## 双向绑定 以initData为例,对option的data的数据进行双向绑定Oberver,其他option参数双向绑定的核心原理是一致的。 ```javascript function initData (vm: Component) { /*得到data数据*/ let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} /*判断是否是对象*/ if (!isPlainObject(data)) { data = {} process.env.NODE_ENV !== 'production' && warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ) } // proxy data on instance /*遍历data对象*/ const keys = Object.keys(data) const props = vm.$options.props let i = keys.length //遍历data中的数据 while (i--) { /*保证data中的key不与props中的key重复,props优先,如果有冲突会产生warning*/ if (props && hasOwn(props, keys[i])) { process.env.NODE_ENV !== 'production' && warn( `The data property "${keys[i]}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else if (!isReserved(keys[i])) { /*判断是否是保留字段*/ /*这里是我们前面讲过的代理,将data上面的属性代理到了vm实例上*/ proxy(vm, `_data`, keys[i]) } } /*Github:https://github.com/answershuto*/ // observe data /*从这里开始我们要observe了,开始对数据进行绑定,这里有尤大大的注释asRootData,这步作为根数据,下面会进行递归observe进行对深层对象的绑定。*/ observe(data, true /* asRootData */) } ``` observe会通过defineReactive对data中的对象进行双向绑定,最终通过Object.defineProperty对对象设置setter以及getter的方法。getter的方法主要用来进行依赖收集,对于依赖收集不了解的同学可以参考笔者的另一篇文章[《依赖收集》](https://github.com/answershuto/learnVue/blob/master/docs/%E4%BE%9D%E8%B5%96%E6%94%B6%E9%9B%86.MarkDown)。setter方法会在对象被修改的时候触发(不存在添加属性的情况,添加属性请用Vue.set),这时候setter会通知闭包中的Dep,Dep中有一些订阅了这个对象改变的Watcher观察者对象,Dep会通知Watcher对象更新视图。 如果是修改一个数组的成员,该成员是一个对象,那只需要递归对数组的成员进行双向绑定即可。但这时候出现了一个问题,如果我们进行pop、push等操作的时候,push进去的对象根本没有进行过双向绑定,更别说pop了,那么我们如何监听数组的这些变化呢? Vue.js提供的方法是重写push、pop、shift、unshift、splice、sort、reverse这七个[数组方法](http://v1-cn.vuejs.org/guide/list.html#变异方法)。修改数组原型方法的代码可以参考[observer/array.js](https://github.com/vuejs/vue/blob/dev/src/core/observer/array.js)以及[observer/index.js](https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js#L45)。 ```javascript export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that has this object as root $data constructor (value: any) { //....... if (Array.isArray(value)) { /* 如果是数组,将修改后可以截获响应的数组方法替换掉该数组的原型中的原生方法,达到监听数组数据变化响应的效果。 这里如果当前浏览器支持__proto__属性,则直接覆盖当前数组对象原型上的原生数组方法,如果不支持该属性,则直接覆盖数组对象的原型。 */ const augment = hasProto ? protoAugment /*直接覆盖原型的方法来修改目标对象*/ : copyAugment /*定义(覆盖)目标对象或数组的某一个方法*/ augment(value, arrayMethods, arrayKeys) /*如果是数组则需要遍历数组的每一个成员进行observe*/ this.observeArray(value) } else { /*如果是对象则直接walk进行绑定*/ this.walk(value) } } } /** * Augment an target Object or Array by intercepting * the prototype chain using __proto__ */ /*直接覆盖原型的方法来修改目标对象或数组*/ function protoAugment (target, src: Object) { /* eslint-disable no-proto */ target.__proto__ = src /* eslint-enable no-proto */ } /** * Augment an target Object or Array by defining * hidden properties. */ /* istanbul ignore next */ /*定义(覆盖)目标对象或数组的某一个方法*/ function copyAugment (target: Object, src: Object, keys: Array) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) } } ``` ```javascript /* * not type checking this file because flow doesn't play well with * dynamically accessing methods on Array prototype */ import { def } from '../util/index' /*取得原生数组的原型*/ const arrayProto = Array.prototype /*创建一个新的数组对象,修改该对象上的数组的七个方法,防止污染原生数组方法*/ export const arrayMethods = Object.create(arrayProto) /** * Intercept mutating methods and emit events */ /*这里重写了数组的这些方法,在保证不污染原生数组原型的情况下重写数组的这些方法,截获数组的成员发生的变化,执行原生数组操作的同时dep通知关联的所有观察者进行响应式处理*/ [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method) { // cache original method /*将数组的原生方法缓存起来,后面要调用*/ const original = arrayProto[method] def(arrayMethods, method, function mutator () { // avoid leaking arguments: // http://jsperf.com/closure-with-arguments let i = arguments.length const args = new Array(i) while (i--) { args[i] = arguments[i] } /*调用原生的数组方法*/ const result = original.apply(this, args) /*数组新插入的元素需要重新进行observe才能响应式*/ const ob = this.__ob__ let inserted switch (method) { case 'push': inserted = args break case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change /*dep通知所有注册的观察者进行响应式处理*/ ob.dep.notify() return result }) }) ``` 从数组的原型新建一个Object.create(arrayProto)对象,通过修改此原型可以保证原生数组方法不被污染。如果当前浏览器支持__proto__这个属性的话就可以直接覆盖该属性使数组对象具有了重写后的数组方法。如果浏览器没有该属性,则必须通过遍历def所有需要重写的数组方法,这种方法效率较低,所以优先使用第一种。 在保证不污染不覆盖数组原生方法添加监听,主要做了两个操作,第一是通知所有注册的观察者进行响应式处理,第二是如果是添加成员的操作,需要对新成员进行observe。 但是修改了数组的原生方法以后我们还是没法像原生数组一样直接通过数组的下标或者设置length来修改数组,可以通过[Vue.set以及splice方法](https://cn.vuejs.org/v2/guide/list.html#%E6%9B%BF%E6%8D%A2%E6%95%B0%E7%BB%84)。 对于更具体的讲解数据双向绑定以及Dep、Watcher的实现可以参考笔者的文章[《从源码角度再看数据绑定》](https://github.com/answershuto/learnVue/blob/master/docs/%E4%BB%8E%E6%BA%90%E7%A0%81%E8%A7%92%E5%BA%A6%E5%86%8D%E7%9C%8B%E6%95%B0%E6%8D%AE%E7%BB%91%E5%AE%9A.MarkDown)。 ## template编译 在$mount过程中,如果是使用独立构建,则会在此过程中将template编译成render function。当然,你也可以采用运行时构建。具体参考[运行时-编译器-vs-只包含运行时](https://cn.vuejs.org/v2/guide/installation.html#运行时-编译器-vs-只包含运行时)。 template是如何被编译成render function的呢? ```javascript function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { /*parse解析得到ast树*/ const ast = parse(template.trim(), options) /* 将AST树进行优化 优化的目标:生成模板AST树,检测不需要进行DOM改变的静态子树。 一旦检测到这些静态树,我们就能做以下这些事情: 1.把它们变成常数,这样我们就再也不需要每次重新渲染时创建新的节点了。 2.在patch的过程中直接跳过。 */ optimize(ast, options) /*根据ast树生成所需的code(内部包含render与staticRenderFns)*/ const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } } ``` baseCompile首先会将模板template进行parse得到一个AST语法树,再通过optimize做一些优化,最后通过generate得到render以及staticRenderFns。 ### parse parse的源码可以参见[https://github.com/answershuto/learnVue/blob/master/vue-src/compiler/parser/index.js#L53](https://github.com/answershuto/learnVue/blob/master/vue-src/compiler/parser/index.js#L53)。 parse会用正则等方式解析template模板中的指令、class、style等数据,形成AST语法树。 ### optimize optimize的主要作用是标记static静态节点,这是Vue在编译过程中的一处优化,后面当update更新界面时,会有一个patch的过程,diff算法会直接跳过静态节点,从而减少了比较的过程,优化了patch的性能。 ### generate generate是将AST语法树转化成render funtion字符串的过程,得到结果是render的字符串以及staticRenderFns字符串。 具体的template编译实现请参考[《聊聊Vue.js的template编译》](https://github.com/answershuto/learnVue/blob/master/docs/%E8%81%8A%E8%81%8AVue%E7%9A%84template%E7%BC%96%E8%AF%91.MarkDown)。 ## Watcher到视图 Watcher对象会通过调用updateComponent方法来达到更新视图的目的。这里提一下,其实Watcher并不是实时更新视图的,Vue.js默认会将Watcher对象存在一个队列中,在下一个tick时更新异步更新视图,完成了性能优化。关于nextTick感兴趣的小伙伴可以参考[《Vue.js异步更新DOM策略及nextTick》](https://github.com/answershuto/learnVue/blob/master/docs/Vue.js%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0DOM%E7%AD%96%E7%95%A5%E5%8F%8AnextTick.MarkDown)。 ```javascript updateComponent = () => { vm._update(vm._render(), hydrating) } ``` updateComponent就执行一句话,_render函数会返回一个新的Vnode节点,传入_update中与旧的VNode对象进行对比,经过一个patch的过程得到两个VNode节点的差异,最后将这些差异渲染到真实环境形成视图。 什么是VNode? ## VNode 在刀耕火种的年代,我们需要在各个事件方法中直接操作DOM来达到修改视图的目的。但是当应用一大就会变得难以维护。 那我们是不是可以把真实DOM树抽象成一棵以JavaScript对象构成的抽象树,在修改抽象树数据后将抽象树转化成真实DOM重绘到页面上呢?于是虚拟DOM出现了,它是真实DOM的一层抽象,用属性描述真实DOM的各个特性。当它发生变化的时候,就会去修改视图。 可以想象,最简单粗暴的方法就是将整个DOM结构用innerHTML修改到页面上,但是这样进行重绘整个视图层是相当消耗性能的,我们是不是可以每次只更新它的修改呢?所以Vue.js将DOM抽象成一个以JavaScript对象为节点的虚拟DOM树,以VNode节点模拟真实DOM,可以对这颗抽象树进行创建节点、删除节点以及修改节点等操作,在这过程中都不需要操作真实DOM,只需要操作JavaScript对象后只对差异修改,相对于整块的innerHTML的粗暴式修改,大大提升了性能。修改以后经过diff算法得出一些需要修改的最小单位,再将这些小单位的视图进行更新。这样做减少了很多不需要的DOM操作,大大提高了性能。 Vue就使用了这样的抽象节点VNode,它是对真实DOM的一层抽象,而不依赖某个平台,它可以是浏览器平台,也可以是weex,甚至是node平台也可以对这样一棵抽象DOM树进行创建删除修改等操作,这也为前后端同构提供了可能。 先来看一下Vue.js源码中对VNode类的定义。 ```javascript export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope functionalContext: Component | void; // only for functional component root nodes key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? constructor ( tag?: string, data?: VNodeData, children?: ?Array, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions ) { /*当前节点的标签名*/ this.tag = tag /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/ this.data = data /*当前节点的子节点,是一个数组*/ this.children = children /*当前节点的文本*/ this.text = text /*当前虚拟节点对应的真实dom节点*/ this.elm = elm /*当前节点的名字空间*/ this.ns = undefined /*编译作用域*/ this.context = context /*函数化组件作用域*/ this.functionalContext = undefined /*节点的key属性,被当作节点的标志,用以优化*/ this.key = data && data.key /*组件的option选项*/ this.componentOptions = componentOptions /*当前节点对应的组件的实例*/ this.componentInstance = undefined /*当前节点的父节点*/ this.parent = undefined /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/ this.raw = false /*静态节点标志*/ this.isStatic = false /*是否作为跟节点插入*/ this.isRootInsert = true /*是否为注释节点*/ this.isComment = false /*是否为克隆节点*/ this.isCloned = false /*是否有v-once指令*/ this.isOnce = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } } ``` 这是一个最基础的VNode节点,作为其他派生VNode类的基类,里面定义了下面这些数据。 tag: 当前节点的标签名 data: 当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息 children: 当前节点的子节点,是一个数组 text: 当前节点的文本 elm: 当前虚拟节点对应的真实dom节点 ns: 当前节点的名字空间 context: 当前节点的编译作用域 functionalContext: 函数化组件作用域 key: 节点的key属性,被当作节点的标志,用以优化 componentOptions: 组件的option选项 componentInstance: 当前节点对应的组件的实例 parent: 当前节点的父节点 raw: 简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false isStatic: 是否为静态节点 isRootInsert: 是否作为跟节点插入 isComment: 是否为注释节点 isCloned: 是否为克隆节点 isOnce: 是否有v-once指令 --- 打个比方,比如说我现在有这么一个VNode树 ```JavaScript { tag: 'div' data: { class: 'test' }, children: [ { tag: 'span', data: { class: 'demo' } text: 'hello,VNode' } ] } ``` 渲染之后的结果就是这样的 ```html
hello,VNode
``` 更多操作VNode的方法,请参考[《VNode节点》](https://github.com/answershuto/learnVue/blob/master/docs/VNode%E8%8A%82%E7%82%B9.MarkDown)。 ## patch 最后_update会将新旧两个VNode进行一次patch的过程,得出两个VNode最小的差异,然后将这些差异渲染到视图上。 首先说一下patch的核心diff算法,diff算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有O(n),是一种相当高效的算法。 ![img](https://i.loli.net/2017/08/27/59a23cfca50f3.png) ![img](https://i.loli.net/2017/08/27/59a2419a3c617.png) 这两张图代表旧的VNode与新VNode进行patch的过程,他们只是在同层级的VNode之间进行比较得到变化(第二张图中相同颜色的方块代表互相进行比较的VNode节点),然后修改变化的视图,所以十分高效。 在patch的过程中,如果两个VNode被认为是同一个VNode(sameVnode),则会进行深度的比较,得出最小差异,否则直接删除旧有DOM节点,创建新的DOM节点。 什么是sameVnode? 我们来看一下sameVnode的实现。 ```JavaScript /* 判断两个VNode节点是否是同一个节点,需要满足以下条件 key相同 tag(当前节点的标签名)相同 isComment(是否为注释节点)相同 是否data(当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息)都有定义 当标签是的时候,type必须相同 */ function sameVnode (a, b) { return ( a.key === b.key && a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) } // Some browsers do not support dynamically changing type for // so they need to be treated as different nodes /* 判断当标签是的时候,type是否相同 某些浏览器不支持动态修改类型,所以他们被视为不同类型 */ function sameInputType (a, b) { if (a.tag !== 'input') return true let i const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type return typeA === typeB } ``` 当两个VNode的tag、key、isComment都相同,并且同时定义或未定义data的时候,且如果标签为input则type必须相同。这时候这两个VNode则算sameVnode,可以直接进行patchVnode操作。 patchVnode的规则是这样的: 1.如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),那么只需要替换elm以及componentInstance即可。 2.新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren,这个updateChildren也是diff的核心。 3.如果老节点没有子节点而新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节点加入子节点。 4.当新节点没有子节点而老节点有子节点的时候,则移除该DOM节点的所有子节点。 5.当新老节点都无子节点的时候,只是文本的替换。 ## updateChildren ```JavaScript function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, elmToMove, refElm // removeOnly is a special flag used only by // to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { /*前四种情况其实是指定key的时候,判定为同一个VNode,则直接patchVnode即可,分别比较oldCh以及newCh的两头节点2*2=4种情况*/ patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { /* 生成一个key与旧VNode的key对应的哈希表(只有第一次进来undefined的时候会生成,也为后面检测重复的key值做铺垫) 比如childre是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}] beginIdx = 0 endIdx = 2 结果生成{key0: 0, key1: 1, key2: 2} */ if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) /*如果newStartVnode新的VNode节点存在key并且这个key在oldVnode中能找到则返回这个节点的idxInOld(即第几个节点,下标)*/ idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null if (isUndef(idxInOld)) { // New element /*newStartVnode没有key或者是该key没有在老节点中找到则创建一个新的节点*/ createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { /*获取同key的老节点*/ elmToMove = oldCh[idxInOld] /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !elmToMove) { /*如果elmToMove不存在说明之前已经有新节点放入过这个key的DOM中,提示可能存在重复的key,确保v-for的时候item有唯一的key值*/ warn( 'It seems there are duplicate keys that is causing an update error. ' + 'Make sure each v-for item has a unique key.' ) } if (sameVnode(elmToMove, newStartVnode)) { /*Github:https://github.com/answershuto*/ /*如果新VNode与得到的有相同key的节点是同一个VNode则进行patchVnode*/ patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) /*因为已经patchVnode进去了,所以将这个老节点赋值undefined,之后如果还有新节点与该节点key相同可以检测出来提示已有重复的key*/ oldCh[idxInOld] = undefined /*当有标识位canMove实可以直接插入oldStartVnode对应的真实DOM节点前面*/ canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { // same key but different element. treat as new element /*当新的VNode与找到的同样key的VNode不是sameVNode的时候(比如说tag不一样或者是有不一样type的input标签),创建一个新的节点*/ createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } } } } if (oldStartIdx > oldEndIdx) { /*全部比较完成以后,发现oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多,所以这时候多出来的新节点需要一个一个创建出来加入到真实DOM中*/ refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { /*如果全部比较完成以后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,老节点多余新节点,这个时候需要将多余的老节点从真实DOM中移除*/ removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } } ``` 直接看源码可能比较难以捋清其中的关系,我们通过图来看一下。 ![img](https://i.loli.net/2017/08/28/59a4015bb2765.png) 首先,在新老两个VNode节点的左右头尾两侧都有一个变量标记,在遍历过程中这几个变量都会向中间靠拢。当oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时结束循环。 索引与VNode节点的对应关系: oldStartIdx => oldStartVnode oldEndIdx => oldEndVnode newStartIdx => newStartVnode newEndIdx => newEndVnode 在遍历中,如果存在key,并且满足sameVnode,会将该DOM节点进行复用,否则则会创建一个新的DOM节点。 首先,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode两两比较一共有2*2=4种比较方法。 当新老VNode节点的start或者end满足sameVnode时,也就是sameVnode(oldStartVnode, newStartVnode)或者sameVnode(oldEndVnode, newEndVnode),直接将该VNode节点进行patchVnode即可。 ![img](https://i.loli.net/2017/08/28/59a40c12c1655.png) 如果oldStartVnode与newEndVnode满足sameVnode,即sameVnode(oldStartVnode, newEndVnode)。 这时候说明oldStartVnode已经跑到了oldEndVnode后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。 ![img](https://ooo.0o0.ooo/2017/08/28/59a4214784979.png) 如果oldEndVnode与newStartVnode满足sameVnode,即sameVnode(oldEndVnode, newStartVnode)。 这说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时真实的DOM节点移动到了oldStartVnode的前面。 ![img](https://i.loli.net/2017/08/29/59a4c70685d12.png) 如果以上情况均不符合,则通过createKeyToOldIdx会得到一个oldKeyToIdx,里面存放了一个key为旧的VNode,value为对应index序列的哈希表。从这个哈希表中可以找到是否有与newStartVnode一致key的旧的VNode节点,如果同时满足sameVnode,patchVnode的同时会将这个真实DOM(elmToMove)移动到oldStartVnode对应的真实DOM的前面。 ![img](https://i.loli.net/2017/08/29/59a4d7552d299.png) 当然也有可能newStartVnode在旧的VNode节点找不到一致的key,或者是即便key相同却不是sameVnode,这个时候会调用createElm创建一个新的DOM节点。 ![img](https://i.loli.net/2017/08/29/59a4de0fa4dba.png) 到这里循环已经结束了,那么剩下我们还需要处理多余或者不够的真实DOM节点。 1.当结束时oldStartIdx > oldEndIdx,这个时候老的VNode节点已经遍历完了,但是新的节点还没有。说明了新的VNode节点实际上比老的VNode节点多,也就是比真实DOM多,需要将剩下的(也就是新增的)VNode节点插入到真实DOM节点中去,此时调用addVnodes(批量调用createElm的接口将这些节点加入到真实DOM中去)。 ![img](https://i.loli.net/2017/08/29/59a509f0d1788.png) 2。同理,当newStartIdx > newEndIdx时,新的VNode节点已经遍历完了,但是老的节点还有剩余,说明真实DOM节点多余了,需要从文档中删除,这时候调用removeVnodes将这些多余的真实DOM删除。 ![img](https://i.loli.net/2017/08/29/59a4f389b98cb.png) 更详细的diff实现参考笔者的文章[VirtualDOM与diff(Vue.js实现)](https://github.com/answershuto/learnVue/blob/master/docs/VirtualDOM%E4%B8%8Ediff(Vue%E5%AE%9E%E7%8E%B0).MarkDown)。 ## 映射到真实DOM 由于Vue使用了虚拟DOM,所以虚拟DOM可以在任何支持JavaScript语言的平台上操作,譬如说目前Vue支持的浏览器平台或是weex,在虚拟DOM的实现上是一致的。那么最后虚拟DOM如何映射到真实的DOM节点上呢? Vue为平台做了一层适配层,浏览器平台见[/platforms/web/runtime/node-ops.js](https://github.com/answershuto/learnVue/blob/master/vue-src/platforms/web/runtime/node-ops.js)以及weex平台见[/platforms/weex/runtime/node-ops.js](https://github.com/answershuto/learnVue/blob/master/vue-src/platforms/weex/runtime/node-ops.js)。不同平台之间通过适配层对外提供相同的接口,虚拟DOM进行操作真实DOM节点的时候,只需要调用这些适配层的接口即可,而内部实现则不需要关心,它会根据平台的改变而改变。 现在又出现了一个问题,我们只是将虚拟DOM映射成了真实的DOM。那如何给这些DOM加入attr、class、style等DOM属性呢? 这要依赖于虚拟DOM的生命钩子。虚拟DOM提供了如下的钩子函数,分别在不同的时期会进行调用。 ```JavaScript const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] /*构建cbs回调函数,web平台上见/platforms/web/runtime/modules*/ for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { cbs[hooks[i]].push(modules[j][hooks[i]]) } } } ``` 同理,也会根据不同平台有自己不同的实现,我们这里以Web平台为例。Web平台的钩子函数见[/platforms/web/runtime/modules](https://github.com/answershuto/learnVue/tree/master/vue-src/platforms/web/runtime/modules)。里面有对attr、class、props、events、style以及transition(过渡状态)的DOM属性进行操作。 以attr为例,代码很简单。 ```JavaScript /* @flow */ import { isIE9 } from 'core/util/env' import { extend, isDef, isUndef } from 'shared/util' import { isXlink, xlinkNS, getXlinkProp, isBooleanAttr, isEnumeratedAttr, isFalsyAttrValue } from 'web/util/index' /*更新attr*/ function updateAttrs (oldVnode: VNodeWithData, vnode: VNodeWithData) { /*如果旧的以及新的VNode节点均没有attr属性,则直接返回*/ if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) { return } let key, cur, old /*VNode节点对应的Dom实例*/ const elm = vnode.elm /*旧VNode节点的attr*/ const oldAttrs = oldVnode.data.attrs || {} /*新VNode节点的attr*/ let attrs: any = vnode.data.attrs || {} // clone observed objects, as the user probably wants to mutate it /*如果新的VNode的attr已经有__ob__(代表已经被Observe处理过了), 进行深拷贝*/ if (isDef(attrs.__ob__)) { attrs = vnode.data.attrs = extend({}, attrs) } /*遍历attr,不一致则替换*/ for (key in attrs) { cur = attrs[key] old = oldAttrs[key] if (old !== cur) { setAttr(elm, key, cur) } } // #4391: in IE9, setting type can reset value for input[type=radio] /* istanbul ignore if */ if (isIE9 && attrs.value !== oldAttrs.value) { setAttr(elm, 'value', attrs.value) } for (key in oldAttrs) { if (isUndef(attrs[key])) { if (isXlink(key)) { elm.removeAttributeNS(xlinkNS, getXlinkProp(key)) } else if (!isEnumeratedAttr(key)) { elm.removeAttribute(key) } } } } /*设置attr*/ function setAttr (el: Element, key: string, value: any) { if (isBooleanAttr(key)) { // set attribute for blank value // e.g. if (isFalsyAttrValue(value)) { el.removeAttribute(key) } else { el.setAttribute(key, key) } } else if (isEnumeratedAttr(key)) { el.setAttribute(key, isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true') } else if (isXlink(key)) { if (isFalsyAttrValue(value)) { el.removeAttributeNS(xlinkNS, getXlinkProp(key)) } else { el.setAttributeNS(xlinkNS, key, value) } } else { if (isFalsyAttrValue(value)) { el.removeAttribute(key) } else { el.setAttribute(key, value) } } } export default { create: updateAttrs, update: updateAttrs } ``` attr只需要在create以及update钩子被调用时更新DOM的attr属性即可。 ## 最后 至此,我们已经从template到真实DOM的整个过程梳理完了。现在再去看这张图,是不是更清晰了呢? ![](https://cn.vuejs.org/images/data.png) ================================================ FILE: docs/从源码角度再看数据绑定.MarkDown ================================================ ## 数据绑定原理 前面已经讲过Vue数据绑定的原理了,现在从源码来看一下数据绑定在Vue中是如何实现的。 首先看一下Vue.js官网介绍响应式原理的这张图。 ![](https://cn.vuejs.org/images/data.png) 这张图比较清晰地展示了整个流程,首先通过一次渲染操作触发Data的getter(这里保证只有视图中需要被用到的data才会触发getter)进行依赖收集,这时候其实Watcher与data可以看成一种被绑定的状态(实际上是data的闭包中有一个Deps订阅者,在修改的时候会通知所有的Watcher观察者),在data发生变化的时候会触发它的setter,setter通知Watcher,Watcher进行回调通知组件重新渲染的函数,之后根据diff算法来决定是否发生视图的更新。 Vue在初始化组件数据时,在生命周期的[beforeCreate](https://github.com/vuejs/vue/blob/dev/src/core/instance/init.js#L55)与[created](https://github.com/vuejs/vue/blob/dev/src/core/instance/init.js#L59)钩子函数之间实现了对[data、props、computed、methods、events以及watch](https://github.com/vuejs/vue/blob/dev/src/core/instance/state.js#L43)的处理。 ## initData 这里来讲一下[initData](https://github.com/vuejs/vue/blob/dev/src/core/instance/state.js#L107),可以参考源码instance下的state.js文件,下面所有的中文注释都是我加的,英文注释是尤大加的,请不要忽略英文注释,英文注释都讲到了比较关键或者晦涩难懂的点。 加注释版的vue源码也可以直接通过[传送门](https://github.com/answershuto/learnVue/tree/master/vue-src)查看,这些是我在阅读Vue源码过程中加的注释,持续更新中。 initData主要是初始化data中的数据,将数据进行Observer,监听数据的变化,其他的监视原理一致,这里以data为例。 ```javascript function initData (vm: Component) { /*得到data数据*/ let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} /*判断是否是对象*/ if (!isPlainObject(data)) { data = {} process.env.NODE_ENV !== 'production' && warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ) } // proxy data on instance /*遍历data对象*/ const keys = Object.keys(data) const props = vm.$options.props let i = keys.length //遍历data中的数据 while (i--) { /*保证data中的key不与props中的key重复,props优先,如果有冲突会产生warning*/ if (props && hasOwn(props, keys[i])) { process.env.NODE_ENV !== 'production' && warn( `The data property "${keys[i]}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else if (!isReserved(keys[i])) { /*判断是否是保留字段*/ /*这里是我们前面讲过的代理,将data上面的属性代理到了vm实例上*/ proxy(vm, `_data`, keys[i]) } } /*Github:https://github.com/answershuto*/ // observe data /*从这里开始我们要observe了,开始对数据进行绑定,这里有尤大大的注释asRootData,这步作为根数据,下面会进行递归observe进行对深层对象的绑定。*/ observe(data, true /* asRootData */) } ``` 其实这段代码主要做了两件事,一是将_data上面的数据代理到vm上,另一件事通过observe将所有数据变成observable。 ## proxy 接下来看一下proxy代理。 ```javascript /*添加代理*/ 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) } ``` 这里比较好理解,通过proxy函数将data上面的数据代理到vm上,这样就可以用app.text代替app._data.text了。 ## observe 接下来是[observe](https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js#L106),这个函数定义在core文件下observer的index.js文件中。 ```javascript /** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */ /* 尝试创建一个Observer实例(__ob__),如果成功创建Observer实例则返回新的Observer实例,如果已有Observer实例则返回现有的Observer实例。 */ export function observe (value: any, asRootData: ?boolean): Observer | void { /*判断是否是一个对象*/ if (!isObject(value)) { return } let ob: Observer | void /*这里用__ob__这个属性来判断是否已经有Observer实例,如果没有Observer实例则会新建一个Observer实例并赋值给__ob__这个属性,如果已有Observer实例则直接返回该Observer实例*/ if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( /*这里的判断是为了确保value是单纯的对象,而不是函数或者是Regexp等情况。*/ observerState.shouldConvert && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) } if (asRootData && ob) { /*如果是根数据则计数,后面Observer中的observe的asRootData非true*/ ob.vmCount++ } return ob } ``` Vue的响应式数据都会有一个__ob__的属性作为标记,里面存放了该属性的观察器,也就是Observer的实例,防止重复绑定。 ## Observer 接下来看一下新建的[Observer](https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js#L34)。Observer的作用就是遍历对象的所有属性将其进行双向绑定。 ```javascript /** * Observer class that are attached to each observed * object. Once attached, the observer converts target * object's property keys into getter/setters that * collect dependencies and dispatches updates. */ export class { 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 /* 将Observer实例绑定到data的__ob__属性上面去,之前说过observe的时候会先检测是否已经有__ob__对象存放Observer实例了,def方法定义可以参考https://github.com/vuejs/vue/blob/dev/src/core/util/lang.js#L16 */ def(value, '__ob__', this) if (Array.isArray(value)) { /* 如果是数组,将修改后可以截获响应的数组方法替换掉该数组的原型中的原生方法,达到监听数组数据变化响应的效果。 这里如果当前浏览器支持__proto__属性,则直接覆盖当前数组对象原型上的原生数组方法,如果不支持该属性,则直接覆盖数组对象的原型。 */ const augment = hasProto ? protoAugment /*直接覆盖原型的方法来修改目标对象*/ : copyAugment /*定义(覆盖)目标对象或数组的某一个方法*/ augment(value, arrayMethods, arrayKeys) /*Github:https://github.com/answershuto*/ /*如果是数组则需要遍历数组的每一个成员进行observe*/ this.observeArray(value) } else { /*如果是对象则直接walk进行绑定*/ 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) /*walk方法会遍历对象的每一个属性进行defineReactive绑定*/ for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]) } } /** * Observe a list of Array items. */ observeArray (items: Array) { /*数组需要遍历每一个成员进行observe*/ for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } } ``` Observer为数据加上响应式属性进行双向绑定。如果是对象则进行深度遍历,为每一个子对象都绑定上方法,如果是数组则为每一个成员都绑定上方法。 如果是修改一个数组的成员,该成员是一个对象,那只需要递归对数组的成员进行双向绑定即可。但这时候出现了一个问题:如果我们进行pop、push等操作的时候,push进去的对象根本没有进行过双向绑定,更别说pop了,那么我们如何监听数组的这些变化呢? Vue.js提供的方法是重写push、pop、shift、unshift、splice、sort、reverse这七个[数组方法](http://v1-cn.vuejs.org/guide/list.html#变异方法)。修改数组原型方法的代码可以参考[observer/array.js](https://github.com/vuejs/vue/blob/dev/src/core/observer/array.js)以及[observer/index.js](https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js#L45)。 ```javascript export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that has this object as root $data constructor (value: any) { //....... if (Array.isArray(value)) { /* 如果是数组,将修改后可以截获响应的数组方法替换掉该数组的原型中的原生方法,达到监听数组数据变化响应的效果。 这里如果当前浏览器支持__proto__属性,则直接覆盖当前数组对象原型上的原生数组方法,如果不支持该属性,则直接覆盖数组对象的原型。 */ const augment = hasProto ? protoAugment /*直接覆盖原型的方法来修改目标对象*/ : copyAugment /*定义(覆盖)目标对象或数组的某一个方法*/ augment(value, arrayMethods, arrayKeys) /*如果是数组则需要遍历数组的每一个成员进行observe*/ this.observeArray(value) } else { /*如果是对象则直接walk进行绑定*/ this.walk(value) } } } /** * Augment an target Object or Array by intercepting * the prototype chain using __proto__ */ /*直接覆盖原型的方法来修改目标对象或数组*/ function protoAugment (target, src: Object) { /* eslint-disable no-proto */ target.__proto__ = src /* eslint-enable no-proto */ } /** * Augment an target Object or Array by defining * hidden properties. */ /* istanbul ignore next */ /*定义(覆盖)目标对象或数组的某一个方法*/ function copyAugment (target: Object, src: Object, keys: Array) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) } } ``` ```javascript /* * not type checking this file because flow doesn't play well with * dynamically accessing methods on Array prototype */ import { def } from '../util/index' /*取得原生数组的原型*/ const arrayProto = Array.prototype /*创建一个新的数组对象,修改该对象上的数组的七个方法,防止污染原生数组方法*/ export const arrayMethods = Object.create(arrayProto) /** * Intercept mutating methods and emit events */ /*这里重写了数组的这些方法,在保证不污染原生数组原型的情况下重写数组的这些方法,截获数组的成员发生的变化,执行原生数组操作的同时dep通知关联的所有观察者进行响应式处理*/ [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method) { // cache original method /*将数组的原生方法缓存起来,后面要调用*/ const original = arrayProto[method] def(arrayMethods, method, function mutator () { // avoid leaking arguments: // http://jsperf.com/closure-with-arguments let i = arguments.length const args = new Array(i) while (i--) { args[i] = arguments[i] } /*调用原生的数组方法*/ const result = original.apply(this, args) /*数组新插入的元素需要重新进行observe才能响应式*/ const ob = this.__ob__ let inserted switch (method) { case 'push': inserted = args break case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change /*dep通知所有注册的观察者进行响应式处理*/ ob.dep.notify() return result }) }) ``` 从数组的原型新建一个Object.create(arrayProto)对象,通过修改此原型可以保证原生数组方法不被污染。如果当前浏览器支持__proto__这个属性的话就可以直接覆盖该属性则使数组对象具有了重写后的数组方法。如果没有该属性的浏览器,则必须通过遍历def所有需要重写的数组方法,这种方法效率较低,所以优先使用第一种。 在保证不污染不覆盖数组原生方法添加监听,主要做了两个操作,第一是通知所有注册的观察者进行响应式处理,第二是如果是添加成员的操作,需要对新成员进行observe。 但是修改了数组的原生方法以后我们还是没法像原生数组一样直接通过数组的下标或者设置length来修改数组,可以通过[Vue.set以及splice方法](https://cn.vuejs.org/v2/guide/list.html#%E6%9B%BF%E6%8D%A2%E6%95%B0%E7%BB%84)。 ## Watcher [Watcher](https://github.com/vuejs/vue/blob/dev/src/core/observer/watcher.js#L24)是一个观察者对象。依赖收集以后Watcher对象会被保存在Deps中,数据变动的时候会由Deps通知Watcher实例,然后由Watcher实例回调cb进行视图的更新。 ```javascript 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; newDeps: Array; depIds: ISet; newDepIds: ISet; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: Object ) { this.vm = vm /*_watchers存放订阅者实例*/ 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 /*把表达式expOrFn解析成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. */ /*获得getter的值并且重新进行依赖收集*/ get () { /*将自身watcher观察者实例设置给Dep.target,用以依赖收集。*/ pushTarget(this) let value const vm = this.vm /* 执行了getter操作,看似执行了渲染操作,其实是执行了依赖收集。 在将Dep.target设置为自身观察者实例以后,执行getter操作。 譬如说现在的的data中可能有a、b、c三个数据,getter渲染需要依赖a跟c, 那么在执行getter的时候就会触发a跟c两个数据的getter函数, 在getter函数中即可判断Dep.target是否存在然后完成依赖收集, 将该观察者对象放入闭包中的Dep的subs中去。 */ if (this.user) { try { value = this.getter.call(vm, vm) } catch (e) { handleError(e, vm, `getter for watcher "${this.expression}"`) } } else { value = this.getter.call(vm, vm) } // "touch" every property so they are all tracked as // dependencies for deep watching /*如果存在deep,则触发每个深层对象的依赖,追踪其变化*/ if (this.deep) { /*递归每一个对象或者数组,触发它们的getter,使得对象或数组的每一个成员都被依赖收集,形成一个“深(deep)”依赖关系*/ traverse(value) } /*将观察者实例从target栈中取出并设置给Dep.target*/ popTarget() this.cleanupDeps() return value } /** * Add a dependency to this directive. */ /*添加一个依赖关系到Deps集合中*/ addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } } /** * 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) } } 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. */ /* 调度者接口,当依赖发生改变的时候进行回调。 */ update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { /*同步则执行run直接渲染视图*/ this.run() } else { /*异步推送到观察者队列中,由调度者调用。*/ queueWatcher(this) } } /** * Scheduler job interface. * Will be called by the scheduler. */ /* 调度者工作接口,将被调度者回调。 */ run () { if (this.active) { 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. /* 即便值相同,拥有Deep属性的观察者以及在对象/数组上的观察者应该被触发更新,因为它们的值可能发生改变。 */ 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. */ /*收集该watcher的所有deps依赖*/ 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. /*从vm实例的观察者列表中将自身移除,由于该操作比较耗费资源,所以如果vm实例正在被销毁则跳过该步骤。*/ if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } } } ``` ## Dep 来看看[Dep](https://github.com/vuejs/vue/blob/dev/src/core/observer/dep.js#L12)类。其实Dep就是一个发布者,可以订阅多个观察者,依赖收集之后Deps中会存在一个或多个Watcher对象,在数据变更的时候通知所有的Watcher。 ```javascript /** * A dep is an observable that can have multiple * directives subscribing to it. */ export default class Dep { static target: ?Watcher; id: number; subs: Array; constructor () { this.id = uid++ this.subs = [] } /*添加一个观察者对象*/ addSub (sub: Watcher) { this.subs.push(sub) } /*移除一个观察者对象*/ removeSub (sub: Watcher) { remove(this.subs, sub) } /*依赖收集,当存在Dep.target的时候添加观察者对象*/ depend () { if (Dep.target) { Dep.target.addDep(this) } } /*通知所有订阅者*/ notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } // the current target watcher being evaluated. // this is globally unique because there could be only one // watcher being evaluated at any time. Dep.target = null /*依赖收集完需要将Dep.target设为null,防止后面重复添加依赖。*/ ``` ## defineReactive 接下来是[defineReactive](https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js#L131)。defineReactive的作用是通过Object.defineProperty为数据定义上getter\setter方法,进行依赖收集后闭包中的Deps会存放Watcher对象。触发setter改变数据的时候会通知Deps订阅者通知所有的Watcher观察者对象进行试图的更新。 ```javascript /** * Define a reactive property on an Object. */ export function defineReactive ( obj: Object, key: string, val: any, customSetter?: Function ) { /*在闭包中定义一个dep对象*/ const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } /*如果之前该对象已经预设了getter以及setter函数则将其取出来,新定义的getter/setter中会将其执行,保证不会覆盖之前已经定义的getter/setter。*/ // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set /*对象的子对象递归进行observe并返回子节点的Observer对象*/ let childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { /*如果原本对象拥有getter方法则执行*/ const value = getter ? getter.call(obj) : val if (Dep.target) { /*进行依赖收集*/ dep.depend() if (childOb) { /*子对象进行依赖收集,其实就是将同一个watcher观察者实例放进了两个depend中,一个是正在本身闭包中的depend,另一个是子元素的depend*/ childOb.dep.depend() } if (Array.isArray(value)) { /*是数组则需要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。*/ dependArray(value) } } return value }, set: function reactiveSetter (newVal) { /*通过getter方法获取当前值,与新值进行比较,一致则不需要执行下面的操作*/ 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方法则执行setter*/ setter.call(obj, newVal) } else { val = newVal } /*新的值需要重新进行observe,保证数据响应式*/ childOb = observe(newVal) /*dep对象通知所有的观察者*/ dep.notify() } }) } ``` 现在再来看这张图是不是更清晰了呢? ![](https://cn.vuejs.org/images/data.png) ================================================ FILE: docs/依赖收集.MarkDown ================================================ ## 为什么要依赖收集 先看下面这段代码 ```javascript new Vue({ template: `
text1: {{text1}} text2: {{text2}}
`, data: { text1: 'text1', text2: 'text2', text3: 'text3' } }); ``` 按照之前[《响应式原理》](https://github.com/answershuto/learnVue/blob/master/docs/%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86.MarkDown)中的方法进行绑定则会出现一个问题——text3在实际模板中并没有被用到,然而当text3的数据被修改(this.text3 = 'test')的时候,同样会触发text3的setter导致重新执行渲染,这显然不正确。 ## 先说说Dep 当对data上的对象进行修改值的时候会触发它的setter,那么取值的时候自然就会触发getter事件,所以我们只要在最开始进行一次render,那么所有被渲染所依赖的data中的数据就会被getter收集到Dep的subs中去。在对data中的数据进行修改的时候setter只会触发Dep的subs的函数。 定义一个依赖收集类Dep。 ```javascript class Dep { constructor () { this.subs = []; } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } /*Github:https://github.com/answershuto*/ notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } function remove (arr, item) { if (arr.length) { const index = arr.indexOf(item) if (index > -1) { return arr.splice(index, 1) } } } ``` ## Watcher 订阅者,当依赖收集的时候会addSub到sub中,在修改data中数据的时候会触发dep对象的notify,通知所有Watcher对象去修改对应视图。 ```javascript class Watcher { constructor (vm, expOrFn, cb, options) { this.cb = cb; this.vm = vm; /*在这里将观察者本身赋值给全局的target,只有被target标记过的才会进行依赖收集*/ Dep.target = this; /*Github:https://github.com/answershuto*/ /*触发渲染操作进行依赖收集*/ this.cb.call(this.vm); } update () { this.cb.call(this.vm); } } ``` ## 开始依赖收集 ```javascript class Vue { constructor(options) { this._data = options.data; observer(this._data, options.render); let watcher = new Watcher(this, ); } } function defineReactive (obj, key, val, cb) { /*在闭包内存储一个Dep对象*/ const dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: ()=>{ if (Dep.target) { /*Watcher对象存在全局的Dep.target中*/ dep.addSub(Dep.target); } }, set:newVal=> { /*只有之前addSub中的函数才会触发*/ dep.notify(); } }) } Dep.target = null; ``` 将观察者Watcher实例赋值给全局的Dep.target,然后触发render操作只有被Dep.target标记过的才会进行依赖收集。有Dep.target的对象会将Watcher的实例push到subs中,在对象被修改触发setter操作的时候dep会调用subs中的Watcher实例的update方法进行渲染。 ================================================ FILE: docs/响应式原理.MarkDown ================================================ ## 关于Vue.js Vue.js是一款MVVM框架,上手快速简单易用,通过响应式在修改数据的时候更新视图。Vue.js的响应式原理依赖于[Object.defineProperty](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty),尤大大在[Vue.js文档](https://cn.vuejs.org/v2/guide/reactivity.html#如何追踪变化)中就已经提到过,这也是Vue.js不支持IE8 以及更低版本浏览器的原因。Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。 ## 将数据data变成可观察(observable)的 那么Vue是如何将所有data下面的所有属性变成可观察的(observable)呢? ```javascript function observe(value, cb) { Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb)) } function defineReactive (obj, key, val, cb) { Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: ()=>{ /*....依赖收集等....*/ /*Github:https://github.com/answershuto*/ return val }, set:newVal=> { val = newVal; cb();/*订阅者收到消息的回调*/ } }) } class Vue { constructor(options) { this._data = options.data; observe(this._data, options.render) } } let app = new Vue({ el: '#app', data: { text: 'text', text2: 'text2' }, render(){ console.log("render"); } }) ``` 为了便于理解,首先考虑一种最简单的情况,不考虑数组等情况,代码如上所示。在[initData](https://github.com/vuejs/vue/blob/dev/src/core/instance/state.js#L107)中会调用[observe](https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js#L106)这个函数将Vue的数据设置成observable的。当_data数据发生改变的时候就会触发set,对订阅者进行回调(在这里是render)。 那么问题来了,需要对app._data.text操作才会触发set。为了偷懒,我们需要一种方便的方法通过app.text直接设置就能触发set对视图进行重绘。那么就需要用到代理。 ## 代理 我们可以在Vue的构造函数constructor中为data执行一个代理[proxy](https://github.com/vuejs/vue/blob/dev/src/core/instance/state.js#L33)。这样我们就把data上面的属性代理到了vm实例上。 ```javascript _proxy.call(this, options.data);/*构造函数中*/ /*代理*/ function _proxy (data) { const that = this; Object.keys(data).forEach(key => { Object.defineProperty(that, key, { configurable: true, enumerable: true, get: function proxyGetter () { return that._data[key]; }, set: function proxySetter (val) { that._data[key] = val; } }) }); } ``` 我们就可以用app.text代替app._data.text了。 ================================================ FILE: docs/聊聊Vue的template编译.MarkDown ================================================ ## $mount 首先看一下mount的代码 ```javascript /*把原本不带编译的$mount方法保存下来,在最后会调用。*/ const mount = Vue.prototype.$mount /*挂载组件,带模板编译*/ Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) /* istanbul ignore if */ if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to or - mount to normal elements instead.` ) return this } const options = this.$options // resolve template/el and convert to render function /*处理模板templete,编译成render函数,render不存在的时候才会编译template,否则优先使用render*/ if (!options.render) { let template = options.template /*template存在的时候取template,不存在的时候取el的outerHTML*/ if (template) { /*当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为DOM节点的时候*/ template = template.innerHTML } else { /*报错*/ if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } } else if (el) { /*获取element的outerHTML*/ template = getOuterHTML(el) } if (template) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') } /*将template编译成render函数,这里会有render以及staticRenderFns两个返回,这是vue的编译时优化,static静态不需要在VNode更新时进行patch,优化性能*/ const { render, staticRenderFns } = compileToFunctions(template, { shouldDecodeNewlines, delimiters: options.delimiters }, this) options.render = render options.staticRenderFns = staticRenderFns /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile end') measure(`${this._name} compile`, 'compile', 'compile end') } } } /*Github:https://github.com/answershuto*/ /*调用const mount = Vue.prototype.$mount保存下来的不带编译的mount*/ return mount.call(this, el, hydrating) } ``` 通过mount代码我们可以看到,在mount的过程中,如果render函数不存在(render函数存在会优先使用render)会将template进行compileToFunctions得到render以及staticRenderFns。譬如说手写组件时加入了template的情况都会在运行时进行编译。而render function在运行后会返回VNode节点,供页面的渲染以及在update的时候patch。接下来我们来看一下template是如何编译的。 ## 一些基础 首先,template会被编译成AST,那么AST是什么? 在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。具体可以查看[抽象语法树](https://zh.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E8%AA%9E%E6%B3%95%E6%A8%B9)。 AST会经过generate得到render函数,render的返回值是VNode,VNode是Vue的虚拟DOM节点,具体定义如下: ```javascript export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope functionalContext: Component | void; // only for functional component root nodes key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? /*Github:https://github.com/answershuto*/ constructor ( tag?: string, data?: VNodeData, children?: ?Array, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions ) { /*当前节点的标签名*/ this.tag = tag /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/ this.data = data /*当前节点的子节点,是一个数组*/ this.children = children /*当前节点的文本*/ this.text = text /*当前虚拟节点对应的真实dom节点*/ this.elm = elm /*当前节点的名字空间*/ this.ns = undefined /*编译作用域*/ this.context = context /*函数化组件作用域*/ this.functionalContext = undefined /*节点的key属性,被当作节点的标志,用以优化*/ this.key = data && data.key /*组件的option选项*/ this.componentOptions = componentOptions /*当前节点对应的组件的实例*/ this.componentInstance = undefined /*当前节点的父节点*/ this.parent = undefined /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/ this.raw = false /*静态节点标志*/ this.isStatic = false /*是否作为跟节点插入*/ this.isRootInsert = true /*是否为注释节点*/ this.isComment = false /*是否为克隆节点*/ this.isCloned = false /*是否有v-once指令*/ this.isOnce = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } } ``` 关于VNode的一些细节,请参考[VNode节点](https://github.com/answershuto/learnVue/blob/master/docs/VNode%E8%8A%82%E7%82%B9.MarkDown)。 ## createCompiler createCompiler用以创建编译器,返回值是compile以及compileToFunctions。compile是一个编译器,它会将传入的template转换成对应的AST、render函数以及staticRenderFns函数。而compileToFunctions则是带缓存的编译器,同时staticRenderFns以及render函数会被转换成Funtion对象。 因为不同平台有一些不同的options,所以createCompiler会根据平台区分传入一个baseOptions,会与compile本身传入的options合并得到最终的finalOptions。 ## compileToFunctions 首先还是贴一下compileToFunctions的代码。 ```javascript /*带缓存的编译器,同时staticRenderFns以及render函数会被转换成Funtion对象*/ function compileToFunctions ( template: string, options?: CompilerOptions, vm?: Component ): CompiledFunctionResult { options = options || {} /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production') { // detect possible CSP restriction try { new Function('return 1') } catch (e) { if (e.toString().match(/unsafe-eval|CSP/)) { warn( 'It seems you are using the standalone build of Vue.js in an ' + 'environment with Content Security Policy that prohibits unsafe-eval. ' + 'The template compiler cannot work in this environment. Consider ' + 'relaxing the policy to allow unsafe-eval or pre-compiling your ' + 'templates into render functions.' ) } } } /*Github:https://github.com/answershuto*/ // check cache /*有缓存的时候直接取出缓存中的结果即可*/ const key = options.delimiters ? String(options.delimiters) + template : template if (functionCompileCache[key]) { return functionCompileCache[key] } // compile /*编译*/ const compiled = compile(template, options) // check compilation errors/tips if (process.env.NODE_ENV !== 'production') { if (compiled.errors && compiled.errors.length) { warn( `Error compiling template:\n\n${template}\n\n` + compiled.errors.map(e => `- ${e}`).join('\n') + '\n', vm ) } if (compiled.tips && compiled.tips.length) { compiled.tips.forEach(msg => tip(msg, vm)) } } // turn code into functions const res = {} const fnGenErrors = [] /*将render转换成Funtion对象*/ res.render = makeFunction(compiled.render, fnGenErrors) /*将staticRenderFns全部转化成Funtion对象 */ const l = compiled.staticRenderFns.length res.staticRenderFns = new Array(l) for (let i = 0; i < l; i++) { res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i], fnGenErrors) } // check function generation errors. // this should only happen if there is a bug in the compiler itself. // mostly for codegen development use /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production') { if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) { warn( `Failed to generate render function:\n\n` + fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'), vm ) } } /*存放在缓存中,以免每次都重新编译*/ return (functionCompileCache[key] = res) } ``` 我们可以发现,在闭包中,会有一个functionCompileCache对象作为缓存器。 ```javascript /*作为缓存,防止每次都重新编译*/ const functionCompileCache: { [key: string]: CompiledFunctionResult; } = Object.create(null) ``` 在进入compileToFunctions以后,会先检查缓存中是否有已经编译好的结果,如果有结果则直接从缓存中读取。这样做防止每次同样的模板都要进行重复的编译工作。 ```javascript // check cache /*有缓存的时候直接取出缓存中的结果即可*/ const key = options.delimiters ? String(options.delimiters) + template : template if (functionCompileCache[key]) { return functionCompileCache[key] } ``` 在compileToFunctions的末尾会将编译结果进行缓存 ```javascript /*存放在缓存中,以免每次都重新编译*/ return (functionCompileCache[key] = res) ``` ## compile ```javascript /*编译,将模板template编译成AST、render函数以及staticRenderFns函数*/ function compile ( template: string, options?: CompilerOptions ): CompiledResult { const finalOptions = Object.create(baseOptions) const errors = [] const tips = [] finalOptions.warn = (msg, tip) => { (tip ? tips : errors).push(msg) } /*做下面这些merge的目的因为不同平台可以提供自己本身平台的一个baseOptions,内部封装了平台自己的实现,然后把共同的部分抽离开来放在这层compiler中,所以在这里需要merge一下*/ if (options) { // merge custom modules /*合并modules*/ if (options.modules) { finalOptions.modules = (baseOptions.modules || []).concat(options.modules) } // merge custom directives if (options.directives) { /*合并directives*/ finalOptions.directives = extend( Object.create(baseOptions.directives), options.directives ) } // copy other options for (const key in options) { /*合并其余的options,modules与directives已经在上面做了特殊处理了*/ if (key !== 'modules' && key !== 'directives') { finalOptions[key] = options[key] } } } /*基础模板编译,得到编译结果*/ const compiled = baseCompile(template, finalOptions) if (process.env.NODE_ENV !== 'production') { errors.push.apply(errors, detectErrors(compiled.ast)) } compiled.errors = errors compiled.tips = tips return compiled } ``` compile主要做了两件事,一件是合并option(前面说的将平台自有的option与传入的option进行合并),另一件是baseCompile,进行模板template的编译。 来看一下baseCompile ## baseCompile ```javascript function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { /*parse解析得到AST*/ const ast = parse(template.trim(), options) /* 将AST进行优化 优化的目标:生成模板AST,检测不需要进行DOM改变的静态子树。 一旦检测到这些静态树,我们就能做以下这些事情: 1.把它们变成常数,这样我们就再也不需要每次重新渲染时创建新的节点了。 2.在patch的过程中直接跳过。 */ optimize(ast, options) /*根据AST生成所需的code(内部包含render与staticRenderFns)*/ const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } } ``` baseCompile首先会将模板template进行parse得到一个AST,再通过optimize做一些优化,最后通过generate得到render以及staticRenderFns。 ### parse parse的源码可以参见[https://github.com/answershuto/learnVue/blob/master/vue-src/compiler/parser/index.js#L53](https://github.com/answershuto/learnVue/blob/master/vue-src/compiler/parser/index.js#L53)。 parse会用正则等方式解析template模板中的指令、class、style等数据,形成AST。 ### optimize optimize的主要作用是标记static静态节点,这是Vue在编译过程中的一处优化,后面当update更新界面时,会有一个patch的过程,diff算法会直接跳过静态节点,从而减少了比较的过程,优化了patch的性能。 ### generate generate是将AST转化成render funtion字符串的过程,得到结果是render的字符串以及staticRenderFns字符串。 --- 至此,我们的template模板已经被转化成了我们所需的AST、render function字符串以及staticRenderFns字符串。 ## 举个例子 来看一下这段代码的编译结果 ```html
{{text}}
hello world

{{item.name}}

{{item.value}}

{{index}}

---

{{text}}
``` 转化后得到AST,如下图: ![img](https://i.loli.net/2017/09/07/59b135001cbfa.png) 我们可以看到最外层的div是这颗AST的根节点,节点上有许多数据代表这个节点的形态,比如static表示是否是静态节点,staticClass表示静态class属性(非bind:class)。children代表该节点的子节点,可以看到children是一个长度为4的数组,里面包含的是该节点下的四个div子节点。children里面的节点与父节点的结构类似,层层往下形成一棵AST。 再来看看由AST得到的render函数 ```javascript with(this){ return _c( 'div', { /*static class*/ staticClass:"main", /*bind class*/ class:bindClass }, [ _c( 'div', [_v(_s(text))]), _c('div',[_v("hello world")]), /*这是一个v-for循环*/ _l( (arr), function(item,index){ return _c( 'div', [_c('p',[_v(_s(item.name))]), _c('p',[_v(_s(item.value))]), _c('p',[_v(_s(index))]), _c('p',[_v("---")])] ) } ), /*这是v-if*/ (text)?_c('div',[_v(_s(text))]):_c('div',[_v("no text")])], 2 ) } ``` ## \_c,\_v,\_s,\_q 看了render function字符串,发现有大量的_c,_v,_s,_q,这些函数究竟是什么? 带着问题,我们来看一下[core/instance/render](https://github.com/answershuto/learnVue/blob/master/vue-src/core/instance/render.js#L124)。 ```javascript /*处理v-once的渲染函数*/ Vue.prototype._o = markOnce /*将字符串转化为数字,如果转换失败会返回原字符串*/ Vue.prototype._n = toNumber /*将val转化成字符串*/ Vue.prototype._s = toString /*处理v-for列表渲染*/ Vue.prototype._l = renderList /*处理slot的渲染*/ Vue.prototype._t = renderSlot /*检测两个变量是否相等*/ Vue.prototype._q = looseEqual /*检测arr数组中是否包含与val变量相等的项*/ Vue.prototype._i = looseIndexOf /*处理static树的渲染*/ Vue.prototype._m = renderStatic /*处理filters*/ Vue.prototype._f = resolveFilter /*从config配置中检查eventKeyCode是否存在*/ Vue.prototype._k = checkKeyCodes /*合并v-bind指令到VNode中*/ Vue.prototype._b = bindObjectProps /*创建一个文本节点*/ Vue.prototype._v = createTextVNode /*创建一个空VNode节点*/ Vue.prototype._e = createEmptyVNode /*处理ScopedSlots*/ Vue.prototype._u = resolveScopedSlots /*创建VNode节点*/ vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) ``` 通过这些函数,render函数最后会返回一个VNode节点,在_update的时候,经过patch与之前的VNode节点进行比较,得出差异后将这些差异渲染到真实的DOM上。 ================================================ FILE: docs/聊聊keep-alive组件的使用及其实现原理.MarkDown ================================================ ## keep-alive keep-alive是Vue.js的一个内置组件。它能够不活动的组件实例保存在内存中,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。 它提供了include与exclude两个属性,允许组件有条件地进行缓存。 具体内容可以参考[官网](https://cn.vuejs.org/v2/api/#keep-alive)。 ## 使用 ### 用法 ```html ``` 这里的component组件会被缓存起来。 ### 举个栗子 ```html ``` ```javascript export default { data () { return { test: true } }, methods: { handleClick () { this.test = !this.test; } } } ``` 在点击button时候,coma与comb两个组件会发生切换,但是这时候这两个组件的状态会被缓存起来,比如说coma与comb组件中都有一个input标签,那么input标签中的内容不会因为组件的切换而消失。 ### props keep-alive组件提供了include与exclude两个属性来允许组件有条件地进行缓存,二者都可以用逗号分隔字符串、正则表达式或一个数组来表示。 ```html ``` 将缓存name为a的组件。 ```html ``` name为a的组件将不会被缓存。 ### 生命钩子 keep-alive提供了两个生命钩子,分别是activated与deactivated。 因为keep-alive会将组件保存在内存中,并不会销毁以及重新创建,所以不会重新调用组件的created等方法,需要用activated与deactivated这两个生命钩子来得知当前组件是否处于活动状态。 --- ## 深入keep-alive组件实现 说完了keep-alive组件的使用,我们从源码角度看一下keep-alive组件究竟是如何实现组件的缓存的呢? ### created与destroyed钩子 created钩子会创建一个cache对象,用来作为缓存容器,保存vnode节点。 ```javascript created () { /* 缓存对象 */ this.cache = Object.create(null) }, ``` destroyed钩子则在组件被销毁的时候清除cache缓存中的所有组件实例。 ```javascript /* destroyed钩子中销毁所有cache中的组件实例 */ destroyed () { for (const key in this.cache) { pruneCacheEntry(this.cache[key]) } }, ``` ### render 接下来是render函数。 ```javascript render () { /* 得到slot插槽中的第一个组件 */ const vnode: VNode = getFirstComponentChild(this.$slots.default) const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions if (componentOptions) { // check pattern /* 获取组件名称,优先获取组件的name字段,否则是组件的tag */ const name: ?string = getComponentName(componentOptions) /* name不在inlcude中或者在exlude中则直接返回vnode(没有取缓存) */ if (name && ( (this.include && !matches(this.include, name)) || (this.exclude && matches(this.exclude, name)) )) { return vnode } const key: ?string = vnode.key == null // same constructor may get registered as different local components // so cid alone is not enough (#3269) ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key /* 如果已经做过缓存了则直接从缓存中获取组件实例给vnode,还未缓存过则进行缓存 */ if (this.cache[key]) { vnode.componentInstance = this.cache[key].componentInstance } else { this.cache[key] = vnode } /* keepAlive标记位 */ vnode.data.keepAlive = true } return vnode } ``` 首先通过getFirstComponentChild获取第一个子组件,获取该组件的name(存在组件名则直接使用组件名,否则会使用tag)。接下来会将这个name通过include与exclude属性进行匹配,匹配不成功(说明不需要进行缓存)则不进行任何操作直接返回vnode,vnode是一个VNode类型的对象,不了解VNode的同学可以参考笔者的另一篇文章[《VNode节点》](https://github.com/answershuto/learnVue/blob/master/docs/VNode%E8%8A%82%E7%82%B9.MarkDown) . ```javascript /* 检测name是否匹配 */ function matches (pattern: string | RegExp, name: string): boolean { if (typeof pattern === 'string') { /* 字符串情况,如a,b,c */ return pattern.split(',').indexOf(name) > -1 } else if (isRegExp(pattern)) { /* 正则 */ return pattern.test(name) } /* istanbul ignore next */ return false } ``` 检测include与exclude属性匹配的函数很简单,include与exclude属性支持字符串如"a,b,c"这样组件名以逗号隔开的情况以及正则表达式。matches通过这两种方式分别检测是否匹配当前组件。 ```javascript if (this.cache[key]) { vnode.componentInstance = this.cache[key].componentInstance } else { this.cache[key] = vnode } ``` 接下来的事情很简单,根据key在this.cache中查找,如果存在则说明之前已经缓存过了,直接将缓存的vnode的componentInstance(组件实例)覆盖到目前的vnode上面。否则将vnode存储在cache中。 最后返回vnode(有缓存时该vnode的componentInstance已经被替换成缓存中的了)。 ### watch 用watch来监听pruneCache与pruneCache这两个属性的改变,在改变的时候修改cache缓存中的缓存数据。 ```javascript watch: { /* 监视include以及exclude,在被修改的时候对cache进行修正 */ include (val: string | RegExp) { pruneCache(this.cache, this._vnode, name => matches(val, name)) }, exclude (val: string | RegExp) { pruneCache(this.cache, this._vnode, name => !matches(val, name)) } }, ``` 来看一下pruneCache的实现。 ```javascript /* 修正cache */ function pruneCache (cache: VNodeCache, current: VNode, filter: Function) { for (const key in cache) { /* 取出cache中的vnode */ const cachedNode: ?VNode = cache[key] if (cachedNode) { const name: ?string = getComponentName(cachedNode.componentOptions) /* name不符合filter条件的,同时不是目前渲染的vnode时,销毁vnode对应的组件实例(Vue实例),并从cache中移除 */ if (name && !filter(name)) { if (cachedNode !== current) { pruneCacheEntry(cachedNode) } cache[key] = null } } } } /* 销毁vnode对应的组件实例(Vue实例) */ function pruneCacheEntry (vnode: ?VNode) { if (vnode) { vnode.componentInstance.$destroy() } } ``` 遍历cache中的所有项,如果不符合filter指定的规则的话,则会执行pruneCacheEntry。pruneCacheEntry则会调用组件实例的$destroy方法来将组件销毁。 ## 最后 Vue.js内部将DOM节点抽象成了一个个的[VNode节点](https://github.com/answershuto/learnVue/blob/master/docs/VNode%E8%8A%82%E7%82%B9.MarkDown),keep-alive组件的缓存也是基于VNode节点的而不是直接存储DOM结构。它将满足条件(pruneCache与pruneCache)的组件在cache对象中缓存起来,在需要重新渲染的时候再将vnode节点从cache对象中取出并渲染。 ================================================ FILE: docs/说说element组件库broadcast与dispatch.MarkDown ================================================ 众所周知,Vue 在 2.0 版本中去除了$broadcast方法以及$dispatch 方法,最近在学习饿了么的[Element](https://github.com/ElemeFE/element)时重新实现了这两种方法,并以 minix 的方式引入。 看一下[源代码](https://github.com/ElemeFE/element/blob/dev/src/mixins/emitter.js) ```javascript function broadcast(componentName, eventName, params) { /*遍历当前节点下的所有子组件*/ this.$children.forEach(child => { /*获取子组件名称*/ var name = child.$options.componentName; if (name === componentName) { /*如果是我们需要广播到的子组件的时候调用$emit触发所需事件,在子组件中用$on监听*/ child.$emit.apply(child, [eventName].concat(params)); } else { /*非所需子组件则递归遍历深层次子组件*/ broadcast.apply(child, [componentName, eventName].concat([params])); } }); } export default { methods: { /*对多级父组件进行事件派发*/ dispatch(componentName, eventName, params) { /*获取父组件,如果以及是根组件,则是$root*/ var parent = this.$parent || this.$root; /*获取父节点的组件名*/ var name = parent.$options.componentName; while (parent && (!name || name !== componentName)) { /*当父组件不是所需组件时继续向上寻找*/ parent = parent.$parent; if (parent) { name = parent.$options.componentName; } } /*找到所需组件后调用$emit触发当前事件*/ if (parent) { parent.$emit.apply(parent, [eventName].concat(params)); } }, /* 向所有子组件进行事件广播。 这里包了一层,为了修改broadcast的this对象为当前Vue实例 */ broadcast(componentName, eventName, params) { broadcast.call(this, componentName, eventName, params); } } }; ``` 其实这里的broadcast与dispatch实现了一个定向的多层级父子组件间的事件广播及事件派发功能。完成多层级分发对应事件的组件间通信功能。 broadcast通过递归遍历子组件找到所需组件广播事件,而dispatch则逐级向上查找对应父组件派发事件。 broadcast需要三个参数,componentName(组件名),eventName(事件名称)以及params(数据)。根据componentName深度遍历子组件找到对应组件emit事件eventName。 dispatch同样道理,需要三个参数,componentName(组件名),eventName(事件名称)以及params(数据)。根据componentName向上级一直寻找对应父组件,找到以后emit事件eventName。 ================================================ FILE: vue-router-src/components/link.js ================================================ /* @flow */ import { createRoute, isSameRoute, isIncludedRoute } from '../util/route' import { _Vue } from '../install' // work around weird flow bug const toTypes: Array = [String, Object] const eventTypes: Array = [String, Array] export default { name: 'RouterLink', props: { to: { type: toTypes, required: true }, tag: { type: String, default: 'a' }, exact: Boolean, append: Boolean, replace: Boolean, activeClass: String, exactActiveClass: String, event: { type: eventTypes, default: 'click' } }, render (h: Function) { const router = this.$router const current = this.$route const { location, route, href } = router.resolve(this.to, current, this.append) const classes = {} const globalActiveClass = router.options.linkActiveClass const globalExactActiveClass = router.options.linkExactActiveClass // Support global empty active class const activeClassFallback = globalActiveClass == null ? 'router-link-active' : globalActiveClass const exactActiveClassFallback = globalExactActiveClass == null ? 'router-link-exact-active' : globalExactActiveClass const activeClass = this.activeClass == null ? activeClassFallback : this.activeClass const exactActiveClass = this.exactActiveClass == null ? exactActiveClassFallback : this.exactActiveClass const compareTarget = location.path ? createRoute(null, location, null, router) : route classes[exactActiveClass] = isSameRoute(current, compareTarget) classes[activeClass] = this.exact ? classes[exactActiveClass] : isIncludedRoute(current, compareTarget) const handler = e => { if (guardEvent(e)) { if (this.replace) { router.replace(location) } else { router.push(location) } } } const on = { click: guardEvent } if (Array.isArray(this.event)) { this.event.forEach(e => { on[e] = handler }) } else { on[this.event] = handler } const data: any = { class: classes } if (this.tag === 'a') { data.on = on data.attrs = { href } } else { // find the first child and apply listener and href const a = findAnchor(this.$slots.default) if (a) { // in case the is a static node a.isStatic = false const extend = _Vue.util.extend const aData = a.data = extend({}, a.data) aData.on = on const aAttrs = a.data.attrs = extend({}, a.data.attrs) aAttrs.href = href } else { // doesn't have child, apply listener to self data.on = on } } return h(this.tag, data, this.$slots.default) } } function guardEvent (e) { // don't redirect with control keys if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return // don't redirect when preventDefault called if (e.defaultPrevented) return // don't redirect on right click if (e.button !== undefined && e.button !== 0) return // don't redirect if `target="_blank"` if (e.currentTarget && e.currentTarget.getAttribute) { const target = e.currentTarget.getAttribute('target') if (/\b_blank\b/i.test(target)) return } // this may be a Weex event which doesn't have this method if (e.preventDefault) { e.preventDefault() } return true } function findAnchor (children) { if (children) { let child for (let i = 0; i < children.length; i++) { child = children[i] if (child.tag === 'a') { return child } if (child.children && (child = findAnchor(child.children))) { return child } } } } ================================================ FILE: vue-router-src/components/view.js ================================================ import { warn } from '../util/warn' /* router-view组件 */ export default { name: 'RouterView', /* https://cn.vuejs.org/v2/api/#functional 使组件无状态 (没有 data ) 和无实例 (没有 this 上下文)。他们用一个简单的 render 函数返回虚拟节点使他们更容易渲染。 */ functional: true, props: { name: { type: String, default: 'default' } }, render (_, { props, children, parent, data }) { /* 标记位,标记是route-view组件 */ data.routerView = true // directly use parent context's createElement() function // so that components rendered by router-view can resolve named slots /* 直接使用父组件的createElement函数 */ const h = parent.$createElement /* props的name,默认'default' */ const name = props.name /* option中的VueRouter对象 */ const route = parent.$route /* 在parent上建立一个缓存对象 */ const cache = parent._routerViewCache || (parent._routerViewCache = {}) // determine current view depth, also check to see if the tree // has been toggled inactive but kept-alive. /* 记录组件深度 */ let depth = 0 /* 标记是否是待用(非alive状态)) */ let inactive = false /* _routerRoot中中存放了根组件的势力,这边循环向上级访问,直到访问到根组件,得到depth深度 */ while (parent && parent._routerRoot !== parent) { if (parent.$vnode && parent.$vnode.data.routerView) { depth++ } /* 如果_inactive为true,代表是在keep-alive中且是待用(非alive状态) */ if (parent._inactive) { inactive = true } parent = parent.$parent } /* 存放route-view组件的深度 */ data.routerViewDepth = depth // render previous view if the tree is inactive and kept-alive /* 如果inactive为true说明在keep-alive组件中,直接从缓存中取 */ if (inactive) { return h(cache[name], data, children) } const matched = route.matched[depth] // render empty node if no matched route /* 如果没有匹配到的路由,则渲染一个空节点 */ if (!matched) { cache[name] = null return h() } /* 从成功匹配到的路由中取出组件 */ const component = cache[name] = matched.components[name] // attach instance registration hook // this will be called in the instance's injected lifecycle hooks /* 注册实例的registration钩子,这个函数将在实例被注入的加入到组件的生命钩子(beforeCreate与destroyed)中被调用 */ data.registerRouteInstance = (vm, val) => { /* 第二个值不存在的时候为注销 */ // val could be undefined for unregistration /* 获取组件实例 */ const current = matched.instances[name] if ( (val && current !== vm) || (!val && current === vm) ) { /* 这里有两种情况,一种是val存在,则用val替换当前组件实例,另一种则是val不存在,则直接将val(这个时候其实是一个undefined)赋给instances */ matched.instances[name] = val } } // also register instance in prepatch hook // in case the same component instance is reused across different routes ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => { matched.instances[name] = vnode.componentInstance } // resolve props let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name]) if (propsToPass) { // clone to prevent mutation propsToPass = data.props = extend({}, propsToPass) // pass non-declared props as attrs const attrs = data.attrs = data.attrs || {} for (const key in propsToPass) { if (!component.props || !(key in component.props)) { attrs[key] = propsToPass[key] delete propsToPass[key] } } } return h(component, data, children) } } function resolveProps (route, config) { switch (typeof config) { case 'undefined': return case 'object': return config case 'function': return config(route) case 'boolean': return config ? route.params : undefined default: if (process.env.NODE_ENV !== 'production') { warn( false, `props in "${route.path}" is a ${typeof config}, ` + `expecting an object, function or boolean.` ) } } } function extend (to, from) { for (const key in from) { to[key] = from[key] } return to } ================================================ FILE: vue-router-src/create-matcher.js ================================================ /* @flow */ import type VueRouter from './index' import { resolvePath } from './util/path' import { assert, warn } from './util/warn' import { createRoute } from './util/route' import { fillParams } from './util/params' import { createRouteMap } from './create-route-map' import { normalizeLocation } from './util/location' export type Matcher = { match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route; addRoutes: (routes: Array) => void; }; export function createMatcher ( routes: Array, router: VueRouter ): Matcher { const { pathList, pathMap, nameMap } = createRouteMap(routes) function addRoutes (routes) { createRouteMap(routes, pathList, pathMap, nameMap) } function match ( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { const location = normalizeLocation(raw, currentRoute, false, router) const { name } = location if (name) { const record = nameMap[name] if (process.env.NODE_ENV !== 'production') { warn(record, `Route with name '${name}' does not exist`) } if (!record) return _createRoute(null, location) const paramNames = record.regex.keys .filter(key => !key.optional) .map(key => key.name) if (typeof location.params !== 'object') { location.params = {} } if (currentRoute && typeof currentRoute.params === 'object') { for (const key in currentRoute.params) { if (!(key in location.params) && paramNames.indexOf(key) > -1) { location.params[key] = currentRoute.params[key] } } } if (record) { location.path = fillParams(record.path, location.params, `named route "${name}"`) return _createRoute(record, location, redirectedFrom) } } else if (location.path) { location.params = {} for (let i = 0; i < pathList.length; i++) { const path = pathList[i] const record = pathMap[path] if (matchRoute(record.regex, location.path, location.params)) { return _createRoute(record, location, redirectedFrom) } } } // no match return _createRoute(null, location) } function redirect ( record: RouteRecord, location: Location ): Route { const originalRedirect = record.redirect let redirect = typeof originalRedirect === 'function' ? originalRedirect(createRoute(record, location, null, router)) : originalRedirect if (typeof redirect === 'string') { redirect = { path: redirect } } if (!redirect || typeof redirect !== 'object') { if (process.env.NODE_ENV !== 'production') { warn( false, `invalid redirect option: ${JSON.stringify(redirect)}` ) } return _createRoute(null, location) } const re: Object = redirect const { name, path } = re let { query, hash, params } = location query = re.hasOwnProperty('query') ? re.query : query hash = re.hasOwnProperty('hash') ? re.hash : hash params = re.hasOwnProperty('params') ? re.params : params if (name) { // resolved named direct const targetRecord = nameMap[name] if (process.env.NODE_ENV !== 'production') { assert(targetRecord, `redirect failed: named route "${name}" not found.`) } return match({ _normalized: true, name, query, hash, params }, undefined, location) } else if (path) { // 1. resolve relative redirect const rawPath = resolveRecordPath(path, record) // 2. resolve params const resolvedPath = fillParams(rawPath, params, `redirect route with path "${rawPath}"`) // 3. rematch with existing query and hash return match({ _normalized: true, path: resolvedPath, query, hash }, undefined, location) } else { if (process.env.NODE_ENV !== 'production') { warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`) } return _createRoute(null, location) } } function alias ( record: RouteRecord, location: Location, matchAs: string ): Route { const aliasedPath = fillParams(matchAs, location.params, `aliased route with path "${matchAs}"`) const aliasedMatch = match({ _normalized: true, path: aliasedPath }) if (aliasedMatch) { const matched = aliasedMatch.matched const aliasedRecord = matched[matched.length - 1] location.params = aliasedMatch.params return _createRoute(aliasedRecord, location) } return _createRoute(null, location) } function _createRoute ( record: ?RouteRecord, location: Location, redirectedFrom?: Location ): Route { if (record && record.redirect) { return redirect(record, redirectedFrom || location) } if (record && record.matchAs) { return alias(record, location, record.matchAs) } return createRoute(record, location, redirectedFrom, router) } return { match, addRoutes } } function matchRoute ( regex: RouteRegExp, path: string, params: Object ): boolean { const m = path.match(regex) if (!m) { return false } else if (!params) { return true } for (let i = 1, len = m.length; i < len; ++i) { const key = regex.keys[i - 1] const val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i] if (key) { params[key.name] = val } } return true } function resolveRecordPath (path: string, record: RouteRecord): string { return resolvePath(path, record.parent ? record.parent.path : '/', true) } ================================================ FILE: vue-router-src/create-route-map.js ================================================ /* @flow */ import Regexp from 'path-to-regexp' import { cleanPath } from './util/path' import { assert, warn } from './util/warn' export function createRouteMap ( routes: Array, oldPathList?: Array, oldPathMap?: Dictionary, oldNameMap?: Dictionary ): { pathList: Array; pathMap: Dictionary; nameMap: Dictionary; } { // the path list is used to control path matching priority const pathList: Array = oldPathList || [] // $flow-disable-line const pathMap: Dictionary = oldPathMap || Object.create(null) // $flow-disable-line const nameMap: Dictionary = oldNameMap || Object.create(null) routes.forEach(route => { addRouteRecord(pathList, pathMap, nameMap, route) }) // ensure wildcard routes are always at the end for (let i = 0, l = pathList.length; i < l; i++) { if (pathList[i] === '*') { pathList.push(pathList.splice(i, 1)[0]) l-- i-- } } return { pathList, pathMap, nameMap } } function addRouteRecord ( pathList: Array, pathMap: Dictionary, nameMap: Dictionary, route: RouteConfig, parent?: RouteRecord, matchAs?: string ) { const { path, name } = route if (process.env.NODE_ENV !== 'production') { assert(path != null, `"path" is required in a route configuration.`) assert( typeof route.component !== 'string', `route config "component" for path: ${String(path || name)} cannot be a ` + `string id. Use an actual component instead.` ) } const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {} const normalizedPath = normalizePath( path, parent, pathToRegexpOptions.strict ) if (typeof route.caseSensitive === 'boolean') { pathToRegexpOptions.sensitive = route.caseSensitive } const record: RouteRecord = { path: normalizedPath, regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), components: route.components || { default: route.component }, instances: {}, name, parent, matchAs, redirect: route.redirect, beforeEnter: route.beforeEnter, meta: route.meta || {}, props: route.props == null ? {} : route.components ? route.props : { default: route.props } } if (route.children) { // Warn if route is named, does not redirect and has a default child route. // If users navigate to this route by name, the default child will // not be rendered (GH Issue #629) if (process.env.NODE_ENV !== 'production') { if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) { warn( false, `Named Route '${route.name}' has a default child route. ` + `When navigating to this named route (:to="{name: '${route.name}'"), ` + `the default child route will not be rendered. Remove the name from ` + `this route and use the name of the default child route for named ` + `links instead.` ) } } route.children.forEach(child => { const childMatchAs = matchAs ? cleanPath(`${matchAs}/${child.path}`) : undefined addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs) }) } if (route.alias !== undefined) { const aliases = Array.isArray(route.alias) ? route.alias : [route.alias] aliases.forEach(alias => { const aliasRoute = { path: alias, children: route.children } addRouteRecord( pathList, pathMap, nameMap, aliasRoute, parent, record.path || '/' // matchAs ) }) } if (!pathMap[record.path]) { pathList.push(record.path) pathMap[record.path] = record } if (name) { if (!nameMap[name]) { nameMap[name] = record } else if (process.env.NODE_ENV !== 'production' && !matchAs) { warn( false, `Duplicate named routes definition: ` + `{ name: "${name}", path: "${record.path}" }` ) } } } function compileRouteRegex (path: string, pathToRegexpOptions: PathToRegexpOptions): RouteRegExp { const regex = Regexp(path, [], pathToRegexpOptions) if (process.env.NODE_ENV !== 'production') { const keys: any = Object.create(null) regex.keys.forEach(key => { warn(!keys[key.name], `Duplicate param keys in route with path: "${path}"`) keys[key.name] = true }) } return regex } function normalizePath (path: string, parent?: RouteRecord, strict?: boolean): string { if (!strict) path = path.replace(/\/$/, '') if (path[0] === '/') return path if (parent == null) return path return cleanPath(`${parent.path}/${path}`) } ================================================ FILE: vue-router-src/history/abstract.js ================================================ /* @flow */ import type Router from '../index' import { History } from './base' export class AbstractHistory extends History { index: number; stack: Array; constructor (router: Router, base: ?string) { super(router, base) this.stack = [] this.index = -1 } push (location: RawLocation, onComplete?: Function, onAbort?: Function) { this.transitionTo(location, route => { this.stack = this.stack.slice(0, this.index + 1).concat(route) this.index++ onComplete && onComplete(route) }, onAbort) } replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { this.transitionTo(location, route => { this.stack = this.stack.slice(0, this.index).concat(route) onComplete && onComplete(route) }, onAbort) } go (n: number) { const targetIndex = this.index + n if (targetIndex < 0 || targetIndex >= this.stack.length) { return } const route = this.stack[targetIndex] this.confirmTransition(route, () => { this.index = targetIndex this.updateRoute(route) }) } getCurrentLocation () { const current = this.stack[this.stack.length - 1] return current ? current.fullPath : '/' } ensureURL () { // noop } } ================================================ FILE: vue-router-src/history/base.js ================================================ /* @flow */ import { _Vue } from '../install' import type Router from '../index' import { inBrowser } from '../util/dom' import { runQueue } from '../util/async' import { warn, isError } from '../util/warn' import { START, isSameRoute } from '../util/route' import { flatten, flatMapComponents, resolveAsyncComponents } from '../util/resolve-components' export class History { router: Router; base: string; current: Route; pending: ?Route; cb: (r: Route) => void; ready: boolean; readyCbs: Array; readyErrorCbs: Array; errorCbs: Array; // implemented by sub-classes +go: (n: number) => void; +push: (loc: RawLocation) => void; +replace: (loc: RawLocation) => void; +ensureURL: (push?: boolean) => void; +getCurrentLocation: () => string; constructor (router: Router, base: ?string) { this.router = router this.base = normalizeBase(base) // start with a route object that stands for "nowhere" this.current = START this.pending = null this.ready = false this.readyCbs = [] this.readyErrorCbs = [] this.errorCbs = [] } listen (cb: Function) { this.cb = cb } onReady (cb: Function, errorCb: ?Function) { if (this.ready) { cb() } else { this.readyCbs.push(cb) if (errorCb) { this.readyErrorCbs.push(errorCb) } } } onError (errorCb: Function) { this.errorCbs.push(errorCb) } transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { const route = this.router.match(location, this.current) this.confirmTransition(route, () => { this.updateRoute(route) onComplete && onComplete(route) this.ensureURL() // fire ready cbs once if (!this.ready) { this.ready = true this.readyCbs.forEach(cb => { cb(route) }) } }, err => { if (onAbort) { onAbort(err) } if (err && !this.ready) { this.ready = true this.readyErrorCbs.forEach(cb => { cb(err) }) } }) } confirmTransition (route: Route, onComplete: Function, onAbort?: Function) { const current = this.current const abort = err => { if (isError(err)) { if (this.errorCbs.length) { this.errorCbs.forEach(cb => { cb(err) }) } else { warn(false, 'uncaught error during route navigation:') console.error(err) } } onAbort && onAbort(err) } if ( isSameRoute(route, current) && // in the case the route map has been dynamically appended to route.matched.length === current.matched.length ) { this.ensureURL() return abort() } const { updated, deactivated, activated } = resolveQueue(this.current.matched, route.matched) const queue: Array = [].concat( // in-component leave guards extractLeaveGuards(deactivated), // global before hooks this.router.beforeHooks, // in-component update hooks extractUpdateHooks(updated), // in-config enter guards activated.map(m => m.beforeEnter), // async components resolveAsyncComponents(activated) ) this.pending = route const iterator = (hook: NavigationGuard, next) => { if (this.pending !== route) { return abort() } try { hook(route, current, (to: any) => { if (to === false || isError(to)) { // next(false) -> abort navigation, ensure current URL this.ensureURL(true) abort(to) } else if ( typeof to === 'string' || (typeof to === 'object' && ( typeof to.path === 'string' || typeof to.name === 'string' )) ) { // next('/') or next({ path: '/' }) -> redirect abort() if (typeof to === 'object' && to.replace) { this.replace(to) } else { this.push(to) } } else { // confirm transition and pass on the value next(to) } }) } catch (e) { abort(e) } } runQueue(queue, iterator, () => { const postEnterCbs = [] const isValid = () => this.current === route // wait until async components are resolved before // extracting in-component enter guards const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid) const queue = enterGuards.concat(this.router.resolveHooks) runQueue(queue, iterator, () => { if (this.pending !== route) { return abort() } this.pending = null onComplete(route) if (this.router.app) { this.router.app.$nextTick(() => { postEnterCbs.forEach(cb => { cb() }) }) } }) }) } updateRoute (route: Route) { const prev = this.current this.current = route this.cb && this.cb(route) this.router.afterHooks.forEach(hook => { hook && hook(route, prev) }) } } function normalizeBase (base: ?string): string { if (!base) { if (inBrowser) { // respect tag const baseEl = document.querySelector('base') base = (baseEl && baseEl.getAttribute('href')) || '/' // strip full URL origin base = base.replace(/^https?:\/\/[^\/]+/, '') } else { base = '/' } } // make sure there's the starting slash if (base.charAt(0) !== '/') { base = '/' + base } // remove trailing slash return base.replace(/\/$/, '') } function resolveQueue ( current: Array, next: Array ): { updated: Array, activated: Array, deactivated: Array } { let i const max = Math.max(current.length, next.length) for (i = 0; i < max; i++) { if (current[i] !== next[i]) { break } } return { updated: next.slice(0, i), activated: next.slice(i), deactivated: current.slice(i) } } function extractGuards ( records: Array, name: string, bind: Function, reverse?: boolean ): Array { const guards = flatMapComponents(records, (def, instance, match, key) => { const guard = extractGuard(def, name) if (guard) { return Array.isArray(guard) ? guard.map(guard => bind(guard, instance, match, key)) : bind(guard, instance, match, key) } }) return flatten(reverse ? guards.reverse() : guards) } function extractGuard ( def: Object | Function, key: string ): NavigationGuard | Array { if (typeof def !== 'function') { // extend now so that global mixins are applied. def = _Vue.extend(def) } return def.options[key] } function extractLeaveGuards (deactivated: Array): Array { return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true) } function extractUpdateHooks (updated: Array): Array { return extractGuards(updated, 'beforeRouteUpdate', bindGuard) } function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard { if (instance) { return function boundRouteGuard () { return guard.apply(instance, arguments) } } } function extractEnterGuards ( activated: Array, cbs: Array, isValid: () => boolean ): Array { return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => { return bindEnterGuard(guard, match, key, cbs, isValid) }) } function bindEnterGuard ( guard: NavigationGuard, match: RouteRecord, key: string, cbs: Array, isValid: () => boolean ): NavigationGuard { return function routeEnterGuard (to, from, next) { return guard(to, from, cb => { next(cb) if (typeof cb === 'function') { cbs.push(() => { // #750 // if a router-view is wrapped with an out-in transition, // the instance may not have been registered at this time. // we will need to poll for registration until current route // is no longer valid. poll(cb, match.instances, key, isValid) }) } }) } } function poll ( cb: any, // somehow flow cannot infer this is a function instances: Object, key: string, isValid: () => boolean ) { if (instances[key]) { cb(instances[key]) } else if (isValid()) { setTimeout(() => { poll(cb, instances, key, isValid) }, 16) } } ================================================ FILE: vue-router-src/history/hash.js ================================================ /* @flow */ import type Router from '../index' import { History } from './base' import { cleanPath } from '../util/path' import { getLocation } from './html5' import { setupScroll, handleScroll } from '../util/scroll' import { pushState, replaceState, supportsPushState } from '../util/push-state' export class HashHistory extends History { constructor (router: Router, base: ?string, fallback: boolean) { super(router, base) // check history fallback deeplinking if (fallback && checkFallback(this.base)) { return } ensureSlash() } // this is delayed until the app mounts // to avoid the hashchange listener being fired too early setupListeners () { const router = this.router const expectScroll = router.options.scrollBehavior const supportsScroll = supportsPushState && expectScroll if (supportsScroll) { setupScroll() } window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => { const current = this.current if (!ensureSlash()) { return } this.transitionTo(getHash(), route => { if (supportsScroll) { handleScroll(this.router, route, current, true) } if (!supportsPushState) { replaceHash(route.fullPath) } }) }) } push (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { pushHash(route.fullPath) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { replaceHash(route.fullPath) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } go (n: number) { window.history.go(n) } ensureURL (push?: boolean) { const current = this.current.fullPath if (getHash() !== current) { push ? pushHash(current) : replaceHash(current) } } getCurrentLocation () { return getHash() } } function checkFallback (base) { const location = getLocation(base) if (!/^\/#/.test(location)) { window.location.replace( cleanPath(base + '/#' + location) ) return true } } function ensureSlash (): boolean { const path = getHash() if (path.charAt(0) === '/') { return true } replaceHash('/' + path) return false } export function getHash (): string { // We can't use window.location.hash here because it's not // consistent across browsers - Firefox will pre-decode it! const href = window.location.href const index = href.indexOf('#') return index === -1 ? '' : href.slice(index + 1) } function getUrl (path) { const href = window.location.href const i = href.indexOf('#') const base = i >= 0 ? href.slice(0, i) : href return `${base}#${path}` } function pushHash (path) { if (supportsPushState) { pushState(getUrl(path)) } else { window.location.hash = path } } function replaceHash (path) { if (supportsPushState) { replaceState(getUrl(path)) } else { window.location.replace(getUrl(path)) } } ================================================ FILE: vue-router-src/history/html5.js ================================================ /* @flow */ import type Router from '../index' import { History } from './base' import { cleanPath } from '../util/path' import { START } from '../util/route' import { setupScroll, handleScroll } from '../util/scroll' import { pushState, replaceState, supportsPushState } from '../util/push-state' export class HTML5History extends History { constructor (router: Router, base: ?string) { super(router, base) const expectScroll = router.options.scrollBehavior const supportsScroll = supportsPushState && expectScroll if (supportsScroll) { setupScroll() } const initLocation = getLocation(this.base) window.addEventListener('popstate', e => { const current = this.current // Avoiding first `popstate` event dispatched in some browsers but first // history route not updated since async guard at the same time. const location = getLocation(this.base) if (this.current === START && location === initLocation) { return } this.transitionTo(location, route => { if (supportsScroll) { handleScroll(router, route, current, true) } }) }) } go (n: number) { window.history.go(n) } push (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { pushState(cleanPath(this.base + route.fullPath)) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { replaceState(cleanPath(this.base + route.fullPath)) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } ensureURL (push?: boolean) { if (getLocation(this.base) !== this.current.fullPath) { const current = cleanPath(this.base + this.current.fullPath) push ? pushState(current) : replaceState(current) } } getCurrentLocation (): string { return getLocation(this.base) } } export function getLocation (base: string): string { let path = window.location.pathname if (base && path.indexOf(base) === 0) { path = path.slice(base.length) } return (path || '/') + window.location.search + window.location.hash } ================================================ FILE: vue-router-src/index.js ================================================ /* @flow */ import { install } from './install' import { START } from './util/route' import { assert } from './util/warn' import { inBrowser } from './util/dom' import { cleanPath } from './util/path' import { createMatcher } from './create-matcher' import { normalizeLocation } from './util/location' import { supportsPushState } from './util/push-state' import { HashHistory } from './history/hash' import { HTML5History } from './history/html5' import { AbstractHistory } from './history/abstract' import type { Matcher } from './create-matcher' /* 导出的VueRouter对象,用来包装store */ export default class VueRouter { static install: () => void; static version: string; app: any; apps: Array; ready: boolean; readyCbs: Array; options: RouterOptions; mode: string; history: HashHistory | HTML5History | AbstractHistory; matcher: Matcher; fallback: boolean; beforeHooks: Array; resolveHooks: Array; afterHooks: Array; constructor (options: RouterOptions = {}) { this.app = null /* 保存vm实例 */ this.apps = [] this.options = options this.beforeHooks = [] this.resolveHooks = [] this.afterHooks = [] this.matcher = createMatcher(options.routes || [], this) let mode = options.mode || 'hash' this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false if (this.fallback) { mode = 'hash' } if (!inBrowser) { mode = 'abstract' } this.mode = mode switch (mode) { case 'history': this.history = new HTML5History(this, options.base) break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) break case 'abstract': this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } } } match ( raw: RawLocation, current?: Route, redirectedFrom?: Location ): Route { return this.matcher.match(raw, current, redirectedFrom) } get currentRoute (): ?Route { return this.history && this.history.current } /* 初始化 */ init (app: any /* Vue component instance */) { /* 未安装就调用init会抛出异常 */ process.env.NODE_ENV !== 'production' && assert( install.installed, `not installed. Make sure to call \`Vue.use(VueRouter)\` ` + `before creating root instance.` ) /* 将当前vm实例保存在app中 */ this.apps.push(app) // main app already initialized. /* 已存在说明已经被init过了,直接返回 */ if (this.app) { return } /* this.app保存当前vm实例 */ this.app = app const history = this.history if (history instanceof HTML5History) { history.transitionTo(history.getCurrentLocation()) } else if (history instanceof HashHistory) { const setupHashListener = () => { history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ) } history.listen(route => { this.apps.forEach((app) => { app._route = route }) }) } beforeEach (fn: Function): Function { return registerHook(this.beforeHooks, fn) } beforeResolve (fn: Function): Function { return registerHook(this.resolveHooks, fn) } afterEach (fn: Function): Function { return registerHook(this.afterHooks, fn) } onReady (cb: Function, errorCb?: Function) { this.history.onReady(cb, errorCb) } onError (errorCb: Function) { this.history.onError(errorCb) } push (location: RawLocation, onComplete?: Function, onAbort?: Function) { this.history.push(location, onComplete, onAbort) } replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { this.history.replace(location, onComplete, onAbort) } go (n: number) { this.history.go(n) } back () { this.go(-1) } forward () { this.go(1) } getMatchedComponents (to?: RawLocation | Route): Array { const route: any = to ? to.matched ? to : this.resolve(to).route : this.currentRoute if (!route) { return [] } return [].concat.apply([], route.matched.map(m => { return Object.keys(m.components).map(key => { return m.components[key] }) })) } resolve ( to: RawLocation, current?: Route, append?: boolean ): { location: Location, route: Route, href: string, // for backwards compat normalizedTo: Location, resolved: Route } { const location = normalizeLocation( to, current || this.history.current, append, this ) const route = this.match(location, current) const fullPath = route.redirectedFrom || route.fullPath const base = this.history.base const href = createHref(base, fullPath, this.mode) return { location, route, href, // for backwards compat normalizedTo: location, resolved: route } } addRoutes (routes: Array) { this.matcher.addRoutes(routes) if (this.history.current !== START) { this.history.transitionTo(this.history.getCurrentLocation()) } } } function registerHook (list: Array, fn: Function): Function { list.push(fn) return () => { const i = list.indexOf(fn) if (i > -1) list.splice(i, 1) } } function createHref (base: string, fullPath: string, mode) { var path = mode === 'hash' ? '#' + fullPath : fullPath return base ? cleanPath(base + '/' + path) : path } /* Vue.use安装插件时候需要暴露的install方法 */ VueRouter.install = install VueRouter.version = '__VERSION__' /* 兼容用script标签引用的方法 */ if (inBrowser && window.Vue) { window.Vue.use(VueRouter) } ================================================ FILE: vue-router-src/install.js ================================================ import View from './components/view' import Link from './components/link' export let _Vue /* Vue.use安装插件时候需要暴露的install方法 */ export function install (Vue) { /* 判断是否已安装过 */ if (install.installed && _Vue === Vue) return install.installed = true /* 保存Vue实例 */ _Vue = Vue /* 判断是否已定义 */ const isDef = v => v !== undefined /* 通过registerRouteInstance方法注册router实例 */ const registerInstance = (vm, callVal) => { let i = vm.$options._parentVnode if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { i(vm, callVal) } } /* 混淆进Vue实例,在boforeCreate与destroyed钩子上混淆 */ Vue.mixin({ /* boforeCreate钩子 */ beforeCreate () { if (isDef(this.$options.router)) { /* 在option上面存在router则代表是根组件 */ /* 保存跟组件vm */ this._routerRoot = this /* 保存router */ this._router = this.$options.router /* VueRouter对象的init方法 */ this._router.init(this) /* Vue内部方法,为对象defineProperty上在变化时通知的属性 */ Vue.util.defineReactive(this, '_route', this._router.history.current) } else { /* 非根组件则直接从父组件中获取 */ this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } /* 通过registerRouteInstance方法注册router实例 */ registerInstance(this, this) }, destroyed () { registerInstance(this) } }) /* 在Vue的prototype上面绑定$router,这样可以在任意Vue对象中使用this.$router访问,同时经过Object.defineProperty,访问this.$router即访问this._routerRoot._router */ Object.defineProperty(Vue.prototype, '$router', { get () { return this._routerRoot._router } }) /* 以上同理,访问this.$route即访问this._routerRoot._route */ Object.defineProperty(Vue.prototype, '$route', { get () { return this._routerRoot._route } }) /* 注册touter-view以及router-link组件 */ Vue.component('RouterView', View) Vue.component('RouterLink', Link) /* 该对象保存了两个option合并的规则 */ const strats = Vue.config.optionMergeStrategies // use the same hook merging strategy for route hooks strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created } ================================================ FILE: vue-router-src/util/async.js ================================================ /* @flow */ export function runQueue (queue: Array, fn: Function, cb: Function) { const step = index => { if (index >= queue.length) { cb() } else { if (queue[index]) { fn(queue[index], () => { step(index + 1) }) } else { step(index + 1) } } } step(0) } ================================================ FILE: vue-router-src/util/dom.js ================================================ /* @flow */ export const inBrowser = typeof window !== 'undefined' ================================================ FILE: vue-router-src/util/location.js ================================================ /* @flow */ import type VueRouter from '../index' import { parsePath, resolvePath } from './path' import { resolveQuery } from './query' import { fillParams } from './params' import { warn } from './warn' export function normalizeLocation ( raw: RawLocation, current: ?Route, append: ?boolean, router: ?VueRouter ): Location { let next: Location = typeof raw === 'string' ? { path: raw } : raw // named target if (next.name || next._normalized) { return next } // relative params if (!next.path && next.params && current) { next = assign({}, next) next._normalized = true const params: any = assign(assign({}, current.params), next.params) if (current.name) { next.name = current.name next.params = params } else if (current.matched.length) { const rawPath = current.matched[current.matched.length - 1].path next.path = fillParams(rawPath, params, `path ${current.path}`) } else if (process.env.NODE_ENV !== 'production') { warn(false, `relative params navigation requires a current route.`) } return next } const parsedPath = parsePath(next.path || '') const basePath = (current && current.path) || '/' const path = parsedPath.path ? resolvePath(parsedPath.path, basePath, append || next.append) : basePath const query = resolveQuery( parsedPath.query, next.query, router && router.options.parseQuery ) let hash = next.hash || parsedPath.hash if (hash && hash.charAt(0) !== '#') { hash = `#${hash}` } return { _normalized: true, path, query, hash } } function assign (a, b) { for (const key in b) { a[key] = b[key] } return a } ================================================ FILE: vue-router-src/util/params.js ================================================ /* @flow */ import { warn } from './warn' import Regexp from 'path-to-regexp' // $flow-disable-line const regexpCompileCache: { [key: string]: Function } = Object.create(null) export function fillParams ( path: string, params: ?Object, routeMsg: string ): string { try { const filler = regexpCompileCache[path] || (regexpCompileCache[path] = Regexp.compile(path)) return filler(params || {}, { pretty: true }) } catch (e) { if (process.env.NODE_ENV !== 'production') { warn(false, `missing param for ${routeMsg}: ${e.message}`) } return '' } } ================================================ FILE: vue-router-src/util/path.js ================================================ /* @flow */ export function resolvePath ( relative: string, base: string, append?: boolean ): string { const firstChar = relative.charAt(0) if (firstChar === '/') { return relative } if (firstChar === '?' || firstChar === '#') { return base + relative } const stack = base.split('/') // remove trailing segment if: // - not appending // - appending to trailing slash (last segment is empty) if (!append || !stack[stack.length - 1]) { stack.pop() } // resolve relative path const segments = relative.replace(/^\//, '').split('/') for (let i = 0; i < segments.length; i++) { const segment = segments[i] if (segment === '..') { stack.pop() } else if (segment !== '.') { stack.push(segment) } } // ensure leading slash if (stack[0] !== '') { stack.unshift('') } return stack.join('/') } export function parsePath (path: string): { path: string; query: string; hash: string; } { let hash = '' let query = '' const hashIndex = path.indexOf('#') if (hashIndex >= 0) { hash = path.slice(hashIndex) path = path.slice(0, hashIndex) } const queryIndex = path.indexOf('?') if (queryIndex >= 0) { query = path.slice(queryIndex + 1) path = path.slice(0, queryIndex) } return { path, query, hash } } export function cleanPath (path: string): string { return path.replace(/\/\//g, '/') } ================================================ FILE: vue-router-src/util/push-state.js ================================================ /* @flow */ import { inBrowser } from './dom' import { saveScrollPosition } from './scroll' export const supportsPushState = inBrowser && (function () { const ua = window.navigator.userAgent if ( (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) && ua.indexOf('Mobile Safari') !== -1 && ua.indexOf('Chrome') === -1 && ua.indexOf('Windows Phone') === -1 ) { return false } return window.history && 'pushState' in window.history })() // use User Timing api (if present) for more accurate key precision const Time = inBrowser && window.performance && window.performance.now ? window.performance : Date let _key: string = genKey() function genKey (): string { return Time.now().toFixed(3) } export function getStateKey () { return _key } export function setStateKey (key: string) { _key = key } export function pushState (url?: string, replace?: boolean) { saveScrollPosition() // try...catch the pushState call to get around Safari // DOM Exception 18 where it limits to 100 pushState calls const history = window.history try { if (replace) { history.replaceState({ key: _key }, '', url) } else { _key = genKey() history.pushState({ key: _key }, '', url) } } catch (e) { window.location[replace ? 'replace' : 'assign'](url) } } export function replaceState (url?: string) { pushState(url, true) } ================================================ FILE: vue-router-src/util/query.js ================================================ /* @flow */ import { warn } from './warn' const encodeReserveRE = /[!'()*]/g const encodeReserveReplacer = c => '%' + c.charCodeAt(0).toString(16) const commaRE = /%2C/g // fixed encodeURIComponent which is more conformant to RFC3986: // - escapes [!'()*] // - preserve commas const encode = str => encodeURIComponent(str) .replace(encodeReserveRE, encodeReserveReplacer) .replace(commaRE, ',') const decode = decodeURIComponent export function resolveQuery ( query: ?string, extraQuery: Dictionary = {}, _parseQuery: ?Function ): Dictionary { const parse = _parseQuery || parseQuery let parsedQuery try { parsedQuery = parse(query || '') } catch (e) { process.env.NODE_ENV !== 'production' && warn(false, e.message) parsedQuery = {} } for (const key in extraQuery) { parsedQuery[key] = extraQuery[key] } return parsedQuery } function parseQuery (query: string): Dictionary { const res = {} query = query.trim().replace(/^(\?|#|&)/, '') if (!query) { return res } query.split('&').forEach(param => { const parts = param.replace(/\+/g, ' ').split('=') const key = decode(parts.shift()) const val = parts.length > 0 ? decode(parts.join('=')) : null if (res[key] === undefined) { res[key] = val } else if (Array.isArray(res[key])) { res[key].push(val) } else { res[key] = [res[key], val] } }) return res } export function stringifyQuery (obj: Dictionary): string { const res = obj ? Object.keys(obj).map(key => { const val = obj[key] if (val === undefined) { return '' } if (val === null) { return encode(key) } if (Array.isArray(val)) { const result = [] val.forEach(val2 => { if (val2 === undefined) { return } if (val2 === null) { result.push(encode(key)) } else { result.push(encode(key) + '=' + encode(val2)) } }) return result.join('&') } return encode(key) + '=' + encode(val) }).filter(x => x.length > 0).join('&') : null return res ? `?${res}` : '' } ================================================ FILE: vue-router-src/util/resolve-components.js ================================================ /* @flow */ import { _Vue } from '../install' import { warn, isError } from './warn' export function resolveAsyncComponents (matched: Array): Function { return (to, from, next) => { let hasAsync = false let pending = 0 let error = null flatMapComponents(matched, (def, _, match, key) => { // if it's a function and doesn't have cid attached, // assume it's an async component resolve function. // we are not using Vue's default async resolving mechanism because // we want to halt the navigation until the incoming component has been // resolved. if (typeof def === 'function' && def.cid === undefined) { hasAsync = true pending++ const resolve = once(resolvedDef => { if (isESModule(resolvedDef)) { resolvedDef = resolvedDef.default } // save resolved on async factory in case it's used elsewhere def.resolved = typeof resolvedDef === 'function' ? resolvedDef : _Vue.extend(resolvedDef) match.components[key] = resolvedDef pending-- if (pending <= 0) { next() } }) const reject = once(reason => { const msg = `Failed to resolve async component ${key}: ${reason}` process.env.NODE_ENV !== 'production' && warn(false, msg) if (!error) { error = isError(reason) ? reason : new Error(msg) next(error) } }) let res try { res = def(resolve, reject) } catch (e) { reject(e) } if (res) { if (typeof res.then === 'function') { res.then(resolve, reject) } else { // new syntax in Vue 2.3 const comp = res.component if (comp && typeof comp.then === 'function') { comp.then(resolve, reject) } } } } }) if (!hasAsync) next() } } export function flatMapComponents ( matched: Array, fn: Function ): Array { return flatten(matched.map(m => { return Object.keys(m.components).map(key => fn( m.components[key], m.instances[key], m, key )) })) } export function flatten (arr: Array): Array { return Array.prototype.concat.apply([], arr) } const hasSymbol = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol' function isESModule (obj) { return obj.__esModule || (hasSymbol && obj[Symbol.toStringTag] === 'Module') } // in Webpack 2, require.ensure now also returns a Promise // so the resolve/reject functions may get called an extra time // if the user uses an arrow function shorthand that happens to // return that Promise. function once (fn) { let called = false return function (...args) { if (called) return called = true return fn.apply(this, args) } } ================================================ FILE: vue-router-src/util/route.js ================================================ /* @flow */ import type VueRouter from '../index' import { stringifyQuery } from './query' const trailingSlashRE = /\/?$/ export function createRoute ( record: ?RouteRecord, location: Location, redirectedFrom?: ?Location, router?: VueRouter ): Route { const stringifyQuery = router && router.options.stringifyQuery let query: any = location.query || {} try { query = clone(query) } catch (e) {} const route: Route = { name: location.name || (record && record.name), meta: (record && record.meta) || {}, path: location.path || '/', hash: location.hash || '', query, params: location.params || {}, fullPath: getFullPath(location, stringifyQuery), matched: record ? formatMatch(record) : [] } if (redirectedFrom) { route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery) } return Object.freeze(route) } function clone (value) { if (Array.isArray(value)) { return value.map(clone) } else if (value && typeof value === 'object') { const res = {} for (const key in value) { res[key] = clone(value[key]) } return res } else { return value } } // the starting route that represents the initial state /* 起始路由 */ export const START = createRoute(null, { path: '/' }) function formatMatch (record: ?RouteRecord): Array { const res = [] while (record) { res.unshift(record) record = record.parent } return res } function getFullPath ( { path, query = {}, hash = '' }, _stringifyQuery ): string { const stringify = _stringifyQuery || stringifyQuery return (path || '/') + stringify(query) + hash } export function isSameRoute (a: Route, b: ?Route): boolean { if (b === START) { return a === b } else if (!b) { return false } else if (a.path && b.path) { return ( a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') && a.hash === b.hash && isObjectEqual(a.query, b.query) ) } else if (a.name && b.name) { return ( a.name === b.name && a.hash === b.hash && isObjectEqual(a.query, b.query) && isObjectEqual(a.params, b.params) ) } else { return false } } function isObjectEqual (a = {}, b = {}): boolean { // handle null value #1566 if (!a || !b) return a === b const aKeys = Object.keys(a) const bKeys = Object.keys(b) if (aKeys.length !== bKeys.length) { return false } return aKeys.every(key => { const aVal = a[key] const bVal = b[key] // check nested equality if (typeof aVal === 'object' && typeof bVal === 'object') { return isObjectEqual(aVal, bVal) } return String(aVal) === String(bVal) }) } export function isIncludedRoute (current: Route, target: Route): boolean { return ( current.path.replace(trailingSlashRE, '/').indexOf( target.path.replace(trailingSlashRE, '/') ) === 0 && (!target.hash || current.hash === target.hash) && queryIncludes(current.query, target.query) ) } function queryIncludes (current: Dictionary, target: Dictionary): boolean { for (const key in target) { if (!(key in current)) { return false } } return true } ================================================ FILE: vue-router-src/util/scroll.js ================================================ /* @flow */ import type Router from '../index' import { assert } from './warn' import { getStateKey, setStateKey } from './push-state' const positionStore = Object.create(null) export function setupScroll () { // Fix for #1585 for Firefox window.history.replaceState({ key: getStateKey() }, '') window.addEventListener('popstate', e => { saveScrollPosition() if (e.state && e.state.key) { setStateKey(e.state.key) } }) } export function handleScroll ( router: Router, to: Route, from: Route, isPop: boolean ) { if (!router.app) { return } const behavior = router.options.scrollBehavior if (!behavior) { return } if (process.env.NODE_ENV !== 'production') { assert(typeof behavior === 'function', `scrollBehavior must be a function`) } // wait until re-render finishes before scrolling router.app.$nextTick(() => { const position = getScrollPosition() const shouldScroll = behavior.call(router, to, from, isPop ? position : null) if (!shouldScroll) { return } if (typeof shouldScroll.then === 'function') { shouldScroll.then(shouldScroll => { scrollToPosition((shouldScroll: any), position) }).catch(err => { if (process.env.NODE_ENV !== 'production') { assert(false, err.toString()) } }) } else { scrollToPosition(shouldScroll, position) } }) } export function saveScrollPosition () { const key = getStateKey() if (key) { positionStore[key] = { x: window.pageXOffset, y: window.pageYOffset } } } function getScrollPosition (): ?Object { const key = getStateKey() if (key) { return positionStore[key] } } function getElementPosition (el: Element, offset: Object): Object { const docEl: any = document.documentElement const docRect = docEl.getBoundingClientRect() const elRect = el.getBoundingClientRect() return { x: elRect.left - docRect.left - offset.x, y: elRect.top - docRect.top - offset.y } } function isValidPosition (obj: Object): boolean { return isNumber(obj.x) || isNumber(obj.y) } function normalizePosition (obj: Object): Object { return { x: isNumber(obj.x) ? obj.x : window.pageXOffset, y: isNumber(obj.y) ? obj.y : window.pageYOffset } } function normalizeOffset (obj: Object): Object { return { x: isNumber(obj.x) ? obj.x : 0, y: isNumber(obj.y) ? obj.y : 0 } } function isNumber (v: any): boolean { return typeof v === 'number' } function scrollToPosition (shouldScroll, position) { const isObject = typeof shouldScroll === 'object' if (isObject && typeof shouldScroll.selector === 'string') { const el = document.querySelector(shouldScroll.selector) if (el) { let offset = shouldScroll.offset && typeof shouldScroll.offset === 'object' ? shouldScroll.offset : {} offset = normalizeOffset(offset) position = getElementPosition(el, offset) } else if (isValidPosition(shouldScroll)) { position = normalizePosition(shouldScroll) } } else if (isObject && isValidPosition(shouldScroll)) { position = normalizePosition(shouldScroll) } if (position) { window.scrollTo(position.x, position.y) } } ================================================ FILE: vue-router-src/util/warn.js ================================================ /* @flow */ export function assert (condition: any, message: string) { if (!condition) { throw new Error(`[vue-router] ${message}`) } } export function warn (condition: any, message: string) { if (process.env.NODE_ENV !== 'production' && !condition) { typeof console !== 'undefined' && console.warn(`[vue-router] ${message}`) } } export function isError (err: any): boolean { return Object.prototype.toString.call(err).indexOf('Error') > -1 } ================================================ FILE: vue-src/compiler/codegen/events.js ================================================ /* @flow */ const fnExpRE = /^\s*([\w$_]+|\([^)]*?\))\s*=>|^function\s*\(/ const simplePathRE = /^\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?']|\[".*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*\s*$/ // keyCode aliases const keyCodes: { [key: string]: number | Array } = { esc: 27, tab: 9, enter: 13, space: 32, up: 38, left: 37, right: 39, down: 40, 'delete': [8, 46] } /*Github:https://github.com/answershuto*/ // #4868: modifiers that prevent the execution of the listener // need to explicitly return null so that we can determine whether to remove // the listener for .once const genGuard = condition => `if(${condition})return null;` const modifierCode: { [key: string]: string } = { stop: '$event.stopPropagation();', prevent: '$event.preventDefault();', self: genGuard(`$event.target !== $event.currentTarget`), ctrl: genGuard(`!$event.ctrlKey`), shift: genGuard(`!$event.shiftKey`), alt: genGuard(`!$event.altKey`), meta: genGuard(`!$event.metaKey`), left: genGuard(`'button' in $event && $event.button !== 0`), middle: genGuard(`'button' in $event && $event.button !== 1`), right: genGuard(`'button' in $event && $event.button !== 2`) } export function genHandlers ( events: ASTElementHandlers, native: boolean, warn: Function ): string { let res = native ? 'nativeOn:{' : 'on:{' for (const name in events) { const handler = events[name] // #5330: warn click.right, since right clicks do not actually fire click events. if (process.env.NODE_ENV !== 'production' && name === 'click' && handler && handler.modifiers && handler.modifiers.right ) { warn( `Use "contextmenu" instead of "click.right" since right clicks ` + `do not actually fire "click" events.` ) } res += `"${name}":${genHandler(name, handler)},` } return res.slice(0, -1) + '}' } function genHandler ( name: string, handler: ASTElementHandler | Array ): string { if (!handler) { return 'function(){}' } if (Array.isArray(handler)) { return `[${handler.map(handler => genHandler(name, handler)).join(',')}]` } const isMethodPath = simplePathRE.test(handler.value) const isFunctionExpression = fnExpRE.test(handler.value) if (!handler.modifiers) { return isMethodPath || isFunctionExpression ? handler.value : `function($event){${handler.value}}` // inline statement } else { let code = '' let genModifierCode = '' const keys = [] for (const key in handler.modifiers) { if (modifierCode[key]) { genModifierCode += modifierCode[key] // left/right if (keyCodes[key]) { keys.push(key) } } else { keys.push(key) } } if (keys.length) { code += genKeyFilter(keys) } // Make sure modifiers like prevent and stop get executed after key filtering if (genModifierCode) { code += genModifierCode } const handlerCode = isMethodPath ? handler.value + '($event)' : isFunctionExpression ? `(${handler.value})($event)` : handler.value return `function($event){${code}${handlerCode}}` } } function genKeyFilter (keys: Array): string { return `if(!('button' in $event)&&${keys.map(genFilterCode).join('&&')})return null;` } function genFilterCode (key: string): string { const keyVal = parseInt(key, 10) if (keyVal) { return `$event.keyCode!==${keyVal}` } const alias = keyCodes[key] return `_k($event.keyCode,${JSON.stringify(key)}${alias ? ',' + JSON.stringify(alias) : ''})` } ================================================ FILE: vue-src/compiler/codegen/index.js ================================================ /* @flow */ import { genHandlers } from './events' import { baseWarn, pluckModuleFunction } from '../helpers' import baseDirectives from '../directives/index' import { camelize, no } from 'shared/util' type TransformFunction = (el: ASTElement, code: string) => string; type DataGenFunction = (el: ASTElement) => string; type DirectiveFunction = (el: ASTElement, dir: ASTDirective, warn: Function) => boolean; // configurable state let warn let transforms: Array let dataGenFns: Array let platformDirectives let isPlatformReservedTag let staticRenderFns let onceCount let currentOptions /*将AST语法树转化成render以及staticRenderFns的字符串*/ export function generate ( ast: ASTElement | void, options: CompilerOptions ): { render: string, staticRenderFns: Array } { // save previous staticRenderFns so generate calls can be nested const prevStaticRenderFns: Array = staticRenderFns const currentStaticRenderFns: Array = staticRenderFns = [] const prevOnceCount = onceCount onceCount = 0 currentOptions = options warn = options.warn || baseWarn transforms = pluckModuleFunction(options.modules, 'transformCode') dataGenFns = pluckModuleFunction(options.modules, 'genData') platformDirectives = options.directives || {} isPlatformReservedTag = options.isReservedTag || no const code = ast ? genElement(ast) : '_c("div")' staticRenderFns = prevStaticRenderFns onceCount = prevOnceCount return { render: `with(this){return ${code}}`, staticRenderFns: currentStaticRenderFns } } /*处理element,分别处理static静态节点、v-once、v-for、v-if、template、slot以及组件或元素*/ function genElement (el: ASTElement): string { if (el.staticRoot && !el.staticProcessed) { /*处理static静态节点*/ return genStatic(el) } else if (el.once && !el.onceProcessed) { /*处理v-once*/ return genOnce(el) } else if (el.for && !el.forProcessed) { /*处理v-for*/ return genFor(el) } else if (el.if && !el.ifProcessed) { /*处理v-if*/ return genIf(el) } else if (el.tag === 'template' && !el.slotTarget) { /*处理template*/ return genChildren(el) || 'void 0' } else if (el.tag === 'slot') { /*处理slot*/ return genSlot(el) } else { // component or element /*处理组件或元素*/ let code if (el.component) { code = genComponent(el.component, el) } else { const data = el.plain ? undefined : genData(el) const children = el.inlineTemplate ? null : genChildren(el, true) code = `_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })` } // module transforms for (let i = 0; i < transforms.length; i++) { code = transforms[i](el, code) } return code } } // hoist static sub-trees out /*处理static静态节点*/ function genStatic (el: ASTElement): string { /*处理过的标记位*/ el.staticProcessed = true staticRenderFns.push(`with(this){return ${genElement(el)}}`) return `_m(${staticRenderFns.length - 1}${el.staticInFor ? ',true' : ''})` } // v-once /*处理v-once*/ function genOnce (el: ASTElement): string { /*处理过的标记位*/ el.onceProcessed = true if (el.if && !el.ifProcessed) { /*同时还存在v-if的时候需要处理v-if*/ return genIf(el) } else if (el.staticInFor) { /* staticInFor标记static的或者有v-once指令同时处于for循环中的节点。 此时表示同时存在于for循环中 */ let key = '' let parent = el.parent /*向上逐级寻找所处的for循环*/ while (parent) { if (parent.for) { key = parent.key break } parent = parent.parent } if (!key) { /*如果v-once出现在for循环中,那必须要给设置v-for的element设置key*/ process.env.NODE_ENV !== 'production' && warn( `v-once can only be used inside v-for that is keyed. ` ) return genElement(el) } return `_o(${genElement(el)},${onceCount++}${key ? `,${key}` : ``})` } else { return genStatic(el) } } /*处理v-if*/ function genIf (el: any): string { /*标记位*/ el.ifProcessed = true // avoid recursion return genIfConditions(el.ifConditions.slice()) } /*处理if条件*/ function genIfConditions (conditions: ASTIfConditions): string { /*表达式不存在*/ if (!conditions.length) { return '_e()' } const condition = conditions.shift() if (condition.exp) { return `(${condition.exp})?${genTernaryExp(condition.block)}:${genIfConditions(conditions)}` } else { return `${genTernaryExp(condition.block)}` } // v-if with v-once should generate code like (a)?_m(0):_m(1) /*v-if与v-once同时存在的时候应该使用三元运算符,譬如说(a)?_m(0):_m(1)*/ function genTernaryExp (el) { return el.once ? genOnce(el) : genElement(el) } } /*处理v-for循环*/ function genFor (el: any): string { const exp = el.for const alias = el.alias const iterator1 = el.iterator1 ? `,${el.iterator1}` : '' const iterator2 = el.iterator2 ? `,${el.iterator2}` : '' if ( process.env.NODE_ENV !== 'production' && maybeComponent(el) && el.tag !== 'slot' && el.tag !== 'template' && !el.key ) { warn( `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` + `v-for should have explicit keys. ` + `See https://vuejs.org/guide/list.html#key for more info.`, true /* tip */ ) } /*标记位,避免递归*/ el.forProcessed = true // avoid recursion return `_l((${exp}),` + `function(${alias}${iterator1}${iterator2}){` + `return ${genElement(el)}` + '})' } function genData (el: ASTElement): string { let data = '{' // directives first. // directives may mutate the el's other properties before they are generated. const dirs = genDirectives(el) if (dirs) data += dirs + ',' // key if (el.key) { data += `key:${el.key},` } // ref if (el.ref) { data += `ref:${el.ref},` } if (el.refInFor) { data += `refInFor:true,` } // pre if (el.pre) { data += `pre:true,` } // record original tag name for components using "is" attribute if (el.component) { data += `tag:"${el.tag}",` } // module data generation functions for (let i = 0; i < dataGenFns.length; i++) { data += dataGenFns[i](el) } // attributes if (el.attrs) { data += `attrs:{${genProps(el.attrs)}},` } // DOM props if (el.props) { data += `domProps:{${genProps(el.props)}},` } // event handlers if (el.events) { data += `${genHandlers(el.events, false, warn)},` } if (el.nativeEvents) { data += `${genHandlers(el.nativeEvents, true, warn)},` } // slot target if (el.slotTarget) { data += `slot:${el.slotTarget},` } // scoped slots if (el.scopedSlots) { data += `${genScopedSlots(el.scopedSlots)},` } // component v-model if (el.model) { data += `model:{value:${ el.model.value },callback:${ el.model.callback },expression:${ el.model.expression }},` } // inline-template if (el.inlineTemplate) { const inlineTemplate = genInlineTemplate(el) if (inlineTemplate) { data += `${inlineTemplate},` } } data = data.replace(/,$/, '') + '}' // v-bind data wrap if (el.wrapData) { data = el.wrapData(data) } return data } function genDirectives (el: ASTElement): string | void { const dirs = el.directives if (!dirs) return let res = 'directives:[' let hasRuntime = false let i, l, dir, needRuntime for (i = 0, l = dirs.length; i < l; i++) { dir = dirs[i] needRuntime = true const gen: DirectiveFunction = platformDirectives[dir.name] || baseDirectives[dir.name] if (gen) { // compile-time directive that manipulates AST. // returns true if it also needs a runtime counterpart. needRuntime = !!gen(el, dir, warn) } if (needRuntime) { hasRuntime = true res += `{name:"${dir.name}",rawName:"${dir.rawName}"${ dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : '' }${ dir.arg ? `,arg:"${dir.arg}"` : '' }${ dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : '' }},` } } if (hasRuntime) { return res.slice(0, -1) + ']' } } function genInlineTemplate (el: ASTElement): ?string { const ast = el.children[0] if (process.env.NODE_ENV !== 'production' && ( el.children.length > 1 || ast.type !== 1 )) { warn('Inline-template components must have exactly one child element.') } if (ast.type === 1) { const inlineRenderFns = generate(ast, currentOptions) return `inlineTemplate:{render:function(){${ inlineRenderFns.render }},staticRenderFns:[${ inlineRenderFns.staticRenderFns.map(code => `function(){${code}}`).join(',') }]}` } } function genScopedSlots (slots: { [key: string]: ASTElement }): string { return `scopedSlots:_u([${ Object.keys(slots).map(key => genScopedSlot(key, slots[key])).join(',') }])` } function genScopedSlot (key: string, el: ASTElement) { return `[${key},function(${String(el.attrsMap.scope)}){` + `return ${el.tag === 'template' ? genChildren(el) || 'void 0' : genElement(el) }}]` } /*处理chidren*/ function genChildren (el: ASTElement, checkSkip?: boolean): string | void { const children = el.children if (children.length) { const el: any = children[0] // optimize single v-for /*优化单个v-for*/ if (children.length === 1 && el.for && el.tag !== 'template' && el.tag !== 'slot') { return genElement(el) } const normalizationType = checkSkip ? getNormalizationType(children) : 0 /*用genNode处理children,内部有子节点也会继续遍历*/ return `[${children.map(genNode).join(',')}]${ normalizationType ? `,${normalizationType}` : '' }` } } // determine the normalization needed for the children array. // 0: no normalization needed // 1: simple normalization needed (possible 1-level deep nested array) // 2: full normalization needed /* 得到子数组所需的序列化类型 0:不需要序列化 1:需要做简单的序列化(可能是一级深层嵌套数组) 2:需要完全序列化 */ function getNormalizationType (children: Array): number { let res = 0 for (let i = 0; i < children.length; i++) { const el: ASTNode = children[i] /*当不是元素节点的时候直接continue*/ if (el.type !== 1) { continue } /*if条件中是存在满足needsNormalization条件的*/ if (needsNormalization(el) || (el.ifConditions && el.ifConditions.some(c => needsNormalization(c.block)))) { res = 2 break } /*if条件中有满足有可能是组件的返回1*/ if (maybeComponent(el) || (el.ifConditions && el.ifConditions.some(c => maybeComponent(c.block)))) { res = 1 } } return res } /*是否需要序列化(元素不是slot标签或者templete,同时不存在于v-for循环中)*/ function needsNormalization (el: ASTElement): boolean { return el.for !== undefined || el.tag === 'template' || el.tag === 'slot' } /*有可能是组件(判断不是平台保留标签,就是组件,当然也有可能是一个乱七八糟的标签并不在compoments中,这个后面在compoments中寻找组件的地方会处理)*/ function maybeComponent (el: ASTElement): boolean { return !isPlatformReservedTag(el.tag) } /*处理节点*/ function genNode (node: ASTNode): string { if (node.type === 1) { return genElement(node) } else { return genText(node) } } /*处理文本*/ function genText (text: ASTText | ASTExpression): string { return `_v(${text.type === 2 ? text.expression // no need for () because already wrapped in _s() : transformSpecialNewlines(JSON.stringify(text.text)) })` } /*Github:https://github.com/answershuto*/ function genSlot (el: ASTElement): string { /*不存在slotName的时候改slot的name为default*/ const slotName = el.slotName || '"default"' const children = genChildren(el) let res = `_t(${slotName}${children ? `,${children}` : ''}` const attrs = el.attrs && `{${el.attrs.map(a => `${camelize(a.name)}:${a.value}`).join(',')}}` const bind = el.attrsMap['v-bind'] if ((attrs || bind) && !children) { res += `,null` } if (attrs) { res += `,${attrs}` } if (bind) { res += `${attrs ? '' : ',null'},${bind}` } return res + ')' } // componentName is el.component, take it as argument to shun flow's pessimistic refinement /*处理compoment*/ function genComponent (componentName: string, el: ASTElement): string { const children = el.inlineTemplate ? null : genChildren(el, true) return `_c(${componentName},${genData(el)}${ children ? `,${children}` : '' })` } function genProps (props: Array<{ name: string, value: string }>): string { let res = '' for (let i = 0; i < props.length; i++) { const prop = props[i] res += `"${prop.name}":${transformSpecialNewlines(prop.value)},` } return res.slice(0, -1) } // #3895, #4268 function transformSpecialNewlines (text: string): string { return text .replace(/\u2028/g, '\\u2028') .replace(/\u2029/g, '\\u2029') } ================================================ FILE: vue-src/compiler/directives/bind.js ================================================ /* @flow */ /*Github:https://github.com/answershuto*/ export default function bind (el: ASTElement, dir: ASTDirective) { el.wrapData = (code: string) => { return `_b(${code},'${el.tag}',${dir.value}${ dir.modifiers && dir.modifiers.prop ? ',true' : '' })` } } ================================================ FILE: vue-src/compiler/directives/index.js ================================================ /* @flow */ import bind from './bind' import { noop } from 'shared/util' /*Github:https://github.com/answershuto*/ export default { bind, cloak: noop } ================================================ FILE: vue-src/compiler/directives/model.js ================================================ /* @flow */ /** * Cross-platform code generation for component v-model */ export function genComponentModel ( el: ASTElement, value: string, modifiers: ?ASTModifiers ): ?boolean { const { number, trim } = modifiers || {} const baseValueExpression = '$$v' let valueExpression = baseValueExpression if (trim) { valueExpression = `(typeof ${baseValueExpression} === 'string'` + `? ${baseValueExpression}.trim()` + `: ${baseValueExpression})` } if (number) { valueExpression = `_n(${valueExpression})` } const assignment = genAssignmentCode(value, valueExpression) el.model = { value: `(${value})`, expression: `"${value}"`, callback: `function (${baseValueExpression}) {${assignment}}` } } /*Github:https://github.com/answershuto*/ /** * Cross-platform codegen helper for generating v-model value assignment code. */ export function genAssignmentCode ( value: string, assignment: string ): string { const modelRs = parseModel(value) if (modelRs.idx === null) { return `${value}=${assignment}` } else { return `var $$exp = ${modelRs.exp}, $$idx = ${modelRs.idx};` + `if (!Array.isArray($$exp)){` + `${value}=${assignment}}` + `else{$$exp.splice($$idx, 1, ${assignment})}` } } /** * parse directive model to do the array update transform. a[idx] = val => $$a.splice($$idx, 1, val) * * for loop possible cases: * * - test * - test[idx] * - test[test1[idx]] * - test["a"][idx] * - xxx.test[a[a].test1[idx]] * - test.xxx.a["asa"][test1[idx]] * */ let len, str, chr, index, expressionPos, expressionEndPos export function parseModel (val: string): Object { str = val len = str.length index = expressionPos = expressionEndPos = 0 if (val.indexOf('[') < 0 || val.lastIndexOf(']') < len - 1) { return { exp: val, idx: null } } while (!eof()) { chr = next() /* istanbul ignore if */ if (isStringStart(chr)) { parseString(chr) } else if (chr === 0x5B) { parseBracket(chr) } } return { exp: val.substring(0, expressionPos), idx: val.substring(expressionPos + 1, expressionEndPos) } } function next (): number { return str.charCodeAt(++index) } function eof (): boolean { return index >= len } function isStringStart (chr: number): boolean { return chr === 0x22 || chr === 0x27 } function parseBracket (chr: number): void { let inBracket = 1 expressionPos = index while (!eof()) { chr = next() if (isStringStart(chr)) { parseString(chr) continue } if (chr === 0x5B) inBracket++ if (chr === 0x5D) inBracket-- if (inBracket === 0) { expressionEndPos = index break } } } function parseString (chr: number): void { const stringQuote = chr while (!eof()) { chr = next() if (chr === stringQuote) { break } } } ================================================ FILE: vue-src/compiler/error-detector.js ================================================ /* @flow */ import { dirRE, onRE } from './parser/index' // these keywords should not appear inside expressions, but operators like // typeof, instanceof and in are allowed const prohibitedKeywordRE = new RegExp('\\b' + ( 'do,if,for,let,new,try,var,case,else,with,await,break,catch,class,const,' + 'super,throw,while,yield,delete,export,import,return,switch,default,' + 'extends,finally,continue,debugger,function,arguments' ).split(',').join('\\b|\\b') + '\\b') // these unary operators should not be used as property/method names const unaryOperatorsRE = new RegExp('\\b' + ( 'delete,typeof,void' ).split(',').join('\\s*\\([^\\)]*\\)|\\b') + '\\s*\\([^\\)]*\\)') // check valid identifier for v-for const identRE = /[A-Za-z_$][\w$]*/ // strip strings in expressions const stripStringRE = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`/g // detect problematic expressions in a template export function detectErrors (ast: ?ASTNode): Array { const errors: Array = [] if (ast) { checkNode(ast, errors) } return errors } function checkNode (node: ASTNode, errors: Array) { if (node.type === 1) { for (const name in node.attrsMap) { if (dirRE.test(name)) { const value = node.attrsMap[name] if (value) { if (name === 'v-for') { checkFor(node, `v-for="${value}"`, errors) } else if (onRE.test(name)) { checkEvent(value, `${name}="${value}"`, errors) } else { checkExpression(value, `${name}="${value}"`, errors) } } } } if (node.children) { for (let i = 0; i < node.children.length; i++) { checkNode(node.children[i], errors) } } } else if (node.type === 2) { checkExpression(node.expression, node.text, errors) } } function checkEvent (exp: string, text: string, errors: Array) { const stipped = exp.replace(stripStringRE, '') const keywordMatch: any = stipped.match(unaryOperatorsRE) if (keywordMatch && stipped.charAt(keywordMatch.index - 1) !== '$') { errors.push( `avoid using JavaScript unary operator as property name: ` + `"${keywordMatch[0]}" in expression ${text.trim()}` ) } checkExpression(exp, text, errors) } function checkFor (node: ASTElement, text: string, errors: Array) { checkExpression(node.for || '', text, errors) checkIdentifier(node.alias, 'v-for alias', text, errors) checkIdentifier(node.iterator1, 'v-for iterator', text, errors) checkIdentifier(node.iterator2, 'v-for iterator', text, errors) } function checkIdentifier (ident: ?string, type: string, text: string, errors: Array) { if (typeof ident === 'string' && !identRE.test(ident)) { errors.push(`invalid ${type} "${ident}" in expression: ${text.trim()}`) } } function checkExpression (exp: string, text: string, errors: Array) { try { new Function(`return ${exp}`) } catch (e) { const keywordMatch = exp.replace(stripStringRE, '').match(prohibitedKeywordRE) if (keywordMatch) { errors.push( `avoid using JavaScript keyword as property name: ` + `"${keywordMatch[0]}" in expression ${text.trim()}` ) } else { errors.push(`invalid expression: ${text.trim()}`) } } } ================================================ FILE: vue-src/compiler/helpers.js ================================================ /* @flow */ import { parseFilters } from './parser/filter-parser' /*Vue 编译器警告*/ export function baseWarn (msg: string) { console.error(`[Vue compiler]: ${msg}`) } export function pluckModuleFunction ( modules: ?Array, key: string ): Array { return modules ? modules.map(m => m[key]).filter(_ => _) : [] } /*将属性放入ele的props属性中*/ export function addProp (el: ASTElement, name: string, value: string) { (el.props || (el.props = [])).push({ name, value }) } /*将属性放入ele的attr属性中*/ export function addAttr (el: ASTElement, name: string, value: string) { (el.attrs || (el.attrs = [])).push({ name, value }) } /*将参数加入到ele的directives中去*/ export function addDirective ( el: ASTElement, name: string, rawName: string, value: string, arg: ?string, modifiers: ?ASTModifiers ) { (el.directives || (el.directives = [])).push({ name, rawName, value, arg, modifiers }) } export function addHandler ( el: ASTElement, name: string, value: string, modifiers: ?ASTModifiers, important?: boolean, warn?: Function ) { // warn prevent and passive modifier /* istanbul ignore if */ if ( process.env.NODE_ENV !== 'production' && warn && modifiers && modifiers.prevent && modifiers.passive ) { warn( 'passive and prevent can\'t be used together. ' + 'Passive handler can\'t prevent default event.' ) } // check capture modifier if (modifiers && modifiers.capture) { delete modifiers.capture name = '!' + name // mark the event as captured } if (modifiers && modifiers.once) { delete modifiers.once name = '~' + name // mark the event as once } /* istanbul ignore if */ if (modifiers && modifiers.passive) { delete modifiers.passive name = '&' + name // mark the event as passive } let events if (modifiers && modifiers.native) { delete modifiers.native events = el.nativeEvents || (el.nativeEvents = {}) } else { events = el.events || (el.events = {}) } const newHandler = { value, modifiers } const handlers = events[name] /* istanbul ignore if */ if (Array.isArray(handlers)) { important ? handlers.unshift(newHandler) : handlers.push(newHandler) } else if (handlers) { events[name] = important ? [newHandler, handlers] : [handlers, newHandler] } else { events[name] = newHandler } } export function getBindingAttr ( el: ASTElement, name: string, getStatic?: boolean ): ?string { /*得到用:或者v-bind:修饰的特殊属性*/ const dynamicValue = getAndRemoveAttr(el, ':' + name) || getAndRemoveAttr(el, 'v-bind:' + name) if (dynamicValue != null) { /*存在特殊属性*/ return parseFilters(dynamicValue) } else if (getStatic !== false) { /*getStatic非false的时候返回静态属性,即一般的属性*/ const staticValue = getAndRemoveAttr(el, name) if (staticValue != null) { return JSON.stringify(staticValue) } } } /*从ele的属性中获取name对应的值并将它从中删除*/ export function getAndRemoveAttr (el: ASTElement, name: string): ?string { let val if ((val = el.attrsMap[name]) != null) { const list = el.attrsList for (let i = 0, l = list.length; i < l; i++) { if (list[i].name === name) { list.splice(i, 1) break } } } return val } ================================================ FILE: vue-src/compiler/index.js ================================================ /* @flow */ import { parse } from './parser/index' import { optimize } from './optimizer' import { generate } from './codegen/index' import { detectErrors } from './error-detector' import { extend, noop } from 'shared/util' import { warn, tip } from 'core/util/debug' function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { /*parse解析得到ast树*/ const ast = parse(template.trim(), options) /* 将AST树进行优化 优化的目标:生成模板AST树,检测不需要进行DOM改变的静态子树。 一旦检测到这些静态树,我们就能做以下这些事情: 1.把它们变成常数,这样我们就再也不需要每次重新渲染时创建新的节点了。 2.在patch的过程中直接跳过。 */ optimize(ast, options) /*根据ast树生成所需的code(内部包含render与staticRenderFns)*/ const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } } /*新建成Funtion对象*/ function makeFunction (code, errors) { try { return new Function(code) } catch (err) { errors.push({ err, code }) return noop } } /*提供一个方法,根据传递的baseOptions(不同平台可以有不同的实现)创建相应的编译器*/ export function createCompiler (baseOptions: CompilerOptions) { /* 作为缓存,防止每次都重新编译。 模板的key为delimiters(https://cn.vuejs.org/v2/api/#delimiters)+template,value为编译结果 */ const functionCompileCache: { [key: string]: CompiledFunctionResult; } = Object.create(null) /*编译,将模板template编译成AST树、render函数以及staticRenderFns函数*/ function compile ( template: string, options?: CompilerOptions ): CompiledResult { const finalOptions = Object.create(baseOptions) const errors = [] const tips = [] finalOptions.warn = (msg, tip) => { (tip ? tips : errors).push(msg) } /*做下面这些merge的目的因为不同平台可以提供自己本身平台的一个baseOptions,内部封装了平台自己的实现,然后把共同的部分抽离开来放在这层compiler中,所以在这里需要merge一下*/ if (options) { // merge custom modules /*合并modules*/ if (options.modules) { finalOptions.modules = (baseOptions.modules || []).concat(options.modules) } // merge custom directives if (options.directives) { /*合并directives*/ finalOptions.directives = extend( Object.create(baseOptions.directives), options.directives ) } // copy other options for (const key in options) { /*合并其余的options,modules与directives已经在上面做了特殊处理了*/ if (key !== 'modules' && key !== 'directives') { finalOptions[key] = options[key] } } } /*基础模板编译,得到编译结果*/ const compiled = baseCompile(template, finalOptions) if (process.env.NODE_ENV !== 'production') { errors.push.apply(errors, detectErrors(compiled.ast)) } compiled.errors = errors compiled.tips = tips return compiled } /*带缓存的编译器,同时staticRenderFns以及render函数会被转换成Funtion对象*/ function compileToFunctions ( template: string, options?: CompilerOptions, vm?: Component ): CompiledFunctionResult { options = options || {} /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production') { // detect possible CSP restriction try { new Function('return 1') } catch (e) { if (e.toString().match(/unsafe-eval|CSP/)) { warn( 'It seems you are using the standalone build of Vue.js in an ' + 'environment with Content Security Policy that prohibits unsafe-eval. ' + 'The template compiler cannot work in this environment. Consider ' + 'relaxing the policy to allow unsafe-eval or pre-compiling your ' + 'templates into render functions.' ) } } } // check cache /*有缓存的时候直接取出缓存中的结果即可*/ const key = options.delimiters ? String(options.delimiters) + template : template if (functionCompileCache[key]) { return functionCompileCache[key] } // compile /*编译*/ const compiled = compile(template, options) // check compilation errors/tips if (process.env.NODE_ENV !== 'production') { if (compiled.errors && compiled.errors.length) { warn( `Error compiling template:\n\n${template}\n\n` + compiled.errors.map(e => `- ${e}`).join('\n') + '\n', vm ) } if (compiled.tips && compiled.tips.length) { compiled.tips.forEach(msg => tip(msg, vm)) } } // turn code into functions const res = {} const fnGenErrors = [] /*将render转换成Funtion对象*/ res.render = makeFunction(compiled.render, fnGenErrors) /*将staticRenderFns全部转化成Funtion对象 */ const l = compiled.staticRenderFns.length res.staticRenderFns = new Array(l) for (let i = 0; i < l; i++) { res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i], fnGenErrors) } // check function generation errors. // this should only happen if there is a bug in the compiler itself. // mostly for codegen development use /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production') { if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) { warn( `Failed to generate render function:\n\n` + fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'), vm ) } } /*存放在缓存中,以免每次都重新编译*/ return (functionCompileCache[key] = res) } return { compile, compileToFunctions } } /*Github:https://github.com/answershuto*/ ================================================ FILE: vue-src/compiler/optimizer.js ================================================ /* @flow */ import { makeMap, isBuiltInTag, cached, no } from 'shared/util' /*标记是否为静态属性*/ let isStaticKey /*标记是否是平台保留的标签*/ let isPlatformReservedTag const genStaticKeysCached = cached(genStaticKeys) /** * Goal of the optimizer: walk the generated template AST tree * and detect sub-trees that are purely static, i.e. parts of * the DOM that never needs to change. * * Once we detect these sub-trees, we can: * * 1. Hoist them into constants, so that we no longer need to * create fresh nodes for them on each re-render; * 2. Completely skip them in the patching process. */ /* 将AST树进行优化 优化的目标:生成模板AST树,检测不需要进行DOM改变的静态子树。 一旦检测到这些静态树,我们就能做以下这些事情: 1.把它们变成常数,这样我们就再也不需要每次重新渲染时创建新的节点了。 2.在patch的过程中直接跳过。 */ export function optimize (root: ?ASTElement, options: CompilerOptions) { if (!root) return /*标记是否为静态属性*/ isStaticKey = genStaticKeysCached(options.staticKeys || '') /*标记是否是平台保留的标签*/ isPlatformReservedTag = options.isReservedTag || no // first pass: mark all non-static nodes. /*处理所有非静态节点*/ markStatic(root) // second pass: mark static roots. /*处理static root*/ markStaticRoots(root, false) } /*静态属性的map表*/ function genStaticKeys (keys: string): Function { return makeMap( 'type,tag,attrsList,attrsMap,plain,parent,children,attrs' + (keys ? ',' + keys : '') ) } /*处理所有非静态节点*/ function markStatic (node: ASTNode) { /*标记一个node节点是否是static的*/ node.static = isStatic(node) if (node.type === 1) { // do not make component slot content static. this avoids // 1. components not able to mutate slot nodes // 2. static slot content fails for hot-reloading /* 不要使组件slot成为静态的,避免下面这两种情况: 1. */ if ( !isPlatformReservedTag(node.tag) && node.tag !== 'slot' && node.attrsMap['inline-template'] == null ) { return } /*遍历子节点*/ for (let i = 0, l = node.children.length; i < l; i++) { const child = node.children[i] markStatic(child) /*如果子节点不是静态的,则本身也不是静态的*/ if (!child.static) { node.static = false } } } } function markStaticRoots (node: ASTNode, isInFor: boolean) { if (node.type === 1) { if (node.static || node.once) { /*标记static的或者有v-once指令同时处于for循环中的节点*/ node.staticInFor = isInFor } // For a node to qualify as a static root, it should have children that // are not just static text. Otherwise the cost of hoisting out will // outweigh the benefits and it's better off to just always render it fresh. /*一个static root节点必须有子节点否则它可能只是一个static的文本节点,而且它不能只有文本子节点*/ if (node.static && node.children.length && !( node.children.length === 1 && node.children[0].type === 3 )) { node.staticRoot = true return } else { node.staticRoot = false } /*遍历子节点*/ if (node.children) { for (let i = 0, l = node.children.length; i < l; i++) { markStaticRoots(node.children[i], isInFor || !!node.for) } } /* ifConditions存储了if条件。 是一个数组,格式为[{exp: xxx, block:xxx}, {exp: xxx, block:xxx}, {exp: xxx, block:xxx}] block存储了element,exp存储了表达式。 */ if (node.ifConditions) { walkThroughConditionsBlocks(node.ifConditions, isInFor) } } } function walkThroughConditionsBlocks (conditionBlocks: ASTIfConditions, isInFor: boolean): void { for (let i = 1, len = conditionBlocks.length; i < len; i++) { markStaticRoots(conditionBlocks[i].block, isInFor) } } /*判断一个node节点是否是static的*/ function isStatic (node: ASTNode): boolean { if (node.type === 2) { // expression return false } if (node.type === 3) { // text return true } return !!(node.pre || ( !node.hasBindings && // no dynamic bindings !node.if && !node.for && // not v-if or v-for or v-else !isBuiltInTag(node.tag) && // not a built-in isPlatformReservedTag(node.tag) && // not a component !isDirectChildOfTemplateFor(node) && Object.keys(node).every(isStaticKey) )) } function isDirectChildOfTemplateFor (node: ASTElement): boolean { while (node.parent) { node = node.parent if (node.tag !== 'template') { return false } if (node.for) { return true } } return false } /*Github:https://github.com/answershuto*/ ================================================ FILE: vue-src/compiler/parser/entity-decoder.js ================================================ /* @flow */ let decoder export function decode (html: string): string { decoder = decoder || document.createElement('div') decoder.innerHTML = html return decoder.textContent } ================================================ FILE: vue-src/compiler/parser/filter-parser.js ================================================ /* @flow */ const validDivisionCharRE = /[\w).+\-_$\]]/ /*解析过滤器*/ export function parseFilters (exp: string): string { let inSingle = false let inDouble = false let inTemplateString = false let inRegex = false let curly = 0 let square = 0 let paren = 0 let lastFilterIndex = 0 let c, prev, i, expression, filters for (i = 0; i < exp.length; i++) { prev = c c = exp.charCodeAt(i) if (inSingle) { // ' 单引号 if (c === 0x27 && prev !== 0x5C) inSingle = false } else if (inDouble) { // " 双引号 if (c === 0x22 && prev !== 0x5C) inDouble = false } else if (inTemplateString) { // ` 模板字符串 if (c === 0x60 && prev !== 0x5C) inTemplateString = false } else if (inRegex) { // / 正则 if (c === 0x2f && prev !== 0x5C) inRegex = false } else if ( // | 管道 c === 0x7C && // pipe exp.charCodeAt(i + 1) !== 0x7C && exp.charCodeAt(i - 1) !== 0x7C && !curly && !square && !paren ) { if (expression === undefined) { // first filter, end of expression lastFilterIndex = i + 1 expression = exp.slice(0, i).trim() } else { pushFilter() } } else { switch (c) { case 0x22: inDouble = true; break // " case 0x27: inSingle = true; break // ' case 0x60: inTemplateString = true; break // ` case 0x28: paren++; break // ( case 0x29: paren--; break // ) case 0x5B: square++; break // [ case 0x5D: square--; break // ] case 0x7B: curly++; break // { case 0x7D: curly--; break // } } if (c === 0x2f) { // / let j = i - 1 let p // find first non-whitespace prev char for (; j >= 0; j--) { p = exp.charAt(j) if (p !== ' ') break } if (!p || !validDivisionCharRE.test(p)) { inRegex = true } } } } if (expression === undefined) { expression = exp.slice(0, i).trim() } else if (lastFilterIndex !== 0) { pushFilter() } function pushFilter () { (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim()) lastFilterIndex = i + 1 } if (filters) { for (i = 0; i < filters.length; i++) { expression = wrapFilter(expression, filters[i]) } } return expression } function wrapFilter (exp: string, filter: string): string { const i = filter.indexOf('(') if (i < 0) { // _f: resolveFilter return `_f("${filter}")(${exp})` } else { const name = filter.slice(0, i) const args = filter.slice(i + 1) return `_f("${name}")(${exp},${args}` } } ================================================ FILE: vue-src/compiler/parser/html-parser.js ================================================ /** * Not type-checking this file because it's mostly vendor code. */ /*! * HTML Parser By John Resig (ejohn.org) * Modified by Juriy "kangax" Zaytsev * Original code by Erik Arvidsson, Mozilla Public License * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js */ import { makeMap, no } from 'shared/util' import { isNonPhrasingTag } from 'web/compiler/util' // Regular Expressions for parsing tags and attributes const singleAttrIdentifier = /([^\s"'<>/=]+)/ const singleAttrAssign = /(?:=)/ const singleAttrValues = [ // attr value double quotes /"([^"]*)"+/.source, // attr value, single quotes /'([^']*)'+/.source, // attr value, no quotes /([^\s"'=<>`]+)/.source ] const attribute = new RegExp( '^\\s*' + singleAttrIdentifier.source + '(?:\\s*(' + singleAttrAssign.source + ')' + '\\s*(?:' + singleAttrValues.join('|') + '))?' ) /*Github:https://github.com/answershuto*/ // 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 = /^]+>/i /*匹配注释*/ const comment = /^') if (commentEnd >= 0) { 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: /*标签不需要处理直接去除*/ const doctypeMatch = html.match(doctype) if (doctypeMatch) { advance(doctypeMatch[0].length) continue } // End tag: 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() if (startTagMatch) { handleStartTag(startTagMatch) continue } } let text, rest, next if (textEnd >= 0) { rest = html.slice(textEnd) 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) } if (textEnd < 0) { text = html html = '' } if (options.chars && text) { options.chars(text) } } else { var stackedTag = lastTag.toLowerCase() var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(]*>)', 'i')) var endTagLength = 0 var rest = html.replace(reStackedTag, function (all, text, endTag) { endTagLength = endTag.length if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') { text = text .replace(//g, '$1') .replace(//g, '$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 () { const start = html.match(startTagOpen) if (start) { const match = { tagName: start[1], 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 } } } 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) || tagName === 'html' && lastTag === 'head' || !!unarySlash 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) { 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 } 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) } } } } ================================================ FILE: vue-src/compiler/parser/index.js ================================================ /* @flow */ import { decode } from 'he' import { parseHTML } from './html-parser' import { parseText } from './text-parser' import { parseFilters } from './filter-parser' import { cached, no, camelize } from 'shared/util' import { genAssignmentCode } from '../directives/model' import { isIE, isEdge, isServerRendering } from 'core/util/env' import { addProp, addAttr, baseWarn, addHandler, addDirective, getBindingAttr, getAndRemoveAttr, pluckModuleFunction } from '../helpers' /*匹配@以及v-on,绑定事件 */ export const onRE = /^@|^v-on:/ /*匹配v-、@以及:*/ export const dirRE = /^v-|^@|^:/ /*匹配v-for中的in以及of*/ export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/ /*v-for参数中带括号的情况匹配,比如(item, index)这样的参数*/ export const forIteratorRE = /\((\{[^}]*\}|[^,]*),([^,]*)(?:,([^,]*))?\)/ /*Github:https://github.com/answershuto*/ const argRE = /:(.*)$/ /*匹配v-bind以及:*/ const bindRE = /^:|^v-bind:/ /*根据点来分开各个级别的正则,比如a.b.c.d解析后可以得到.b .c .d*/ const modifierRE = /\.[^.]+/g const decodeHTMLCached = cached(decode) // configurable state export let warn let delimiters let transforms let preTransforms let postTransforms let platformIsPreTag let platformMustUseProp let platformGetTagNamespace /** * Convert HTML string to AST. */ /*将HTML字符串转换成AST*/ export function parse ( template: string, options: CompilerOptions ): ASTElement | void { /*警告函数,baseWarn是Vue 编译器默认警告*/ warn = options.warn || baseWarn platformGetTagNamespace = options.platformGetTagNamespace || no platformMustUseProp = options.mustUseProp || no /*检测是否是
标签*/
  platformIsPreTag = options.isPreTag || no
  preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
  transforms = pluckModuleFunction(options.modules, 'transformNode')
  postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
  delimiters = options.delimiters

  /*存放ele*/
  const stack = []
  const preserveWhitespace = options.preserveWhitespace !== false
  let root
  let currentParent
  /*标志位,是否有v-pre属性*/
  let inVPre = false
  /*标志位,是否是pre标签*/
  let inPre = false
  let warned = false

  /*只发出一次的warning*/
  function warnOnce (msg) {
    if (!warned) {
      warned = true
      warn(msg)
    }
  }

  function endPre (element) {
    // check pre state
    /*是否有v-pre属性,存在则标志位变为false,因为这里已经是结束end,存在v-pre时在start中会被标志为true*/
    if (element.pre) {
      inVPre = false
    }
    /*检测是否是
标签*/
    if (platformIsPreTag(element.tag)) {
      inPre = false
    }
  }

  /*解析HTML*/
  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    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 */
      /*处理IE的svg bug*/
      if (isIE && ns === 'svg') {
        attrs = guardIESVGBug(attrs)
      }

      const element: ASTElement = {
        type: 1,
        tag,
        attrsList: attrs,
        attrsMap: makeAttrsMap(attrs),
        parent: currentParent,
        children: []
      }
      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++) {
        preTransforms[i](element, options)
      }

      if (!inVPre) {
        /*
          处理v-pre属性
          v-pre元素及其子元素被跳过编译
          https://cn.vuejs.org/v2/api/#v-pre
        */
        processPre(element)
        if (element.pre) {
          inVPre = true
        }
      }
      /*检测是否是
标签*/
      if (platformIsPreTag(element.tag)) {
        inPre = true
      }
      /*如果有v-pre属性,元素及其子元素不会被编译*/
      if (inVPre) {
        processRawAttrs(element)
      } else {
        /*匹配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)
        /*处理key属性 https://cn.vuejs.org/v2/api/#key*/
        processKey(element)

        // determine whether this is a plain element after
        // removing structural attributes
        /*去掉属性后,确定这是一个普通元素。*/
        element.plain = !element.key && !attrs.length

        /*处理ref属性 https://cn.vuejs.org/v2/api/#ref*/
        processRef(element)
        /*处理slot属性 https://cn.vuejs.org/v2/api/#slot*/
        processSlot(element)
        /*处理组件*/
        processComponent(element)
        /*转换*/
        for (let i = 0; i < transforms.length; i++) {
          transforms[i](element, options)
        }
        /*处理属性*/
        processAttrs(element)
      }

      /*监测根级元素的约束*/
      function checkRootConstraints (el) {
        if (process.env.NODE_ENV !== 'production') {
          /*slot以及templete不能作为根级元素*/
          if (el.tag === 'slot' || el.tag === 'template') {
            warnOnce(
              `Cannot use <${el.tag}> as component root element because it may ` +
              'contain multiple nodes.'
            )
          }
          /*以及根级元素不能有v-for*/
          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
        /*
          根级元素是可以用v-if、v-else来写多个条件下的多个根级元素的
          比如说
          
          是完全允许的
        */
        if (root.if && (element.elseif || element.else)) {
          /*监测根级元素的约束*/
          checkRootConstraints(element)
          /*在el的ifConditions属性中加入condition*/
          addIfCondition(root, {
            exp: element.elseif,
            block: element
          })
        } else if (process.env.NODE_ENV !== 'production') {
          /*在根级元素包含多个ele的时候,有不含v-else的ele则报出打印*/
          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.`
          )
        }
      }
      /*forbidden标志是否是被禁止的标签(style标签或者script标签)*/
      if (currentParent && !element.forbidden) {
        if (element.elseif || element.else) {
          /*当遇到当前ele有v-else或者v-elseif属性的时候,需要处理if属性,在其上级兄弟元素中必然存在一个v-if属性*/
          processIfConditions(element, currentParent)
        } else if (element.slotScope) { // scoped slot
          currentParent.plain = false
          /*slot如果没有则是默认的default*/
          const name = element.slotTarget || '"default"'
          /*
              scopedSlots中存放slot元素 https://cn.vuejs.org/v2/api/#vm-scopedSlots
          */
          ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
        } else {
          currentParent.children.push(element)
          element.parent = currentParent
        }
      }
      if (!unary) {
        currentParent = element
        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
      /*从stack中取出最后一个ele*/
      const element = stack[stack.length - 1]
      /*获取该ele的最后一个子节点*/
      const lastNode = element.children[element.children.length - 1]
      /*该子节点是非
标签的文本*/
      if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
        element.children.pop()
      }
      // pop stack
      /*ele出栈*/
      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
          })
        }
      }
    }
  })
  return root
}

/*
  处理v-pre属性
  v-pre元素及其子元素被跳过编译
  https://cn.vuejs.org/v2/api/#v-pre
*/
function processPre (el) {
  if (getAndRemoveAttr(el, 'v-pre') != null) {
    el.pre = true
  }
}

/*处理原生属性,将其放入attrs中,以{name, value}的形式*/
function processRawAttrs (el) {
  const l = el.attrsList.length
  if (l) {
    const attrs = el.attrs = new Array(l)
    for (let i = 0; i < l; i++) {
      attrs[i] = {
        name: el.attrsList[i].name,
        value: JSON.stringify(el.attrsList[i].value)
      }
    }
  } else if (!el.pre) {
    // non root node in pre blocks with no attributes
    el.plain = true
  }
}

/*处理key属性 https://cn.vuejs.org/v2/api/#key*/
function processKey (el) {
  const exp = getBindingAttr(el, 'key')
  if (exp) {
    if (process.env.NODE_ENV !== 'production' && el.tag === 'template') {
      warn(`