开发者社区> 问答> 正文

深入解析React和Vue中监听变量变化

深入解析React和Vue中监听变量变化

展开
收起
社区秘书 2019-12-16 13:59:51 1116 0
1 条回答
写回答
取消 提交回答
  • 
    
    
    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');
     }//欢迎加入前端全栈开发交流圈一起学习交流:864305860
    }
    执行的是一个警告方法
    
    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;
    }//欢迎加入前端全栈开发交流圈一起学习交流:864305860
    我们在组件的构造方法当中将 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);
     ////欢迎加入前端全栈开发交流圈一起学习交流:864305860
     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
      )))//欢迎加入前端全栈开发交流圈一起学习交流:864305860
      : (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;
     }//欢迎加入前端全栈开发交流圈一起学习交流:864305860
    
     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);
     }//欢迎加入前端全栈开发交流圈一起学习交流:864305860
     }
     // 获取改变的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 */)
    }//欢迎加入前端全栈开发交流圈一起学习交流:864305860
    /**
     * 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
    }//欢迎加入前端全栈开发交流圈一起学习交流:864305860
    来看下 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)
     }//欢迎加入前端全栈开发交流圈一起学习交流:864305860
     }
    再来看下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]
     }//欢迎加入前端全栈开发交流圈一起学习交流:864305860
     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
     },//欢迎加入前端全栈开发交流圈一起学习交流:864305860
     // 重点看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()
     }//欢迎加入前端全栈开发交流圈一起学习交流:864305860
     // 将新的值赋值
     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
     },//欢迎加入前端全栈开发交流圈一起学习交流:864305860
     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)
     }//欢迎加入前端全栈开发交流圈一起学习交流:864305860
    }
    使用
    
    我们在执行watch回调前没有对新老赋值进行比较,原因是微信当中对data中的变量赋值,即使给引用变量赋值还是相同的值,也会因为引用地址不同,判断不相等。如果想对新老值进行比较就不能使用 === ,可以先对obj或者array转换为json字符串再比较。
    
    //index.js
    //获取应用实例
    const app = getApp()
    import {watch} from '../../utils/watcher';
    Page({//欢迎加入前端全栈开发交流圈一起学习交流:864305860
     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);
     },//欢迎加入前端全栈开发交流圈一起学习交流:864305860
    
     userInfo(newVal, oldVal) {
     console.log('newVal', newVal, 'oldVal', oldVal);
     },
    
     tableData(newVal, oldVal) {
     console.log('newVal', newVal, 'oldVal', oldVal);
     }
     }); 
     },
     onClickChangeStringData() {
     this.setData({
     motto: 'hello'
     });//欢迎加入前端全栈开发交流圈一起学习交流:864305860
     },
     onClickChangeObjData() {
     this.setData({
     userInfo: {
     name: 'helo'
     }
     });
     },
     onClickChangeArrayDataA() {
     const tableData = [];
     this.setData({
     tableData
     });//欢迎加入前端全栈开发交流圈一起学习交流:864305860
     }//面向1-3年前端人员
    })//帮助突破技术瓶颈,提升思维能力
    结语
    
    感谢您的观看,如有不足之处,欢迎批评指正。
    
    2019-12-17 09:31:31
    赞同 展开评论 打赏
问答排行榜
最热
最新

相关电子书

更多
React Native 全量化实践 立即下载
Vue.js在前端服务化上的实践与探索 立即下载
React在大型后台管理项目中的工程实践 立即下载

相关镜像