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

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中监听变量变化的方法,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • 在 React、Vue项目中使用SVG的方法

    在一些现代的扁平化设计网站,特别是移动端网站,经常会包含许多简单而清晰的小图标,例如网站图标.用户的默认头像.移动端网页首页底部固定的切换栏等,这些小图标一般都是由美工做好,可能会放到精灵图上,前端再进行裁切展示. 而实际上,这些简单的小图标完全没必要让美工来做,前端完全可以通过 svg使用代码把这些简单的图标画出来,并且,因为这些图标是用代码描述出来的,所以如果想要修改这些图标,例如改变图标的颜色.图标的形状.大小等,都只是改几行代码的事情,非常简单,根本无需美工返工重做. 本文不是阐述如何利

  • 干货!教大家如何选择Vue和React

    两者之间的相同之处 同样是基于组件开发的轻量级框架,同样是专注于用户界面的视图view层. 如何选择 1.1 如果喜欢用模板搭建应用(或者有这个想法)选择Vue Vue应用默认的是把markup放在HTML中,数据绑定表达式和Angular一样,采用{{}}的形式,而指令(特殊的HTML属性)用来向模板中添加功能 <div> <p>{{ message }}</p> <button v-on:click="reverseMessage">

  • Vue.js 2.0 和 React、Augular等其他前端框架大比拼

    React React 和 Vue 有许多相似之处,它们都有: 使用 Virtual DOM 提供了响应式(Reactive)和组件化(Composable)的视图组件. 保持注意力集中在核心库,伴随于此,有配套的路由和负责处理全局状态管理的库. 相似的作用域,我们会用更多的时间来讲这一块的比较.不仅我们要保持技术的准确性,同时兼顾平衡.我们指出React比Vue更好的地方,例如,他们的生态系统和丰富的自定义渲染器. React社区在这里非常积极地帮助我们实现这一平衡,特别感谢来自 React

  • 前端框架学习总结之Angular、React与Vue的比较详解

    近几年前端的技术发展很快,细分下来,主要可以分成四个方面: 1.开发语言技术,主要是ES6&7,coffeescript,typescript等: 2.开发框架,如Angular,React,Vue.js,Angular2等: 3.开发工具的丰富和前端工程化,像Grunt,Gulp,Webpack,npm,eslint,mocha这些技术: 4.前端开发范围的扩展,如服务端的nodejs,express,koa,meteor,GraphQL;移动端和跨平台的PhoneGap,ionic,Reac

  • VueJs与ReactJS和AngularJS的异同点

    React && Vue React 和 Vue 有许多相似之处,它们都有: 使用 Virtual DOM 提供了响应式(Reactive)和组件化(Composable)的视图组件. 将注意力集中保持在核心库,伴随于此,有配套的路由和负责处理全局状态管理的库. 由于有着众多的相似处,我们会用更多的时间在这一块进行比较.这里我们不只保证技术内容的准确性,同时也兼顾了平衡的考量.我们需要指出 React 比 Vue 更好的地方,像是他们的生态系统和丰富的自定义渲染器. React &

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

    React 中 本地调试React代码的方法 yarn build 场景 假设有这样一个场景,父组件传递子组件一个A参数,子组件需要监听A参数的变化转换为state. 16之前 在React以前我们可以使用 componentWillReveiveProps 来监听 props 的变换 16之后 在最新版本的React中可以使用新出的 getDerivedStateFromProps 进行props的监听, getDerivedStateFromProps 可以返回 null 或者一个对象,如果

  • vue中监听路由参数的变化及方法

    在vue项目中,假使我们在同一个路由下,只是改变路由后面的参数值,期望达到数据的更新. mounted: () =>{ this.id = this.$route.query.id; this.getdetail() } getDetail()方法中会用到this.id这个参数,在同一页面切换id的值,并不会触发vue的声明周期函数. 可以添加路由监听: watch: { $route: { handler() { this.id = this.$route.query.id; this.get

  • vue在App.vue文件中监听路由变化刷新页面操作

    在路由跳转时,会出现页面需要重新刷新一遍才能获取数据加载页面,这时添加一个监听器,如果跳转到页面刷新一次. export default { name: 'App', provide(){ return{ reload:this.reload } }, data(){ return { isRouterAlive:true, } }, //监听器 watch: { // 方法1 '$route' (to, from) { //监听路由是否变化 // console.log(999) if(to.

  • Vue 实时监听窗口变化 windowresize的两种方法

    下面给大家分享两种方法来介绍Vue 实时监听窗口变化 windowresize,具体内容如下所示: 方法一: First-step : 定义变量 data(){ return{ formLabelWidth : '123px' } }, Second-step:   根据生命周期 在mounted 中绑定 窗口变化 mounted(){ const that = this window.onresize = () => { return (() => { window.screenWidth

  • RecyclerView中监听EditText变化的BUG的解决方法

    需求:有一个列表,列表中有一个edittext(只能输整形),外部有一个整形变量Int,每次改变列表中其中一项的edittext的值时,外部的Int都会改变. 既然这样,我们就需要对edittext进行addTextChangedListener监听,一般做法是在afterTextChanged中对外部进行循环累加,但是想想,每一次你改变edittext都要进行一次时间复杂度为n的循环的话,想想就觉得这个算法很那啥,所以我想了另一个算法,每次改变其中一个item的值时,用总的值减去原item的e

  • elementUi vue el-radio 监听选中变化的实例代码

    elementUi vue el-radio 监听选中变化的实例代码如下所述: //change是radio-group标签的事件 <div> <el-radio-group v-model="radioSex" @change="changeHandler"> <el-radio class="radio" label="man">男</el-radio> <el-rad

  • vue.js使用watch监听路由变化的方法

    watch除了可以监听数据的变化,路由的变化也能被其监听到 效果如下: 具体代码 当路由发生变化后,在watch中写具体的业务逻辑 let vm = new Vue({ el: "#app", data: {}, router, watch: { '$route.path': function (newVal, oldVal) { if (newVal === '/login') { console.log('欢迎进入登录页面'); } if (newVal === '/registe

  • vue自定义键盘信息、监听数据变化的方法示例【基于vm.$watch】

    本文实例讲述了vue自定义键盘信息.监听数据变化的方法.分享给大家供大家参考,具体如下: @keydown.up @keydown.enter @keydown.a/b/c.... 自定义键盘信息: Vue.directive('on').keyCodes.ctrl=17; Vue.directive('on').keyCodes.myenter=13; @keydown.a/b/c.... <input type="text" @keydown.c="show&quo

  • 解决vue中监听input只能输入数字及英文或者其他情况的问题

    如下所示: 1. <input placeholder="请输入条形码" class="disPriceStyle" v-model = 'searcBarCode'></input>. (只能输入数字,英文) <input placeholder="请输入商品条形码" class="disPriceStyle" maxlength="20" v-model = 'search

  • vue中监听返回键问题

    问题:在项目中,我们常常有需求,当用户在填写表单时,点击返回的时候,我们希望加一个弹窗,确认离开吗,确认将保存为草稿 解决方案:利用 H5的 pushstate(个人理解为增加页面栈)特性与onpopup事件 分析:pushstate 从该方法名我们可以知道 是增加某种状态,会为history对象的length增加长度, 点击返回键的时候会触发onpopup事件(可以联想到 pop其实是数组删除的最后一个元素,符合  '栈' 的思想): onpopup触发返回上一页,而 pushstate仅仅是

随机推荐