Content abstract

Can you describe the rendering mechanics of Vue? While this is a well-asked question, it really tests the candidate’s technical depth and understanding of the framework. To answer this question, you need to understand the rendering mechanism of Vue. This article focuses on a simple example. The rendering mechanism is introduced based on the relationships among the four objects Watcher(observer), Dep(dependent object), Scheduler(Scheduler) and Component.

Write a simple component

The following demo is a basic information editing component that defines several attributes using Data, listens for attribute changes using Watch, and listens for multiple attributes using computed. When we modify the table content, the basic information on the right side of the screen will also be updated.

<template> <div class="info-editor"> <div class="info-editor-form"> <div class="info-editor-item"> <span> Name </span> <input V-model ="name" /> </div> <div class="info-editor-item"> <span> <input V-model ="phone" /> </div> <div Class ="info-editor-item"> <span> <span> <input V-model ="addr" /> </div> </div> <div class="info-editor-view"> <div>{{message}}</div> </div> </div> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ Data () {return {name: 'li lei ', addr:' Beijing Wudaokou '}}, watch: {phone: function phoneChange(value) { if (! value || value.length ! == 11) {console.log(' Wrong phone number format.')}}}, computed: {message: function message() { return `${this.name},${this.phone},${this.addr}` } } }) </script>Copy the code

Initialize the

The _init function directly hangs on the prototype of the Vue. This function will initialize the life cycle, render, state, etc. The familiar life cycle events such as beforeCreate and created will be triggered during initialization. InitState and vm $mount (vm. $options. El).

Vue.prototype._init = function (options? : Object) { ... vm._self = 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') ... if (vm.$options.el) { vm.$mount(vm.$options.el) } }Copy the code

The initState function initializes the prop, data, Watch, computed properties we defined in the component, and $mount initializes the rendering process and triggers the first rendering. InitProps, initMethod, and initData get the default values and bind the corresponding properties to this. For example, we define the name property in data. Through initData, the user can access this. Here we focus on the initComputed and initWatch functions. These two functions were introduced in Vue’s Introduction to Watcher and Scheduler, so we’ll review them here.

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 {// Convert the _data attribute to an Observer. true /* asRootData */) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

The initComputed function iterates through all the calculated attributes defined by the user and creates a Watcher object for each attribute, passing four parameters when instantiating watcher, with VM representing the component itself; The getter represents the value of a property, such as the message calculation property defined in Demo. The getter is the function function message() that obtains the value. The third argument is a callback function, which is represented by an empty function noop because no callback is needed to evaluate the property. The last one is an optional argument to the Watcher constructor. Only the calculated properties that create the Watcher are labeled lazy. We’ll talk about what that does when we talk about Watcher objects.

function initComputed (vm: Component, computed: Object) { const watchers = vm._computedWatchers = Object.create(null) for (const key in computed) { const userDef = Computed [key] // User-defined performing functions may be of the form {get: function() {}} const getter = typeof userDef === 'function'? userDef : UserDef. Get / / for the user to define each computed properties create watcher object which [key] = new watcher (vm, getter | | it, Function message() noop, {lazy: true})}... }Copy the code

InitWatch initializes the component’s custom listener in the watch property. The function iterates over the watch property, where key is the component’s defined “phone” property. $watch(expOrFn, handler, options), vM is the component itself, expOrFn is “phone “string, Handler defines the phoneChange function for us.

function initWatch (vm: Component, watch: Object) {
    for (const key in watch) {
      const handler = watch[key]
      if (Array.isArray(handler)) {
        for (let i = 0; i < handler.length; i++) {
          createWatcher(vm, key, handler[i])
        }
      } else {
        createWatcher(vm, key, handler)
      }
    }
  }
Copy the code

Observe (data, true /* asRootData */) in initData to convert a data object into an Observer. A Watcher is created whenever there is a place to read a property in data. For example, for phone, the <input V-model =”phone” /> in template and the message function return phone, as well as the phone listener in Watch, All read the Phone property in data, so three Watcher objects are generated.

<input v-model="phone" />

computed: {
    message: function message() {
      return `${this.name},${this.phone},${this.addr}`
    }
}
Copy the code

Observe converts phone defineProperty to {get, set}. In get, check whether dep. target is empty or not. Dep.target is the corresponding Watcher. For example, a watch listener on a phone generates a Watcher. Dep. Depend appends the Watcher to subs. When we reset the phone value, for example via input, the set function will be triggered, and dep.notify will be called to trigger each watcher’s update function to perform the notification.

export function defineReactive (
    obj: Object,
    key: string,
    val: any
  ) {
    ...
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        const value = getter ? getter.call(obj) : val
        if (Dep.target) {
            dep.depend()
            ...
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        ...
        dep.notify()
      }
    })
  }
Copy the code

Begin to render

$mount function definition

A Vue project typically instantiates the Vue object in the main.js function. After the constructor passes in the render function, the $mount function is called to initiate rendering.

new Vue({
    render: h => h(App),
  }).$mount('#app')
Copy the code

The $mount function converts el to a Dom(e.g., using selector to find the Dom element corresponding to #app). The attached Dom element cannot be body or documentElement. Lines 18 to 48 query the template string. The innerHTML content is retrieved using the idToTemplate function. If template is empty, the EL’s outerHTML is assigned directly to the template.

const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el? : string | Element, hydrating? : Boolean): Component {el = EL && Query (el) // If el is body or documentElement, throw a warning that Vue is not allowed to attach Vue to those elements. 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 // Convert template to render function if (! options.render) { let template = options.template if (template) { if (typeof template === 'string') { // Such as $mount (' # app), Get the innerHTML string of the element 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')} // Convert render templates to render functions, and compileToFunctions parse templates with options 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', Return mount. Call (this, el, hydrating)}Copy the code

The template string is then converted to fetch by calling compileToFunctions, which first translates the template to the Abstract Syntax Tree (AST) and then generates an anonymous fetch from the AST. All template nodes correspond to a _c function that creates the corresponding tag(such as div) to generate the virtual node vNode. For example, if the input element has attributes defined in data, such as name and phone, the _c function will fetch values from _vm.name, _vm.phone, and _vm.addr, which will hit the property’s get function. The get function calls dep.depend() to register the observer with the observation target. The observer here refers to the Render Watcher(mountComponent function created, we can call it Render Watcher). The object to observe is the deP corresponding to the attributes name, phone, addr. Render Watcher is appended to the DEP of all properties, so that when the property is updated via the set function, dep.notify is called to tell Render Watcher to rerender.

/ / rendering anonymous function function and _render () {var _vm = this var _h = _vm $createElement method var _c = _vm. Love. _c | | _h return _c (" div ", { staticClass: "info-editor" }, [ _c("div", { staticClass: "info-editor-form" }, [ _c("div", { staticClass: "The info - editor - item"}, [_c (" span ", [_vm. _v (" name ")]), _c (" input ", {directives: [{name: "model," rawName: "v-model", value: _vm.name, expression: "name", }, ], domProps: { value: _vm.name }, on: { input: function ($event) { if ($event.target.composing) { return } _vm.name = $event.target.value }, }, }), ]), _c("div", {staticClass: "info - editor - item"}, [_c (" span ", [_vm. _v (" phone ")]), _c (" input ", {directives: [{name: "model", rawName: "v-model", value: _vm.phone, expression: "phone", }, ], domProps: { value: _vm.phone }, on: { input: function ($event) { if ($event.target.composing) { return } _vm.phone = $event.target.value }, }, }), ]), _c("div", {staticClass: "info - editor - item"}, [_c (" span ", [_vm. _v (" address ")]), _c (" input ", {directives: [{name: "model", rawName: "v-model", value: _vm.addr, expression: "addr", }, ], domProps: { value: _vm.addr }, on: { input: function ($event) { if ($event.target.composing) { return } _vm.addr = $event.target.value }, }, }), ]), ]), _c("div", { staticClass: "info-editor-view" }, [ _c("div", [_vm._v(_vm._s(_vm.message))]), ]), ]) }Copy the code

Render Watcher is introduced here, but it also mentions when it was created. Mount review $mount function, the final behavior. The call (this, el, hydrating), the function of the Vue initially defined $mount function, defined in SRC/platforms/web/runtime/index. The js file.

const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el? : string | Element, hydrating? : boolean ): Component { ... mount.call(this, el, hydrating) }Copy the code

Then we look at the SRC/platforms/web/runtime/index in the js file $mount function is how to define, first of all determine whether the current browser environment, converts el Dom elements, the last line calls the mountComponenta function.

// public mount method Vue.prototype.$mount = function ( el? : string | Element, hydrating? : boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }Copy the code

MountComponent first triggers beforeMount hook events. We can define beforeMount events in components that are automatically triggered during component rendering. The updateComponent function defined within the function will call _render and _update to render and update, bringing the latest value (for example, name changed from Li Lei to Han Meimei) to the interface.

Next to instantiate the Watcher object, first review the Watcher the constructor of the constructor (vm: Component, expOrFn: string | Function, cb: the Function, the options? : Object, isRenderWatcher? New Watcher: Boolean), contains 5 parameters.

  1. Vm: the VM corresponds to the component itself.
  2. ExpOrFn: expOrFn can be a string or a function, for example, “A.B.C” in watch listening format, updateComponent corresponds to expOrFn;
  3. Cb: Updated callback function, noop passed in here is empty function;
  4. Options: Optional arguments. The before property passed in here is called before Watcher runs.
  5. IsRenderWatcher: Whether or not it is a render Watcher, the internal logic of the Watcher will do special processing for the render Watcher. This parameter is passed in as true, indicating that the Watcher created in the mountComponent function is a render Watcher.
@param {*} vm * @param {*} el * @param {*} hydrating * @returns */ export function mountComponent ( vm: Component, el: ? Element, hydrating? : boolean ): Component { vm.$el = el ... CallHook (VM, 'beforeMount') let updateComponent = () => {//_render function performs rendering to generate a new virtual node vnode, The _update function updates the original PrevNode in conjunction with the __Patch__ patch algorithm, and eventually to the Dom element. Vm._update (vm._render(), hydrating)} // New Watcher(vm, updateComponent, noop, Before () {if (vm._isMounted &&! Vm._isdestroyed) {callHook(vm, 'beforeUpdate')}} isRenderWatcher $forceUpdate will trigger watcher.update}, true /* isRenderWatcher */) return vm}Copy the code

Trigger render and update

So far we only know that the updateComponent function performs the render and update functions, but when updateComponent will be triggered is unknown. The mountComponent function instantiates the Render Watcher, so let’s see what logic is included in the new Watcher. First, isRenderWatcher is used to determine whether the current watcher is a rendering watcher. If it is, vm._watcher is assigned to it. After the component is created, vm._watcher can be used to trigger re-rendering. The watcher created within the component is appended to the Vm. _Watchers array to compile subsequent batch operations, such as $destroy to batch unregister all watchers.

Getters are generally chained representations of values (such as A.B.C), or functions that get values, such as the message function that evaluates properties defined in the demo. But render Watcher calls the updateComponent function that triggers the render and update as the getter, which is a clever part of Vue’s design, as we’ll explain later.

This is the last line of code to get the latest value for this.value. As mentioned earlier, only the lazy of the property watcher will be true.

export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options? : Object, isRenderWatcher? : Boolean) {this.vm = vm if (isRenderWatcher) {// Attach the current Watcher to vm._watcher. $forceUpdate will use _watcher vm._watcher = this} // component to create watcher in the _Watchers queue, for example, $destroy will destroy it. vm._watchers.push(this) ... If (typeof expOrFn === 'function') {this.getter = expOrFn} else {// expOrFn === 'function') For example, to convert 'A.B.C' to a function getValue this.getter = parsePath(expOrFn) if (! Getter) {this.getter = noop}} // Trigger get function this.value = this.lazy? undefined : this.get() } }Copy the code

The last line of the Watcher constructor calls the get function, appends the current Render Watcher to the global DEP. target via pushTarget, and then calls the getter to get the latest value. The Render Watcher getter is the updateComponent function, so the updateComponent function will be executed at this point.

/** * Get () {// Append the current Watcher to the global dep.target, PushTarget (this) let value const vm = this.vm try {// Execute getter to read value value = this.gett. call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, 'Getter for watcher "${this.expression}"')} else {throw e}} finally {// If deep is true, // The deP for all nested attributes is appended to the current watcher, and the DEP for all child attributes is recursively iterated through all nested attributes and fires their getters from push(dep.target) if (this.deep) {// Traverse (value)} // exit stack popTarget() // cleanupDeps dependency this.cleanupdeps ()} return value}Copy the code

Inside updateComponent, vm._render() is called to generate virtual nodes for the template, and c_ is called inside _render to generate vNodes for each template tag, For example, div[class=’info-editor-view’] defined in the template generates the corresponding virtual node vNode. Vm. _render executes and returns the template root vnode, the virtual node corresponding to <div class=”info-editor”>. UpdateComponent then calls the _update function to generate the final real DOM element from the virtual node. Of course, the __patch__ patch function is called inside the _update function to do diff updates to ensure minimal performance overhead.

_c("div", { staticClass: "Info - editor - view"}, [_c (" div ", [_vm. _v (_vm. _s (_vm. Message))]),])/vm / _c definition. _c = (a, b, c, d) = > createElement method (vm, a, b, c, d, false)Copy the code

Call (vm, vm) to execute the getter function. The getter for Watcher is updateComponent, so _render is triggered. _vm.message and _vm.name are used internally to get attributes. As mentioned earlier, in the Vue component, attributes defined by us will be converted to {get,set} mode, so calling _vm.message will trigger its get function, which will call dep.depend(). Depnd function appends depnd. Target (Render Watcher) to the Watcher list it maintains. In this way, when _render is finished executing, the DEP of all properties in the component will be registered with Render Watcher. As soon as the property is updated, set is triggered, and dep.notify() is called to tell Render Watcher to update, triggering rerendering.The get function determines this.deep and, if true, recursively iterates through the nested properties of all values and fires its getter to append its corresponding DEP to the current Watcher. For example, in data we define extra: {city: ‘wuhan ‘, code: ‘1000’}, when we monitor extra in watch, the generated Watcher will also be attached to the DEP corresponding to city and code attributes, so that watch can also monitor when extra. City changes.

The Watcher entity itself maintains the dependencies attached during pushTarget and PopTarget with the newDeps list, then calls cleanupDeps to cleanup the DEPS list (dependencies from the previous version) and assigns the new dependency newDeps to the DEps, This wraps up the newly collected dependencies.

To render

In addition to triggering a rendering during initialization, sometimes we call $forceUpdate to trigger a re-rendering. This function calls the vm._watcher. Update method. And bind to vm with vm._watcher = this in the constructor of Watcher. So what does the _watcher update function do?

Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
      vm._watcher.update()
    }
  }
Copy the code

The Watcher update function evaluates three scenarios. If the Watcher attribute is calculated, lazy is true. In a synchronous scenario, the run function is called directly to get the latest value and notify the cb callback. Otherwise, the queueWatcher function is called to create the run function for microtasks to execute the Watcher list in batches, notifyupdates. Run and queueWatcher are described in detail in Vue’s Watcher and Scheduler principles. We only need to remember that run calls the getter function passed in by the constructor to get the latest value. Its getter is the updateComponent function, which calls _render and _update internally to render and update, so that the purpose of re-rendering is done.

update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
Copy the code

conclusion

With a simple demo, this article shows how to create Render Watcher, Computed Watcher, and Watch Watcher for components during initialization, When we create the Render Watcher, we attach it to the DEP of the various properties that we rely on in the template, and then fire the getter(the updateComponent function passed in) to perform the Render and update. When $forceUpdate is called in the program, the updateComponent of Render Watcher will be executed to implement the re-rendering.

This article mentions rendering and patch update, but not details, the next topic will introduce how Vue generates virtual node vnode based on template, and how Vnode implements local UPDATE of DOM based on patch algorithm, waiting… .

Write in the last

If you have other questions can be directly message, discuss together! Recently I will continue to update Vue source code introduction, front-end algorithm series, interested can continue to pay attention to.