In addition to Weibo, there is also WeChat
Please pay attention
WeChat public account
Shulou
2025-02-23 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >
Share
Shulou(Shulou.com)06/02 Report--
This article mainly introduces "what is the implementation principle of Watcher and Scheduler in Vue". In daily operation, I believe that many people have doubts about the implementation principle of Watcher and Scheduler in Vue. The editor consulted all kinds of materials and sorted out simple and easy-to-use operation methods. I hope it will be helpful to answer the question of "what is the realization principle of Watcher and Scheduler in Vue"! Next, please follow the editor to study!
Vue is aware of the change of state through the data detection mechanism. The previous article "how to implement data detection by Vue" mentioned Watcher objects. When the data is updated, for example, when this.title = "have I changed?", call dep.notify in the setter function to notify watcher to perform the update (specifically execute the watcher.update function).
So when Vue creates Watcher, how to schedule Watcher queues through Scheduler, and how the update of watcher is finally reflected in the rendering of the view, this article mainly introduces the principle of Watcher implementation of Vue around these three issues.
1. When to create a Watcher
Components go through a series of lifecycles from creation to destruction, of which we are familiar with beforeMount, mounted, beforeUpdate, and updated. It is much easier to understand the lifecycle and understand when Watcher is created. The Watcher object is created in three places in Vue: the mount event, the $watch function, the computed and watch properties, the mount event creation Watcher for rendering notifications, and the Watcher created by watch and computed for listening for user-defined attribute changes.
1.1 mount event
The file core/instance/lifecycle.js contains functions related to the life cycle of Vue, such as $forupdate, $destroy and the mountComponent function that instantiates Watcher. The mountComponent function is triggered when the component is loaded and executes $mount. The function first triggers the beforeMount hook event. When instantiating Watcher, the before function is passed, and before will trigger beforeUpdate hook. When a component has a property update, watcher triggers the beforeUpdate event before the watcher.run. IsRenderWatcher indicates that the rendering Watcher is created and hung directly on the vm._watcher attribute. When $forceUpdate refresh rendering is enforced, vm._watcher.update triggers the rendering process and the corresponding update hook.
/ * * Lifecycle mount event trigger function * @ param {*} vm * @ param {*} el * @ param {*} hydrating * @ returns * / export function mountComponent (vm: Component, el:? Element, hydrating?: boolean): Component {vm.$el = el callHook (vm, 'beforeMount') let updateComponent = () = > {vm._update (vm._render (), hydrating)} / / instantiate Watcher object Establish the relationship between Watcher and vm in the Watcher constructor new Watcher (vm, updateComponent, noop, {/ / trigger the before hook event before () {if (vm._isMounted & &! vm._isDestroyed) {callHook (vm, 'beforeUpdate')} / / isRenderWatcher represents the Watcher for rendering before executing the before hook function Watcher.update}, true / * isRenderWatcher * /) return vm} export default class Watcher {constructor (vm: Component, expOrFn: string | Function, cb: Function, options?:? Object, isRenderWatcher?: boolean) {this.vm = vm if (isRenderWatcher) {vm._watcher = this} vm._watchers.push (this) this.getter = expOrFn this.value = this.lazy? Undefined: this.get ()} Vue.prototype.$forceUpdate = function () {const vm: Component = this if (vm._watcher) {vm._watcher.update ()}} 1.2.$watch function
In the component, in addition to using watch and computed methods to listen for property changes, Vue defines a $watch function to listen for property changes. For example, when a.b.c nested properties change, you can use $watch to monitor for subsequent processing. $watch is equivalent to writing the watch attribute directly in the component, which supports the dynamic addition of dependency listeners at run time. For example, the keep-alive component in the Vue source code uses $watch to listen for include and exclude attribute changes in the mounted event.
Vm.$watch (expOrFn, callback, [options]) Parameter: {string | Function} expOrFn {Function | Object} callback {Object} [options] {boolean} deep {boolean} immediate return value: {Function} unwatch// key path vm.$watch ('a.b.censor, function (newVal, oldVal) {/ / do something}) / keep-alive.js file mounted () {this.cacheVNode () this.$watch (' include') Val = > {pruneCache (this, name = > matches (val, name))}) this.$watch ('exclude', val = > {pruneCache (this, name = >! matches (val, name))})}
The difference between the $watch function and the mountComponent function is that mountComponent is used for rendering listening and triggers related hook events, while the responsibility of $watch is more specific and handles expOrFn listening. In addition, the cb parameter of $watch can be a function, object, or string. When it is a string, it represents the name of the function defined in the Vue object. For example, if the nameChange function is defined in the Vue component, after defining vm.$watch ('name',' nameChange'), if there is an update in name, the nameChange function of the Vue entity will be triggered.
/ / listening attribute changes Vue.prototype.$watch = function (expOrFn: string | Function, cb: any, options?: Object): Function {const vm: Component = this / / cb may be a pure JS object Then the callback is cb.handler if (isPlainObject (cb)) {return createWatcher (vm, expOrFn, cb, options)} const watcher = new Watcher (vm, expOrFn, cb, options) / / returns the watch logout listener function return function unwatchFn () {watcher.teardown ()}} function createWatcher (vm: Component, expOrFn: string | Function, handler: any, options?: Object) {/ / when the execution function is an object Call the handler of handler to the execution function / / where options is the configuration information of the watch function if (isPlainObject (handler)) {options = handler handler = handler.handler} if (typeof handler = 'string') {handler = vm [handler]} return vm.$watch (expOrFn, handler, options)} 1.3.watch and computed attributes
Using Vue to develop components, these two attributes must be familiar, such as using watch to define the listening of firstName and secondName attributes, and using computed to define fullName property listening. When firstName and secondName are updated, fullName also triggers the update.
New Vue ({el:'# app', data () {return {firstName: 'Li', secondName:' Lei'}}, watch: {secondName: function (newVal, oldVal) {console.log ('second name changed:' + newVal)}}, computed: {fullName: function () {return this.firstName + this.secondName}} Mounted () {this.firstName = 'Han' this.secondName =' MeiMei'}})
When we define listening for properties in watch and computed, when does Vue convert it to a Watcher object to perform listening? The constructor of Vue calls _ init (options) to perform initialization. The source code core/components/instance/init.js file defines the _ init function and performs a series of initialization operations, such as initialization life cycle, event, state, and so on. The initState function contains the initialization of watch and computed.
/ / core/components/instance/init.js// Vue constructor function Vue (options) {this._init (options)} / / core/components/instance/init.jsVue.prototype._init = function (options?: Object) {initLifecycle (vm) initEvents (vm) initRender (vm) callHook (vm, 'beforeCreate') initInjections (vm) / / resolve injections before data/props initState (vm) initProvide (vm) / / resolve provide after data/props callHook (vm) 'created')} / core/components/state.jsexport function initState (vm: Component) {vm._watchers = [] const opts = vm.$options... If (opts.computed) initComputed (vm, opts.computed) if (opts.watch & & opts.watch! = = nativeWatch) {initWatch (vm, opts.watch)} 1.3.1 computed attribute
InitComputed initializes the computed attribute, and each Vue entity contains a watcher object that the _ computedWatchers object uses to store all computed attributes. First, we iterate through the computed object and create a new Watcher object for each key. Its lazy property is true, which means that the Watcher caches the calculated value. If the dependent properties (such as firstName, secondName) are not updated, the current computed property (such as fullName) will not trigger the update. The attributes defined in computed can be accessed through this (for example, this.fullName), and defineComputed mounts all computed attributes to the Vue entity.
/ / if lazy is true, caching is required. Generally, only the computed attribute will use const computedWatcherOptions = {lazy: true} function initComputed (vm: Component, computed: Object) {const watchers = vm._computedWatchers = Object.create (null) for (const key in computed) {const userDef = computed [key] / / the user-defined execution function may be in the form of {get: function () {}} const getter = typeof userDef = 'function'? UserDef: userDef.get / / create a watcher object for each user-defined computed attribute watchers [key] = new Watcher (vm, getter | | noop, noop, computedWatcherOptions) / / the computed property of the component itself is defined on the component prototype chain, we only need to define the instantiated computed property. / / for example, we defined in computed that fullName,defineComputed will attach it to the properties of the Vue object if (! (key in vm)) {defineComputed (vm, key, userDef)}}
The defineComputed function converts the calculated property to {get, set} form, but the calculated property does not require set, so the code directly assigns a noop null function to it. The get function of the calculated attribute is encapsulated by createComputedGetter. First, the watcher object of the corresponding attribute is found. If the dirty of the watcher is true, it means that the dependent attribute is updated. You need to call the evaluate function to recalculate the new value.
/ / convert the attributes defined by computed to {get, set} and attach them to the Vue entity, so that you can call export function defineComputed (target: any, key: string, userDef: Object | Function) {if (typeof userDef = 'function') {sharedPropertyDefinition.get = createComputedGetter (key) sharedPropertyDefinition.set = noop} else {sharedPropertyDefinition.get = userDef.get? CreateComputedGetter: noop sharedPropertyDefinition.set = userDef.set | | noop} Object.defineProperty (target, key, sharedPropertyDefinition)} / / define the exclusive getter function function createComputedGetter (key) {return function computedGetter () {/ / _ computedWatchers of computed. The Watcher object const watcher = this._computedWatchers & & this._ computedWatchers [key] if (watcher) {/ / dirty is true is defined for each computed attribute. Indicates that the dependency attribute has changed if (watcher.dirty) {/ / recalculated value watcher.evaluate ()} if (Dep.target) {/ / append Dep.target (watcher) to the current watcher dependency watcher.depend ()} return watcher.value}
If Dep.target has a value, attach other Watcher that depends on the current evaluation property (for example, a dependency Watcher that uses the fullName) to the dep collection of the property on which the current evaluation property depends. For example, the following code creates a listener for the fullName calculation property, which we name watcher3. Then the dep object of both firstName and secondName will be appended with a watcher3 viewer, and any change in its property will trigger the update function of watcher3 and re-read the value of the fullName property.
Vm.$watch ('fullName', function (newVal, oldVal) {/ / do something}) 1.3.2 watch attribute
The initWatch function logic is relatively simple, traversing the dependencies of each attribute, and if the dependencies are arrays, traversing the array, creating a separate Watcher observer for each dependency. As mentioned earlier, the createWatcher function uses $watch to create a new watcher entity.
/ / initialize the Watch attribute function initWatch (vm: Component, watch: Object) {for (const key in watch) {const handler = watch [key] / / if the corresponding attribute key has multiple dependencies, traverse to create a watcher if (Array.isArray (handler)) {for (let I = 0; I) for each dependency
< handler.length; i++) { createWatcher(vm, key, handler[i]) } } else { createWatcher(vm, key, handler) } }}2.Scheduler调度处理 Vue在core/observer/scheduler.js文件定义了调度函数,一共有两处使用,Watcher对象以及core/vdom/create-component.js文件。watcher对象在执行更新时,会被附加到调度队列中等待执行。create-component.js主要处理渲染过程,使用scheduler的主要作用是触发activated hook事件。这里重点阐述Watcher对Scheduler的使用。 当执行watcher的update函数,除了lazy(计算属性watcher)、sync(同步watcher),所有watcher都将调用queueWatcher函数附加到调度队列中。 export default class Watcher { /** * 通知订阅,如果依赖项有更新,该函数会被触发 */ update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } }} queueWatcher函数定义如下,函数的目的是将watcher附加到调度队列中,对调度队列创建微任务(microTask),等待执行。关于microTask和macroTask的区别,看查看参考8"宏任务macroTask和微任务microTask的区别"。如果微任务flushSchedulerQueue还未执行(flushing为false),直接将watcher附加到queue即可。否则,还需判断当前微任务的执行进度,queue会按watcher的id做升序排序,保证先创建的watcher先执行。index为微任务中正在被执行的watcher索引,watcher将会插入到大于index且符合id升序排列的位置。最后队列执行函数flushSchedulerQueue将通过nextTick创建一个微任务等待执行。 /** 附加watcher到队列中,如果有重复的watcher直接跳过。* 如果调度队列正在执行(flushing为true),将watcher放到合适的位置*/export function queueWatcher (watcher: Watcher) { // 所有watcher都有一个递增的唯一标识, const id = watcher.id // 如果watcher已经在队列中,不做处理 if (has[id] == null) { has[id] = true if (!flushing) { // 如果队列还未执行,则直接附加到队列尾部 queue.push(watcher) } else { // 如果正在执行,基于id将其附加到合适的位置。 // index为当前正在执行的watcher索引,并且index之前的watcher都被执行了。 // 先创建的watcher应该被先执行,和队列中的watcher比较id大小,插入到合适的位置。 let i = queue.length - 1 while (i >The position of index & & queue [I] .id > watcher.id) {imurt -} / / I, indicating that watcher [i1] .id
< watcher[i].id < watcher[i + 1].id queue.splice(i + 1, 0, watcher) } // 如果未排队,开始排队,nextick将执行调度队列。 if (!waiting) { waiting = true nextTick(flushSchedulerQueue) } } } nextTick将会选择适合当前浏览器的微任务执行队列,例如MutationObserver、Promise、setImmediate。flushSchedulerQueue函数将遍历所有watcher并执行更新,首先需要将queue做升序排序,确保先创建的watcher先被执行,例如父组件的watcher优先于子组件执行。接着遍历queue队列,先触发watcher的before函数,例如前文中介绍mountComponent函数在创建watcher时会传入before事件,触发callHook(vm, 'beforeUpdate')。接下来就具体执行更新(watcher.run)操作。当队列执行完后,调用resetSchedulerState函数清空队列、重置执行状态。最后callActivatedHooks和callUpdatedHooks将触发对应的activated、updated hook事件。 /** * 遍历执行所有的watchers */ function flushSchedulerQueue () { currentFlushTimestamp = getNow() flushing = true let watcher, id // 遍历之前先排序队列 // 排序的队列能确保: // 1.父组件先于子组件更新,因为父组件肯定先于子组件创建。 // 2.组件自定义的watcher将先于渲染watcher执行,因为自定义watcher先于渲染watcher创建。 // 3.如果组件在父组件执行wtcher期间destroyed了,它的watcher集合可以直接被跳过。 queue.sort((a, b) =>A.id-b.id) / / do not cache the length, because the queue queue is constantly adjusted while traversing the queue to execute the wacher. For (index = 0; index)
< queue.length; index++) { watcher = queue[index] if (watcher.before) { // 通过before可触发hook,例如执行beforeUpdated hook watcher.before() } id = watcher.id has[id] = null // 执行watcher的更新 watcher.run() } // 由于activatedChildren和queue两个队列一直在更新,因为需要拷贝处理 const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() // 重置掉队队列状态 resetSchedulerState() // 触发activated和updated hooks callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue)}3.Watcher更新 调度队列会执行watcher的run函数触发更新,每个watcher有active状态,表明当前watcher是否处于激活状态,当组件执行$destroy函数,会调用watcher的teardown函数将active设置为false。在执行更新通知回调cb之前,有三个条件判断,首先判断值是否相等,对于简单值string或number类型的可直接判断;如果value为对象或需要深度遍历(deep为true),例如用户自定义了person属性,其值为对象{ age: number, sex: number },我们使用$watch('person', cb)监听了person属性,但当person.age发生变化时,cb不会被执行。如果改成$watch('person', cb, { deep: true }),任何嵌套的属性发生变化,cb都会被触发。满足三个条件其中之一,cb回调函数将被触发。 export default class Watcher { /** * 调度接口,将被调度器执行 */ run () { // 仅当watcher处于激活状态,才会执行更新通知 // 当组件destroyed时,会调用watcher的teardown将其重置到非激活状态 if (this.active) { // 调用get获取值 const value = this.get() if ( // 如果新计算的值更新了 value !== this.value || // 如果value为对象或数组,不管value和this.value相等否,则其深度watchers也应该被触发 // 因为其嵌套属性可能发生变化了 isObject(value) || this.deep ) { const oldValue = this.value this.cb.call(this.vm, value, oldValue) } } }}this.$watch('person', () =>{this.message = 'age:' + this.person.age}, / / when deep is true, callback will be triggered when age is updated; if deep is false,age update, callback {deep: true} will not be triggered)
The run function calls get to get the latest value. In the get function, first call the pushTarget function to attach the current Watcher to the global Dep.target, and then execute getter to get the latest value. In the finally module, if deep is true, then calling traverse to recursively traverse the latest value,value may be Object or Array, so you need to traverse the sub-attribute and trigger its getter function, and append its dep attribute to Dep.target (current Watcher), so that any change in the value of the sub-attribute will be notified to the current watcher. As to why, you can review the previous article "how to detect the state of data by Vue".
Export default class Watcher {/ * execute getter and re-collect dependencies * / get () {/ / attach the current Watcher to the global Dep.target And store pushTarget (this) let value const vm = this.vm try {/ / execute getter read value value = this.getter.call (vm, vm)} catch (e) {if (this.user) {handleError (e, vm, `getter for watcher "${this.expression}" `)} else {throw e} finally {/ / if deep is true Traverse + Recursive value object / / append the dep of all nested attributes to the current watcher, and the corresponding dep of all child attributes will recursively traverse all nested attributes from push (Dep.target) if (this.deep) {/ / and trigger their getter Append its corresponding dep to the current watcher traverse (value)} / / exit stack popTarget () / / clear the dependency this.cleanupDeps ()} return value}}
We can explain why traverse recursive traversal sub-attributes are performed in the get function. For example, {person: {age: 18, sex: 0, addr: {city: 'Beijing', detail: 'Wudaokou'}} are defined in data. Vue will call observe to convert person to the following Observer object, and sub-attributes (if they are objects) will also be converted to Observer objects. Simple attributes will define get and set functions.
When watcher.get executes the traverse function, it recursively traverses the child attributes. When traversing to the addr attribute, it triggers the get function, which will call its dep.depend to append the current Watcher to the dependency, so that we are executing this.person.age = 18, and its set function calls dep.notify to trigger the update function of watcher to monitor the person object.
Get: function reactiveGetter () {const value = getter? Getter.call (obj): val if (Dep.target) {dep.depend ()} return value} set: function reactiveSetter (newVal) {... Dep.notify ()} at this point, the study on "what is the implementation principle of Watcher and Scheduler in Vue" is over. I hope to be able to solve your doubts. The collocation of theory and practice can better help you learn, go and try it! If you want to continue to learn more related knowledge, please continue to follow the website, the editor will continue to work hard to bring you more practical articles!
Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.
Views: 0
*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.
Continue with the installation of the previous hadoop.First, install zookooper1. Decompress zookoope
"Every 5-10 years, there's a rare product, a really special, very unusual product that's the most un
© 2024 shulou.com SLNews company. All rights reserved.