Vue JS source code analysis - responsive principle (19)

Vue source code analysis - responsive principle

Welcome to the personal website: blog 5coder. cn

Course objectives

  • Vue.js static member and instance member initialization process
    • vue.use(),vue.set(),vue. The creation process of these global members, such as extended()
    • vm. e l , ' v m . el,`vm. el,‘vm.data,vm.$on,vm.$ The creation process of these instance members such as Mount
  • First render pass
    • After the vue instance is created and the data is transferred to the vue, how the data is rendered to the page inside the vue is based on this process in the subsequent analysis of the source code
  • Data responsive principle (core)

preparation

Acquisition of Vue source code

  • Project address: https://github.com/vuejs/vue
  • A Fork is sent to its own warehouse and cloned locally. You can write your own comments and submit them to github
  • Why analyze vue2 six
    • So far vue3 The official version of 0 has not been released yet
    • After the new version is released, existing projects will not be upgraded to 3.0, 2.0 X has a long transition period
    • 3.0 project address: https://github.com/vuejs/vue-next

Source directory structure

vue
    ├─dist  The packaged results contain different versions
    ├─examples  Examples
    ├─flow  
    ├─packages
    ├─scripts
    ├─src
        ├─compiler  Compile related (convert template to render Function, render The function creates a virtual DOM)
        ├─core  Vue Core library
        	├─components  definition vue Self contained keep-alive assembly
        	├─global-api  definition vue Static methods in, including vue.component(),vue.filter(),vue.extend()etc.
        	├─instance  establish vue Example location, definition vue Constructor for and vue Initialization and life cycle response function of
        	├─observer  Location of responsive mechanism implementation
        	├─utils  Public member
        	├─vdom  fictitious DOM,vue Virtual in DOM Rewritten snabbdom,Added component form
        ├─platforms Platform related code
        	├─web  web Relevant codes under the platform
                ├─compiler
                ├─runtime
                ├─server
                ├─util
                ├─entry-compiler.js  Package entry file
                ├─entry-runtime.js  Package entry file
                ├─entry-runtime-with-compiler.js  Package entry file
                ├─entry-server-basic-renderer.js  Package entry file
                ├─entry-server-renderer.js  Package entry file
        	├─weex  week Relevant codes under the platform( week yes vue (based on the framework developed under the mobile terminal)
        ├─server SSR,Server rendering
        ├─sfc .vue File compiled as js Object( Single File Component Single file component)
        └─shared Public code

Understand Flow

  • Official website: https://flow.org/
  • Static type checker for JavaScript
  • The static type check error of Flow is realized through static type inference
    • The beginning of the file is declared by / / @ flow or / * @ flow * /

Debug settings

pack
  • Packaging tool Rollup

    • vue.js source code packaging tool uses Rollup, which is cooler than webpack
    • Webpack treats all files as modules, and Rollup only processes js files, which is more suitable for Vue js
    • Rollup packaging does not generate redundant code
  • Installation dependency

    yarn
    
  • Set sourcemap

    • package. Add a parameter -- sourcemap to the dev script of the script in the JSON file

      "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:webfull-dev"
      
  • Execute dev

    • Delete the dist directory before packaging, and rollup will automatically generate the dist directory

    • yarn dev performs packaging with rollup, and the - w parameter is firm and stable. The file changes are repackaged automatically

    • result

debugging
  • Vue. Is introduced into the examples Min.js, change it to Vue js

  • Open source in Chrome's debugging tool

Different component versions of Vue

  • Run yarn build to repackage all files

  • Official documents- Explanation of different builds

  • dist\README.md

    UMDCommonJSES Module
    Fullvue.jsvue.common.jsvue.esm.js
    Runtime-onlyvue.runtime.jsvue.common.min.jsvue.esm.min.js
    Full(Production)vue.min.js
    Runtime-only(Production)vue.runtime.min.js
term
  • Full version: contains both compiler and runtime versions
  • Compiler: code used to compile template strings into JavaScript rendering functions, which is bulky and inefficient
  • Runtime: code used to create Vue instances, render and process virtual DOM, etc., with small volume and high efficiency. It's basically removing the compiler's code
  • UMD : UMD version is a general module version and supports multiple module modes. vue.js default file is the UMD version of the runtime + compiler
  • CommonJS (cjs): common JS version is used to cooperate with old packaging tools, such as Browserify or webpack 1
  • ES Module : starting from 2.6, Vue will provide two ES Module(ESM) build robust versions for modern packaging tools
Runtime + Compiler VS Runtime-only
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>

</head>
<body>
<div id="app">
  Hello World
</div>

<script src="../../dist/vue.js"></script>
<script>
  // Compiler
  // The compiler is required to convert the template into the render function
  const vm = new Vue({
    el: '#app',
    template: '<h1>{{ msg }}</h1>',
    data: {
      msg: "Hello Vue"
    }
  })
</script>
</body>
</html>

Change vue version to vue runtime. JS, if you find that the browser reports an error, you will be prompted to change to the render function or use compiler included build.

Change the template as follows:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>

</head>
<body>
<div id="app">
  Hello World
</div>

<script src="../../dist/vue.runtime.js"></script>
<script>
  // Compiler
  // The compiler is required to convert the template into the render function
  const vm = new Vue({
    el: '#app',
    // template: '<h1>{{ msg }}</h1>',
    render(h) {
      return h('h1', this.msg)
    },
    data: {
      msg: "Hello Vue"
    }
  })
</script>
</body>
</html>

Viewing vue component versions when creating a project using vue cli

In the project created with vue create projectName, view the build version of Vue. Due to Vue's influence on webpack config. JS is deeply encapsulated, so its configuration file cannot be seen in the directory, but Vue provides a command line to view the configuration file.

vue inspect  # Direct output to console
vue inspect > output.js  # Output the result of executing vue inspect command to output JS file

output.js is not a valid webpack configuration file and cannot be used directly.

You can see that in alias in resolve, vue cli uses vue runtime. esm. JS (runtime version, modularized as ES Module) as the build version, the $symbol in vue $is an exact match, and the import Vue from vue is used directly.

Comparing runtime+compiler with runtime (ast: abstract syntax tree), it can be seen from the following process that runtime only has higher performance.

  • runtime+compiler
    • template -> ast -> render -> vdom ->UI
  • runtime-only
    • render -> vdom -> UI

The above content comes from coderwhy beep beep beep animation section 96 video

Find entry file

  • View dist / Vue JS construction process

Execute build

yarn dev
# "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
# --Environment TARGET: Web Full dev sets the environment variable TARGET
  • script/config.js execution process

    • Role: generate configuration files built by rollup
    • Use the environment variable target = Web Full dev
    // Judge whether there is TARGET in the environment variable
    // Use genConfig() to generate the rullup configuration file, if any
    if (process.env.TARGET) {
        module.exports = genConfig(process.env.TARGET)
    } else {
        // Otherwise, get all configurations
        exports.getBuild = getConfig
        exports.getAllBuilds = () => Objet.keys(builds).map(genconfig)
    }
    
  • genConfig(name)

    • Obtain configuration information according to the environment variable TARGET
    • builds[name] get the information of build configuration
    // Runtime+compiler development build (Browser)
    'web-full-dev': {
        entry: resolve('web/entry-runtime-with-compiler.js'),
        dest: resolve('dist/vue.js'),
        format: 'umd',
        env: 'development',
        alias: { he: './entity-decoder'},
        banner
    },
    
  • resolve()

    • Gets the absolute path of the entry and exit files
    const aliases = require('./alias')
    const resolve = p => {
        // Find the alias in alias according to the first half of the path
        const base = p.split('/')[0]
        if (aliases[base]) {
            return path.resolve(aliases[base], p.splice(base.length + 1))
        } else {
            return path.resolve(__dirname, '../', p)
        }
    }
    

result

  • Put Src / platforms / Web / entry runtime with compiler JS is built as dist / Vue JS, if -- sourcemap is set, Vue. JS will be generated js. Map file
  • Under the src/platform folder, Vue can be built into libraries used on different platforms. At present, there are weex, web, and server-side rendering SSR libraries

Start at the entrance

  • src/platform/web/entry-runtime-with-compiler.js

Solve the following problems by viewing the source code

  • Observe the following code and answer the results output on the page by reading the source code
const vm = new Vue({    el: '#app',    template: '<h3>Hello Template</h3>',    render(h) {        return h('h4', 'Hello Render')    }})
  • Read the source code record

    • el cannot be a body or html tag
    • If there is no render, convert the template to the render function
    • If there is a render method, directly call mount to mount the DOM
    // 1. el cannot be body or htmlif (EL = = = document. Body | El = = = document. Documentelement) {process. Env. Node_env! = = 'production' & & warn (` do not mount Vue to < HTML > or < body > - mount to normal elementsinstead. `) return this}const options = this$ Optionsif (! Options. Render) {/ / 2. Convert template/el to render function...} / / 3 Call the mount method to mount domreturn mount call(this, el, hydrating)
    
    • Debug code
      • Debugging method
    const vm = new Vue({    el: '#app',    template: '<h3>Hello template</h3>',    render (h) {    	return h('h4', 'Hello render')    }})
    

Where is Vue's constructor

Where is Vue's constructor?

Where do Vue instance members / Vue static members come from?

  • src/platform/web/entry-runtime-with-compiler.js refers to '/ runtime/index’

  • src/platform/web/runtime/index.js

    • Set Vue config
    • Set platform related instructions and components
      • Instructions: v-model, v-show
      • Component transition, transition group
    • Set platform related__ patch__ Method (patching method, comparing old and new vnodes)
    • Set the $mount method to mount the DOM
    / install platform runtime directives & components// Register the platform related instructions and components extend(Vue.options.directives, platformDirectives) / / register the instructions v-model and v-showextend(Vue.options.components, platformComponents) / / register the components v-transition and v-transitiongroup. / / install platform patch function. / / if it is a browser environment, it returns patch; otherwise, it returns the NOOP empty function Vue prototype.__ patch__ =  inBrowser ?  patch : noop// public mount methodVue. prototype.$ Mount = function (EL?: string | element, drawing?: Boolean): component {El = EL & & inbrowser? Query (EL): undefined return mountcomponent (this, El, drawing) / / render DOM}
    
    • src/platform/web/runtime/index.js refers to 'core/index'
    • src/core/index.js
      • Defines the static methods of Vue
      • initGlobalAPI(Vue)
    • src/core/index.js refers to '/ instance/index’
    • src/core/instance/index.js
      • Defines the constructor for Vue
    / Not here class The reason is that it is convenient for follow-up Vue Instance mixing into instance members 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')  }  // Call_ init() method this_ Init (options)} / / register vm_ init() method, initialize vminitMixin(Vue) / / register vm's $data/$props/$set/$delete/$watchstateMixin(Vue) / / initialize event related methods / / $on/$once/$off/$emiteventsMixin(Vue) / / initialize life cycle related mixing methods / /_ update/$forceUpdate/$destroylifecycleMixin(Vue) / / mixed with render// $nextTick/_renderrenderMixin(Vue)
    

Four modules to export Vue

  • src/platform/web/entry-runtime-with-compiler. JS (core function: added the function of compilation)
    • web platform related portal
    • Overriding the platform dependent $mount() method
    • In addition to using the $mount method to convert the template string to the render() function, Vue. Com is also defined The compile () method can convert the template string into a render() function
    • Registered Vue The compile () method passes an HTML string and returns the render function
  • src/platform/web/runtime/index.js
    • web platform related
    • Register platform related global instructions: v-model, v-show
    • Register platform related global components: v-transition and v-transition-group
    • Global method:
      • __ patch__: Convert virtual DOM to real DOM
      • $mount: mount method to render DOM to the interface
  • src/core/index.js
    • Platform independent
    • Set the static method of Vue, initGlobalAPI(Vue)
  • src/core/instance/index.js
    • Platform independent
    • Vue constructor is defined and this is called_ Init (options) method
    • Vue is mixed with common instance members

Initialization of Vue

src/core/global-api/index.js

  • Initialize the static method of Vue
/* @flow */

import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  // Initialize Vue Config object
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  // These tools and methods are not considered part of the global API. Don't rely on them unless you are aware of some risks
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }
  // Static method set/delete/nextTick
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  // Make an object responsive
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }
 // Initialize Vue Options object and extend it
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue
  // Set the keep alive component
  extend(Vue.options.components, builtInComponents)

  // Register Vue Use () is used to register the plug-in
  initUse(Vue)
  // Register Vue Mixin() implements blending
  initMixin(Vue)
  // Register Vue Extend () returns the constructor of a component based on the passed in options
  initExtend(Vue)
  // Register Vue directive(),Vue.component(),Vue.filter()
  initAssetRegisters(Vue)
}
  • src/core/global-api/use.js
/* @flow */import { toArray } from '../util/index'export function initUse (Vue: GlobalAPI) {  Vue.use = function (plugin: Function | Object) {    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))    if (installedPlugins.indexOf(plugin) > -1) {      return this    }    // additional parameters / / remove the first element (plugin) from the array, followed by the parameter const args = toArray (arguments, 1) args of the parameter of the install method or plugin Unshift (this) / / insert this(Vue) into the position of the first element if (typeof plugin. Install = = = 'function') {plugin. Install. Apply (plugin, args)} else if (typeof plugin = = = 'function') {plugin. Apply (null, args)} installedplugins push(plugin)    return this  }}
  • src/core/global-api/mixin.js
/* @flow */import { mergeOptions } from '../util/index'export function initMixin (Vue: GlobalAPI) {  Vue.mixin = function (mixin: Object) {    this.options = mergeOptions(this.options, mixin)    return this  }}
  • src/core/global-api/extend.js
/* @flow */

import { ASSET_TYPES } from 'shared/constants'
import { defineComputed, proxy } from '../instance/state'
import { extend, mergeOptions, validateComponentName } from '../util/index'

export function initExtend (Vue: GlobalAPI) {
  /**
   * Each instance constructor, including Vue, has a unique
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheritance and cache them.
   */
  Vue.cid = 0
  let cid = 1

  /**
   * Class inheritance
   */
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    // Constructor for Vue
    const Super = this
    const SuperId = Super.cid
    // The constructor that loads the component from the cache
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      // If it is the development environment, verify the name of the component
      validateComponentName(name)
    }

    // Constructor corresponding to component
    const Sub = function VueComponent (options) {
      // Call - init() initialization
      this._init(options)
    }
    // The prototype is inherited from Vue
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    // Merge options
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
  }
}
  • src/core/global-api/extend.js
/* @flow */

import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'

export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  // Traverse ASSET_TYPE array that defines the response method for Vue
  // ASSET_TYPE includes directive, component and filter
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        // Vue.component('comp', { template: '' })
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          // Convert component configuration to component constructor
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        // Register globally, store resources and assign values
        // this.options['components']['comp'] = definition
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

src/core/instance/index.js

  • Define the constructor for Vue
  • Initialize instance members of Vue
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

// Vue constructor. class is not used here because it is convenient to mix instance members into Vue instances later
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')
  }
  // Call_ init() method
  this._init(options)
}
// Register vm_ init() method to initialize vm
initMixin(Vue)
// Register $data/$props/$set/$delete/$watch of vm
stateMixin(Vue)
// Initialize event related methods
// $on/$once/$off/$emit
eventsMixin(Vue)
// Initialize lifecycle related mashup methods
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)
// Blend render
// $nextTick/_render
renderMixin(Vue)

export default Vue
  • initMixin(Vue)----(src/core/instance/init.js)

    • Initialization_ init() method

      /* @flow */
      
      import config from '../config'
      import { initProxy } from './proxy'
      import { initState } from './state'
      import { initRender } from './render'
      import { initEvents } from './events'
      import { mark, measure } from '../util/perf'
      import { initLifecycle, callHook } from './lifecycle'
      import { initProvide, initInjections } from './inject'
      import { extend, mergeOptions, formatComponentName } from '../util/index'
      
      let uid = 0
      
      export function initMixin (Vue: Class<Component>) {
        // Attach init method to the prototype of vue
        // Merge options / initialization operation
        Vue.prototype._init = function (options?: Object) {
          const vm: Component = this
          // a uid
          vm._uid = uid++
      
          let startTag, endTag
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            startTag = `vue-perf-start:${vm._uid}`
            endTag = `vue-perf-end:${vm._uid}`
            mark(startTag)
          }
      
          // a flag to avoid this being observed
          // If it is a Vue instance, it does not need to be observe d
          vm._isVue = true
          // merge options
          // Merge options
          if (options && options._isComponent) {
            // optimize internal component instantiation
            // since dynamic options merging is pretty slow, and none of the
            // internal component options needs special treatment.
            initInternalComponent(vm, options)
          } else {
            vm.$options = mergeOptions(
              resolveConstructorOptions(vm.constructor),
              options || {},
              vm
            )
          }
          /* istanbul ignore else */
          if (process.env.NODE_ENV !== 'production') {
            initProxy(vm)
          } else {
            vm._renderProxy = vm
          }
          // expose real self
          vm._self = vm
          // Vm
          // Initialization of vm lifecycle related variables
          // $children/$parent/$root/$refs
          initLifecycle(vm)
          
          // vm event listening initialization. The parent component is bound to the event of the current component
          initEvents(vm)
          
          // vm compilation render initialization
          // $slots/$scopedSlots_c/$createElement/$attrs/$listeners
          initRender(vm)
          
          // beforeCreate callback of life hook
          callHook(vm, 'beforeCreate')
          
          // Inject the member of inject into vm
          initInjections(vm) // resolve injections before data/props
          
          // Initializing vm_ props/methods/_data/computed/watch
          initState(vm)
          
          // Initialize provide
          initProvide(vm) // resolve provide after data/props
          
          // create callback of life hook
          callHook(vm, 'created')
      
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            vm._name = formatComponentName(vm, false)
            mark(endTag)
            measure(`vue ${vm._name} init`, startTag, endTag)
          }
      
          if (vm.$options.el) {
            vm.$mount(vm.$options.el)
          }
        }
      }
      
      export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
        const opts = vm.$options = Object.create(vm.constructor.options)
        // doing this because it's faster than dynamic enumeration.
        const parentVnode = options._parentVnode
        opts.parent = options.parent
        opts._parentVnode = parentVnode
      
        const vnodeComponentOptions = parentVnode.componentOptions
        opts.propsData = vnodeComponentOptions.propsData
        opts._parentListeners = vnodeComponentOptions.listeners
        opts._renderChildren = vnodeComponentOptions.children
        opts._componentTag = vnodeComponentOptions.tag
      
        if (options.render) {
          opts.render = options.render
          opts.staticRenderFns = options.staticRenderFns
        }
      }
      
      export function resolveConstructorOptions (Ctor: Class<Component>) {
        let options = Ctor.options
        if (Ctor.super) {
          const superOptions = resolveConstructorOptions(Ctor.super)
          const cachedSuperOptions = Ctor.superOptions
          if (superOptions !== cachedSuperOptions) {
            // super option changed,
            // need to resolve new options.
            Ctor.superOptions = superOptions
            // check if there are any late-modified/attached options (#4976)
            const modifiedOptions = resolveModifiedOptions(Ctor)
            // update base extend options
            if (modifiedOptions) {
              extend(Ctor.extendOptions, modifiedOptions)
            }
            options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
            if (options.name) {
              options.components[options.name] = Ctor
            }
          }
        }
        return options
      }
      
      function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
        let modified
        const latest = Ctor.options
        const sealed = Ctor.sealedOptions
        for (const key in latest) {
          if (latest[key] !== sealed[key]) {
            if (!modified) modified = {}
            modified[key] = latest[key]
          }
        }
        return modified
      }
      
      
  • stateMixin(Vue)

  • eventsMixin(Vue)

  • lifecycleMixin(Vue)

  • renderMixin(Vue)

First render pass

  • After Vue initialization, start the real execution
  • Initialization is complete before calling new Vue()
  • Record the first rendering process by debugging the code

Data responsive principle

Refer to previous articles: Simulate Vue JS responsive principle , this paper simulates the principle of response and realizes the simple version of response mechanism. The ideas and methods are the same as Vue JS source code is consistent, which can be viewed by comparison.

Solve the following problems by viewing the source code

  • vm. MSG = {count: 0}, reassign the attribute. Is it responsive?
  • vm.arr[0] = 4, assign a value to the array element, will the view be updated?
  • vm.arr.length = 0. Modify the length of the array. Will the view be updated?
  • vm.arr.push(4), will the view be updated?

Entrance to responsive processing

The whole process of responsive processing is complex. Let's start with

  • src/core/instance/init.js

    • Initstate (VM) initialization of VM state
    • Initialized_ data,_ props, methods, etc
  • src/core/instance/state.js

    // Initialization of data
    if (opts.data) {
        initData(vm)
    } else {
        observe(vm._data = {}, true /* asRootData */)
    }
    
  • Initdata (VM) initialization of VM data

    function initData (vm: Component) {
      let data = vm.$options.data
      // Initialization_ Data. In the component, data is a function. Calling the function returns the result
      // Otherwise, data is returned directly
      data = vm._data = typeof data === 'function'
        ? getData(data, vm)
        : data || {}
      if (!isPlainObject(data)) {
        data = {}
        process.env.NODE_ENV !== 'production' && warn(
          'data functions should return an object:\n' +
          'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
          vm
        )
      }
      // proxy data on instance
      // Get all properties in data
      const keys = Object.keys(data)
      // Get props / methods
      const props = vm.$options.props
      const methods = vm.$options.methods
      let i = keys.length
      // Judge whether the member on data has the same name as props/methods
      while (i--) {
        const key = keys[i]
        if (process.env.NODE_ENV !== 'production') {
          if (methods && hasOwn(methods, key)) {
            warn(
              `Method "${key}" has already been defined as a data property.`,
              vm
            )
          }
        }
        if (props && hasOwn(props, key)) {
          process.env.NODE_ENV !== 'production' && warn(
            `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
            vm
          )
        } else if (!isReserved(key)) {
          proxy(vm, `_data`, key)
        }
      }
      // observe data
      // Responsive processing
      observe(data, true /* asRootData */)
    }
    
  • src/core/observer/index.js

    • observe(value, asRootData)

    • Responsible for creating an observer instance for each Object type value

      export function observe (value: any, asRootData: ?boolean): Observer | void {  // Judge whether value is an object if (! Isobject (value) | value instanceof vnode) {return} let ob: observer | void / / if value has__ ob__(observer object) end of attribute if (hasown (value, '_'ob_') & & value__ ob__  instanceof Observer) {    ob = value.__ob__  }  else if (    shouldObserve &&    !isServerRendering() &&    (Array.isArray(value) || isPlainObject(value)) &&    Object. isExtensible(value) &&    ! value._ Isvue) {/ / create an observer object OB = new observer (value)} if (asrootdata & & OB) {ob. Vmcount + +} return ob}
      

Observer

  • src/core/observer/index.js

    • Respond to objects

    • Respond to the array

      export class Observer {  // Observation object value: any// Dependent object dep: Dep// Instance counter vmCount: number// Number of VMS that have this object as root $data constructor (value: any) {this.value = value this.dep = new dep() / / the vmCount of the initialized instance is 0 this.vmCount = 0 / / mount the instance to the _ ob_ attribute def (value, _ ob_ ', this) / / the responsive processing of the array if (array.isarray (value)) {if (hasproto) {protoaugust (value, arraymethods)} else {copyaugust (value, arraymethods, arraykeys)} / / create an observer instance this for each object in the array Observearray (value)} else {/ / traverse each property in the object and convert it into setter / getter 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) {/ / obtain each attribute of the observation object const keys = Object.keys(obj) / / traverse each attribute and set it to the responsive data 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])    }  }}
      
    • wakl(obj)

      • Traverse all properties of obj, call defineReactive() method for each property, and set getter/setter

defineReactive()

  • src/core/observer/index.js
  • defineReactive(obj, key, val, customSetter, shallow)
    • Define a responsive attribute for an object, and each attribute corresponds to a dep object
    • If the value of this property is an object, continue calling observe
    • If you assign a new value to the property, continue calling observe
    • Send notification if data is updated

Object responsive processing

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 1. Create dependent object instances for each attribute
  const dep = new Dep()
  // Gets the property descriptor of obj
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // Provides predefined accessor functions
  // 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]
  }
  // 2. Judge whether to recursively observe the sub object, convert the sub object properties into getters / setters, and return the sub observation object
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // If a predefined getter exists, value is equal to the return value of the getter call
      // Otherwise, attribute values are directly assigned
      const value = getter ? getter.call(obj) : val
      // If there is a current dependency target, that is, the watcher object, the dependency is established
      if (Dep.target) {
        // dep() adds interdependencies
        // A component corresponds to a watcher object
        // One watcher corresponds to multiple DEPs (there are many attributes to be observed)
        // We can manually create multiple watchers to monitor the change of a property, and a dep can correspond to multiple watchers
        dep.depend()
        // If the sub observation object target exists, establish the dependency of the sub object
        if (childOb) {
          childOb.dep.depend()
          // If the property is an array, special processing collects array object dependencies
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      // Return property value
      return value
    },
    set: function reactiveSetter (newVal) {
      // If a predefined getter exists, value is equal to the return value of the getter call
      // Otherwise, attribute values are directly assigned
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // If the new value is equal to the old value or the old and new values are NaN, it is not executed
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // If there is no setter, return directly
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      // Call if the predefined setter exists, otherwise update the new value directly
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 3. If the new value is an object, observe the child object and return the observer object of the child object
      childOb = !shallow && observe(newVal)
      // 4. Distribute updates (issue change notice)
      dep.notify()
    }
  })
}

Responsive processing of arrays

  • In the constructor of Observer

    // Responsive processing of arrays
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // Create an observer instance for each object in the array
      this.observeArray(value)
    } else {
      // Traverse each property in the object and convert it into setter/getter
      this.walk(value)
    }
    
    
    // helpers
    
    /**
     * Augment a target Object or Array by intercepting
     * the prototype chain using __proto__
     */
    function protoAugment (target, src: Object) {
      /* eslint-disable no-proto */
      target.__proto__ = src
      /* eslint-enable no-proto */
    }
    
    /**
     * Augment a target Object or Array by defining
     * hidden properties.
     */
    /* istanbul ignore next */
    function copyAugment (target: Object, src: Object, keys: Array<string>) {
      for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i]
        def(target, key, src[key])
      }
    }
    
  • Method for processing array modification data

    • src/core/observer/array.js

      /*
       * not type checking this file because flow doesn't play well with
       * dynamically accessing methods on Array prototype
       */
      
      import { def } from '../util/index'
      
      const arrayProto = Array.prototype
      // Create a new object using the prototype of the array (clone the prototype of the array)
      export const arrayMethods = Object.create(arrayProto)
      // Methods of modifying array elements
      const methodsToPatch = [
        'push',
        'pop',
        'shift',
        'unshift',
        'splice',
        'sort',
        'reverse'
      ]
      
      /**
       * Intercept mutating methods and emit events
       */
      methodsToPatch.forEach(function (method) {
        // cache original method
        // Original method of saving array
        const original = arrayProto[method]
        // Call object Defineproperty() redefines the method of modifying the array
        def(arrayMethods, method, function mutator (...args) {
          // Execute the original method of the array
          const result = original.apply(this, args)
          // Gets the ob object of the array object
          const ob = this.__ob__
          let inserted
          switch (method) {
            case 'push':
            case 'unshift':
              inserted = args
              break
            case 'splice':
              inserted = args.slice(2)
              break
          }
          // For the inserted new element, re traverse the array element and set it as responsive data
          if (inserted) ob.observeArray(inserted)
          // notify change
          // The method of modifying the array is called, and the ob object of the array is called to send a notification
          ob.dep.notify()
          return result
        })
      })
      
      
    • def method

      /**
       * Define a property.
       */
      export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
        Object.defineProperty(obj, key, {
          value: val,
          enumerable: !!enumerable,
          writable: true,
          configurable: true
        })
      }
      

Dep class

  • src/core/observer/dep.js
  • Dependent object
  • Record watcher object
  • Depend() -- the dep corresponding to the watcher record
  • deliver an announcement
  1. Create a dep object in the getter in defineReactive(), judge whether Dep.target has a value (we'll see when it's worth it later), and call dep.depend()
  2. Dep.dependent() internally calls dep.target AddDep (this), the addDep() method of watcher, calls dep.addSub(this) internally, adding watcher objects to dep.subs.. Push (watcher), that is, add the subscriber to the sub array of DEP, and call the update() method of the watcher object when the data changes
  3. When did you set Dep.target? Through simple case debugging observation. When the mountComponent() method is called, a render watcher object is created and the get() method in the watcher is executed
  4. The get() method calls pushTarget(this) internally to put the current Dep.target = watcher and put the current watcher on the stack. When parent-child components are nested, put the watcher corresponding to the parent component on the stack first, and then process the watcher of the child component. After the processing of the child component is completed, put the watcher corresponding to the parent component off the stack and continue the operation
  5. Dep.target is used to store the watcher currently in use. Globally unique, and only one watcher can be used at a time
/* @flow */

import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0
// dep is an observable object, and multiple instructions can subscribe to it
/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  // Static properties, watcher object
  static target: ?Watcher;
  // dep instance Id
  id: number;
  // watcher object / subscriber array corresponding to dep instance
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  // Add a new subscriber watcher object
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  // Remove subscriber
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  // Establish dependency between observation object and watcher
  depend () {
    if (Dep.target) {
      // If the target exists, add the dep object to the dependency of the watcher
      Dep.target.addDep(this)
    }
  }

  // deliver an announcement
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    // Call the update method of each subscriber to implement the update
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
// Dep.target is used to store the watcher currently in use
// Globally unique, and only one watcher can be used at a time
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
// Stack and assign the current watcher to Dep.target
// When parent-child components are nested, first put the watcher corresponding to the parent component into the stack,
// Then process the watcher of the sub component. After the sub component is processed, take the watcher corresponding to the parent component out of the stack and continue the operation
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  // Out of stack operation
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

Watcher class

  • There are three types of watchers, namely, coordinated Watcher, user Watcher (listener) and rendering Watcher

  • Render Watcher creation timing

    • 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)
          }
        }
      	// Create a render Watcher with exOrFn as updateComponent
        // 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 && !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
          callHook(vm, 'mounted')
        }
        return vm
      }
      
  • Where the render watcher was created: lifecycle JS in the mountComponent function

  • The constructor of watcher is initialized and exOrFn is processed (the processing of render watcher is different from that of listener. The render watcher is updateComponent. Compare the old and new vdom and render it to the page)

  • Call this Get(), which calls pushTarget(), and then this getter. Call (VM, VM) (call updateComponent for render watcher). If it is a user, the Watcher will return to the value of the attribute (trigger the get operation)

  • When data is updated, the notify() method is invoked in dep, and the update() method calling watcher in notify().

  • Calling queueWatcher() in update()

  • queueWatcher() is a core method that removes duplicate operations and calls flushSchedulerQueue() to refresh the queue and execute the watcher

  • In flushSchedulerQueue(), sort the watchers and traverse all watchers. If there is before, trigger the hook function beforeUpdate of the life cycle and execute the watcher Run (), which internally calls this Get (), and then call this.. cb () (the cb of the rendered wacher is noop, and the function of the listener)

    1. The order of component update is from parent component to child component (because the parent component is created first and then the child component is created)
    2. The user watcher of the component runs before rendering the watcher (because the user watcher (initState) is created before rendering the watcher (mountComponent)
    3. If a component is destroyed before the parent component executes, it should be skipped
  • End of the whole process

Debug responsive data execution process

  • The core process of array responsive processing and the process of array collection dependency

  • When the data of the array changes, the execution process of the watcher

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>observe</title>
    </head>
    <body>
      <div id="app">
        {{ arr }}
      </div>
    
      <script src="../../dist/vue.js"></script>
      <script>
        const vm = new Vue({
          el: '#app',
          data: {
            arr: [2, 3, 5]
          }
        })
    
        // vm.arr.push(8)
        // vm.arr[0] = 100
        // vm.arr.length = 0
        
      </script>
    </body>
    </html>
    

Answer the following questions

  • Precautions for detecting changes

    methods: {
      handler() {
        this.obj.count = 555
        this.arr[0] = 1
        this.arr.length = 0
        this.arr.push(4)
      }
    }
    
  • Convert to responsive data

    methods: {
      handler() {
        this.$set(this.obj, 'count', 555)
        this.$set(this.arr, 0, 1)
        this.arr.splice(0)
      }
    }
    

Summary of data responsive principle

Dynamically add a responsive attribute

When we dynamically add an object to a responsive object, is this property responsive?

Sample code

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>set</title>
</head>
<body>
  <div id="app">
    {{ obj.title }}
    <hr>
    {{ obj.name }}
    <hr>
    {{ arr }}
  </div>

  <script src="../../dist/vue.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        obj: {
          title: 'Hello Vue'
        },
        arr: [1, 2, 3]
      }
    })
  </script>
</body>
</html>

Open the browser developer mode and type the following:

  • vm.obj.name = 'abc'

    It can be found that the name attribute is dynamically added to obj, but the view is not updated, indicating that the name attribute is not responsive at this time

  • vm.$set(vm.obj, 'name', 'zhangsan')

    You can use VM$ Set (VM. Obj, 'name', 'Zhangsan') to dynamically add responsive properties to responsive objects (or use Vue.set()).

    Using vm s e t ( ) square method change change number group of The first one individual element element of value : ' v m . The set() method changes the value of the first element of the array: ` vm The set () method changes the value of the first element of the array: 'VM set(vm.arr, 0, 100)`. vm.$set() Official documents

    You cannot dynamically add responsive properties to Vue instances or array objects of Vue instances.

Instance method / data

vm.$set

Define location

  • Vue.set()

    • global-api/index.js
    // Static method set/delete/nextTick
    Vue.set = set
    Vue.delete = del
    Vue.nextTick = nextTick
    
  • vm.$set()

    • instance/index.js
    // Register $data/$props/$set/$delete/$watch of vm
    // instance/state.js
    stateMixin(Vue)
    
    // instance/state.js
    Vue.prototype.$set = set
    Vue.prototype.$delete = del
    

Source code

  • set() method

    • observer/index.js
    /**
     * Set a property on an object. Adds the new property and
     * triggers change notification if the property doesn't
     * already exist.
     */
    export function set (target: Array<any> | Object, key: any, val: any): any {
      if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
      ) {
        warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
      }
      // Judge whether the target is an array and whether the key value is a legal index
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key)
        // Replace the element at the key position through splice
        // Splice is in array JS has carried out responsive processing. The splice here is no longer the native splice method
        target.splice(key, 1, val)
        return val
      }
      // If the key already exists in the object, assign it directly
      if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
      }
      // Get the observer object in target
      const ob = (target: any).__ob__
      // If the target is a Vue instance or $data is returned directly, if it is $data, the vmCount in its observe object is 1, otherwise it is 0
      if (target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
          'Avoid adding reactive properties to a Vue instance or its root $data ' +
          'at runtime - declare it upfront in the data option.'
        )
        return val
      }
      // If ob does not exist and target is not a responsive object, you can assign a value directly at this time
      if (!ob) {
        target[key] = val
        return val
      }
      // Set the key as a responsive property
      defineReactive(ob.value, key, val)
      // Send notification
      ob.dep.notify()
      return val
    }
    

debugging

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>set</title>
</head>
<body>
  <div id="app">
    {{ obj.title }}
    <hr>
    {{ obj.name }}
    <hr>
    {{ arr }}
  </div>

  <script src="../../dist/vue.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        obj: {
          title: 'Hello Vue'
        },
        arr: [1, 2, 3]
      }
    })
  </script>
</body>
</html>

Review childOb in defineReactive and set an ob for each responsive object

When calling $set, the OB object will be obtained and passed through ob Dep.notify() send notification

vm.$delete

  • function

    Delete the properties of the object. If the object is responsive, make sure that deletion triggers an update of the view. This method is mainly used to avoid the restriction that Vue cannot detect the deletion of attributes, but you should rarely use it.

    Note: the target cannot be a Vue instance or a data object of a Vue instance

  • Examples

    vm.$delete(vm.obj, 'title')
    

Define location

  • Vue.delete()

    • global-api/index.js
    // Static method set/delete/nextTick
    Vue.set = set
    Vue.delete = del
    Vue.nextTick = nextTick
    
  • vm.$delete()

    • instance/index.js
    // Register $data/$props/$set/$delete/$watch of vm
    stateMixin(Vue)
    
    // instance/state.js
    Vue.prototype.$set = set
    Vue.prototype.$delete = del
    

Source code

  • src/core/observer/index.js

    /**
     * Delete a property and trigger change if necessary.
     */
    export function del (target: Array<any> | Object, key: any) {
      if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
      ) {
        warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
      }
      // Judge whether it is an array and whether the key is legal
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        // If it is an array, delete it through splice
        // splice has done responsive processing
        target.splice(key, 1)
        return
      }
      // Gets the ob object of target
      const ob = (target: any).__ob__
      // If target is a Vue instance or $data object, it is returned directly
      if (target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
          'Avoid deleting properties on a Vue instance or its root $data ' +
          '- just set it to null.'
        )
        return
      }
      // If the target object returns directly without the key attribute, the judgment is based on whether the key directly belongs to the target attribute rather than inherited
      // If it is inherited or does not have this property, it is returned directly
      if (!hasOwn(target, key)) {
        return
      }
      // Delete attribute
      delete target[key]
      if (!ob) {
        return
      }
      // Send notifications via ob
      ob.dep.notify()
    }
    

vm.$watch

vm.$watch(expOrFn, callback, [options]),Official documents

  • function

    An expression that observes changes in Vue instances or evaluates property functions. The parameters obtained by the callback function are new values and old values. Expressions accept only supervised key paths. For more responsible expressions, replace them with a function

  • parameter

    • expOrFn: the attribute in $data to be monitored, which can be an expression or a function
    • callback: function executed after data changes
      • Functions: callback functions
      • Object: has a handler attribute (string or function). If the attribute is a string, the corresponding definition in methods
    • Options: optional options
      • Deep: Boolean type, deep listening
      • immediate: Boolean type, whether to execute the callback function immediately
  • Example 1

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>watcher</title>
    </head>
    <body>
      <div id="app">
        {{ user.fullName }}
      </div>
    
      <script src="../../dist/vue.js"></script>
      <script>
        const vm = new Vue({
          el: '#app',
          data: {
            user: {
              firstName: 'Zhuge',
              lastName: 'bright',
              fullName: ''
            }
          }
        })
    
        vm.$watch('user',
          function (newValue, oldValue) {
            this.user.fullName = newValue.firstName + ' ' + newValue.lastName
          }
        )
      </script>
    </body>
    </html>
    

    Open the browser and find that Zhuge Liang is not displayed immediately. At this time, you need to add a third parameter options, which is: {immediate: true}, which means immediate execution. Refresh the page again and find that Zhuge Liang is displayed on the page.

    When we need to monitor vm d a t a . u s e r . f i r s t N a m e Time , hair present after Continued can can still want prison hear v m . data. user. At firstname, it is found that you may need to listen to vm data. user. At firstName, it is found that you may need to listen to VM data. user. LastName, so it is very inconvenient to write multiple watch es at this time. At this time, add: deep: true in options, that is, deep listening. Listen not only for changes in the user object, but also for changes in its internal properties. At this point, modify the firstName and find that the view will also be updated.

  • Example 2

    const vm = new Vue({
      el: '#app',
      data: {
        a: '1',
        b: '2',
        msg: 'Hello Vue',
        user: {
          firstName: 'Zhuge',
          lastName: 'bright'
        }
      }
    })
    
    // expOrFn is an expression
    vm.$watch('msg', function (newVal, oldVal) {
      congole.log(newVal)
    })
    vm.$watch('user.firstName', function (newVal, oldVal) {
      congole.log(newVal)
    })
    
    // expOrFn is a function
    vm.$watch(function () {
      return this.a + this.b
    }, function (newVal, oldVal) {
      console.log(newVal)
    })
    
    // When deep is true, performance consumption is compared
    vm.$watch('user', function (newVal, oldVal) {
      console.log(newVal)
    }, {
      deep: true
    })
    
    // immediate is true
    vm.$watch('msg', function (newVal, oldVal) {
      console.log(newVal)
    }, {immediate: true})
    

Three types of Watcher objects

  • There is no static method because an instance of vue is used in the $watch method
  • There are three types of watchers: calculation attribute Watcher, user Watcher (listener) and rendering Watcher
  • Creation order: calculate attribute Watcher, user Watcher (listener), render Watcher
  • vm.$watch()
    • src/core/instance/state.js

Source code

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  // Get Vue instance this
  const vm: Component = this
  if (isPlainObject(cb)) {
    // If cb is an object, execute createWatcher
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  // Mark as user watcher
  options.user = true
  // Create user watcher object
  const watcher = new Watcher(vm, expOrFn, cb, options)
  // Judge if immediate is true
  if (options.immediate) {
    // A cb callback is executed immediately and the current value is passed in
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    }
  }
  // Returns the method of canceling listening
  return function unwatchFn () {
    watcher.teardown()
  }
}

debugging

  • View the order in which watcher s are created

  • Test code

    • <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>watcher</title>
      </head>
      <body>
        <div id="app">
          {{ reversedMessage }}
          <hr>
          {{ user.fullName }}
        </div>
      
        <script src="../../dist/vue.js"></script>
        <script>
          const vm = new Vue({
            el: '#app',
            data: {
              message: 'Hello Vue',
              user: {
                firstName: 'Zhuge',
                lastName: 'bright',
                fullName: ''
              }
            },
            computed: {
              reversedMessage: function () {
                return this.message.split('').reverse().join('')
              }
            },
            watch: {
              // 'user.firstName': function (newValue, oldValue) {
              //   this.user.fullName = this.user.firstName + ' ' + this.user.lastName
              // },
              // 'user.lastName': function (newValue, oldValue) {
              //   this.user.fullName = this.user.firstName + ' ' + this.user.lastName
              // },
              'user': {
                handler: function (newValue, oldValue) {
                  this.user.fullName = this.user.firstName + ' ' + this.user.lastName
                },
                deep: true,
                immediate: true
              }
            }
          })
        </script>
      </body>
      </html>
      

      Set the breakpoint to Src / core / observer / watcher The watcher constructor in JS

    • Calculate attribute watcher

    • User watcher (listener)

    • Render watcher

      • View the execution of the render watcher
        • When data is updated, dep.notify() is called in the set method of defineReactive.
        • Call the update() of the watcher
        • Call ququeWatcher() to store the watcher in the queue. If it already exists, it will not be added repeatedly
        • Call flushSchedulerQueue() in a loop
          • Call flushShedulerQueue() before the end of the message loop through nextTick()
        • Call watcher run()
          • Call watcher Get() get the latest value
          • If it is rendering, wacher ends
          • If the user watcher, call this cb()

Asynchronous update queue - nextTick()

  • Vue updates the DOM asynchronously and in batches
    • A deferred callback is performed after the end of the next DOM update cycle. Use this method immediately after modifying the data to get the updated dom
  • vm.$nextTick(function() {/ * DOM * /} / Vue$ nextTick(function () {})

vm.$nextTick() code demonstration

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>nextTick</title>
</head>
<body>
  <div id="app">
    <p id="p" ref="p1">{{ msg }}</p>
    {{ name }}<br>
    {{ title }}<br>
  </div>
  <script src="../../dist/vue.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        msg: 'Hello nextTick',
        name: 'Vue.js',
        title: 'Title'
      },
      mounted() {
        this.msg = 'Hello World'
        this.name = 'Hello snabbdom'
        this.title = 'Vue.js'
  
        Vue.nextTick(() => {
          console.log(this.$refs.p1.textContent)
        })
      }
    })

    
  </script>
</body>
</html>

vm.$nextTick() code demonstration

Define location

  • src/core/instance/render.js
Vue.prototype.$nextTick = function (fn: Function) {
  return nextTick(fn. this)
}

Source code

  • Manually call VM$ nextTick()

  • Execute nextTick() in Watcher's queueWatcher

  • src/core/util/next-tick.js

  • $nextTick() instance method

  • $nextTick() static method

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // Store cb and exception handling into the callbacks array
  callbacks.push(() => {
    if (cb) {
      try {
        // Call cb()
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    // call
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    // Return promise object
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
  • timerFunc()
/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // Store cb and exception handling into the callbacks array
  callbacks.push(() => {
    if (cb) {
      try {
        // Call cb()
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    // call
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    // Return promise object
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

Keywords: Vue observer

Added by adrianuk29 on Thu, 20 Jan 2022 15:51:00 +0200