React和Vue中监听变量变化的方法


Posted in Javascript onNovember 14, 2018

React 中

本地调试React代码的方法

yarn build

场景

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

16之前

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

16之后

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

getDerivedStateFromProps触发条件

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

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

setState 在 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');
};
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.js classComponentUpdater

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() 方法,而这个方法实际就是改写 set 和 get 方法

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

参考

如何阅读React源码

React 16.3 ~ React 16.5 一些比较重要的改动

总结

以上所述是小编给大家介绍的React和Vue中监听变量变化的方法,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
JavaScript 事件属性绑定带参数的函数
Mar 13 Javascript
基于JQuery的密码强度验证代码
Mar 01 Javascript
编写可维护面向对象的JavaScript代码[翻译]
Feb 12 Javascript
jquery $.ajax相关用法分享
Mar 16 Javascript
jQuery中的val()示例应用
Feb 26 Javascript
使用Sticker.js实现贴纸效果
Jan 28 Javascript
jQuery实现邮箱下拉列表自动补全功能
Sep 08 Javascript
js遍历获取表格内数据的方法(必看)
Apr 06 Javascript
详解微信小程序 登录获取unionid
Jun 27 Javascript
Angularjs自定义指令实现分页插件(DEMO)
Sep 16 Javascript
Vue实现根据hash高亮选项卡
May 27 Javascript
vue打包npm run build时候界面报错的解决
Aug 13 Javascript
详解jQuery获取特殊属性的值以及设置内容
Nov 14 #jQuery
浅谈vue中关于checkbox数据绑定v-model指令的个人理解
Nov 14 #Javascript
js html实现计算器功能
Nov 13 #Javascript
JavaScript使用类似break机制中断forEach循环的方法
Nov 13 #Javascript
小程序登录态管理的方法示例
Nov 13 #Javascript
Vuex 使用 v-model 配合 state的方法
Nov 13 #Javascript
vue代码分割的实现(codesplit)
Nov 13 #Javascript
You might like
php_screw安装使用教程(另一个PHP代码加密实现)
2014/05/29 PHP
php实现通用的从数据库表读取数据到数组的函数实例
2015/03/21 PHP
jquery退出each循环的写法
2014/02/26 Javascript
jquery的each方法使用示例分享
2014/03/25 Javascript
如何让浏览器支持jquery ajax load 前进、后退功能
2014/06/12 Javascript
使用jQuery.wechat构建微信WEB应用
2014/10/09 Javascript
移动设备web开发首选框架:zeptojs介绍
2015/01/29 Javascript
jQuery DOM插入节点操作指南
2015/03/03 Javascript
基于BootStrap的文本编辑器组件Summernote
2017/10/27 Javascript
p5.js入门教程和基本形状绘制
2018/03/15 Javascript
详解node.js的http模块实例演示
2018/07/12 Javascript
JS字典Dictionary类定义与用法示例
2019/02/01 Javascript
详解如何使用router-link对象方式传递参数?
2019/05/02 Javascript
js 计数排序的实现示例(升级版)
2020/01/12 Javascript
vue中使用WX-JSSDK的两种方法(推荐)
2020/01/18 Javascript
在 Vue 中使用 JSX 及使用它的原因浅析
2020/02/10 Javascript
多页vue应用的单页面打包方法(内含打包模式的应用)
2020/06/11 Javascript
[51:28]EG vs Mineski 2018国际邀请赛小组赛BO2 第一场 8.16
2018/08/16 DOTA
使用Python进行二进制文件读写的简单方法(推荐)
2016/09/12 Python
Python实现好友全头像的拼接实例(推荐)
2017/06/24 Python
python GUI实例学习
2017/11/21 Python
Python3.6笔记之将程序运行结果输出到文件的方法
2018/04/22 Python
Python 普通最小二乘法(OLS)进行多项式拟合的方法
2018/12/29 Python
python实现在cmd窗口显示彩色文字
2019/06/24 Python
python通过TimedRotatingFileHandler按时间切割日志
2019/07/17 Python
linux面试题参考答案(9)
2015/01/07 面试题
环保倡议书50字
2014/05/15 职场文书
小学教师培训方案
2014/06/09 职场文书
门面房租房协议书
2014/12/01 职场文书
年度考核表个人总结
2015/03/06 职场文书
兴趣班停课通知
2015/04/24 职场文书
2015年教研工作总结
2015/05/23 职场文书
2019年浪漫婚礼证婚词
2019/06/27 职场文书
劳务派遣管理制度(样本)
2019/08/23 职场文书
解析redis hash应用场景和常用命令
2021/08/04 Redis
python中取整数的几种方法
2021/11/07 Python