序文
Vueインスタンスの作成プロセスを理解することは、Vueの内部実装メカニズムを理解するのに役立ち、フック関数の役割と使用シナリオをより深く理解するのに役立ちます。このブログ投稿はVue2.Xに基づいており、3.0のいくつかの特別なメカニズムと構文は当面考慮されていません。記事全体を読み、约12分钟
フック機能の使用法と使用シナリオのみを理解し、予約が必要4分钟
です。
テキスト
Vueインスタンスを作成するには、新しいVue()を実装する必要があることは誰もが知っています。Vueインスタンスが作成および破棄されると、どのような変更と関数が呼び出されますか?どのシーンがフック関数の呼び出しをトリガーしますか?以下では、Vueインスタンスの作成プロセスを説明することにより、Vueフック関数の呼び出しタイミングとシーンについて説明します。
一般に、Vueインスタンスのライフサイクルは、初期化フェーズ、データ応答フェーズ、破棄フェーズの3つのフェーズに分けることができます。これらのフェーズとライフサイクルフック機能の使用については、複数のブログ投稿で紹介します。
Vueインスタンスの初期化
通常の単純なVueインスタンス化ステートメントは次のとおりです。
var vm = new Vue({
el:#app',
data:{
msg:'xxxx'
}
})
Vueコンストラクターを呼び出してVueインスタンスを作成します。このコンストラクターでは、Vueをインスタンス化するために必要なelやデータなどを含むオブジェクトを渡します。インスタンスの初期化フェーズでは、Vueは渡されたオブジェクトに基づいてさまざまなinit()関数を内部的に呼び出します。期間中、Vueの4つのライフサイクルのフック関数も呼び出されます。beforeCreate、created、beforeMount、Mounted
初期化プロセスはおおまかに次のとおりです。
通常の開発プロセスでは、各フック関数の使用シナリオと前提を理解するだけで済みます。 、および内部の内部実装メカニズムにあまり注意を払う必要はないので、最初に各フック関数の使用シナリオについて説明しましょう。
beforeCreate
beforeCreateフック関数は、その名前が示すように、Vueインスタンスがデータやメソッドなどを作成しない前に呼び出しをトリガーするフック関数です。この関数の呼び出しをトリガーする前に、Vueは主にいくつかの簡単なイベントの初期化やその他のタスクを実行しますなどなどemit,on,once,off
、通常の開発で使用される可能性は低くなります。
created
作成されたフック関数はprops、methods、data、computed、watch
、初期化後に呼び出されるフック関数です。ただし、属性elはまだ作成されていませんが、インスタンスのいくつかの基本属性が作成されています。作成されたフック関数では、作成されたデータとメソッドを操作できます。また、非同期関数を呼び出して、ページの初期化に必要なデータを取得することもできます。
beforeMount
これをbeforeMountと呼ぶ前に、Vueはテンプレートをコンパイルして仮想DOMに変換し、elは要素ノードのマウントも完了しました。このフック関数では、updatedおよびbeforeUpdateフック関数の呼び出しをトリガーせずにデータデータを変更でき、使用頻度も低くなります。
mounted
マウントされたフック関数は、仮想DOM$el
が実際のDOMツリーに置き換えられた後に呼び出されます。この時点で、ページ内のhtmlがレンダリングされています。created
対照的に、DOMツリーを操作する必要がある場合mounted
は、完全。このとき、this。$ refを介してrefによってバインドされた要素ノードを取得することもできます。同時に、この時点でデータのデータを変更すると、それに応じてupdatedおよびbeforeUpdateフック関数の呼び出しがトリガーされます。
初期化フェーズでのフック関数の呼び出しについて説明した後、このプロセスをソースコードと一緒に解釈してみましょう。コンテンツの次の部分は、ソースコードの解釈を含みます。時間がない友人は、最初にそれを収集し、次にそれを調べて理解することができます。
まず、簡単なインスタンス化コードを示しましょう。
// 传入了一个包含el、data属性的对象来构建一个Vue实例
var vm = new Vue({
el:'#app',
data:{
msg:''
}
})
Vueインスタンスの構築は新しいVue()を介して行われるため、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)
}
ここでは_init関数が呼び出され、init関数は主に次のようになります。
// src\core\instance\init.js
// 省略部分代码
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
...
// Vue内部调用,正常不会走到这个分支
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
// 使用策略对象合并参数选项合并父子组件的一些同名属性,包括指令、过滤器等,这里不做深入
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {
},
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')
...
//如果存在el,则
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
ご覧のとおり、beforeCreate、created
フック関数の呼び出しはこのinit関数で完了します。そして、一連の初期化作業を行いました。ここではinitState
、キーコードを逆アセンブルするだけです。その他の詳細については、上記のコメントがあり、理解が詳細すぎて、あまり使用する必要がありません。
//代码路径:src\core\instance\state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.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)
}
}
上記のように、最初のinitStateはbeforeCreateの後に呼び出され、それぞれinitStateprops、methods、data、computed、watch
で初期化されます。また、propsとメソッドはデータの前に初期化されるため、データでは、initStateの実行後にPropsとメソッドにアクセスできます。 、作成したフック関数を後で実行すると、データとメソッドに正常にアクセスできます。
createdを実行した
後、createdを実行した後、最初に_init()が実行された場所から次のコードを確認できます。
// 代码路径:src\core\instance\init.js
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
これは、elが存在するかどうかを判断することにより、実際にはライフサイクル図に対応し、存在する場合は、関連する呼び出しをトリガーします。ただし、最初にこの$ mountの実装を確認する必要があります。
// 代码路径:src\platforms\web\runtime\index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 这个query(el)获取el绑定的dom元素,如果没有,则创建一个div元素返回
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
ご覧のとおり、$mount
内部ではelとブラウザ環境かどうかを判断します。そうである場合は、elをトラバースして対応するdom要素を取得し、次のmountComponentメソッドを実行します。ここではmountComponentについて具体的に説明しません。いう。この場所は宣言され$mount
たコア関数であり、実際に呼び出される場所はまだ下にあります。
// src\platforms\web\entry-runtime-with-compiler.js
//这里将$mount赋值给了mount,然后给$mount覆盖了一个新的方法,在新方法的末尾,
//通过mount.call(this, el, hydrating)去触发核心代码的调用。
//这种处理方式被称之为函数劫持,即在原始功能上新增一些其他功能。
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
// el元素不建议绑定html、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
// 判断是否存在render,如果不存在,则判断将template是否存在
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) {
// 如果不存在则根据el绑定的元素节点,将对应的DOM转换为template
template = getOuterHTML(el)
}
// 存在则将模板渲染通过compileToFunctions转化为render函数
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, {
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)
}
上記のコードは、主にテンプレートを対応するレンダリング関数に変換するためのものです。存在するtemplate
場合は通常どおりに変換されます。それ以外の場合は、elでバインドされたDOMテンプレートに従ってテンプレートにcompileToFunctions
変換され、方法。これらを処理した後、一時的に保存し$mount
たコア関数、つまりフック関数の呼び出しを呼び出します。ことに留意すべきである$mount
ように、コンパイルする多くの方法がある方法$mount
返さ上記mountComponent()
レンダリングが判定される。なし()レンダリング対応がない場合、対応する通知、警告が与えられると、上記の関数をAとして実装され、フルバージョンのコンパイル機能。詳細については、Vueの公式ドキュメントを参照してください:ランタイム+コンパイラとランタイムのみ
そうは言っても、フック関数beforeMountが呼び出される前に、実際にはテンプレートコンパイルの作業を実行し、レンダリング関数を返すことがわかります。次のステップは、実際の取り付けフェーズmountComponent()
です。これが実行されます。具体的な実装コードは次のとおりです。
// 代码路径:src\core\instance\lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
// 如果render不存在,会创建一个空的VNode节点
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
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._render()将会为我们得到一份最新的VNode节点树
// 如果调用了updateComponent函数,就会将最新的模板内容渲染到视图页面中
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
//这里对该vm注册一个Watcher实例,Watcher的getter为updateComponent函数,
//用于触发所有渲染所需要用到的数据的getter,进行依赖收集,该Watcher实例会存在所有
//渲染所需数据的闭包Dep中
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
// 调用mounted钩子
callHook(vm, 'mounted')
}
return vm
}
この時点で、Vueの初期化プロセスとフック関数の呼び出しシーン全体がわかっています。もちろん、スペースは限られており、$ elをマウントする方法や、マウントする前に仮想DOMを実際に置き換える方法などの詳細をすべて紹介することはできません。 DOM(私の記事の最後にあるリファレンスリンクを参照してください)、これらは詳細には説明されていませんが、Vueインスタンス化段階でのフック関数呼び出しは比較的明確である必要があります。それを読んだ後、VueのライフサイクルにおけるVueのインスタンス化についてより深く理解できることを願っています。
コードワードは簡単ではありません、いくつかの励ましを与えるためにワンキースリーリンクを歓迎します〜
同時に、記事にいくつかの間違いがあります。コメントと訂正を歓迎します。同時に関連する証拠を指摘し、提供することが最善です。みんなが一緒に改善することを歓迎します。
参照:
Vue gitHub