1.vue2.6.11文件结构
vue文件结构下的详情:
dist | vue本身构建生成的文件 | |
src | ||
--compiler | 编译器 | |
--codegen | 键盘事件生成 | |
--event.js | 声明键盘事件和在Weex上生成具有绑定参数的处理程序代码 | |
--index.js | codegen入口文件,结合ASTElemen、键盘事件状态、插槽作用域确定规范(涉及: sub-trees、v-once、v-if、v-for、key、pre、component、props、event handlers、v-model、inline-template、v-bind、v-on、data:finction、directives等等 )并生成渲染code | |
--directives | 指令 | |
--bind.js | v-bind语法糖的实现(结合ASTElement和ASTDirective) | ast详解 |
--index.js | 1.暴露v-bind和v-model语法糖出去 2.引入并暴露shared/util.js 的noop方法(不执行任何操作,不留下无用的传输代码,严格检查函数调用 Arity) | |
--model.js | v-model语法糖的实现(结合ASTElement和ASTModifiers) | ast详解 |
--on.js | v-on语法糖的实现(结合ASTElement和ASTDirective) | ast详解 |
--parser | 解析器 | |
--entity-decoder.js | 通过创建div标签来解码传入的html内容 | |
--filter-parser.js | 过滤函数解析(filter允许用在两个地方,一个是双括号插值,一个是v-bind表达式后面,如果解析到这两种情况,执行parseFilters解析filter) | 解析思路 |
--html-parser.js | html解析器 | 不检查此文件的类型,因为它主要是供应商代码。 |
--index.js | parser入口文件,主要结合entity-decoder.js、filter-parser.js和text-parser.js生成ASTElement树 | |
--text-parser.js | 将text里的文本内容 和 {{}}插值内容一并存入TextParseResult类型的对象中并返回, 插值里的内容先进行过滤器解析 | |
--codeframe.js | code生成模板方法 | |
--create-compiler.js | 创建编译器 | |
--error-detector.js | 错误检测器(定义错误规则并检测) | |
--helpers.js | 编译助手(给el添加、绑定属性、指令) | |
--index.js | 创建编译器生成ast树且渲染 | |
--optimizer.js | 优化器 | /目标:遍历生成的模板AST树 并检测纯静态的子树,即 永远不需要改变的DOM。 一旦我们检测到这些子树: 1.将它们提升为常数,这样我们就不再需要在每次重新渲染时为其创建新节点; 2、在修补过程中完全跳过它们。 |
--to-function.js | 编译器将模板转换成函数 | |
--core | 核心实现 | |
--components | 核心组件 | |
--index.js | components入口文件,引入KeepAlive并导出 | |
--keep-alive.js | KeepAlive(缓存组件)的实现 | |
--global-api | 核心接口 | |
--assets.js | Vue组件资源 | 创建资源注册方法 |
--extend.js | Vue.extend()扩展接口 | 每个实例构造函数(包括Vue)都有一个唯一的cid。这使我们能够创建包装的“子对象”构造函数”,并缓存它们。 对于PROP和计算属性,我们在上定义代理getter扩展时的Vue实例,在扩展原型上。为创建的每个实例调defineProperty。 允许进一步扩展/混合/插件使用 创建资产寄存器,以便扩展类 也可以拥有他们的私人资源。 启用递归自查找,在扩展时保留引用。在实例化时,可以检查Super的选项是否已更新。并缓存构造函数 |
--index.js | 全局api入口文件 | |
--mixin.js | Vue.mixin()合并接口 | 通过调用../util/index中的mergeOptions来实现 |
--use.js | Vue.use()插件接口 | 通过调用../util/index中的 toArray来实现 |
--instance | 核心实例 | |
--render-helpers | 渲染助手 | |
--bind-dynamic-keys.js | 帮助处理v-bind和v-on中动态参数的动态键 | 将vue模板(如: <div id="app" :[key]="value"> )编译(如:_c('div', { attrs: bindDynamicKeys({ "id": "app" }, [key, value]) }) ) |
--bind-object-listeners.js | 对象侦听器 | 处理v-on=’{}'到vnode对象 data上 |
--bind-object-props.js | 用于将v-bind=“object”合并到VNode数据中的运行时助手 | |
--check-keycodes.js | 用于从配置中检查键代码的运行时帮助程序。作为Vue.prototype公开,将eventKeyName作为最后一个参数单独传递给向后兼容。 | |
--index.js | render-helpers(入口文件) | |
--render-list.js | 用于呈现v-for列表的运行时助手 | |
--render-slot.js | 插槽渲染助手 | |
--render-static.js | 渲染静态树运行助手 | |
--resolve-filter.js | filters过滤器渲染助手 | |
--resolve-scoped-slots.js | 同resolve-slots.js作用一样,不同的是编译父组件模板时,会生成一个返回结果为VNode的函数。当子组件匹配到父组件传递作用域插槽函数时,调用该函数生成对应VNode | |
--resolve-slots.js | 将children VNodes解析为插槽对象运行助手 | |
--events.js | 核心事件($on、$off、$emit、$once)接口 | 通过'../vdom/helpers/index'的 updateListeners监听来实现 |
--index.js | 核心实例(入口文件)接口:(导入)代理,状态,渲染,事件,生命周期,基础工具 | |
--init.js | 封装vue初始化(实例化)init接口 | |
--inject.js | provide和inject接口的实现 | |
--lifecycle.js | 生命周期接口的实现 | |
--proxy.js | 核心代理接口(判断平台是否支持Proxy功能;注册Proxy的处理函数;初始化代理initProxy() 注册vm._renderProxy属性) | |
--render.js | 核心渲染接口 | |
--state.js | 核心状态接口(初始化顺序:props属性,methods属性,data属性,computed属性watch属性) | |
--observer | 监视器接口 | |
--array.js | 数组监控 | 数组方法:'push','pop','shift', 'unshift','splice',sort','reverse' |
--dep.js | 消息订阅器 | |
--index.js | 数据监控器observer | |
--scheduler.js | 事件(队列)侦听器 | |
--traverse.js | 对象侦听 | 递归遍历对象以调用所有已转换的getter,使对象内的每个嵌套属性,作为“深度”依赖项收集。 |
--watcher.js | 消息订阅 | 观察者解析表达式,收集依赖项,并在表达式值更改时激发回调。这用于$watch()api和指令。 |
--util | 核心工具 | |
--debug.js | debug工具 | |
--env.js | 环境工具 | |
--error.js | 错误工具(在处理错误程序时,停用deps跟踪避免无限渲染) | |
--index.js | 入口文件(导入工具接口) | |
--lang.js | 语言扩展工具 | |
--next-tick.js | nextTick工具 | 微任务异步延迟包装器,nextTick行为利用了可以访问的微任务队列;添加空计时器“强制”刷新微任务队列;策略 Promise、MutationObserver、setImmediate、setTimeout |
--options.js | 选项覆盖(父值和子值的合并策略及转换为最终结果) | |
--perf.js | 忽略工具 | |
--props.js | props属性工具 | |
--vdom | ||
--helpers | 助手 | |
--extract-props.js | prop提取助手 | |
--get-first-component-child.js | 获取第一个组件 | |
--index.js | 入口文件 | |
--is-async-placeholder.js | 异步占位符判断 | |
--merge-hook.js | 合并钩子 | |
--normalize-children.js | 规范化子项 | |
--normalize-scoped-slots.js | 规范化作用域插槽 | |
--resolve-async-component.js | 解析异步组件 | |
--update-listeners.js | 更新侦听器 | |
--modules | 模块 | |
--directives.js | 指令 | |
--index.js | 入口文件 | |
--ref.js | ref | |
--create-component.js | 提供createComponent方法创建组件返回vnode | |
--create-element.js | 提供createElement方法 | |
--create-functional-component.js | 提供createFunctionalComponent方法 | |
--patch.js | 基于Snabbdom的虚拟DOM修补算法 | |
--vnode.js | 入口文件(提供VNode类) | |
--config.js | 核心配置文件 | |
--index.js | 核心入口文件,引入和导出vue api接口 | |
--platforms | 应用平台 | |
--web | web平台 | |
--compiler | 编译器 | |
--directives | 指令 | |
--html.js | 添加html元素及内容 | |
--index.js | 入口文件 | |
--model.js | model指令 | |
--text.js | text指令 | |
--modules | 模块 | |
--class.js | dom节点样式class解析 | |
--index.js | 入口文件 | |
--model.js | 表单v-model解析 | |
--style.js | dom节点样式style解析 | |
--index.js | 入口文件 | |
--options.js | 编译器选项 | |
--util.js | 标签及属性判断归类方法 | |
--runtime | 运行 | |
--components | 组件 | |
--transition-group.js | 提供列表项支持 | |
--transition.js | 提供单个元素/组件支持 | |
--directives | 指令 | |
--index.js | 入口文件 | |
--model.js | model指令 | |
--show.js | show指令 | |
--modules | 模块 | |
--attrs.js | attrs属性 | |
--class.js | class属性 | |
--dom-props.js | dom的props属性 | |
--events.js | 事件 | |
--index.js | 入口文件 | |
--style.js | 样式优先级判断及更新 | |
--transition.js | dom节点的过渡 | |
--class-util.js | 封装添加/移除与SVG兼容的样式class | |
--index.js | 入口文件 | |
--node-ops.js | node节点的方法集成封装 | |
--patch.js | 内置模块(最后应用指令模块) | |
--transition-util.js | 过渡集成封装 | |
--server | 服务端 | |
--directives | 指令 | |
--index.js | 入口文件 | |
--model.js | model指令 | |
--show.js | show指令 | |
--modules | 模块 | |
--attrs.js | attrs属性 | |
--class.js | class属性 | |
--dom-props.js | dom的props属性 | |
--index.js | 入口文件 | |
--style.js | 样式优先级判断及更新 | |
--compiler.js | 重命名并暴露编译器方法 | |
--util.js | 标签属性及样式属性判断归类方法 | |
--util | 应用 | |
--attrs.js | web保留的attrs属性编译处理 | |
--class.js | 样式class编译处理 | |
--compat.js | 检查当前浏览器是否在属性值内编码字符 | |
--element.js | html dom元素编译处理 | |
--index.js | 入口文件 | |
--style.js | 样式style元素编译处理 | |
--entry-compiler.js | 条目编译器 | |
--entry-runtime-with-compiler.js | 带编译器的入口运行 | |
--entry-runtime.js | 编译器运行入口文件 | |
--entry-server-basic-renderer.js | 服务端基本渲染器 | |
--entry-server-renderer.js | 服务端渲染器 | |
--weex | weex平台 | |
--compiler | ||
--directives | ||
--index.js | 入口文件 | |
--model.js | model指令 | |
--modules | 模块 | |
--recycle-list | 回收列表 | |
--component-root.js | 根组件 | |
--component.js | 组件 | |
--index.js | 入口文件 | |
--recycle-list.js | 回收列表 | |
--text.js | 文本解析器 | |
--v-bind.js | v-bind解析器 | |
--v-for.js | v-for解析器 | |
--v-if.js | v-if解析器 | |
--v-on.js | v-on解析器 | |
--v-once.js | v-once解析器 | |
--append.js | 追加 | |
--class.js | 样式class解析器 | |
--index.js | 入口文件 | |
--props.js | props解析器 | |
--style.js | 样式style解析器 | |
--index.js | 入口文件 | |
--runtime | ||
--components | ||
--index.js | 入口文件 | |
--richtext.js | 富文本 | |
--transition-group.js | 过渡组 | |
--transition.js | 过渡 | |
--directives | 指令 | |
--index.js | 入口文件 | |
--modules | 模块 | |
--attrs.js | attrs属性编译处理 | |
--class.js | 样式class编译处理 | |
--event.js | 事件 | |
--index.js | 入口文件 | |
--style.js | 样式style元素编译处理 | |
--transition.js | 过渡编译处理 | |
--recycle-list | 回收列表 | |
--render-component-template.js | 渲染组件模板 | |
--virtual-component.js | 虚拟组建 | |
--index.js | 入口文件 | |
--node-ops.js | node节点的方法集成封装 | |
--patch.js | 内置模块(最后应用指令模块) | |
--node-ops.js | 节点文本 | |
--util | 应用 | |
--element.js | html dom元素编译处理 | |
--index.js | 入口文件 | |
--parser.js | 解析器 | |
--entry-compiler.js | 条目编译器 | |
--entry-framework.js | 入口框架 | |
--entry-runtime-factory.js | 函数构建包装 | 用于为每个Weex实例生成Vue的新副本 |
--server | 支持服务器端渲染,所有服务器端渲染相关的逻辑 | |
--bundle-renderer | ||
--create-bundle-renderer.js | 创建结束渲染器 | |
--create-bundle-runner.js | 创建结束运行程序 | |
--source-map-support.js | 源地图支持 | |
--optimizing-compiler | ||
--codegen.js | 通过扩展默认codegen来进行SSR优化节点 | |
--index.js | 入口文件 | |
--modules.js | 模块 | |
--optimizer.js | 在SSR中,vdom树只生成一次,从不修补,因此我们可以将大多数元素/树优化为纯字符串渲染函数。 *SSR优化器遍历AST树以检测可优化的元素和树。SSR优化的标准比静态树要宽松一些检测(设计用于客户端重新渲染)。在SSR中,我们只为组件/插槽/自定义指令。 | |
--runtime-helpers.js | 运行帮助程序 | |
--template-renderer | ||
--create-async-file-mapper.js | 创建异步映射器 | 创建映射器,映射服务器端渲染期间使用的组件在客户端构建中异步区块文件,以便我们可以内联它们直接在呈现的HTML中,以避免瀑布式请求。 |
--index.js | 入口文件 | |
--parse-template.js | 分析模板 | |
--template-stream.js | 模板流 | |
--webpack-plugin | webpack插件 | |
--client.js | 客户端 | |
--server.js | 服务端 | |
--util.js | 应用 | |
--create-basic-renderer.js | 创建基本渲染器 | |
--create-renderer.js | 创建渲染器 | |
--render-context.js | 渲染上下文 | |
--render-stream.js | 渲染流 | |
--render.js | 服务器渲染函数 | |
--util.js | 判断js、css文件并创建promise回调 | |
--write.js | 创建写入函数(会判断堆栈) | |
--sfc | single-file components(单文件组件) | 简单说:转换单文件组件(*.vue) 解析成一个javascript对象 |
--parser.js | 主要内容:将单个文件组件(*.vue)文件解析为SFC描述对象。 | |
--shared | 共享 | |
--constants.js | 常量:数据渲染,组件,指令,过滤器,生命周期,错误机制和SSR服务端渲染(组件在服务器上呈现之前解析,异步) | |
--util.js | 封装了些简单实用明确的js方法(检查是是否为js原始值,undefined判断,对象判断,两个值是否大致相等,值在数组中的第一个索引,等) | |
types | 一些ts文件 | |
LICENSE | 作者尤雨溪及版权声明 | |
package.json | vue2配置文件 | |
README.md | vue的一些赞助商 |
2.vue实例化过程
vue构造函数,源码位置:src\core\instance\index.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'
/**
*
* @param {*} options 是用户传递过来的配置项,如data、methods等常用的方法
*/
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)
}
// 定义 _init
initMixin(Vue)
// 定义 $set $get $delete $watch 等
stateMixin(Vue)
// 定义事件$on $once $off $emit
eventsMixin(Vue)
// 定义 _update $forceUpdate $destroy
lifecycleMixin(Vue)
// 定义 _render 返回虚拟dom
renderMixin(Vue)
export default Vue
initMixin方法,源码位置:src\core\instance\init.js
/* @flow */
import config from '../config'
import { initProxy } from './proxy'
import { initState } from './state'
import { initRender } from './render'
import { initEvents } from './events'
import { mark, measure } from '../util/perf'
import { initLifecycle, callHook } from './lifecycle'
import { initProvide, initInjections } from './inject'
import { extend, mergeOptions, formatComponentName } from '../util/index'
let uid = 0
export function initMixin (Vue: Class<Component>) {
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
// 合并属性,判断初始化的是否是组件,这里合并主要是 mixins 或 extends 的方法
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 {
// 合并vue属性
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// 初始化proxy拦截器
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化组件生命周期标志位
initLifecycle(vm)
// 初始化组件事件侦听
initEvents(vm)
// 初始化渲染方法
initRender(vm)
callHook(vm, 'beforeCreate')
// 初始化依赖注入内容,在初始化data、props之前
initInjections(vm) // resolve injections before data/props
// 初始化props/data/method/watch/methods
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)
}
}
}
结论:可以看出,
1)在调用beforeCreate
之前,数据初始化并未完成,像data
、props
这些属性无法访问到
2)到了created
的时候,数据已经初始化完成,能够访问data
、props
这些属性,但这时候并未完成dom
的挂载,因此无法访问到dom
元素
3)挂载方法是调用vm.$mount
方法
initState
方法,完成props/methods/data/computed/watch的初始化,源码位置:src\core\instance\state.js
export function initState(vm: Component) {
// 初始化组件的watcher列表
vm._watchers = []
const opts = vm.$options
// 初始化props属性
if (opts.props) initProps(vm, opts.props)
// 初始化methods方法
if (opts.methods) initMethods(vm, opts.methods)
// 初始化data函数/对象
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// 初始化computed方法
if (opts.computed) initComputed(vm, opts.computed)
// 初始化watch 方法
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
结论: 可以看出,初始化顺序为:
props => methods => data => computed => watch
可以根据这个初始化顺序,我们可以定义一些策略,如:data直接调用methods方法,computed调用methods方法,而不会产生undefine
initData方法,源码位置:src\core\instance\state.js
function initData (vm: Component) {
let data = vm.$options.data
// 获取组件上的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
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
// 属性名不能与方法名重复
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
// 属性名不能与props中的名称重复
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) { // 验证key值的合法性
// 将_data中的数据挂载到组件vm上,这样就可以通过this.xxx访问到组件上的数据
proxy(vm, `_data`, key)
}
}
结论:可以看出,
1)data定义的时候可选择函数形式或者对象形式(组件只能为函数形式)
2)data的属性名会在methods方法和props属性中判断是否重名
3)data会被响应式监听数据变化
vm.$mount挂载方法,源码位置:
web平台:src/platforms/web/runtime/index.js
weex平台:src/platform/weex/runtime/index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
// 渲染组件
return mountComponent(this, el, hydrating)
}
mountComponent渲染组件,源码位置src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 如果没有获取解析的render函数,则会抛出警告
// render是解析模板文件生成的
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
)
}
}
}
// 执行beforeMount钩子
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
// 监听当前组件状态,当有数据变化时,更新组件
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
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
//手动装载实例,调用自行装载
//在其插入的挂钩中为渲染创建的子组件调用mounted
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
结论:可以看出,
1) 会触发beforeCreate钩子
2) 定义updateComponent渲染页面视图的方法
3) 监听组件数据,一旦发生变化,触发beforeUpdate生命钩子
4) updateComponent方法主要执行在vue初始化时声明的render,update方法
render方法的主要作用是生成vnode, 源码位置:src/core/instance/render.js
// 定义vue 原型上的render方法
Vue.prototype._render = function (): VNode {
const vm: Component = this
// render函数来自于组件的option
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// 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 {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
// 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNode
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' && 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
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// 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
}
_update方法通过调用patch,将vnode转换为真实DOM,并且更新到页面中;源码位置:
src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
// 设置当前激活的作用域
const restoreActiveInstance = setActiveInstance(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)
}
restoreActiveInstance()
// 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.
}
$mount在web平台具体实现(src/platform/web/entry-runtime-with-compiler.js)
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 获取或查询元素
el = el && query(el)
/* istanbul ignore if */
// vue 不允许直接挂载到body或页面文档上
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
// 存在template模板,解析vue模板文件
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')
}
// 将temmplate解析ast tree, 将ast tree转换成render语法字符串, 生成render方法
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
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)
}
结论:可以看出,
1)不要将根元素放到body或者html上
2)可以在对象中定义template/render或者直接使用template、el表示元素选择器
3)最终都会解析成render函数,调用compileToFunctions,会将template解析成render函数
4)template解析步骤:
将html文档片段解析成ast描述符
将ast描述符解析成字符串
生成render函数
生成render函数,挂载到vm上后,会再次调用mount方法
initLifecycle方法初始化一些生命周期相关属性,源码位置:
src/core/instance/lifecycle.js
export function initLifecycle(vm: Component) {
// 初始化参数$options
const options = vm.$options
// locate first non-abstract parent
/**
* 定位第一个“非抽象”的父组件
* 抽象组件:<keep-alive>包裹动态组件时,会缓存不活动的组件实例,而不销毁。和<transition>相似
* <keep-alive>是一个抽象组件:它自身不会渲染一个dom元素,也不会出现在父组件链中
*/
let parent = options.parent
if (parent && !options.abstract) {
// 持续找组件的父组件直到找到非抽象组件
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
// 指定创建的实例的父实例,两者之间建立父子关系
vm.$parent = parent
// 当前组件树的根Vue实例;如果当前实例没有父实例,此实例将会是自己
vm.$root = parent ? parent.$root : vm
// 当前实例的直接子组件。但并不保证顺序,也不响应式
vm.$children = []
// 注册过ref的所有子组件
vm.$refs = {}
// 当前实例组件的watcher实例对象
vm._watcher = null
// keep-alive组件状态
vm._inactive = null
// keep-alive组件状态的属性
vm._directInactive = false
// 当前实例是否完成挂载
vm._isMounted = false
// 当前实例是否已被销毁
vm._isDestroyed = false
// 当前实例是否正在被销毁
vm._isBeingDestroyed = false
}
结论:可以看出,
1)确定父子组件关系
2)初始化ref,watcher,ref属性,判断组件是否挂载、是否销毁和销毁中
stateMixin方法,源码位置:src/core/instance/state.js
export function stateMixin (Vue: Class<Component>) {
// flow somehow has problems with directly declared definition object
// when using Object.defineProperty, so we have to procedurally build up
// the object here.
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
if (process.env.NODE_ENV !== 'production') {
dataDef.set = function () {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
)
}
propsDef.set = function () {
warn(`$props is readonly.`, this)
}
}
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn () {
watcher.teardown()
}
}
}
结论:可以看出,通过定义dataDef和propsDef,来设置响应式data和data和props的get和set方法,并给vue实例添加set、delete和watch方法
eventsMixin方法:源码位置:src/core/instance/events.js
export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$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
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
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
}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// all
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// specific event
const cbs = vm._events[event]
if (!cbs) {
return vm
}
if (!fn) {
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
}
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)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
}
结论:可以看出,定义实现Vue实例的$on、$once、$off和$emit方法
lifecycleMixin方法,源码位置:src/core/instance/lifecycle.js
export function lifecycleMixin (Vue: Class<Component>) {
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(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)
}
restoreActiveInstance()
// 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.
}
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
}
结论:可以看出,定义实现vue实例的_update,$forceUpdate,$destroy方法
renderMixin方法,源码位置:src/core/instance/render.js
export function renderMixin (Vue: Class<Component>) {
// install runtime convenience helpers
// 安装运行助手
installRenderHelpers(Vue.prototype)
// 定义$nextTick
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
// 定义vue 原型上的render方法
Vue.prototype._render = function (): VNode {
const vm: Component = this
// render函数来自于组件的option
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
// 设置父vnode。这允许渲染函数具有访问权限到占位符节点上的数据。
vm.$vnode = _parentVnode
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
// 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNode
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' && 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
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// 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
}
}
结论:可以看出,定义实现vue实例的 _render 并返回返回虚拟dom
错误处理handleError方法,源码地址:src/core/util/error.js
export function handleError (err: Error, vm: any, info: string) {
// Deactivate deps tracking while processing error handler to avoid possible infinite rendering.
// See: https://github.com/vuejs/vuex/issues/1505
// 在处理错误处理程序时停用deps跟踪,以避免可能的无限渲染。
pushTarget()
try {
if (vm) {
let cur = vm
while ((cur = cur.$parent)) {
// vue实例的errorCaptured生命周期
const hooks = cur.$options.errorCaptured
if (hooks) {
for (let i = 0; i < hooks.length; i++) {
try {
const capture = hooks[i].call(cur, err, vm, info) === false
if (capture) return
} catch (e) {
globalHandleError(e, cur, 'errorCaptured hook')
}
}
}
}
}
globalHandleError(err, vm, info)
} finally {
popTarget()
}
}
function globalHandleError (err, vm, info) {
if (config.errorHandler) {
try {
return config.errorHandler.call(null, err, vm, info)
} catch (e) {
// if the user intentionally throws the original error in the handler,
// do not log it twice
if (e !== err) {
logError(e, null, 'config.errorHandler')
}
}
}
logError(err, vm, info)
}
function logError (err, vm, info) {
if (process.env.NODE_ENV !== 'production') {
warn(`Error in ${info}: "${err.toString()}"`, vm)
}
/* istanbul ignore else */
if ((inBrowser || inWeex) && typeof console !== 'undefined') {
console.error(err)
} else {
throw err
}
}
结论:可以看出,错误处理机制会触发vue实例的errorCaptured生命周期(捕获一个来自子孙组件的错误时被调用)
3.思考总结
从vue实例化的过程中,可以看出其生命周期的调用过程,而不是简单意义上理解的8大生命周期;具体如下:
生命周期 | 描述 |
beforeCreate | 组件实例被创建之初 |
created | 组件实例已经完全创建 |
beforeMount | 组件挂载之前 |
mounted | 组件挂载到实例上去之后 |
beforeUpdate | 组件数据发生变化,更新之前 |
updated | 组件数据更新之后 |
beforeDestroy | 组件实例销毁之前 |
destroyed | 组件实例销毁之后 |
activated | keep-alive 缓存的组件激活时 |
deactivated | keep-alive 缓存的组件停用时调用 |
errorCaptured | 捕获一个来自子孙组件的错误时被调用 |