preface
Before reading this article, to clarify the patch process and event mechanism, please refer to the previous article. All the processes involved in this article have been analyzed in detail in the previous article
To get down to business, take a look at the Demo. Here are two custom directives, one bound to the normal tag and the other to the component tag
<div id="app">
<div v-check="123"></div>
<child v-test="456"></child>
</div>
Copy the code
The compiled code looks like this
with (this) {
return _c(
'div',
{ attrs: { id: 'app' } },
[
_c('div', {
directives: [ / / here
{
name: 'check'.rawName: 'v-check'.value: 123.expression: '123',
},
],
}),
_v(' '),
_c('child', {
directives: [ / / here
{
name: 'test'.rawName: 'v-test'.value: 456.expression: '456'},],}),],1)}Copy the code
It is not hard to see that if the label is bound to directives, in the compiled code we add an array property directives that stores the bound directives. Caching
All works
Next, let’s take a look at the principle of instruction. As described in the previous chapter, hook functions supported by the current platform will be collected before the patch starts and executed at different times during the patch process
const hooks = ['create'.'activate'.'update'.'remove'.'destroy']
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
// Put all values exported from modules into CBS
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
// ...
}
Copy the code
Put all values exported from modules into CBS, which has the following data structure
cbs = {
create: [].activate: [],... }Copy the code
Hook function, which contains the instructions it defined in SRC/core/vdom/modules/directives. Js, first look at the export
export default {
create: updateDirectives,
update: updateDirectives,
destroy: function unbindDirectives (vnode: VNodeWithData) {
updateDirectives(vnode, emptyNode)
}
}
Copy the code
CBS’s create, Update, and destroy arrays all contain the directive’s hook functions
Let’s review the execution timing of create and UPDATE
Create:
- After the child VNode creates the DOM element and inserts it into the target location, it is called before the current VNode inserts the target location. Pass in the current VNode
- Called after the DOM tree of the child component is created and inserted into the target location, passed into the component VNode
- During the update process, if the root element of the child component is different from the root element of the old node, when the child component is updated, the VNode component is updated
elm
Property and call this hook function to pass in the component VNode.
update
:
patchVnode
Method is calledupdate
The hook updates all the current vNodesupdate
Hook function
Starting with the CREATE hook, the directive’s Created hook function, the updateDirectives method, is called when the div child node is created and inserted into the target
function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (oldVnode.data.directives || vnode.data.directives) {
_update(oldVnode, vnode)
}
}
Copy the code
OldVnode is always empty for created hook functions, since the DIV VNode’s data.directives are value, execute the _UPDATE method
function _update (oldVnode, vnode) {
// If oldVnode is an empty node, it is the first time it has been created
// oldVnode is empty during the update phase. For details, see section 3 of create hook execution timing above
const isCreate = oldVnode === emptyNode
const isDestroy = vnode === emptyNode
// Format the directive object and find the directive's property values
const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)
const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)
// ...
}
Copy the code
Determine whether the current is the create or destroy phase based on the passed parameters, and then call normalizeDirectives to convert the instruction array of the new and old nodes into the form of instruction objects
const emptyModifiers = Object.create(null)
function normalizeDirectives (
dirs: ?Array<VNodeDirective>,
vm: Component
) :{ [key: string]: VNodeDirective } {
const res = Object.create(null)
if(! dirs) {return res
}
let i, dir
for (i = 0; i < dirs.length; i++) {
dir = dirs[i]
if(! dir.modifiers) { dir.modifiers = emptyModifiers } res[getRawDirName(dir)] = dir// Get the definition of the directive
dir.def = resolveAsset(vm.$options, 'directives', dir.name, true)}return res
}
Copy the code
Turns all directives into objects whose property names are directive names and whose property values are directive contents
res = {
v-check: {
name: 'check'.// Does not include the 'v-' prefix
rawName: 'v-check'.value: 123.expression: '123'.def: {}, // Directive definition
arg: ' ', parameters passed to the instruction, for example`v-check:foo`Where, the parameter is"foo"
modifiers: {} // An object containing modifiers. For example, in v-model.sync, the modifier object is {sync: true}.}}Copy the code
Go back to _update, get the instruction object, and continue execution
const dirsWithInsert = []
const dirsWithPostpatch = []
let key, oldDir, dir
for (key in newDirs) {
oldDir = oldDirs[key]
dir = newDirs[key]
if(! oldDir) {/ / the bind
callHook(dir, 'bind', vnode, oldVnode)
// If the INSERTED hook function is defined, add dir to dirsWithInsert
if (dir.def && dir.def.inserted) {
dirsWithInsert.push(dir)
}
} else {
dir.oldValue = oldDir.value
dir.oldArg = oldDir.arg
// called when the VNode of the component is updated, but
callHook(dir, 'update', vnode, oldVnode)
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir)
}
}
}
// ...
Copy the code
Next, all instructions in the new node are iterated, executing the following logic for each instruction
- Called if it is the first time created or if there are no instructions in the old node
callHook
executive-orderbind
Hook function. If there is one in the instruction definitioninserted
The hook function adds the instruction object todirsWithInsert
In the - Otherwise, it is an update process. Add to the new instruction object
oldValue
(old value) andoldArg
(old argument) property and executes the instructionupdate
Hook function; As it says,cbs
theupdate
The hook function will be inpatchVnode
Method to execute, so instructionupdate
The hook function occurs before its child VNode is updated. If the instruction hascomponentUpdated
The hook function adds the instruction object todirsWithPostpatch
In the
Let’s look at the callHook function
function callHook (dir, hook, vnode, oldVnode, isDestroy) {
// According to the corresponding hook function in the fetch definition
const fn = dir.def && dir.def[hook]
if (fn) {
try {
// Execute the hook function
/** * vnode.elm: dir: an object * https://cn.vuejs.org/v2/guide/custom-directive.html#%E9%92%A9%E5%AD%90%E5%87%BD%E6%95%B0 */
fn(vnode.elm, dir, vnode, oldVnode, isDestroy)
} catch (e) {}
}
Copy the code
_update The execution continues
if (dirsWithInsert.length) {
const callInsert = () = > {
for (let i = 0; i < dirsWithInsert.length; i++) {
callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
}
}
if (isCreate) {
mergeVNodeHook(vnode, 'insert', callInsert)
} else {
callInsert()
}
}
Copy the code
The dirsWithInsert store instructions with the INSERTED hook function, and if the length is not empty, create a callback called callInsert. If this is the creation phase (more accurately, oldVnode is an empty VNode), call mergeVNodeHook and add the callback function to vNode.data.hook. Insert. Instead, call the callback function directly. Inserted hook function inside the callback is the inserted hook function that executes all the instruction objects of the current VNode.
Take a look at the mergeVNodeHook method
export function mergeVNodeHook (def: Object, hookKey: string, hook: Function) {
if (def instanceof VNode) {
// The component vNode is created with a hook attached, rendering vNode is not
def = def.data.hook || (def.data.hook = {})
}
let invoker
const oldHook = def[hookKey]
function wrappedHook () {
hook.apply(this.arguments)
remove(invoker.fns, wrappedHook)
}
if (isUndef(oldHook)) {
// The vnode is being rendered, and no hooks are currently bound to the vnode
invoker = createFnInvoker([wrappedHook])
} else {
if (isDef(oldHook.fns) && isTrue(oldHook.merged)) {
// The hooks are already bound, and are bound via mergeVNodeHook
invoker = oldHook
invoker.fns.push(wrappedHook)
} else {
// This is a component vnode that binds the component hook to the instruction key
invoker = createFnInvoker([oldHook, wrappedHook])
}
}
invoker.merged = true
def[hookKey] = invoker
}
Copy the code
Get or initialize the vNode.data. hook object first, because the component vnode is created with a hook bound, rendering vnode does not have one. Get the existing INSERT hook function and create a callback function wrappedHook, followed by the following logic
- If not on VNode
insert
The hook function indicates that this is a render VNode and does notinsert
The hook function is calledcreateFnInvoker
To create ainvoker
Function, and will[wrappedHook]
Mount to theinvoker.fns
on - Otherwise, it is a component VNode, or it may exist
insert
Of the hook functionRender/Components
VNode. Then determine which of the preceding hook functions it is based on the existing hook functions.- If it is a component VNode
createFnInvoker
createinvoker
Function, and will[oldHook, wrappedHook]
Added to theinvoker.fns
In the - if
Render/Components
There is a pass on VNodemergeVNodeHook
The binding ofinsert
The hook function that will be createdwrappedHook
Added to theinvoker.fns
In the
- If it is a component VNode
Finally, set invoker.merged, which means that if VNode binds hook functions via mergeVNodeHook, its invoker.merged is true. Add the function invoker to vnode.data.hook. Insert
Not only do custom directives bind hook functions to vNodes via mergeVNodeHook, but also the Transition component.
Next, we will talk about the subsequent patch process. During the patch process, the insert hook function of the current VNode will be collected every time the DOM is created and inserted into the target location. When all DOM mounts are complete, the collected INSERT hook function (wrappedHook in the case of instructions) is executed. After execution, the current hook function is deleted to ensure that it is executed only once. One reason is that when the root element of the child component is different from the old node, the insert hook function of the component VNode directive is reassigned. If not removed, it will be added repeatedly. The reasons for binding again will be explained below. The other reason is for regular VNode updates, which, like the first reason, prevent repeated additions.
After all the children of the current component are updated, if the root element is different from the old root element, the elm property of the component VNode will be updated, and the cbS.create hook function will be called again. The component VNode is passed in, and if the component VNode has a directive that has the Inserted hook function bound again. And re-execute all insert hook functions in the component VNode (note the comments). This is because if there is a DOM operation in the inserted hook function of the directive, the DOM will not be up to date after the update, so it needs to be executed again
// issue #6513
const insert = ancestor.data.hook.insert
if (insert.merged) {
// Start with 1, because the first insert hook is mounted
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
Copy the code
Go back to _update and continue
if (dirsWithPostpatch.length) {
mergeVNodeHook(vnode, 'postpatch'.() = > {
for (let i = 0; i < dirsWithPostpatch.length; i++) {
// call after the VNode where the directive resides and its child VNodes are updated
callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
}
})
}
if(! isCreate) {for (key in oldDirs) {
if(! newDirs[key]) { callHook(oldDirs[key],'unbind', oldVnode, oldVnode, isDestroy)
}
}
}
Copy the code
And inserted, if the instruction is componentUpdated hook function, then add the hooks to the vnode. Data. J hook. Postpatch. Finally, the unbind hook function is called if it is currently in the destruction phase
summary
The whole process has been broken down and summarized. Look down the demo
<div id="app">
<div v-check="123"></div>
<child v-test="456"></child>
</div>
Copy the code
Create a stage
We start with the creation phase, creating vNodes and component vNodes, and binding hook functions to component vNodes. During patch, after the DOM is created by the children of the first div and inserted into the target location, all the hook functions in cbS.create are called and the VNode is passed in. The create function of the directive is triggered, the BIND hook function of V-Check is called, and the inserted hook function is collected, adding all the installed hook functions to vnode.data.hook. Insert. Go back to the patch process and collect the current VNode insert hook function.
When the DOM of the render VNode of the Child component is created and inserted into the target location, the elm property of the VNode component of the Child component is updated, and all the hook functions in cbS. create are called again. The bind hook function executes the instruction as above. Add the inserted hook function to vnode.data.hook. Insert; Then you collect the component VNode’s INSERT hook function.
Once the DOM tree is created and inserted into the page, all the insert hook functions collected are executed, including the V-Check (installed) hook function, the Mounted lifecycle of child (Installed), and the V-test (inserted) hook function. The corresponding callback is deleted after the inserted hook function of the instruction completes, preventing it from firing again
Update the stage
There are two cases where the child itself is updated or the current component is updated
If the new root element of the child is different from the old root element, the elm property of the child component VNode will be updated, and all the hook functions in cbS. create will be called again and the component VNode will be passed in. The create function of the directive is triggered, and the directive inserted hook function is added to the data.hook. Insert of the component VNode. Cbs. create triggers all hooks in the vnode.data.hook. Insert array from 1. Vnode.data.hook. Insert [0] is a mounted life cycle function of a component. The logic follows if the new root element is the same as the old root element.
If the component is currently updated, the attributes of the first div will be batch updated, that is, cbs.update will be called and the old and new vNodes will be passed in. The update function of the directive will be triggered, and the update hook function of all directives on the current VNode will be executed. And collect all the componentUpdated hook function, after the completion of the collection, add the hooks to the vnode. Data. J hook. Postpatch. When the first div and its children are updated, all postPatch hook functions on VNode are executed. The same is true of the instruction above the child. The updated hook function is called when the VNode and its child vNodes are all updated.
v-model
V-models can be bound to form elements or to components. Let’s look at the difference between the two
Form element Input
Take input as an example. Take a look at demo first
<div class="app">
<input v-model="test" />
</div>
Copy the code
Compiled code
with (this) {
return _c("div", { attrs: { id: "app" } }, [
_c("input", {
directives: [{name: "model".rawName: "v-model".value: test, expression: "test"}].domProps: { value: test },
on: {
input: function($event) {
if ($event.target.composing) return; test = $event.target.value; }}})]); }Copy the code
Compared to custom directives, there is an input event and a DOM property value in addition to a reported array in the INPUT property
Look at the definition of v – model under the instruction, the code in the SRC/platforms/web/runtime/directives/model. Js
const directive = {
inserted (el, binding, vnode, oldVnode) {},
componentUpdated (el, binding, vnode) {}
}
export default directive
Copy the code
The V-model defines the INSERTED hook function and componentUpdated, which only applies to SELECT.
The binding and execution flow of the instruction is as described above, depending on what the V-Model inserted hook function does
Once the entire DOM tree is created and inserted to the destination, the inserted hook function is called as follows
const isTextInputType = makeMap('text,number,password,search,email,tel,url');
Copy the code
inserted (el, binding, vnode, oldVnode) {
if (vnode.tag === 'select') {
// ...
} else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
el._vModifiers = binding.modifiers
if(! binding.modifiers.lazy) { el.addEventListener('compositionstart', onCompositionStart)
el.addEventListener('compositionend', onCompositionEnd)
// Safari < 10.2&uiWebView doesn't fire compositionEnd When
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
el.addEventListener('change', onCompositionEnd)
}
}
},
Copy the code
For the input tag isTextInputType set to true in demo, first mount the modifier into the EL._vmodifiers. If lazy is not in the modifier, add compositionStart, COMPOSItionEnd, change listener
Compositionstart keyboard input pinyin trigger; Compositionend is triggered when pinyin corresponding Chinese characters are selected
The three listener callbacks are as follows
function onCompositionStart (e) {
e.target.composing = true
}
function onCompositionEnd (e) {
if(! e.target.composing)return
e.target.composing = false
trigger(e.target, 'input')}function trigger (el, type) {
const e = document.createEvent('HTMLEvents')
// Initialization, event type, whether to bubble, whether to block the browser's default behavior
e.initEvent(type, true.true)
el.dispatchEvent(e)
}
Copy the code
The input event is mounted to the DOM during the creation phase via el.addeventListener. For details, see Vue source code (7) event mechanism
{
input: function($event) {
if ($event.target.composing) return; test = $event.target.value; }}Copy the code
When the user enters, the input event is emitted and the value of the test property is modified.
The purpose of adding comPOSItionStart and COMPOSItionEnd was that when pinyin was entered, the input event would not be triggered because the CompositionStart callback was triggered and $event.target.com posture was set to true. After being entered in, the compositionEnd callback is executed to set $event.target.com posture to false and the input event is manually triggered.
summary
For the V-Model directive for the input tag, the input event and DOM attribute value are automatically added to the input tag during compilation. If the lazy modifier is set, the input event is changed to the change event. While the V-model INSERTED hook function is executed, the compositionStart and ComPOSItionEnd events are added, so that the INPUT event is not triggered when pinyin is typed, but when Chinese is selected.
V – model components
<div class="app">
<child v-model="test" />
</div>
Copy the code
Compiled code
with (this) {
return _c(
'div',
{ attrs: { id: 'app' } },
[
_c('child', {
model: {
value: title,
callback: function ($$v) {
title = $$v
},
expression: 'title',}})],1)}Copy the code
The V-Model on the component is completely different from that on the form. There is no reported array on the component, but there is an additional Model property
To see how this works, when render is executed, createComponent is called to create the component VNode
export function createComponent (
Ctor: Class<Component> | Function | Object | void, data: ? VNodeData, context: Component, children: ?Array<VNode>, tag? : string) :VNode | Array<VNode> | void {
// ...
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// ...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? ` -${name}` : ' '}`,
data, undefined.undefined.undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
Copy the code
When a model is present in data, the transformModel method is called to process the model properties. Before looking at this method, take a look at the model API on the website
Allows a custom component to customize prop and Event when using the V-Model. By default, a V-Model on a component uses value as a prop and input as an event, but some input types such as checkboxes and checkbox buttons may want to use Value Prop for different purposes. Using model options can sidestep the conflicts that arise from these situations.
function transformModel (options, data: any) {
const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
(data.attrs || (data.attrs = {}))[prop] = data.model.value
const on = data.on || (data.on = {})
const existing = on[event]
const callback = data.model.callback
if (isDef(existing)) {
if (
Array.isArray(existing)
? existing.indexOf(callback) === -1: existing ! == callback ) { on[event] = [callback].concat(existing) } }else {
on[event] = callback
}
}
Copy the code
The value prop of the child component and the distributed input event name are configurable. Therefore, the transformModel method first obtains the prop and event names defined in the child component. If they are not defined, the default values are used.
Next, add value Prop to data.attrs, where the property value is the parent component’s response property name. Mount the event name to data.on and follow the following logic
- if
data.on
Mounts the current custom event todata.on
on - if
data.on
Has a custom event with the current name, andcallback
Is not the same as an existing event functiondata.on[event]
To an array, willcallback
Add to it
The next step is to create an instance to mount all custom events from data.on to vm._events, which will execute when the child component fires $emit
In other words, for a V-model on a component, you are adding props properties and setting custom events to the component. These steps are taken while creating the component VNode