Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2018/11/05 - React和Vue中,是如何监听变量变化的 #22

Open
Baifann opened this issue Nov 5, 2018 · 0 comments
Open

2018/11/05 - React和Vue中,是如何监听变量变化的 #22

Baifann opened this issue Nov 5, 2018 · 0 comments

Comments

@Baifann
Copy link
Contributor

Baifann commented Nov 5, 2018

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

-w766

方法指向的是,在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 ★,转载请注明出处。

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant