Vue source code interpretation
Version: [email protected] Preface: This article is purely personal arrangement of knowledge points, not the whole original. Some questions and answers are borrowed from other articles, and integrated into their own understanding, and finally produced this article.
Before this, I occasionally read the source code of Vue related knowledge points, such as seeing some interviews: “What is the principle of nextTick?” , or: “please say about the implementation principle of the response type”, or encountered a tricky bug in the development (delay my work), also have to go to find information or read the relevant source code, above and so on…. Are you like me? (Say yes, PLS)
I know, so, the knowledge based on this situation is too fragmented
Is there something that connects what we’ve learned? Of course!
Ben Yu has compiled a simple mind map for reading Vue source code, which is convenient for you to understand the mechanism of the entire Vue operation, and also convenient for knowledge review. The mind map can help you find the feel faster and more directly
Nonsense not much Lao, first dry goods:
To zoom freely click on the Vue source mind map
It all starts at……
Anyway, let’s start with new Vue()
The life cycle
Am I supposed to pull something here??
What are the life cycles of Vue components?
-
beforeCreate
After instance initialization, data Observer and Event/Watcher events are called before configuration.
-
created
Called immediately after the instance is created. In this step, the instance completes the configuration of data Observer, property and method operations, and watch/ Event event callbacks. However, the mount phase has not yet started and $EL Property is not currently available.
-
beforeMount
Called before the mount begins: The associated render function is called for the first time. This hook is not called during server-side rendering.
-
mounted
Called after the instance is mounted, when el is replaced by the newly created vm.$el. If the root instance is mounted to an element in a document, vm.$el is also in the document when Mounted is called. Note that Mounted does not ensure that all child components are mounted together. If you want to wait until the whole view is rendered, you can use vm.$nextTick inside Mounted: this hook is not called during server-side rendering.
-
beforeUpdate
Called when data is updated and occurs before the virtual DOM is patched. This is a good place to access the existing DOM before updating, such as manually removing event listeners that have been added. This hook is not called during server-side rendering because only the initial rendering takes place on the server side.
-
updated
This hook is called after the virtual DOM is re-rendered and patched due to data changes. This hook is not called during server-side rendering.
-
activated
Called when activated by a component cached by keep-alive. This hook is not called during server-side rendering.
-
deactivated
Called when a component cached by keep-alive is disabled. This hook is not called during server-side rendering.
-
beforeDestroy
Called before instance destruction. At this step, the instance is still fully available. This hook is not called during server-side rendering.
-
destroyed
Called after instance destruction. When this hook is called, all instructions for the Vue instance are unbound, all event listeners are removed, and all subinstances are destroyed. This hook is not called during server-side rendering.
What is the component lifecycle call sequence in Vue?
-
Components are called in the order of parent after child, rendering is completed in the order of child after parent.
-
The destruction of a component is parent before child, and the destruction is completed in the order of child after parent.
Description:
There is a parent component, and two child components, child1 and child2. Child1 has a child component, child1Child
Then the order of the declaration cycle should be:
Components are called in parentafter child order. Why?
I don’t have much to say about that. Do we have to put the son before the father?
The order of rendering completion is child before parent, why?
InsertedVnodeQueue Queue of inserted virtual nodes
function patch(vnode) {
// 1. Virtual node queue
const insertedVnodeQueue = [];
// 2. Create a node. View the following functions
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// 3. Clear the insertedVnodeQueue
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
}
/** Creates the DOM element and appends it to the parent element */
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
// 1. Create the DOM
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
// 2. Recursively create the DOM of the child vNode
createChildren(vnode, children, insertedVnodeQueue)
// key!!
// 3. After the child vNodes are recursively created, the parent virtual node is placed behind the child virtual node
if (isDef(data)) {
Insertedvnodequeue.push (vnode)
invokeCreateHooks(vnode, insertedVnodeQueue)
}
/** the DOM operation appends the generated DOM to target DOM (parentvNode.elm) */
insert(parentElm, vnode.elm, refElm)
}
/** appends the DOM to the parent element */
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
// Empty the insertedVnodeQueue queue
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
Copy the code
Here’s the logic again:
-
When a new VNode is created for the first patch, an insertedVnodeQueue is defined and then createElm is called to create a DOM element and collect vNodes into the insertedVnodeQueue
What is the order of collection?
Grandson: The DOM of the child vnode is mounted first, grandson: the DOM of the child vnode is mounted first, grandson: the DOM of the child vnode is mounted first, grandson: the DOM of the child vnode is mounted first, grandson: the DOM of the child vnode is mounted first, grandson: the DOM of the child vnode is mounted first, grandson: the DOM of the child vnode is mounted first.
— parent
— child
— grandson
The first step would generate the PARENT’s DOM, the second recursive step would generate the Child’s DOM, andso on, the GRANDSON’s DOM
Grandson’s vnode is added to the insertedVnodeQueue, child, parent, InsertedVnodeQueue [grandsonVnode, childVnode, parentVnode]
The parent then inserts the generated DOM into the end of the last patch under the target element and invokeInsertHook, essentially emptying the insertedVnodeQueue
Summary: The order of rendering completion is child before parent
The destruction of a component is parent before child, and the destruction is completed in the order of child after parent.
The component is destroyed by calling the component instance’s destruction method vm.$destroy()
Vue.prototype.$destroy = function () {
const vm: Component = this
// Start destroying the component
callHook(vm, 'beforeDestroy')
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// The destruction is complete
callHook(vm, 'destroyed')}Copy the code
The destroy method first calls the beforeDestroy hook
The patch function is then called to destroy the VNode
The hook Destroyed is finally called
So, what is the process of calling patch function to destroy vNode?
First, take a look at the patch function
function patch() {
/** if -> destroy: If newVnode passes' null 'and oldVnode passes' null', destroy oldVnode */
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return}}Copy the code
Therefore, the Patch function mainly invokeDestroyHook to destroy the VNode
So what does invokeDestroyHook basically do?
/** * Destroy vNodes and child Vnodes ** /
function invokeDestroyHook (vnode) {
let i, j
const data = vnode.data
if (isDef(data)) {
/** * Destroy vnode **/
if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
}
/** * recursive child node vnode, perform the destruction operation **/
if (isDef(i = vnode.children)) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j])
}
}
}
Copy the code
As you can see above, the invokeDestroyHook function calls the destroy hook of the current VNode.
vnode destory hook
componentVNodeHooks = {
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if(! componentInstance._isDestroyed) {// The instance destruction method is called
componentInstance.$destroy()
}
}
}
Copy the code
Then, in the recursive child node vNode, the instance is destroyed one by one. However, the parent vnode will call the deStoryed hook only after the child vNode is destroyed. Therefore, the component destruction operation is parent before child, and the destruction is completed in the order of child after parent.
At what stage can I access the DOM manipulation?
In the Mounted phase, you can access the DOM operation.
Description:
When the component mounts the $mount method, the beforeMount hook is called before the DOM is generated and some preparations for generating the DOM begin:
- perform
_render
Method to generate a VNode - perform
__patch__
Method produces the DOM and inserts it into the parent element.VirtualDOM with diffIn thepatch
), DOM is mounted
Call mounted hook.
In which lifecycle do your interface requests go?
Created, beforeMount, and Mounted work.
Because data has been created in the three hook functions, the data returned by the server side can be assigned. But I’m used to creating in development for the following reasons:
- Server-side data can be retrieved as early as possible
beforeMount
、mounted
Not called during server-side rendering,created
Is called, so creating helps with consistency.
The beforeCreate hook is used for both the forecreate and the forecreate hooks. For both the forecreate and the forecreate hooks, the interface requests are asynchronous.
Answer: If it’s a pure interface request, I think what you’re saying is fine; For this reason, the beforeCreate hook is not stable enough for both applications and for both applications. For this reason, the beforeCreate hook is not stable enough for both applications and for both applications. For this reason, the beforeCreate hook is not stable for both applications and for both applications.
Response system based on publish and subscribe model
What is the publish subscribe model? View the publish/subscribe mode mind map in the lower left corner of the Vue source mind map
Why is data in a component a function?
In the official document: data is of type Object | Function, but gives a limit: the definition of the component only accept Function.
My simple answer: objects that are not returned by a function. Multiple instances of the same component share the same data object, and there is a danger of reference types.
Official answer: When a component is defined, data must be declared as a function that returns an initial data object, because the component may be used to create multiple instances. If data were still a pure object, all instances would share references to the same data object! By providing the data function, each time a new instance is created, we can call the data function to return a new copy of the original data.
Parse (json.stringify (…)) by passing vm.$data to json.parse (json.stringify (…)) ) to get a deep copy of the original data object.
Talk about Vue’s bidirectional data binding
According to the mind map of Vue source code, it can be found that the responsibility of Vue data occurs between beforeCreate and created
Response principle
The main principle is based on the ES5 API: Object.defineProperty lets data’s property have getTer&setters and is therefore responsive
function initData (vm: Component) {
let data = vm.$options.data
// 1. Delegate each attribute to the instance VM
// proxy data on instance
const keys = Object.keys(data)
let i = keys.length
while (i--) {
proxy(vm, `_data`, key)
}
// Observe data
// observe data
observe(data, true /* asRootData */)}export function observe (value: any) {
Object.keys(value).forEach((key) = > defineReactive(value, key, value[key]))
}
/** * Define a reactive property on an Object. */
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function, shallow? : boolean) {
// Only configurable descriptors can be modified
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if((! getter || setter) &&arguments.length === 2) {
val = obj[key]
}
// Redefine the property so that it has getters & setters
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if(newVal === value || (newVal ! == newVal && value ! == value)) {return
}
// #7981: for accessor properties without setter
if(getter && ! setter)return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
}
})
}
Copy the code
Depend on the collection
What is the process of dependency collection? Who collects whom? How is it collected? What is the purpose of the collection?
Brain jump out so many questions, then I rely on these questions one by one to find out the answer, let’s go!
$mount(‘#app’) $mount(‘#app’)
Why? follow me.
$mount(‘#app’) ¶ $mount(‘#app’) ¶
Vue.prototype.$mount = function (el) {
const vm = this;
// ...
callHook(vm, 'beforeMount')
// ...
// Core point: create a render 'Watcher' here
new Watcher(vm, () = > {
// 1. Call render to return a virtual node
const vnode = vm._render();
// 2. Call patch to generate a real DOM
// Render the real DOM, using the data for template padding
// When data is used, it will be monitored by the listener 'Observer'
// This triggers the 'getter' of data's property
vm.__patch__(vm.$el, vnode);
})
// ...
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')}// ...
}
Copy the code
Monitor the Observer
Observer mentioned above, what is this stuff? Why didn’t I mention it before?
Ok… An Observer should be created using the observe method in the principle of Responsiveness above.
export function observe (value: any, asRootData: ? boolean) :Observer | void {
let ob: Observer | void
// ...
// This is where the listener is created
ob = new Observer(value)
// ...
return ob
}
Copy the code
Let’s look at the listener Observer class:
export class Observer {
value: any;
constructor (value: any) {
this.value = value
// 1. Assign a listener instance to the object
def(value, '__ob__'.this)
// 2. Make an in-depth observation of the data
// Here are some prototype methods for data that are copied
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
/** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/** * Observe a list of Array items. */
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
Copy the code
So what exactly is going on with renderWatcher?
Observer Watcher
class Watcher {
value: any;
getter: Function;
constructor (
vm: Component,
expOrFn: string | Function
) {
this.vm = vm
// The getter is a render function
this.getter = expOrFn
this.value = this.get()
}
/** * Evaluate the getter, and re-collect dependencies. */
get () {
let value;
// Push current render 'Watcher' to global 'dep.target'
pushTarget(this)
// After executing the render function, the Watcher can be observed by firing property.getter, which is the instance of the current Watcher. The publisher of the property (Dep) collects the Watcher (dep.target)
value = this.getter();
// Pop up the current 'Watcher'
popTarget()
return value
}
}
Copy the code
The publisher Dep
You see Dep in the Watcher class, so where is the publisher defined? Where did you come from??
Ok… Dep is created when the reactive attribute defineReactive is defined. Here we add:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function, shallow? : boolean) {
// 1. A publisher is created here
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.// 2. Collect dependencies in 'getter'
get: function reactiveGetter () {
if (Dep.target) {
dep.depend()
}
},
// 3. Notify the observer of the update when 'setter'
set: function reactiveSetter (newVal) {
dep.notify()
}
})
}
Copy the code
Conclusion:
-
What is the process of dependency collection?
The first collection occurs during render, and the rendering function’s acquisition of the data is intercepted by the listener Observer
For example, if there is a property name, render the value of name
A listener Observer intercepts the fetch operation and fires the getter for the name property
Getter collects dependencies on the current observer: dep.depend() ->dep.addSub(watcher)
-
Who collects whom?
Three characters are mentioned in the collection:
- The listener
Observer
- The observer
Watcher
- The publisher
Dep
Conclusion based on the first point: the publisher Dep collects the Watcher
- The listener
And so on… So what’s the purpose of the collection?
Look down…
Data update notification
<template> <div> My name is {{name}}. </div> </template> <script> export default { data() { return { name: '? ' } }, created() { setTimeout(() => { this.name = 'Yu'; }, 2000); } } </script>Copy the code
In the component above, there is a property value name, and a hook event: after 2s, update name with the value Yu
At first render, the string template render gets the name property, is heard by the name property listener, fires the getter, and then the publisher DEP relies on the current observer Watcher collection.
After 2s, the name property is updated, which is also heard by the listener for the name property, but fires the setter, as mentioned above:
// 2. Notify the observer of the update when 'setter'
set: function reactiveSetter (newVal) {
dep.notify()
}
Copy the code
class Dep {
subs: Array<Watcher>;
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Copy the code
So, here’s the answer:
What the setter does is the publisher notifies collected observers of updates, namely: watcher.update()
This enters the data update process
Data update process
Here is the update function, the main logic:
- Data updates are not lazy
lazy
If yes, the data is marked as dirty data. When the data is retrieved, it is recalculated based on whether the data is dirty. All I know is the calculated propertiescomputed
. - Are data updates synchronized
sync
If yes, perform the update immediatelywatcher.run()
- Otherwise, updates are pushed to an asynchronous update queue, followed by uniform updates
class Watcher {
/** * Subscriber interface. * Will be called when a dependency changes. */
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)}}}Copy the code
The whole process roughly look at the Vue source mind map data update process
I won’t go into detail here.
The important thing here is asynchronous update queues, you can go to an asynchronous update queue
Asynchronous update queue
/** * Pushes the observer into the observer queue. * Watcher with a duplicate ID will be skipped * pushed when the queue is refreshed. * /
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// Watcher with duplicate ID is ignored
if (has[id] == null) {
has[id] = true
// If there is no flushing, the queue has not been emptied
// Add the task watcher to the queue
if(! flushing) { queue.push(watcher) }else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1.0, watcher)
}
// When not waiting, start the flush queue, whoooo ~ ~ ~
// Only open once
//
// queue the flush
if(! waiting) { waiting =true
if(process.env.NODE_ENV ! = ='production' && !config.async) {
flushSchedulerQueue()
return
}
// This is asynchronous.
// A new watcher is added to the queue before it is actually flushed,
// Until all synchronized code has been executed and there are no new Watcher, the flush really starts
nextTick(flushSchedulerQueue)
}
}
}
Copy the code
Rinse the queue
This part of the code will be relatively simple, which is to iterate to execute the run method of each Watcher, recalculate the new value, execute the CB function of watch, and manage the execution status of watcher to ensure that each Watcher will be executed only once.
function flushSchedulerQueue () {
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
// Execute the observer's run method
watcher.run()
}
}
Copy the code
class Watcher {
run () {
if (this.active) {
const value = this.get()
if( value ! = =this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
}
Copy the code
nextTick
A deferred callback is performed after the end of the next DOM update loop, and nextTick is used to retrieve the updated DOM immediately after the data is modified.
NextTick can be used in two ways
- Pass the
cb
The function callback does not return onePromise
- No cb callback is passed, but one is returned
Promise
Source code first:
export function nextTick (cb? :Function, ctx? :Object) {
// Promise's resolve controller
let _resolve
// Rewrap the passed 'cb' or '_resolve', depending on the parameters
callbacks.push(() = > {
// If there is a CB callback, try to call it
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')}}else if (_resolve) {
// If not, return a 'Promise'
_resolve(ctx)
}
})
// The purpose of this code is to ensure that an update is only started once, not multiple times
if(! pending) { pending =true
// an asynchronous scheme that returns the final result based on browser differences
timerFunc()
}
// If the cb parameter is passed and the client has a 'Promise' object, return a 'Promise'
// This is the second way to use it
// $flow-disable-line
if(! cb &&typeof Promise! = ='undefined') {
return new Promise(resolve= > {
_resolve = resolve
})
}
}
Copy the code
NextTick mainly uses macro and micro tasks
NextTick: timerFunc (timerFunc);
This function mainly tries to adopt according to the execution environment:
Promise, MutationObserver, setImmediate
If all else fails, setTimeout defines an asynchronous method,
Multiple calls to nextTick queue the method, and the current queue is emptied by this asynchronous method.
Conclusion: Ok, the whole process of data update is like this, if there are any questions or important details missing, please feedback ~
Related articles
Mp.weixin.qq.com/s/y9olkntgR…