const idToTemplate = cached(id => { const el = query(id) return el && el.innerHTML })
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 <html> or <body> - mount to normal elements instead.` ) returnthis }
const options = this.$options // resolve template/el and convert to render function if (!options.render) { let template = options.template if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template) /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !template) { warn( `Template element not found or is empty: ${options.template}`, this ) } } } elseif (template.nodeType) { template = template.innerHTML } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } returnthis } } elseif (el) { template = getOuterHTML(el) } if (template) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') }
/* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile end') measure(`vue ${this._name} compile`, 'compile', 'compile end') } } } return mount.call(this, el, hydrating) }
/** * Get outerHTML of elements, taking care * of SVG elements in IE as well. */ functiongetOuterHTML (el: Element): string{ if (el.outerHTML) { return el.outerHTML } else { const container = document.createElement('div') container.appendChild(el.cloneNode(true)) return container.innerHTML } }
// public mount method Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component{ el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
// ...
exportdefault Vue
关键代码是import Vue from 'core/index'这一行,其他的都是一些对 Vue 的拓展,看一下 src/core/index.js,这里才是真正初始化的地方
functionVue (options) { if (process.env.NODE_ENV !== 'production' && !(thisinstanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) }
// exposed util methods. // NOTE: these are not considered part of the public API - avoid relying on // them unless you are aware of the risk. Vue.util = { warn, extend, mergeOptions, defineReactive }
Vue.set = set Vue.delete = del Vue.nextTick = nextTick
// this is used to identify the "base" constructor to extend all plain-object // components with in Weex's multi-instance scenarios. Vue.options._base = Vue
Vue 官网中介绍的全局 API 都可以在这里找到,需要注意的是 Vue.util 暴露的方法最好不要依赖,可能经常会发生变化。
数据驱动
Vue.js 一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据。它相比我们传统的前端开发,如使用 jQuery 等前端库直接修改 DOM,大大简化了代码量。特别是当交互复杂的时候,只关心数据的修改会让代码的逻辑变的非常清晰,因为 DOM 变成了数据的映射,我们所有的逻辑都是对数据的修改,而不用碰触 DOM,这样的代码非常利于维护。
new Vue 发生了什么
Vue 的类在 src/core/instance/index.js 中
1 2 3 4 5 6 7 8 9
functionVue (options) { if (process.env.NODE_ENV !== 'production' && // 此处判断是否通过new调用 !(thisinstanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) }
Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++
let startTag, endTag /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) }
// a flag to avoid this being observed vm._isVue = true // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { 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) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created')
/* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) }
if (vm.$options.el) { vm.$mount(vm.$options.el) } }
const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component{ // 根据传入的内容决定挂载节点,如果是string就通过document.querySelector获取,如果取不到就新建一个div,如果传入一个dom节点就直接使用 el = el && query(el)
/* istanbul ignore if */ // 不可以使用body 和 html 标签 if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ) returnthis }
const options = this.$options // resolve template/el and convert to render function if (!options.render) { let template = options.template if (template) { if (typeof template === 'string') { // 容错处理如果传入id转为dom 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 ) } } // 是dom的情况 } elseif (template.nodeType) { template = template.innerHTML } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } returnthis } } elseif (el) { template = getOuterHTML(el) } if (template) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') }
if (!vm.$options.render) { // render方法的功能是创建一个空的vnode vm.$options.render = createEmptyVNode if (process.env.NODE_ENV !== 'production') { /* istanbul ignore if */ if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') || vm.$options.el || el) { warn( 'You are using the runtime-only build of Vue where the template ' + 'compiler is not available. Either pre-compile the templates into ' + 'render functions, or use the compiler-included build.', vm ) } else { warn( 'Failed to mount component: template or render function not defined.', vm ) } } } // 触发生命周期钩子 callHook(vm, 'beforeMount')
let updateComponent /* istanbul ignore if */ // 非生产环境记录性能 if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { const name = vm._name const id = vm._uid const startTag = `vue-perf-start:${id}` const endTag = `vue-perf-end:${id}`
// we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true/* isRenderWatcher */) hydrating = false
// manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook // $vnode表示Vue实例的父虚拟Node,如果是null的话表示为根Vue实例 if (vm.$vnode == null) { // 标记实例已经挂载 vm._isMounted = true callHook(vm, 'mounted') } return vm }
Vue.prototype._render = function (): VNode{ const vm: Component = this const { render, _parentVnode } = vm.$options
// reset _rendered flag on slots for duplicate slot check if (process.env.NODE_ENV !== 'production') { for (const key in vm.$slots) { // $flow-disable-line vm.$slots[key]._rendered = false } }
if (_parentVnode) { vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject }
// set parent vnode. this allows render functions to have access // to the data on the placeholder node. vm.$vnode = _parentVnode // render self let vnode try { // render方法是从$options上获取的 vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render`) // return error render result, // or previous vnode to prevent render error causing blank component /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { if (vm.$options.renderError) { try { vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) } catch (e) { handleError(e, vm, `renderError`) vnode = vm._vnode } } else { vnode = vm._vnode } } else { vnode = vm._vnode } } // return empty vnode in case the render function errored out if (!(vnode instanceof VNode)) { if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { warn( 'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.', vm ) } vnode = createEmptyVNode() } // set parent vnode.parent = _parentVnode return vnode }
exportfunctioninitRender (vm: Component) { // ... // bind the createElement fn to this instance // so that we get proper render context inside it. // args order: tag, data, children, normalizationType, alwaysNormalize // internal version is used by render functions compiled from templates // 内部使用的方法 vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) // normalization is always applied for the public version, used in // user-written render functions. // 外部使用的方法,跟内部的区别就是alwaysNormalize这个参数不同 vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) }
// DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { returnthis.componentInstance } }
Vue.js 借鉴了 snabbdom Virtual DOM 除了它的数据结构的定义,映射到真实的 DOM 实际上要经历 VNode 的 create、diff、patch 等过程,那么在 Vue.js 中,VNode 的 create 是通过之前提到的 createElement 方法创建的,我们接下来分析这部分的实现。
// The template compiler attempts to minimize the need for normalization by // statically analyzing the template at compile time. // // For plain HTML markup, normalization can be completely skipped because the // generated render function is guaranteed to return Array<VNode>. There are // two cases where extra normalization is needed:
// 1. When the children contains components - because a functional component // may return an Array instead of a single root. In this case, just a simple // normalization is needed - if any child is an Array, we flatten the whole // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep // because functional components already normalize their own children.
// simpleNormalizeChildren 方法调用场景是 render 函数是编译生成的。 // 理论上编译生成的 children 都已经是 VNode 类型的,但这里有一个例外, //就是 functional component 函数式组件返回的是一个数组而不是一个根节点, //所以会通过 Array.prototype.concat 方法把整个 children 数组打平,让它的深度只有一层。 exportfunctionsimpleNormalizeChildren (children: any) { for (let i = 0; i < children.length; i++) { if (Array.isArray(children[i])) { returnArray.prototype.concat.apply([], children) } } return children }
// 2. When the children contains constructs that always generated nested Arrays, // e.g. <template>, <slot>, v-for, or when the children is provided by user // with hand-written render functions / JSX. In such cases a full normalization // is needed to cater to all possible types of children values.
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this 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. // 此处可以判断出是初始化还是更新 // 核心就是这里 __patch__ 的调用 if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false/* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } activeInstance = prevActiveInstance // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook. }
_update 的核心就是调用 vm.patch 方法,这个方法实际上在不同的平台,比如 web 和 weex 上的定义是不一样的,因此在 web 平台中它的定义在src/platforms/web/runtime/index.js中:
let isInitialPatch = false const insertedVnodeQueue = []
if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { 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)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } elseif (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 ' + '<p>, or missing <tbody>. Bailing hydration and performing ' + 'full client-side render.' ) } } // either not server-rendered, or hydration failed. // create an empty node and replace it oldVnode = emptyNodeAt(oldVnode) }
// create new node 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) )
// update parent placeholder node element, recursively if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } // #6513 // invoke insert hooks that may have been merged by create hooks. // e.g. for directives that uses the "inserted" hook. const insert = ancestor.data.hook.insert if (insert.merged) { // start at index 1 to avoid re-invoking component mounted hook for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } }
// destroy old node if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0) } elseif (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } }