zoukankan      html  css  js  c++  java
  • React和Vue中,是如何监听变量变化的

    React 中事件监听

    本地调试React代码的方法

    • 先将React代码下载到本地,进入项目文件夹后yarn build
    • 利用create-react-app创建一个自己的项目
    • 把react源码和自己刚刚创建的项目关联起来,之前build源码到build文件夹下面,然后cd到react文件夹下面的build文件夹下。里面有node_modules文件夹,进入此文件夹。发现有react文件夹和react-dom文件夹。分别进入到这两个文件夹。分别运行yarn link。此时创建了两个快捷方式。react和react-dom
    • cd到自己项目的目录下,运行yarn link react react-dom 。此时在你项目里就使用了react源码下的build的相关文件。如果你对react源码有修改,就刷新下项目,就能里面体现在你的项目里。

    场景

    假设有这样一个场景,父组件传递子组件一个A参数,子组件需要监听A参数的变化转换为state。

    16之前

    在React以前我们可以使用componentWillReveiveProps来监听props的变换

    16之后

    在最新版本的React中可以使用新出的getDerivedStateFromProps进行props的监听,getDerivedStateFromProps可以返回null或者一个对象,如果是对象,则会更新state

    getDerivedStateFromProps触发条件

    我们的目标就是找到 getDerivedStateFromProps的 触发条件

    我们知道,只要调用setState就会触发getDerivedStateFromProps,并且props的值相同,也会触发getDerivedStateFromProps(16.3版本之后)

    setStatereact.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');
    };
    
    
    ReactNoopUpdateQueue {
        //...部分省略
        
        enqueueSetState: function (publicInstance, partialState, callback, callerName) {
        warnNoop(publicInstance, 'setState');
      }
    }
    

    执行的是一个警告方法

    
    function warnNoop(publicInstance, callerName) {
      {
        // 实例的构造体
        var _constructor = publicInstance.constructor;
        var componentName = _constructor && (_constructor.displayName || _constructor.name) || 'ReactClass';
        // 组成一个key 组件名称+方法名(列如setState)
        var warningKey = componentName + '.' + callerName;
        // 如果已经输出过警告了就不会再输出
        if (didWarnStateUpdateForUnmountedComponent[warningKey]) {
          return;
        }
        // 在开发者工具的终端里输出警告日志 不能直接使用 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;
      }
    }
    

    看来ReactNoopUpdateQueue是一个抽象类,实际的方法并不是在这里实现的,同时我们看下最初updater赋值的地方,初始化Component时,会传入实际的updater

    
    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;
    }
    

    我们在组件的构造方法当中将this进行打印

    
    class App extends Component {
      constructor(props) {
        super(props);
        //..省略
    
        console.log('constructor', this);
      }
    }
    

    方法指向的是,在react-dom.development.jsclassComponentUpdater

    
    var classComponentUpdater = {
      // 是否渲染
      isMounted: isMounted,
      enqueueSetState: function(inst, payload, callback) {
        // inst 是fiber
        inst = inst._reactInternalFiber;
        // 获取时间
        var currentTime = requestCurrentTime();
        currentTime = computeExpirationForFiber(currentTime, inst);
        // 根据更新时间初始化一个标识对象
        var update = createUpdate(currentTime);
        update.payload = payload;
        void 0 !== callback && null !== callback && (update.callback = callback);
        // 排队更新 将更新任务加入队列当中
        enqueueUpdate(inst, update);
        //
        scheduleWork(inst, currentTime);
      },
      // ..省略
    }
    

    enqueueUpdate
    就是将更新任务加入队列当中

    
    function enqueueUpdate(fiber, update) {
      var alternate = fiber.alternate;
      // 如果alternat为空并且更新队列为空则创建更新队列
      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));
    }
    

    我们看scheduleWork下

    
    function scheduleWork(fiber, expirationTime) {
      // 获取根 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")));
    }
    
    
    function requestWork(root, expirationTime) {
      // 将需要渲染的root进行记录
      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);
        }
        // 执行到这边直接return,此时setState()这个过程已经结束
        return;
      }
    
      // TODO: Get rid of Sync and use current time?
      if (expirationTime === Sync) {
        performSyncWork();
      } else {
        scheduleCallbackWithExpirationTime(root, expirationTime);
      }
    }
    

    太过复杂,一些方法其实还没有看懂,但是根据断点可以把执行顺序先理一下,在setState之后会执行performSyncWork,随后是如下的一个执行顺序

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

    最终方法是执行

    
    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);
            }
          }
          // 获取改变的state
          var partialState = getDerivedStateFromProps(nextProps, prevState);
          {
            // 对一些错误格式进行警告
            warnOnUndefinedDerivedState(ctor, partialState);
          } // Merge the partial state and the previous state.
          // 判断getDerivedStateFromProps返回的格式是否为空,如果不为空则将由原的state和它的返回值合并
          var memoizedState = partialState === null || partialState === undefined ? prevState : _assign({}, prevState, partialState);
          // 设置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;
          }
    }
    

    Vue

    vue监听变量变化依靠的是watch,因此我们先从源码中看看,watch是在哪里触发的。

    Watch触发条件

    src/core/instance中有initState()

    /core/instance/state.js

    在数据初始化时initData(),会将每vue的data注册到objerserver

    
    function initData (vm: Component) {
      // ...省略部分代码
      
      // observe data
      observe(data, true /* asRootData */)
    }
    
    
    /**
     * 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) || value instanceof 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
      ) {
        // 创建observer
        ob = new Observer(value)
      }
      if (asRootData && ob) {
        ob.vmCount++
      }
      return ob
    }
    

    来看下observer的构造方法,不管是array还是obj,他们最终都会调用的是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)
          // 遍历array中的每个值,然后调用walk
          this.observeArray(value)
        } else {
          this.walk(value)
        }
      }
    

    我们再来看下walk方法,walk方法就是将object中的执行defineReactive()方法,而这个方法实际就是改写setget方法

    
    /**
    * 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])
        }
    }
    

    /core/observer/index.js
    defineReactive方法最为核心,它将set和get方法改写,如果我们重新对变量进行赋值,那么会判断变量的新值是否等于旧值,如果不相等,则会触发dep.notify()从而回调watch中的方法。

    
    /**
     * Define a reactive property on an Object.
     */
    export function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean
    ) {
      // dep当中存放的是watcher数组 
      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) { 
        // 如果第三个值没有传。那么val就直接从obj中根据key的值获取
        val = obj[key]
      }
    
      let childOb = !shallow && observe(val)
        
        Object.defineProperty(obj, key, {
        enumerable: true,
        // 可设置值
        configurable: true,
        get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
          if (Dep.target) {
            // dep中生成个watcher
            dep.depend()
            if (childOb) {
              childOb.dep.depend()
              if (Array.isArray(value)) {
                dependArray(value)
              }
            }
          }
          return value
        },
        // 重点看set方法
        set: function reactiveSetter (newVal) {
          // 获取变量原始值
          const value = getter ? getter.call(obj) : val
          /* eslint-disable no-self-compare */
          // 进行重复值比较 如果相等直接return
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          /* eslint-enable no-self-compare */
          if (process.env.NODE_ENV !== 'production' && customSetter) {
            // dev环境可以直接自定义set
            customSetter()
          }
            
          // 将新的值赋值
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          childOb = !shallow && observe(newVal)
          // 触发watch事件
          // dep当中是一个wacher的数组
          // notify会执行wacher数组的update方法,update方法触发最终的watcher的run方法,触发watch回调
          dep.notify()
        }
      })
    }
    

    小程序

    自定义Watch

    小程序的data本身是不支持watch的,但是我们可以自行添加,我们参照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)
      }
    }
    

    使用

    我们在执行watch回调前没有对新老赋值进行比较,原因是微信当中对data中的变量赋值,即使给引用变量赋值还是相同的值,也会因为引用地址不同,判断不相等。如果想对新老值进行比较就不能使用===,可以先对obj或者array转换为json字符串再比较。

    
    //index.js
    //获取应用实例
    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
        });
      }
    })
    
    

    参考

    广而告之

    本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

    欢迎讨论,点个赞再走吧 。◕‿◕。 ~

  • 相关阅读:
    MySQL 快速删除大量数据(千万级别)的几种实践方案——附源码
    Elasticsearch 通过Scroll遍历索引,构造pandas dataframe 【Python多进程实现】
    MySQL LOAD DATA INFILE—从文件(csv、txt)批量导入数据
    【Java】 NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException、ArrayIndexOutOfBoundsException、ArrayStoreException、ArithmeticException等没有异常堆栈信息
    技术人“结构化思维”训练的一点想法和实践
    gitlab内存消耗大,频繁出现502错误的解决办法
    Tesseract-OCR 4.1.0 安装和使用— windows及CentOS
    Tika结合Tesseract-OCR 实现光学汉字识别(简体、宋体的识别率百分之百)—附Java源码、测试数据和训练集下载地址
    记一次Elasticsearch OOM(内存溢出)的优化过程—基于segments force merge 和 store type 转为 hybridfs
    ElasticSearch如何一次查询出全部数据——基于Scroll
  • 原文地址:https://www.cnblogs.com/lalalagq/p/9956845.html
Copyright © 2011-2022 走看看