vue design pattern

I have written three vue analysis before. The one that follows the source code order is a bit messy. After reading a lot of information, I have compiled a version.

First, let's talk about the source code structure, the source code is under src

src
├── compiler        # 编译相关 
├── core            # 核心代码 
├── platforms       # 不同平台的支持
├── server          # 服务端渲染
├── sfc             # .vue 文件解析
├── shared          # 共享代码

compiler

The compiler directory contains all the code related to Vue.js compilation. It includes functions such as parsing the template into an ast syntax tree, ast syntax tree optimization, and code generation.

Compilation can be done at build time (with the help of auxiliary plug-ins such as webpack and vue-loader); it can also be done at runtime, using Vue.js that includes the build function. Obviously, compilation is a performance-consuming task, so the former is more recommended-offline compilation.

core

The core directory contains the core code of Vue.js, including built-in components, global API packaging, Vue instantiation, observers, virtual DOM, utility functions, and so on.

The code here is the soul of Vue.js, and it is also the place we need to focus on later.

platform

Vue.js is a cross-platform MVVM framework. It can run on the web or on the native client with weex. The platform is the entrance of Vue.js, and the 2 directories represent the 2 main entrances, which are packaged into Vue.js running on the web and weex respectively.
Our main target is the web side, weex can see for yourself if you are interested

server

Vue.js 2.0 supports server-side rendering, and all logic related to server-side rendering is in this directory. Note: This part of the code is Node.js running on the server side, and should not be confused with Vue.js running on the browser side.

The main job of server-side rendering is to render components into server-side HTML strings, send them directly to the browser, and finally "mix" static tags into a fully interactive application on the client.

sfc

Usually we develop Vue.js with the help of webpack to build, and then write components through a single .vue file.
The code logic in this directory will parse the content of the .vue file into a JavaScript object.

shared

Vue.js will define some tool methods. The tool methods defined here will be shared by Vue.js on the browser side and Vue.js on the server side.

It can be seen from the catalog design of Vue.js that the author splits the functional modules very clearly, the related logic is maintained in an independent catalog, and the reused code is also extracted into an independent catalog.
This kind of catalog design makes the code readability and maintainability stronger, and it is very worth learning.


Preparation for commissioning

First of all, here is annotated a copy of the source code of vue2.6, you can download it and follow along

vue2.6 source code load address

  1. Open the setting in vscode and temporarily set javascript.validate.enable to false, do not check the syntax of javascript, prevent flow from reporting errors, flow is similar to typescript and used for type detection.
  2. Part of the code in the source code is not highlighted. vscode downloads a plug-in, and Babel javascript opens the others to be highlighted.
  3. Then you can install a global package, serve, and then run serve in the examples directory, start this directory as a static server, and then put all the vue test examples in the file, and then change the vue address of src to ../ ../dist/vue.js", this is the vue constructed after we changed the command below, so that we can freely interrupt debugging.

Let's change the scripts configuration of the vue source code package.json to facilitate subsequent debugging
The dev command was changed to

"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"

Turn on hot reload -w -c Set configuration file Turn on sourcemap Set environment variables

Find scripts/configs.js, search for the corresponding environment variable value web-full-dev, find the corresponding entry src/platforms/web/entry-runtime-with-compiler.js
What we are debugging here is the full amount of vue, vue-cli is the runtime version, the full version can better debug all the code of vue.

Because of the vue source code, there are jumping around, it is difficult to string together the logic for the first time, and here is a general mind map.
vue渲染流程 (1).png
There are 3 key places for watcher creation in the above picture, and there is also a corresponding mind map here
watcher (4).png
watcher diagram
image.png
Note: From the watcher being notified by dep.notify() and then executing watcher.update() we can see that the calculated watcher.update is only dirty=true to mark the cache invalidation, and the dependent value is updated. It will not execute queueWatcher( this) Put the current calculated watcher into the queue to update the watcher.
When the user watcher and rendering watcher execute update, they will basically execute queueWatcher(this) and put the current watcher in the queue, because the calculation watcher is cached.
Need to pay attention here.

The unreduced version of the map is too big and there are too many steps. Here is a reduced version of the mind map.

![Uploading...]() 首次渲染过程 (1).png


Start from the entrance

We found the final definition of Vue according to my mind map and found that Vue is a constructor. It is actually a class implemented with Function. We can only instantiate it through new Vue.

Some students can't help but ask, why doesn't Vue use ES6 Class to implement it? We will see later that there are a lot of xxxMixin function calls, and Vue is passed in as a parameter. Their functions are to extend some methods to the Vue prototype (the specific details will be introduced in a later article, and will not be expanded here), Vue distributes these extensions into multiple modules to implement them according to their functions, instead of implementing them all in one module. This method is difficult to implement with Class. The advantage of this is that it is very convenient to maintain and manage the code, and this programming skill is also worth learning.

Data driven

So when we actually use vue, we will call new Vue() like this. According to the source code, we see that the initialization function this._init() is actually called. According to my mind map and the source code, you will find that Vue is initialized. Init mainly does several things, merging configuration, initializing life cycle, initializing event center, initializing rendering, initializing data, props, computed, watcher, etc.

The initialization logic of Vue is written very clearly. The different functional logic is divided into separate functions for execution, so that the main line logic is clear at a glance. This kind of programming idea is very worthy of reference and learning.

Vue instance mount

In Vue, we use the $mount instance method to mount the vm. The $mount method is defined in multiple files, such as src/platform/web/entry-runtime-with-compiler.js, src/platform/web/ runtime/index.js, src/platform/weex/runtime/index.js. Because the implementation of the $mount method is related to the platform and the construction method. Next, we will focus on the $mount implementation with the compiler version, because aside from webpack's vue-loader, we analyze the working principle of Vue in a pure front-end browser environment, which will help us deepen our understanding of the principle.

The compiler version of the $mount implementation is very interesting. Let’s take a look at the definition in the src/platform/web/entry-runtime-with-compiler.js file:

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  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
    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')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        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)
}

This code first caches the $mount method on the prototype, and then redefines the method. Let's analyze this code first. First, it restricts el, Vue cannot be mounted on root nodes such as body and html. The next is the key logic-if the render method is not defined, the el or template string will be converted to the render method. Here we must keep in mind that in the Vue 2.0 version, the rendering of all Vue components ultimately requires the render method. Whether we develop the component in a single file .vue way, or write the el or template attribute, it will eventually be converted to the render method. Then this process is an "online compilation" process of Vue, which is implemented by calling the compileToFunctions method. The compilation process will be introduced later. Finally, call the $mount method on the original prototype to mount.

The $mount method on the original prototype is defined in src/platform/web/runtime/index.js. The reason for this design is entirely for reuse, because it can be used directly by the runtime only (runtime) version of Vue.

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

Finally, mountComponent will be called, src/core/instance/lifecycle.js

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  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 {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  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._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) {
        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
}

As you can see from the above code, the core of mountComponent is to instantiate a render Watcher (it is in the mind map), and call the updateComponent method in its callback function. In this method, call the vm._render method to generate a virtual Node. Finally, vm._update is called to update the DOM.

Watcher plays two roles here, one is to execute the callback function when it is initialized, and the other is to execute the callback function when the monitored data in the vm instance changes.

When the function finally judges as the root node, set vm._isMounted to true, indicating that the instance has been mounted, and the mounted hook function is executed at the same time. Note here that vm.$vnode represents the parent virtual Node of the Vue instance, so if it is Null, it means that it is currently the root Vue instance.

The logic of the mountComponent method is also very clear. It will complete the entire rendering work. Next, we will focus on analyzing the details, which are the two core methods: vm._render and vm._update.


Charon
57 声望16 粉丝

世界核平