In the React

A method for native debugging React code

  • Download the React code locally and enter the project folderyarn build
  • Create your own project using create-react-app
  • Connect the react source code to the project you just created. Put the build source code in the build folder and CD it to the Build folder in the React folder. There is the node_modules folder. Go to that folder. The react folder and the React -dom folder are found. Go to these two folders separately. Run Yarn Link respectively. At this point, two shortcuts are created. React and react to the dom
  • CD Go to the directory of your project and run yarn Link react react-dom. Use the React source code for build files in your project. If you make changes to the React source code, refresh the project and it will be included in your project.

scenario

Consider A scenario where the parent component passes an A parameter to the child component, and the child component needs to listen for changes in the A parameter to convert to state.

16 before

We can use before the React componentWillReveiveProps to monitor props transformation

After 16

In React, getDerivedStateFromProps can be used to listen on props. GetDerivedStateFromProps can return either null or an object. If it is an object, the state is updated

GetDerivedStateFromProps Trigger condition

Our goal is to find the trigger condition for getDerivedStateFromProps

We know that a call to setState will trigger getDerivedStateFromProps, and the same value will trigger getDerivedStateFromProps(after version 16.3).

SetState is in react.development.js

Component.prototype.setState = function (partialState, callback) {
  !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null)? invariant(false.'setState(...) : takes an object of state variables to update or a function which returns an object of state variables.') : void 0;
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
Copy the code
ReactNoopUpdateQueue {
    / /... Part of the omitted
    
    enqueueSetState: function (publicInstance, partialState, callback, callerName) {
    warnNoop(publicInstance, 'setState'); }}Copy the code

A warning method is executed

function warnNoop(publicInstance, callerName) {{// The instance constructor
    var _constructor = publicInstance.constructor;
    var componentName = _constructor && (_constructor.displayName || _constructor.name) || 'ReactClass';
    // Form a key component name + method name (column such as setState)
    var warningKey = componentName + '. ' + callerName;
    // If a warning has already been printed, it will not be printed again
    if (didWarnStateUpdateForUnmountedComponent[warningKey]) {
      return;
    }
    // Exporting warning logs in the developer tool terminal cannot be called directly with component.setstate
    warningWithoutStack$1(false."Can't call %s on a component that is not yet mounted. " + 'This is a no-op, but it might indicate a bug in your application. ' + 'Instead, assign to `this.state` directly or define a `state = {}; ` ' + 'class property with the desired state in the %s component.', callerName, componentName);
    didWarnStateUpdateForUnmountedComponent[warningKey] = true; }}Copy the code

ReactNoopUpdateQueue appears to be an abstract class, and the actual method is not implemented here. Also, if we look at the initial updater assignment, the actual updater is passed in when we initialize the Component

function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}
Copy the code

We print this in the constructor of the component

class App extends Component {
  constructor(props) {
    super(props);
    / /.. omit

    console.log('constructor'.this); }}Copy the code

The method points to the classComponentUpdater in the react-dom.development.js

var classComponentUpdater = {
  // Whether to render
  isMounted: isMounted,
  enqueueSetState: function(inst, payload, callback) {
    / / inst is fiber
    inst = inst._reactInternalFiber;
    // Get the time
    var currentTime = requestCurrentTime();
    currentTime = computeExpirationForFiber(currentTime, inst);
    // Initializes an identity object based on the update time
    var update = createUpdate(currentTime);
    update.payload = payload;
    void 0! == callback &&null! == callback && (update.callback = callback);// Queue the update task to queue
    enqueueUpdate(inst, update);
    //
    scheduleWork(inst, currentTime);
  },
  / /.. omit
}
Copy the code

EnqueueUpdate is to queue the update task

function enqueueUpdate(fiber, update) {
  var alternate = fiber.alternate;
  // If alternat is empty and the update queue is empty, the update queue is created
  if (null === alternate) {
    var queue1 = fiber.updateQueue;
    var queue2 = null;
    null === queue1 &&
      (queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState));
  } else

    (queue1 = fiber.updateQueue),
      (queue2 = alternate.updateQueue),
      null === queue1
        ? null === queue2
          ? ((queue1 = fiber.updateQueue = createUpdateQueue(
              fiber.memoizedState
            )),
            (queue2 = alternate.updateQueue = createUpdateQueue(
              alternate.memoizedState
            )))
          : (queue1 = fiber.updateQueue = cloneUpdateQueue(queue2))
        : null === queue2 &&
          (queue2 = alternate.updateQueue = cloneUpdateQueue(queue1));
  null === queue2 || queue1 === queue2
    ? appendUpdateToQueue(queue1, update)
    : null === queue1.lastUpdate || null === queue2.lastUpdate
      ? (appendUpdateToQueue(queue1, update),
        appendUpdateToQueue(queue2, update))
      : (appendUpdateToQueue(queue1, update), (queue2.lastUpdate = update));
}
Copy the code

Let’s look at scheduleWork

function scheduleWork(fiber, expirationTime) {
  // Get the root node
  var root = scheduleWorkToRoot(fiber, expirationTime);
  null! == root && (! isWorking &&0! == nextRenderExpirationTime && expirationTime < nextRenderExpirationTime && ((interruptedBy = fiber), resetStack()), markPendingPriorityLevel(root, expirationTime), (isWorking && ! isCommitting$1 && nextRoot === root) ||
      requestWork(root, root.expirationTime),
    nestedUpdateCount > NESTED_UPDATE_LIMIT &&
      ((nestedUpdateCount = 0), reactProdInvariant("185")));
}
Copy the code
function requestWork(root, expirationTime) {
  // Record the root that needs to be rendered
  addRootToSchedule(root, expirationTime);
  if (isRendering) {
    // Prevent reentrancy. Remaining work will be scheduled at the end of
    // the currently rendering batch.
    return;
  }

  if (isBatchingUpdates) {
    // Flush work at the end of the batch.
    if (isUnbatchingUpdates) {
      / /... unless we're inside unbatchedUpdates, in which case we should
      // flush it now.
      nextFlushedRoot = root;
      nextFlushedExpirationTime = Sync;
      performWorkOnRoot(root, Sync, true);
    }
    // The setState() process is complete
    return;
  }

  // TODO: Get rid of Sync and use current time?
  if (expirationTime === Sync) {
    performSyncWork();
  } else{ scheduleCallbackWithExpirationTime(root, expirationTime); }}Copy the code

PerformSyncWork is executed after setState, followed by the following order of execution

performSyncWork => performWorkOnRoot => renderRoot => workLoop => performUnitOfWork => beginWork => applyDerivedStateFromProps

The ultimate method is execution

function applyDerivedStateFromProps(workInProgress, ctor, getDerivedStateFromProps, nextProps) {
  var prevState = workInProgress.memoizedState;
      {
        if (debugRenderPhaseSideEffects || debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) {
          // Invoke the function an extra time to help detect side-effects.getDerivedStateFromProps(nextProps, prevState); }}// Get the changed state
      var partialState = getDerivedStateFromProps(nextProps, prevState);
      {
        // Warn against some bad formats
        warnOnUndefinedDerivedState(ctor, partialState);
      } // Merge the partial state and the previous state.
      // Check whether the format returned by getDerivedStateFromProps is empty or if it is not, the original state will be merged with its return value
      var memoizedState = partialState === null || partialState === undefined ? prevState : _assign({}, prevState, partialState);
      / / set the state
      // Once the update queue is empty, the derived state is left in the base state
      workInProgress.memoizedState = memoizedState; // Once the update queue is empty, persist the derived state onto the
      // base state.
      var updateQueue = workInProgress.updateQueue;

      if(updateQueue ! = =null&& workInProgress.expirationTime === NoWork) { updateQueue.baseState = memoizedState; }}Copy the code

Vue

Vue relies on watch to listen for variable changes, so let’s take a look at the source code to see where watch is triggered.

Watch trigger condition

InitState () in SRC /core/instance

/core/instance/state.js

InitData () registers each vue’s data with objerServer during data initialization

function initData (vm: Component) {
  / /... Omit some code
  
  // observe data
  observe(data, true /* asRootData */)}Copy the code
/** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */
export function observe (value: any, asRootData: ? boolean) :Observer | void {
  if(! isObject(value) || valueinstanceof VNode) {
    return
  }
  let ob: Observer | void
  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 the observer
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
Copy the code

Take a look at the observer constructors, whether array or obj, they will eventually call this.walk().

constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__'.this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      // Iterate over each value in the array and call walk
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
Copy the code

Let’s look at the walk method. The walk method calls defineReactive() on object, which overwrites the set and get methods

/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
}
Copy the code

/core/observer/index.js defineReactive rewrites the set and get methods. If we reassign a value to a variable, we will determine whether the new value of the variable is equal to the old value. If not, Dep.notify () is triggered to call back the methods in Watch.

/** * Define a reactive property on an Object. */
export function defineReactive (obj: Object, key: string, val: any, customSetter? :? Function, shallow? : boolean) {
  // Dep contains the watcher array
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if((! getter || setter) &&arguments.length === 2) { 
    // If the third value is not passed. Val is fetched directly from obj based on the value of key
    val = obj[key]
  }

  letchildOb = ! shallow && observe(val)Object.defineProperty(obj, key, {
    enumerable: true.// Configurable value
    configurable: true.get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        // Generate a watcher in dep
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    // focus on set
    set: function reactiveSetter (newVal) {
      // Get the original value of the variable
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // If the value is equal, return
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return
      }
      /* eslint-enable no-self-compare */
      if(process.env.NODE_ENV ! = ='production' && customSetter) {
        // dev environment can directly customize set
        customSetter()
      }
        
      // Assign the new value
      if (setter) {
        setter.call(obj, newVal)
      } else{ val = newVal } childOb = ! shallow && observe(newVal)// Trigger the watch event
      // Dep is an array of wacher
      // Notify executes the wacher array update method, which triggers the final Watcher run method, which triggers the watch callback
      dep.notify()
    }
  })
}
Copy the code

Small program

Custom Watch

The data itself of the small program does not support Watch, but we can add it by ourselves. We can write one by referring to the writing method of Vue. watcher.js

export function defineReactive (obj, key, callbackObj, val) {
  const property = Object.getOwnPropertyDescriptor(obj, key);
  console.log(property);

  const getter = property && property.get;
  const setter = property && property.set;

  val = obj[key]

  const callback = callbackObj[key];

  Object.defineProperty(obj, key, {
    enumerable: true.get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      
      return value
    },
    set: (newVal) = > {
      console.log('start set');
      const value = getter ? getter.call(obj) : val

      if (typeof callback === 'function') {
        callback(newVal, val);
      }

      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      console.log('finish set', newVal); }}); }export function watch(cxt, callbackObj) {
  const data = cxt.data
  for (const key in data) {
    console.log(key);
    defineReactive(data, key, callbackObj)
  }
}
Copy the code

use

We did not compare the old and new values before executing the watch callback, because the variable in data was assigned in wechat, even if the value assigned to the reference variable was the same, it would be judged to be unequal due to different reference addresses. If you want to compare old and new values, you cannot use ===. Instead, you can convert obj or array to A JSON string before comparing.

//index.js
// Get the application instance
const app = getApp()

import {watch} from '.. /.. /utils/watcher';

Page({
  data: {
    motto: 'hello world'.userInfo: {},
    hasUserInfo: false.canIUse: wx.canIUse('button.open-type.getUserInfo'),
    tableData: []},onLoad: function () {
    this.initWatcher();
  },
  initWatcher () {
    watch(this, {
      motto(newVal, oldVal) {
        console.log('newVal', newVal, 'oldVal', oldVal);
      },

      userInfo(newVal, oldVal) {
        console.log('newVal', newVal, 'oldVal', oldVal);
      },

      tableData(newVal, oldVal) {
        console.log('newVal', newVal, 'oldVal', oldVal); }}); }, onClickChangeStringData() {this.setData({
      motto: 'hello'
    });
  },
  onClickChangeObjData() {
    this.setData({
      userInfo: {
        name: 'helo'}}); }, onClickChangeArrayDataA() {const tableData = [];
    this.setData({ tableData }); }})Copy the code

reference

  • How to read the React source code
  • React 16.3 ~ React 16.5 Some important changes

Spread the word

This article is published in Front of Mint Weekly. Welcome to Watch & Star.

Welcome to the discussion. Give it a thumbs up before we go. ◕ ‿ ◕. ~