preface

Last time I talked about the Vue source code series, chapter 1 of the Vue source code series, processes. Now I’m going to talk about the implementation of the publish and subscribe model. I should have talked about Data, Watch, computed separately, but a lot of people have done a good job of digging gold, so I’m going to talk about it from publish and subscribe (collecting dependencies at initialization, Start with props, data, Watch, and computed.

As usual, start with a simple piece of code

// main.js
import App from './App'
new Vue({
  el: '#app'.components: { App},
  template: `<app :name="name"></app>`,
  data () {
    return {
      name: 'Wayag'}}})Copy the code
// App.js
import HelloWorld from './components/HelloWorld'
export default {
  name: 'App'.props: {
    name: {
      type: String.default: ' '}},computed: {
    total () {
      return this.num + 1}},watch: {
    total () {
      console.log(this.total)
    },
    num () {
      console.log(this.num)
    }
  },
  data () {
    return {
      num: 0}},methods: {
    add () {
      this.num++
    }
  }
}
Copy the code

_render executes _c(‘app’,{attrs:{name: ‘Wayag’}}) from vm._render to createComponent to generate the component’s constructor Sub and pass data = {attrs:{name: ‘Wayag’ } }

function createComponent (Ctor, data, context, children, tag) {
  var baseCtor = context.$options._base; // Vue
  // Vue.extend = function (extendOptions) {
  // extendOptions = extendOptions || {};
  // var Super = this;
  // var SuperId = Super.cid;
  // var Sub = function VueComponent (options) {
  // this._init(options);
  / /};
  // Sub.prototype = Object.create(Super.prototype);
  // Sub.prototype.constructor = Sub;
  // Sub.cid = cid++;
  // Sub.options = mergeOptions(
  // Super.options,
  // extendOptions
  / /);
  // if (Sub.options.props) {
  // Set the proxy to access vm.name the proxy accesses vm._props_
  // // Object.defineProperty(Sub.prototype, 'name', {
  // // get: function() {
  // // return this['_props'][key]}
  / / / /}
  // // set: function(val) {
  // // this['_props'][key]} = val
  / / / /}
  / / / /})
  // initProps$1(Sub);
  / /}
  // if (Sub.options.computed) {
  // Set up the proxy, add computed object properties to sub. prototype, and set it to responsive data
  // // Object.defineProperty(Sub.prototype, 'total', createComputedGetter)
  // initComputed$1(Sub);
  / /}

  // Sub.extend = Super.extend;
  // Sub.mixin = Super.mixin;
  // Sub.use = Super.use;

  // if (name) {
  // Sub.options.components[name] = Sub; //
  / /}

  // Sub.superOptions = Super.options;
  // Sub.extendOptions = extendOptions;
  // Sub.sealedOptions = extend({}, Sub.options);
  // cachedCtors[SuperId] = Sub;
  // return Sub
  / /};
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor); // The vue.extend function is called
  }

  data = data || {};
  // function extractPropsFromVNodeData (
  // data,
  // Ctor,
  // tag
  / /) {
  // var propOptions = Ctor.options.props; // Props defined internally by the child component
  // var res = {};
  // var attrs = data.attrs; // The argument passed by the parent component
  // var props = data.props;
  // if (isDef(attrs) || isDef(props)) {
  // for (var key in propOptions) {
            // Check whether the parent component is passed, and the child component is defined in props
  // checkProp(res, props, key, altKey, true) ||
  // checkProp(res, attrs, key, altKey, false);
  / /}
  / /}
  // return res
  / /}
  var propsData = extractPropsFromVNodeData(data, Ctor, tag); // { name: 'Wayag' }
  Hook: {init: function() {}, prepatch: function() {}, insert:
  // function() {}, destroy: function() {}}
  installComponentHooks(data); 
  var name = Ctor.options.name || tag;
  // propsData = {name: 'Wayag'}
  // vnode.componentOptions, Ctor is the constructor Sub, where data: {attrs: {}}, attrs attribute
  / / was removed in propsData assignment and data. The attrs removal is within the extractPropsFromVNodeData function implementation.
  var vnode = new VNode(
    ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : ' ')),
    data, undefined.undefined.undefined, context,
    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
    asyncFactory
  );

  return vnode
}
Copy the code

DefineProperty (target, key, {get: createComputedGetter, set: Noop}), computed attributes do not allow direct modification.

function createComputedGetter (key) {
  return function computedGetter () {
    // this._computedWatchers[key] has a value after vm._init initializes initComputed,
    // So actually changing computed properties to responsive here does not execute the computedGetter function, which is in
    // This is called only when the user Watcher is listening or when the render function is accessing the vm. Total, or computed property
    // computedGetter function, and then
    var watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) { 
        // Recalculation involves dependency collection, depending on which attribute computed Watcher depends on.
        watcher.evaluate();
      }
      // After implementing the watcher.evaluate function, the computedWatcher is removed from the stack, and dep. target is removed from the computedWatcher to the userWatcher or render watcher.
      if (Dep.target) {
        // This is the point of "matchmaker", let the data property XXX relationship with userWatcher or render watcher
        watcher.depend(); 
      }
      return watcher.value
    }
  }
}

Copy the code

Now you know how the child VM accesses the parameters passed by the parent, as shown in the following code: vm.$options.propsData

function initInternalComponent (vm, options) {
  var opts = vm.$options = Object.create(vm.constructor.options);
  var parentVnode = options._parentVnode;
  opts.parent = options.parent;
  opts._parentVnode = parentVnode;

  var vnodeComponentOptions = parentVnode.componentOptions;
  // vm.$options.propsData = parentVnode.componentOptions.propsData
  opts.propsData = vnodeComponentOptions.propsData; 
  opts._parentListeners = vnodeComponentOptions.listeners;
  opts._renderChildren = vnodeComponentOptions.children;
  opts._componentTag = vnodeComponentOptions.tag;
}
Copy the code

$options gets the parameters passed by the parent component. The premise of the publishing-subscribe model is to make data responsive. In the _init function.

function initState(vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.props) { initProps(vm, opts.props); }
  if (opts.methods) { initMethods(vm, opts.methods); }
  if (opts.data) {
    initData(vm);
  } else {
    observe(vm._data = {}, true /* asRootData */);
  }
  if (opts.computed) { initComputed(vm, opts.computed); }
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

Copy the code
// Initialize props
function initProps (vm, propsOptions) {
  var propsData = vm.$options.propsData || {};
  var props = vm._props = {};
  var loop = function ( key ) {
    keys.push(key);
    var value = validateProp(key, propsOptions, propsData, vm);
    defineReactive$$1(props, key, value);
    if(! (keyin vm)) {
      proxy(vm, "_props", key); }};for (var key in propsOptions) loop( key );
  toggleObserving(true);
}
Copy the code
// Initialize data
function initData (vm) {
  var data = vm.$options.data;
  data = vm._data
  var keys = Object.keys(data);
  while (i--) {
    var key = keys[i];
    proxy(vm, "_data", key);
  }
  observe(data, true /* asRootData */);
}

Copy the code
// Observe in initData
function observe (value, asRootData) {
  if(! isObject(value) || valueinstanceof VNode) {
    return
  }
  var ob;
  The __ob__ attribute indicates that the data has been processed in response mode and does not need to be redefined in response mode
  // Scenario: If name is an object passed in from the parent and two data references the same name object in data, then the following line is entered
  // An if statement.
  // props: {
  // name: {
  // type: Object,
  // default: () => {
  // return {}
  / /}
  / /}
  / /},
  // data () {
  // return {
  // num: 0,
  // b: this.name,
  // c: this.name
  / /}
  },
  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 ) { ob =new Observer(value);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob
}
Copy the code
// Observe function in the observe function when initData
var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  // Add an __ob__ attribute to the object to see if the data is already being listened on
  def(value, '__ob__'.this);
  if (Array.isArray(value)) {
    if (hasProto) {
      protoAugment(value, arrayMethods);
    } else {
      copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value);
  } else {
    this.walk(value); }};Copy the code
// Convert data to responsive functions, including data, and props
function defineReactive$$1 (obj, key, val, customSetter, shallow) {
  // The properties in props and data are used to collect dependent box dePs
  var dep = new Dep();
  
  var property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var getter = property && property.get;
  var setter = property && property.set;
  if((! getter || setter) &&arguments.length === 2) {
    val = obj[key];
  }

  varchildOb = ! shallow && observe(val);_data[key] [key]
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      // We use val directly unless we write getters and setters by hand
      var value = getter ? getter.call(obj) : val; 
      // New Watcher() assigns its own Watcher to dep.target =, calling props or data
      // This, so the deP collection depends on which new Watcher() is currently executed.
      / / userWatcher, renderWatcher
      if (Dep.target) { 
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) { dependArray(value); }}}return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return
      }
      if(process.env.NODE_ENV ! = ='production' && customSetter) {
        customSetter();
      }
      if(getter && ! setter) {return }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    }
  });
}
Copy the code
// Initialize computed
function initComputed (vm, computed) {
  // Define vm._computedWatchers
  var watchers = vm._computedWatchers = Object.create(null);
  // computed properties are just getters during SSR
  var isSSR = isServerRendering();
  // debugger
  for (var key in computed) {
    var userDef = computed[key];
    var getter = typeof userDef === 'function' ? userDef : userDef.get;
    if(process.env.NODE_ENV ! = ='production' && getter == null) {
      warn(
        ("Getter is missing for computed property \"" + key + "\"."),
        vm
      );
    }

    if(! isSSR) {// Add computedWatcher to vm._computedWatchers, add attributes in computed to
      // vm._computedWatchers
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      );
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if(! (keyin vm)) {
      defineComputed(vm, key, userDef);
    } else if(process.env.NODE_ENV ! = ='production') {
      if (key in vm.$data) {
        warn(("The computed property \"" + key + "\" is already defined in data."), vm);
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(("The computed property \"" + key + "\" is already defined as a prop."), vm); }}}}Copy the code
// Initialize watch
 function initWatch (vm, watch) {
  for (var key in watch) {
    var handler = watch[key];
    if (Array.isArray(handler)) {
      for (var i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]); }}else{ createWatcher(vm, key, handler); }}}function createWatcher (vm, expOrFn, handler, options) {
    if (isPlainObject(handler)) {
      options = handler;
      handler = handler.handler;
    }
    if (typeof handler === 'string') {
      handler = vm[handler];
    }
    return vm.$watch(expOrFn, handler, options)
  }

  Vue.prototype.$watch = function (expOrFn, cb, options) {
    var vm = this;
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {};
    options.user = true;
    var watcher = new Watcher(vm, expOrFn, cb, options);
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value);
      } catch (error) {
        handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\" ")); }}return function unwatchFn () { watcher.teardown(); }};Copy the code
/ / Watcher function
var Watcher = function Watcher (vm, expOrFn, cb, options, isRenderWatcher) {
  this.vm = vm;
  if (isRenderWatcher) {
    vm._watcher = this;
  }
  vm._watchers.push(this);
  // options
  if (options) {
    this.deep = !! options.deep;this.user = !! options.user;this.lazy = !! options.lazy;this.sync = !! options.sync;this.before = options.before;
  } else {
    this.deep = this.user = this.lazy = this.sync = false;
  }
  this.cb = cb;
  this.id = ++uid$2; // uid for batching
  this.active = true;
  this.dirty = this.lazy; // for lazy watchers
  this.deps = [];
  this.newDeps = [];
  this.depIds = new _Set();
  this.newDepIds = new _Set();
  this.expression = process.env.NODE_ENV ! = ='production'
    ? expOrFn.toString()
    : ' ';
  // parse expression for getter
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    // The parsePath function, which is called by the watch variable and called by vm.total, is triggered
    // reactive listening done in initComputed, triggers the evaluate function, and performs the computedWatcher get function.
    // function parsePath (path) {
    // if (bailRE.test(path)) {
    // return
    / /}
    // var segments = path.split('.');
    // return function (obj) {
    // for (var i = 0; i < segments.length; i++) {
    // if (! obj) { return }
    // obj = obj[segments[i]];
    / /}
    // return obj
    / /}
    // }
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = noop; process.env.NODE_ENV ! = ='production' && warn(
        "Failed watching path: \"" + expOrFn + "\" " +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.', vm ); }}// If this is a computedWatcher, this.lazy = true will not execute this.get when the following is triggered
  // Perform this on the evaluate function (userWatcher or renderWatcher accesses computed)
  // Watcher.prototype.evaluate = function evaluate () {
  // this.value = this.get();
  // this.dirty = false;
  // };
  this.value = this.lazy
    ? undefined
    : this.get();
};

// this.get
Watcher.prototype.get = function get () {
  // When the current watcher calls get, it assigns the current watcher to dep.target
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\" "));
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
};
Copy the code

Description: The watch monitors the total attribute, or the rendering function reads the VM. Total triggers total of computed data, and the process of triggering computed data is called “matchmaking”. If the watch monitors the total attribute, That fires the evaluate function for computedWatcher, that is, watcher.prototype. get, and then executes the total function in computed, if (generally) it relies on the XXX attribute in initData, Dep. Target = computedWatcher, then computedWatcher (total) is collected into the dep. Subs of the XXX attribute. In turn, computedWatcher collects the DEP into this.deps, and then removes the computedWatcher from the stack. Dep.targer transfers the computedWatcher to userWatcher or renderWatcher. Deps collected by this.deps collect dep.target (userWatcher or renderWatcher). I collected them into xxx.subs.

Watcher.prototype.depend = function depend () {
  var i = this.deps.length;
  while (i--) {
    this.deps[i].depend(); }};Copy the code

After subscribing (the process of collecting dependencies), I move on to publishing (the process of updating data). We know that the deP of the data attribute collects subs, or subscribers, and that modifying the data attribute triggers the set function in object.defineProperty above, which only needs to do two things: 1. Set val = newVal to the value. 2. Run dep.notify() to publish data

   Object.defineProperty(vm._data, key, {
     set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return
      }
      if(process.env.NODE_ENV ! = ='production' && customSetter) {
        customSetter();
      }
      if(getter && ! setter) {return }
      if (setter) {
        setter.call(obj, newVal);
      } else{ val = newVal; } childOb = ! shallow && observe(newVal);// dep.notify(); }})Copy the code

Now let’s look at the triggering process

Dep.prototype.notify = function notify () {
  // stabilize the subscriber list first
  var subs = this.subs.slice();
  if(process.env.NODE_ENV ! = ='production' && !config.async) {
    subs.sort(function (a, b) { return a.id - b.id; });
  }
  for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); }}; Watcher.prototype.update =function update () {
  if (this.lazy) { // this.lazy is when watcher is computedWatcher, only this.dirty = is required
                   // False will do, and recalculation can be triggered later when the computed property is accessed.
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else { // Other userWatcher or renderWatcher conditions will enter here
    queueWatcher(this); }};function queueWatcher (watcher) {
  var id = watcher.id; 
  if (has[id] == null) {
    has[id] = true; // Save watcher. Id to avoid adding watcher repeatedly
    if(! flushing) { queue.push(watcher); }else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      var i = queue.length - 1;
      while (i > index && queue[i].id > watcher.id) {
        i--;
      }
      queue.splice(i + 1.0, watcher);
    }
    if(! waiting) { waiting =true;

      if(process.env.NODE_ENV ! = ='production' && !config.async) {
        flushSchedulerQueue();
        return
      }
      NextTick is a microtask that executes the flushSchedulerQueue until the next microtask (or macro task)
      // If the same data is changed several times, it will trigger the watcher to collect the data several times
      // The steps that trigger the watcher are filtered out, and adding nextTick is not a microtask (or possibly a macro task) but is performed immediately
      // Only the first modified value can be retrieved, while microtasks can get the last modified value due to asynchrony.nextTick(flushSchedulerQueue); }}}function nextTick (cb, ctx) {
  var _resolve;
  callbacks.push(function () {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, 'nextTick'); }}else if(_resolve) { _resolve(ctx); }});if(! pending) { pending =true;
    timerFunc();
  }
  // $flow-disable-line
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(function (resolve) { _resolve = resolve; }}})// timerFunc is an asynchronous, priority Promise > setImmediate > setTimeout
var timerFunc;
if (typeof Promise! = ='undefined' && isNative(Promise)) {
  var p = Promise.resolve();
  timerFunc = function () {
    p.then(flushCallbacks);
    if (isIOS) { setTimeout(noop); }}; isUsingMicroTask =true;
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  var counter = 1;
  var observer = new MutationObserver(flushCallbacks);
  var textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true
  });
  timerFunc = function () {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
  isUsingMicroTask = true;
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
  timerFunc = function () {
    setImmediate(flushCallbacks);
  };
} else {
  // Fallback to setTimeout.
  timerFunc = function () {
    setTimeout(flushCallbacks, 0);
  };
}
Copy the code
/ / flushSchedulerQueue function
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow();
  flushing = true;
  var watcher, id;

  queue.sort(function (a, b) { return a.id - b.id; });
  // Let len = queue.length, because the queue is changing, by what? Is the root
  FlushSchedulerQueue is in the microtask. In the current component, the data changes
  // The function will execute the watcher that depends on this data, and only the current component has a chance to access it. How about subcomponents that pass values to props
  // How about adding its watcher to the queue based on the data passed by the parent component? It's actually watcher.run()
  // Do this, and is the rendering watcher of the current component. To see the execution flow (from bottom to top), render Watcher to execute
  // Render (patch); prepatch (updateChildComponent)
  // props[key] = validateProp(key, propOptions, propsData, VM)
  QueueWatcher = notify -> update -> queueWatcher
  // queue.length is always changing. This process can also be said to be when the parent component's data property changes, how does the child component depend on the parent
  // Data changes while re-rendering.
  
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    // Life cycle updateBefore
    if (watcher.before) {
      watcher.before();
    }
    id = watcher.id;
    has[id] = null; / / release from the [id]
    // The main logic, this.get(), and the callback this.cb()
    watcher.run();
  }

  var activatedQueue = activatedChildren.slice();
  var updatedQueue = queue.slice();
  / / reset
  resetSchedulerState(); // has = {}, queue.length = 0
}

Watcher.prototype.run = function run () {
  if (this.active) {
    var value = this.get();
    if( value ! = =this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      var oldValue = this.value;
      this.value = value;
      // Call the ComputedWatcher and userWatcher callback functions and pass in value, which can be retrieved in the callback function
      / / to the value.
      this.cb.call(this.vm, value, oldValue); }}}; Watcher.prototype.get =function get () {
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\" "));
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
};
Copy the code