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 vvorld 在线加密破解方法
Nov 13 Javascript
基于jQuery的消息提示插件之旅 DivAlert(三)
Apr 01 Javascript
基于Jquery的仿照flash放大图片效果代码
Mar 16 Javascript
浏览器解析js生成的html出现样式问题的解决方法
Apr 16 Javascript
jquery实现弹出层登录和全屏层注册特效
Aug 28 Javascript
Bootstrap复选框和单选按钮美化插件(推荐)
Nov 23 Javascript
完美实现js焦点轮播效果(二)(图片可滚动)
Mar 07 Javascript
Django1.7+JQuery+Ajax验证用户注册集成小例子
Apr 08 jQuery
JavaScript中toLocaleString()和toString()的区别实例分析
Aug 14 Javascript
推荐一个基于Node.js的表单验证库
Feb 15 Javascript
浅谈Vue.js 关于页面加载完成后执行一个方法的问题
Apr 01 Javascript
JavaScript实现网页下拉菜单效果
Nov 20 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 清除网页病毒的方法
2008/12/05 PHP
PHP合并数组+与array_merge的区别分析
2010/08/01 PHP
PHP利用header跳转失效的解决方法
2014/10/24 PHP
php生成rss类用法实例
2015/04/14 PHP
利用PHP抓取百度阅读的方法示例
2016/12/18 PHP
PHP命名空间与自动加载机制的基础介绍
2019/08/25 PHP
模拟用户操作Input元素,不会触发相应事件
2007/05/11 Javascript
bgsound 背景音乐 的一些常用方法及特殊用法小结
2010/05/11 Javascript
jQuery ajax serialize()方法的使用以及常见问题解决
2013/01/27 Javascript
异步动态加载js与css文件的js代码
2013/09/15 Javascript
JS常用表单验证方法总结
2014/05/22 Javascript
基于javascript实现的搜索时自动提示功能
2014/12/26 Javascript
jQuery实现平滑滚动的标签分栏切换效果
2015/08/28 Javascript
JS获取文件大小方法小结
2015/12/08 Javascript
jquery跟随屏幕滚动效果的实现代码
2016/04/13 Javascript
详解关于vue-area-linkage走过的坑
2018/06/27 Javascript
[14:51]DOTA2 HEROS教学视频教你分分钟做大人-卓尔游侠
2014/06/13 DOTA
python使用urllib模块和pyquery实现阿里巴巴排名查询
2014/01/16 Python
windows系统下Python环境的搭建(Aptana Studio)
2017/03/06 Python
PyQt5 pyqt多线程操作入门
2018/05/05 Python
pandas DataFrame实现几列数据合并成为新的一列方法
2018/06/08 Python
详解Python3 基本数据类型
2019/04/19 Python
基于python分析你的上网行为 看看你平时上网都在干嘛
2019/08/13 Python
Python中*args和**kwargs的区别详解
2019/09/17 Python
Django实现网页分页功能
2019/10/31 Python
pytorch使用tensorboardX进行loss可视化实例
2020/02/24 Python
PyCharm2020最新激活码+激活码补丁(亲测最新版PyCharm2020.2激活成功)
2020/11/25 Python
大学生职业生涯规划书前言
2014/01/09 职场文书
给客户的道歉信
2014/01/13 职场文书
大学生毕业求职信
2014/06/12 职场文书
2014教师党员自我评议(5篇)
2014/09/20 职场文书
2015年学生会工作总结范文
2015/03/31 职场文书
全陪导游词开场白
2015/05/29 职场文书
读书笔记怎么写
2015/07/01 职场文书
golang 实现对Map进行键值自定义排序
2021/04/28 Golang
MySQL的存储过程和相关函数
2022/04/26 MySQL