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

  • 2022-11-23 11:00:03

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中监听变量变化的方法,希望对大家有所帮助

猜你喜欢