合规国际互联网加速 OSASE为企业客户提供高速稳定SD-WAN国际加速解决方案。 广告
[TOC] >PS:个人笔记,仅供参考,需要深入了解请阅读参考资料。以下几个章节都是从《Vue 技术揭秘》copy 一部分下来的,以后有空会附上自己的理解。 # 参考资料 [https://ustbhuangyi.github.io/vue-analysis/prepare/directory.html#sfc](https://ustbhuangyi.github.io/vue-analysis/prepare/directory.html#sfc) # 源码目录设计 Vue.js 的源码都在 src 目录下,其目录结构如下。 ```shell src ├── compiler # 编译相关 ├── core # 核心代码 ├── platforms # 不同平台的支持 ├── server # 服务端渲染 ├── sfc # .vue 文件解析 ├── shared # 共享代码 ``` ## compiler compiler 目录包含 Vue.js 所有编译相关的代码。它包括把模板解析成 ast 语法树,ast 语法树优化,代码生成等功能。 编译的工作可以在构建时做(借助 webpack、vue-loader 等辅助插件);也可以在运行时做,使用包含构建功能的 Vue.js。显然,编译是一项耗性能的工作,所以更推荐前者——离线编译。 ## core core 目录包含了 Vue.js 的核心代码,包括内置组件、全局 API 封装,Vue 实例化、观察者、虚拟 DOM、工具函数等等。 这里的代码可谓是 Vue.js 的灵魂,也是我们之后需要重点分析的地方。 ## platform Vue.js 是一个跨平台的 MVVM 框架,它可以跑在 web 上,也可以配合 weex 跑在 native 客户端上。platform 是 Vue.js 的入口,2 个目录代表 2 个主要入口,分别打包成运行在 web 上和 weex 上的 Vue.js。 ## server Vue.js 2.0 支持了服务端渲染,所有服务端渲染相关的逻辑都在这个目录下。注意:这部分代码是跑在服务端的 Node.js,不要和跑在浏览器端的 Vue.js 混为一谈。 服务端渲染主要的工作是把组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。 ## sfc 通常我们开发 Vue.js 都会借助 webpack 构建, 然后通过 .vue 单文件来编写组件。 这个目录下的代码逻辑会把 .vue 文件内容解析成一个 JavaScript 的对象。 ## shared Vue.js 会定义一些工具方法,这里定义的工具方法都是会被浏览器端的 Vue.js 和服务端的 Vue.js 所共享的。 # new Vue 发生了什么 ![](https://img.kancloud.cn/c2/70/c2709e297ce56bc46e69bfe5d28efce6_732x351.png) ## Vue 的定义 在`src/core/instance/index.js`中可以看到 Vue 实际上就是一个用 Function 实现的类,我们只能通过`new Vue`去实例化它: ```js import { initMixin } from './init' import { stateMixin } from './state' import { renderMixin } from './render' import { eventsMixin } from './events' import { lifecycleMixin } from './lifecycle' import { warn } from '../util/index' 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) } initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) export default Vue ``` 可以看到 Vue 实例的初始化会调用`this._init`方法,该方法定义在`src/core/instance/init.js`中: ```js 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) } // 初始化的最后,如果检测到有 el 属性,就调用 vm.$mount 方法挂载 vm,挂载的目标 // 就是把模板渲染成最终的 DOM if (vm.$options.el) { vm.$mount(vm.$options.el) } } ``` Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。 ## Vue 实例挂载的实现 Vue 中我们是通过`$mount`实例方法去挂载`vm`的,`$mount`方法在多个文件中都有定义,如`src/platform/web/entry-runtime-with-compiler.js`、`src/platform/web/runtime/index.js`、`src/platform/weex/runtime/index.js`。因为`$mount`这个方法的实现是和平台、构建方式都相关的。下面只分析带`compiler`版本的`$mount`实现,因为抛开 webpack 的 vue-loader,我们在纯前端浏览器环境分析 Vue 的工作原理,有助于我们对原理理解的深入。 `compiler`版本的`$mount`定义在`src/platform/web/entry-runtime-with-compiler.js`: ```js const mount = Vue.prototype.$mount // 缓存原型上的 $mount 方法 Vue.prototype.$mount = function ( // 重新定义原型上的 $mount 方法 el?: string | Element, // el 表示挂载的元素 hydrating?: boolean // 第二个参数和服务端渲染相关,浏览器环境下不需要 ): Component { el = el && query(el) /* istanbul ignore if */ // 限制 el,不能挂载在 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.` ) return this } const options = this.$options // resolve template/el and convert to render function // 如果没有定义 render 方法,则把 template 或 el 转换成 render 方法 if (!options.render) { let template = options.template if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template) /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !template) { warn( `Template element not found or is empty: ${options.template}`, this ) } } } else if (template.nodeType) { template = template.innerHTML } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } } else if (el) { template = getOuterHTML(el) } if (template) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') } // 通过 compileToFunctions 转换为 render 方法,所有 Vue 组件的渲染都需要 render 方法 const { render, staticRenderFns } = compileToFunctions(template, { shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile end') measure(`vue ${this._name} compile`, 'compile', 'compile end') } } } return mount.call(this, el, hydrating) // 最后,调用原先的原型上的 mount 方法 } ``` `$mount`方法实际上会去调用`mountComponent`方法,这个方法定义在`src/core/instance/lifecycle.js`文件中: ```js export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el if (!vm.$options.render) { 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}` mark(startTag) const vnode = vm._render() mark(endTag) measure(`vue ${name} render`, startTag, endTag) mark(startTag) vm._update(vnode, hydrating) mark(endTag) measure(`vue ${name} patch`, startTag, endTag) } } else { updateComponent = () => { vm._update(vm._render(), hydrating) } } // 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 // 实例化一个渲染 Watcher,在其回调函数中调用 updateComponent 方法 // 在此方法中调用 vm._render 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM。 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 if (vm.$vnode == null) { vm._isMounted = true // 表示该实例已挂载 callHook(vm, 'mounted') // 执行 mounted 钩子函数 } return vm } ``` 这里可以回顾下 Vue 对其生命周期钩子的描述: `beforeCreate`(创建前) 在实例初始化之后,数据观测 (data observer) 和event/watcher 事件配置之前被调用。从下面截取的 vue 源码可以看到`beforeCreate`调用的时候,是获取不到 props 或者 data 中的数据的,因为这些数据的初始化都在`initState`中: ```js Vue.prototype._init = function(options) { initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') // 拿不到 props data initInjections(vm) initState(vm) initProvide(vm) callHook(vm, 'created') } ``` `created`(创建后) 在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),属性和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,`$el`属性目前不可见。 然后就到了这里的`beforemounted`和`mounted`了: `beforeMount`(载入前) 在挂载开始之前被调用,相关的 render 函数首次被调用。实例已完成以下的配置:编译模板,把 data 里面的数据和模板生成 html。注意此时还没有挂载 html 到页面上。 `mounted`(载入后) 在 el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的 html 内容替换 el 属性指向的 DOM 对象。完成模板中的 html 渲染到 html 页面中。 ## render 上面有说到`mountComponent`方法会完成整个渲染工作,其最核心的 2 个方法是`vm._render`和`vm._update`。 Vue 的`_render`方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node。它的定义在`src/core/instance/render.js`文件中: ```js 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 { 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 } ``` 这里调用了`render`方法,我们在平时的开发工作中手写`render`方法的场景比较少,而写的比较多的是`template`模板,在之前的`mounted`方法的实现中,会把`template`编译成`render`方法,但这个编译过程是非常复杂的。 在 Vue 的官方文档中介绍了`render`函数的第一个参数是`createElement`,那么结合之前的例子: ```html <div id="app"> {{ message }} </div> ``` 相当于我们编写如下`render`函数: ```js render: function (createElement) { return createElement('div', { attrs: { id: 'app' }, }, this.message) } ``` `vm._render`最终是通过执行`createElement`方法并返回的是`vnode`,它是一个虚拟 Node。因此在分析`createElement`的实现前,我们先了解一下 Virtual DOM 的概念。 ## Virtual DOM Virtual DOM 就是用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多。在 Vue.js 中,Virtual DOM 是用`VNode`这么一个 Class 去描述,它是定义在`src/core/vdom/vnode.js`中的。 ```js export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal 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? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching fnScopeId: ?string; // functional scope id support constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions, asyncFactory?: Function ) { this.tag = tag this.data = data this.children = children this.text = text this.elm = elm this.ns = undefined this.context = context this.fnContext = undefined this.fnOptions = undefined this.fnScopeId = undefined this.key = data && data.key this.componentOptions = componentOptions this.componentInstance = undefined this.parent = undefined this.raw = false this.isStatic = false this.isRootInsert = true this.isComment = false this.isCloned = false this.isOnce = false this.asyncFactory = asyncFactory this.asyncMeta = undefined this.isAsyncPlaceholder = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } } ``` 其实 VNode 是对真实 DOM 的一种抽象描述,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其它属性都是都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。 Virtual DOM 除了它的数据结构的定义,映射到真实的 DOM 实际上要经历 VNode 的 create、diff、patch 等过程。那么在 Vue.js 中,VNode 的 create 是通过之前提到的`createElement`方法创建的。 ## createElement Vue.js 利用 createElement 方法创建 VNode,它定义在`src/core/vdom/create-elemenet.js`中。其过程比较复杂,简单来说每个 VNode 有`children`,`children`每个元素也是一个 VNode,这样就形成了一个 VNode Tree,它很好的描述了我们的 DOM Tree。 回到`mountComponent`函数的过程,我们已经知道`vm._render`是如何创建了一个 VNode,接下来就是要把这个 VNode 渲染成一个真实的 DOM 并渲染出来,这个过程是通过`vm._update`完成的。 ## update Vue 的`_update`是实例的一个私有方法,它被调用的时机有 2 个,一个是首次渲染,一个是数据更新的时候,这里仅看其首次渲染时发挥的作用。`_update`方法的作用是把 VNode 渲染成真实的 DOM,它的定义在`src/core/instance/lifecycle.js`中。 ```js 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. 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__`方法,实例化一个组件的时候,其整个过程就是遍历 VNode Tree 递归创建了一个完整的 DOM 树并插入到 Body 上。