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
UMD CommonJS ES Module Full vue.js vue.common.js vue.esm.js Runtime-only vue.runtime.js vue.common.min.js vue.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
- The ESM format is designed to be statically analyzed, so the packaging tool can use this to "tree shaking" and exclude unused code from the final package
- Differences between ES6 module and CommonJS module
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
- 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()
- 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
- 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
- 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
- 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)
- 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)
- The user watcher of the component runs before rendering the watcher (because the user watcher (initState) is created before rendering the watcher (mountComponent)
- 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()
- View the execution of the render watcher
-
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 }) } }