This is the third day of my participation in the August More Text Challenge

directory

NextTick $set $del () nextTick $set $del ()Copy the code

Vue 2.6.14 is used as an example. Space is limited, only excerpted key source code display

I. Implementation principle

To summarize, not everyone likes the bushi source code.

Official picture town building

  • When you pass a normal JavaScript object to a Vue instance as the data option, Vue will iterate over all of the object’s properties and use theObject.definePropertyConvert all of these propertiesgetter/setter
  • Each of these components has its own watcher instance, which, from the brief introduction in the previous article, is what this is all aboutrender-watcher, which records “touched” data properties as dependencies during component rendering. Watcher is then notified when the setter for the dependency fires, causing its associated component to be re-rendered
  • Form tags (input, SELECT, and so on) are bound at compile timeinputMethod to trigger an event to modify the corresponding data after the user finishes editing

Two, basic knowledge

Design patterns

There are many articles on the web about whether Vue’s two-way data binding is an “observer” or a “publish and subscribe” model. To answer this question, it is important to understand the following two models.

First of all, it should be made clear that there is a difference between these two modes. The difference mainly lies in the role of Broker.

In Observer mode, the instance object of the changed() method is a Subject, or Observable, which simply maintains a collection of observers that implement the same interface, The Subject only needs to know which unified method needs to be invoked when notifying the Observer; the two are loosely coupled





Observer model


In the publish-subscribe model, the publisher does not directly inform the subscriber that the two do not know each other. Publishers only need to send messages to the Broker, and subscribers need to read (subscribe) messages from the Broker.





Publish and subscribe model


Speaking of which mode do you think Vue data binding should fall into?


Strictly speaking, the Vue data binding model is not universal, and it is difficult to say which one it belongs to. If anything, it should be closer to the publish-subscribe model.

  • Publisher: setter

    • When data is updated, the set function is triggered and the set function notifies the Dep to perform notify
  • Broker: Dep

    • Responsible for collecting dependencies and executing notify when receiving the command from the publisher, the dispatch center notifies Wather to execute the update method.
    • When Watcher calls its update method, it essentially executes the callback it got when it was new Watcher.
  • Subscriber: getter

    • Subscribe/collect Wather

Object.defineProperty

The object.defineProperty () method directly defines a new property on an Object, or modifies an existing property of an Object, and returns the Object.

Syntax: Object.defineProperty(obj, prop, Descriptor)

  • Obj: Object for which attributes are to be defined.
  • Prop: The name or Symbol of the property to be defined or modified.
  • Descriptor: Property descriptor to define or modify.

Descriptors have the following options

describe The default value
configurable When true, the descriptor of the property can be changed and deleted from the corresponding object false
enumerable True indicates that the property is enumerable false
value The value corresponding to this property undefined
writable Indicates whether the property is writable; When true, value can be modified false
get Property getter function; When the property is accessed, this function is called and the return value is the value of the property; No arguments are passed, but this object is passed (because of inheritance, this is not necessarily the object that defines the property) undefined
set Setter functions for property; This function is called when the property value is modified; This method takes an argument (that is, the new value being assigned) and passes in the this object at the time of assignment undefined

Set cannot detect adding or deleting objects or modifying arrays

Three, architecture,

As we all know, Vue is an MVVM framework, but what is an MVVM framework?





Image credit: Wikipedia


As shown in the figure above, MVVM consists of three parts: View, ViewModel, and Model. Designed to leverage data binding to better facilitate the separation of view layer development from the rest of the pattern.

Data binding in Vue is bidirectional, that is, changes made to data at the view layer also modify data at the model layer, and changes made to model-layer data are directly reflected in the view. Of course this is all done by the framework.

View -> Model

Let’s use an example here

<div id="app"></div>
<script>
  new Vue({
    el: "#app".template: '<input v-model="msg"/>'.data: () = > {
      return { msg: "hello world"}; }});</script>
Copy the code

First, the parse (SRC/Compiler /parser) method parses the template into an AST at compile time, resulting in the following

{
  "type": 1."tag": "input"."attrsList": [{"name": "v-model"."value": "msg"."start": 7."end": 20],},"attrsMap": {
    "v-model": "msg",},"rawAttrsMap": {
    "v-model": {
      "name": "v-model"."value": "msg"."start": 7."end": 20,}},"children": []."start": 0."end": 22."plain": false."hasBindings": true."directives": [{"name": "model"."rawName": "v-model"."value": "msg"."arg": null."isDynamicArg": false."start": 7."end": 20],},"static": false."staticRoot": false};Copy the code

The detailed compilation process is not the focus of this article and will not be expanded.

Generate (SRC/Compiler/codeGen) for, if, slot, etc. I don’t even have this example, so I go straight to the derective, which is v-model, and the node I use here is input. I call genDefaultModel to process model

GenDefaultModel is also important here

  • addvalueAnd has a value of(msg)
  • Add an event to determine whether it is lazy and whether the node type is rangechange,__rorinputThe last binding here isinputAnd has a value ofif($event.target.composing)return; msg=$event.target.value

After processing the model, the AST has two properties props and Events

{
  "props": [{ "name": "value"."value": "(msg)"}]."events": {
    "input": {
      "value": "if($event.target.composing)return; msg=$event.target.value"}}}Copy the code

The result is the render function, with the _c method used to create the node

function anonymous() {
  with (this) {
    return _c("input", {
      directives: [{name: "model".rawName: "v-model".value: msg, expression: "msg"},].domProps: { value: msg },
      on: {
        input: function ($event) {
          if ($event.target.composing) return; msg = $event.target.value; ,}}}); }}Copy the code

5. Model -> View

As we said in the previous article, data is initialized by calling the initData method during initialization

// state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // Check whether the format and attribute names of data are valid.// observe data
  observe(data, true /* asRootData */)}Copy the code

After retrieving data, call Observe, and this is where the dream begins

Observer

observe

Direct source code

export function observe (value: any, asRootData: ? boolean) :Observer | void {... ob =new Observer(value)
  ...
  return ob
}
Copy the code

New an Observer, passing in the data of the previous step

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number;

  constructor (value: any) {
    // step 1
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__'.this)
    // step 2.this.walk(value)
  }

  walk (obj: Object) {
    // step 3
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  ...
}
Copy the code
  • Step 1: Initialize and create a Dep instance. Maps the current Observer instance to an __ob__ property that can be read, modified, deleted, but not enumerated
  • Step 2: Call its own walk method, passing in value (data)
  • Step 3: Iterate over the data and call defineReactive

Step 1: Dep = Dep = Dep = Dep = Dep = Dep = Dep = Dep = Dep = Dep

This. Dep is not used in the Observer, but is useful in the implementation of $set and $del.

defineReactive

Focus on defineReactive

export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function, shallow? : boolean) {
  // step 1
  const dep = new Dep();
  // step 2
  const property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return;
  }
  // step 3
  const getter = property && property.get;
  const setter = property && property.set;
  if((! getter || setter) &&arguments.length === 2) {
    val = obj[key];
  }
  // step 4
  letchildOb = ! shallow && observe(val)// step 5
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter() {
      // step 6
      const value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        // step 7
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value;
    },
    set: function reactiveSetter(newVal) {
      // step 8
      const value = getter ? getter.call(obj) : val;
      // step 9
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return}...// step 10
      if(getter && ! setter)return;
      // step 11
      if (setter) {
        setter.call(obj, newVal);
      } else{ val = newVal; }...// step 12dep.notify(); }}); }Copy the code
  • Step 1: Create the Dep instance, distinguishing it from the Dep created in the Observer constructor
  • If the default object description of data is set to false, the property cannot be modified, and is directly returned
  • Step 3:
    • Caches default getters and setters for data
    • If getter does not exist or stter exists and defineReactive has only two input arguments (only obj and key), obj[key] is assigned to val
  • Shallow indicates whether only shallow listening is performed (default: shallow). Shallow indicates whether only shallow listening is performed (default: shallow).
    • If shallow is true, the subsequent ones are not executedobserve(val), childOb to false
    • If shallow is false/undefined, the following is executedobserve(val)The observe method returns an Observer instance, and childOb equals the returned Observer instance
  • Step 5: UseObject.definePropertySet the property to enumerable, modifiable, and getter and setter
  • Step 6: Trigger the getter when accessing this property
    • Check whether there is a getter for the current attribute. If there is, the getter is directly called to obtain the attribute value. If there is no getter, val obtained by step 3 is used
    • ifDep.targetIf yes, calldep.dependDo dependency collection, hereDep.targetIt points to Watcher
  • If childOb does not equal false, that is, childOb refers to an Observer instance, call dep.depend to collect the value of the attribute
  • Step 8: The setter is triggered when the value of the property is modified, and the original value of the property is obtained as in step 6
  • Step 9:
    • Check whether the newly set attribute value is consistent with the original attribute value. If the value is consistent, no action is taken
    • This also determines that the new value is not equal to itself and the original value is not equal to itself. There are only NaN and Symbol values that can occur in this case
  • Step 10: If there is a getter but no setter, return the getter directly
  • Step 11:
    • If a setter exists, call the setter to set a new property value for the property
    • If it exists, the new attribute value is assigned to the val parameter
  • Step 12: Finally call dep.notify

$set = $del = childOb

Dep

export default class Dep {
  statictarget: ? Watcher; id: number; subs:Array<Watcher>;
  constructor() {
    // step 1
    this.id = uid++;
    this.subs = [];
  }
  // step 2
  addSub(sub: Watcher) {
    this.subs.push(sub); }...// step 3
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this); }}// step 4
  notify(){...for (let i = 0, l = subs.length; i < l; i++) { subs[i].update(); }}}Copy the code
  • Step 1: Dep constructor
    • The default value of uid is 0. The value assigned to the ID is incremented by 1
    • Subs is initialized to an empty array that stores the Watcher instance
  • Step 2: Add the Watcher instance to the subs
  • If dep. target exists, call Watcher’saddDepMethod to associate the current Dep instance with Watcher
    • When creating a non-lazy watcher (lazy = false), dep.target is equal to the currently created watcher instance
  • Step 4: Iterate through subs, calling the Update method of the Watcher instance one by one

Watcher

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  // There is only a limited space to show some attributes.// step 1
  addDep(dep: Dep){... dep.addSub(this); }...// step 2
  update() {
    if (this.lazy) {
      // step 3
      this.dirty = true;
    } else if (this.sync) {
      // step 4
      this.run();
    } else {
      // step 5
      queueWatcher(this); }}... }Copy the code
  • Push the current watcher into the WATcher queue (subs) of the DEP instance
  • Step 2: Essentially execute the callback function (cb) passed in when creating the Watcher instance.
  • Step 3: If the Watcher configurationlazyIf true, willdirtySet to true, executed only if the property being listened on by the current Watcher instance is referencedevaluateThen it is set to false again, waiting for the next reference
  • Step 4:syncIndicates whether to execute immediately, and if so, execute the callback (cb) immediately.
  • Step 5: Call if lazy is false and sync is also falsequeueWatcher

This queueWatcher maintains a queue of Watcher instances, which are sorted by their IDS on the next event loop before calling the Watcher.run method one by one. How do I make these actions wait until the next event loop? This is where nextTick is used

Dependency collection is generally referred to as watcher collecting data attributes that it depends on. However, the source code implementation actually pushes the Watcher object into the WATcher queue (subs) of the Dep instance. It is more like the Dep “collecting” the Watcher.

render-watcher

In the last article, we introduced the execution process of Vue. Render – Watcher is created in the mount phase, that is, after created, before Mounted, check whether EL is configured. If el is configured, then mount it

// initMixin
if (vm.$options.el) {
  vm.$mount(vm.$options.el);
}
Copy the code

During the mount process, a new Watcher instance is created, called render- Watcher

// lifecycle.js / mountComponent
new Watcher(
  vm,
  updateComponent,
  noop,
  {
    before() {
      if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,"beforeUpdate"); }}},true /* isRenderWatcher */
);
Copy the code

This instance is attached to the _watcher property of the current component instance and is responsible for listening for data changes and updating the view. At the heart of listening for changes in data to complete view updates is render- Watcher.

user-watcher

User-watcher, as the name implies, is a watch created or configured by the user. It is created at the initialization stage of initWatch, which is responsible for processing the watch configured by the user, after beforeCreate, and before creation.

export function stateMixin (Vue: Class<Component>) {... Vue.prototype.$watch =function (
    expOrFn: string | Function, cb: any, options? :Object
  ) :Function {...const watcher = newWatcher(vm, expOrFn, cb, options) ... }}Copy the code

Similar to render-watcher, except that the user-watcher callback is defined by the user, whereas render- Watcher’s callback is used to update the view

computed-watcher

Compute-watcher is the watcher used by the computed property, and its creation also occurs during initialization, before initWatch, initComputed; Also after beforeCreate

// state.js
const computedWatcherOptions = { lazy: true };
function initComputed(vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null); .if(! isSSR) {// create internal watcher for the computed property.
    watchers[key] = newWatcher( vm, getter || noop, noop, computedWatcherOptions ); }... }Copy the code

After the Watcher is created, use Object.defineProperty to intercept getters for computed defined properties, which are fired when the properties are read.

Lazy is true when compute-watcher is created, and does not assign the current watcher instance to DEP. target, but when properties of computed are read on the page, dep. target is equal to computed-watcher, After the getter is executed, the dependencies (DEPS) are cleared, the DP. Target is again equal to the last watcher instance, and the dependencies are collected again computed-watcher

6. Extra surprises

nextTick

role

From the previous introduction, we learned that watcher’s Update method is triggered when data is changed, and queueWatcher’s method is called for changes without the sync property set, which ends up using the nextTick method

What the nextTick method does is queue all updates to the task and execute them in the same event loop

The official documentation also describes this feature:

Vue performs DOM updates asynchronously. Whenever a data change is observed, Vue opens a queue and buffers all data changes that occur in the same event loop.

(For those unfamiliar with task queues, check out my previous post on EventLoop and microtasks from the browser perspective)

The source code

Take a look at the source code

export function nextTick(cb? :Function, ctx? :Object) {
  let _resolve;
  // step 1
  callbacks.push(() = > {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, "nextTick"); }}else if(_resolve) { _resolve(ctx); }});// step 2
  if(! pending) { pending =true;
    timerFunc();
  }
  // step 3
  if(! cb &&typeof Promise! = ="undefined") {
    return new Promise((resolve) = >{ _resolve = resolve; }); }}Copy the code
  • Step 1: Push the callback function cb into the Callbacks array
  • Step 2: Pending indicates whether the callback function is currently executing
    • If pending is false, set pending to true and call timeFunc
  • Step 3: If no callback function is passed in and Promise is available, return Promise

To get into the timeFunc method, let’s take a look at flushCallbacks, which executes functions in callbacks one by one

function flushCallbacks() {
  pending = false;
  // Shallow copy callbacks
  const copies = callbacks.slice(0);
  callbacks.length = 0;
  for (let i = 0; i < copies.length; i++) {
    // Execute the functions in the callback array one by onecopies[i](); }}Copy the code

Finally, timerFunc is used to select the right solution to queue callbacks into asynchronous tasks depending on the platform

let timerFunc;
if (typeof Promise! = ="undefined" && isNative(Promise)) {
  // step 1
  const p = Promise.resolve();
  timerFunc = () = > {
    p.then(flushCallbacks);
    if (isIOS) setTimeout(noop);
  };
  isUsingMicroTask = true;
} else if (
  !isIE &&
  typeofMutationObserver ! = ="undefined" &&
  (isNative(MutationObserver) ||
    MutationObserver.toString() === "[object MutationObserverConstructor]")) {// step 2
  let counter = 1;
  // step 2-1
  const observer = new MutationObserver(flushCallbacks);
  // step 2-2
  const textNode = document.createTextNode(String(counter));
  // step 2-3
  observer.observe(textNode, {
    characterData: true});// step 2-4
  timerFunc = () = > {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
  isUsingMicroTask = true;
} else if (typeofsetImmediate ! = ="undefined" && isNative(setImmediate)) {
  // step 3
  timerFunc = () = > {
    setImmediate(flushCallbacks);
  };
} else {
  // step 4
  timerFunc = () = > {
    setTimeout(flushCallbacks, 0);
  };
}
Copy the code
  • Step 1: Judge whether Promise is available and give priority to use Promise. In order to deal with the abnormal status of microtask queue in IOS, an empty timer is set in addition
  • Step 2: If Promsie is not supported, further determine whether the MutationObserver API is supported
    • Step 2-1: Instantiate an observer object
    • Step 2-2: Create a text node
    • Step 2-3: Monitor text node changes
    • Step 2-4: Manually change the text node data. MutationObserver hears the change and immediately triggers the callbacks
  • Use setImmediate
  • Step 4: Use setTimeout

The priorities are as follows:

  1. Promise (Microtasks)
  2. MutationObserver (Microtasks)
  3. SetImmediate (Macro task)
  4. SetTimeout (macro task)

application

NextTick is useful when you need to wait for a view update to complete before performing any action

A common scenario is that when you use code to generate a new node, you immediately need to retrieve the newly rendered node

In the previous section, we learned that you can use nextTick not only as a callback function, but also as a Thenable object

So, either way is ok

Vue.$nextTick(() = > {
  console.log("from callback");
});
Copy the code
Vue.$nextTick.then(() = > {
  console.log("from then");
});
Copy the code

$set

role

Due to the limitations of Object.defineProperty, the addition of an Object attribute cannot be detected

Const a = {}; a.b = ‘test’

Therefore, Vue provides a $set method that manually changes the attributes of the object, triggering view updates.

The source code

export function set(target: Array<any> | Object, key: any, val: any) :any {...// step 1
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val;
  }
  // step 2
  if (key intarget && ! (keyin Object.prototype)) {
    target[key] = val;
    return val;
  }
  // step 3
  const ob = (target: any).__ob__;
  // step 4
  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;
  }
  // step 5
  if(! ob) { target[key] = val;return val;
  }
  // step 6
  defineReactive(ob.value, key, val);
  // step 7
  ob.dep.notify();
  return val;
}
Copy the code
  • Step 1: Check whether target is an array and key is an array subscript (whether it is a positive integer).
    • If so, it resets the array length and calls splice to replace the element corresponding to the key with val, returning val
  • Step 2: Check whether the key attribute already exists in the target, and the key is not an attribute in the Object prototype
    • If so, change target’s key property to val and return val
  • Step 3: Get target’s__ob__attribute
    • During the initialization phase, when an Observer is created from Data, the current Observer instance is mapped to__ob__Property while creating a Dep assignment todepattribute
  • Step 4:
    • The key is not an existing target property, but a new property
    • Determine whether target is the Vue instance itself. If so, return val. If a warning is raised in non-production mode, adding responsive attributes to the Vue instance is not allowed
  • Step 5: ifobIf no, the key is a non-responsive attribute. Modify the key attribute of target directly and return val
  • Step 6: PassdefineReactiveChange the target key property value to val
  • Step 7: CallobTheir owndepnotifyMethod triggers the callback function, which in this case updates the view.

When did the DEP complete the dependency collection?

Define Active adds getter and setter interceptors to each property of data during initialization. In getter interceptors, the following code snippet is included

.letchildOb = ! shallow && observe(val);Object.defineProperty(obj, key, {
  ...
  get: function reactiveGetter () {...if (Dep.target) {
      dep.depend()
      if(childOb) { childOb.dep.depend() ... }}returnvalue }, ... }...Copy the code

The childOb is the __ob__ attribute corresponding to the data attribute. If childOb exists, the deP. Depend is called for dependency collection

$delete

role

$delete also occurs because Object.defineProperty cannot detect the deletion of an Object attribute

Const a = {b:’test’}; delete a.b;

The source code

export function del (target: Array<any> | Object, key: any) {...// step 1
  const ob = (target: any).__ob__
  ...
  // step 2
  if(! hasOwn(target, key)) {return
  }
  // step 3
  delete target[key]
  // step 4
  if(! ob) {return
  }
  // step 5
  ob.dep.notify()
}
Copy the code
  • Step 1: Get target’s__ob__attribute
  • Step 2: Check whether the key attribute already exists in the target. If not, return the key attribute directly
  • Step 3: UsedeleteKeyword to complete the operation of deleting attributes
  • Step 4: ifobIf no, the key is a non-responsive attribute and is returned directly
  • Step 5: CallobTheir owndepnotifyMethod triggers the callback function, which in this case updates the view

Overriding array methods

role

Limitations of Object.defineProperty. Array changes cannot be detected directly. When Vue responds to an element in an object returned by the data method, if the element is an array, only the array itself is responsified, not the elements inside the array. Therefore, there are officially seven array methods: push(), pop(), Shift (), unshift(), splice(), sort(), reverse()

The source code

// step 1
const arrayProto = Array.prototype
// step 2
export const arrayMethods = Object.create(arrayProto)

// step 3
const methodsToPatch = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]

// step 4
methodsToPatch.forEach(function (method) {
  // step 5
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (. args) {
    // step 6
    const result = original.apply(this, args)
    // step 7
    const ob = this.__ob__
    // step 8
    let inserted
    switch (method) {
      case 'push':
        case 'unshift':
          inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // step 9
    ob.dep.notify()
    return result
  })
})
Copy the code
  • Step 1: Cache array prototypes
  • Step 2: Create a new object using arrayProto as the __proto__ of arrayMethods
  • Step 3: Array methods that need to be extended
  • Step 4: Iterate over the number group method
  • Step 5: Cache the native array method
  • Step 6: Execute and cache the results of the native array method
  • Step 7: Obtain ob attributes
  • Step 8: If yespush,unshift,spliceMethod, which involves appending elements, uses OB’s observeArray method to set the new element to reactive
  • Step 9: InvokeobTheir owndepnotifyMethod triggers a view update

The article is also posted on my official account, welcome to follow MelonField

reference

  • github.com/vuejs/vue