本文已被前端面试题库收录
一、思考
咱们都听过知其然知其所以然这句话
那么不晓得大家是否思考过 new Vue()
这个过程中到底做了些什么?
过程中是如何实现数据的绑定,又是如何将数据渲染到视图的等等
一、剖析
首先找到 vue
的构造函数
源码地位:src\core\instance\index.js
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)
}
options
是用户传递过去的配置项,如 data、methods
等罕用的办法
vue
构建函数调用 _init
办法,但咱们发现本文件中并没有此办法,但认真能够看到文件下方定定义了很多初始化办法
initMixin(Vue); // 定义 _init
stateMixin(Vue); // 定义 $set $get $delete $watch 等
eventsMixin(Vue); // 定义事件 $on $once $off $emit
lifecycleMixin(Vue);// 定义 _update $forceUpdate $destroy
renderMixin(Vue); // 定义 _render 返回虚构 dom
首先能够看 initMixin
办法,发现该办法在 Vue
原型上定义了 _init
办法
源码地位:src\core\instance\init.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
// 合并属性,判断初始化的是否是组件,这里合并次要是 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)
}
}
仔细阅读下面的代码,咱们失去以下论断:
- 在调用
beforeCreate
之前,数据初始化并未实现,像data
、props
这些属性无法访问到 - 到了
created
的时候,数据曾经初始化实现,可能拜访data
、props
这些属性,但这时候并未实现dom
的挂载,因而无法访问到dom
元素 - 挂载办法是调用
vm.$mount
办法
initState
办法是实现 props/data/method/watch/methods
的初始化
源码地位: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)
if (opts.data) {
// 初始化 data
initData(vm)
} else {observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch)
}
}
咱们和这里次要看初始化 data
的办法为 initData
,它与initState
在同一文件上
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
)
}
}
// 属性名不能与 state 名称反复
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)
}
}
// observe data
// 响应式监听 data 是数据的变动
observe(data, true /* asRootData */)
}
仔细阅读下面的代码,咱们能够失去以下论断:
- 初始化程序:
props
、methods
、data
data
定义的时候可选择函数模式或者对象模式(组件只能为函数模式)
对于数据响应式在这就不开展具体阐明
上文提到挂载办法是调用 vm.$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')
}
/**
* 1. 将 temmplate 解析 ast tree
* 2. 将 ast tree 转换成 render 语法字符串
* 3. 生成 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)
}
浏览下面代码,咱们能失去以下论断:
- 不要将根元素放到
body
或者html
上 - 能够在对象中定义
template/render
或者间接应用template
、el
示意元素选择器 - 最终都会解析成
render
函数,调用compileToFunctions
,会将template
解析成render
函数
对 template
的解析步骤大抵分为以下几步:
- 将
html
文档片段解析成ast
描述符 - 将
ast
描述符解析成字符串 - 生成
render
函数
生成 render
函数,挂载到 vm
上后,会再次调用 mount
办法
源码地位:src\platforms\web\runtime\index.js
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {el = el && inBrowser ? query(el) : undefined
// 渲染组件
return mountComponent(this, el, hydrating)
}
调用 mountComponent
渲染组件
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 {
// 没有获取到 vue 的模板文件
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 = () => {
// 理论调⽤是在 lifeCycleMixin 中定义的_update 和 renderMixin 中定义的_render
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
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
浏览下面代码,咱们失去以下论断:
- 会触发
boforeCreate
钩子 - 定义
updateComponent
渲染页面视图的办法 - 监听组件数据,一旦发生变化,触发
beforeUpdate
生命钩子
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.
}
三、论断
-
new Vue
的时候调用会调用_init
办法- 定义
$set
、$get
、$delete
、$watch
等办法 - 定义
$on
、$off
、$emit
、$off
等事件 - 定义
_update
、$forceUpdate
、$destroy
生命周期
- 定义
- 调用
$mount
进行页面的挂载 - 挂载的时候次要是通过
mountComponent
办法 - 定义
updateComponent
更新函数 - 执行
render
生成虚构DOM
_update
将虚构DOM
生成实在DOM
构造,并且渲染到页面中
参考文献
- https://www.cnblogs.com/gerry…
- https://github.com/vuejs/vue/…
- https://vue3js.cn
返回 github 面试题库查看更多