CommanderXL / Biu-blog

个人博客
432 stars 39 forks source link

【Mpx】响应式系统 #45

Open CommanderXL opened 4 years ago

CommanderXL commented 4 years ago

小程序也是通过数据去驱动视图的渲染,需要手动的调用setData去完成这样一个动作。同时小程序的视图层也提供了用户交互的响应事件系统,在 js 代码中可以去注册相关的事件回调并在回调中去更改相关数据的值。Mpx 使用 Mobx 作为响应式数据工具并引入到小程序当中,使得小程序也有一套完成的响应式的系统,让小程序的开发有了更好的体验。

还是从组件的角度开始分析 mpx 的整个响应式的系统。每次通过createComponent方法去创建一个新的组件,这个方法将原生的小程序创造组件的方法Component做了一层代理,例如在 attched 的生命周期钩子函数内部会注入一个 mixin:

// attached 生命周期钩子 mixin

attached() {
  // 提供代理对象需要的api
  transformApiForProxy(this, currentInject)
  // 缓存options
  this.$rawOptions = rawOptions // 原始的,没有剔除 customKeys 的 options 配置
  // 创建proxy对象
  const mpxProxy = new MPXProxy(rawOptions, this) // 将当前实例代理到 MPXProxy 这个代理对象上面去
  this.$mpxProxy = mpxProxy // 在小程序实例上绑定 $mpxProxy 的实例
  // 组件监听视图数据更新, attached之后才能拿到properties
  this.$mpxProxy.created()
}

在这个方法内部首先调用transformApiForProxy方法对组件实例上下文this做一层代理工作,在 context 上下文上去重置小程序的 setData 方法,同时拓展 context 相关的属性内容:

function transformApiForProxy (context, currentInject) {
  const rawSetData = context.setData.bind(context) // setData 绑定对应的 context 上下文
  Object.defineProperties(context, {
    setData: { // 重置 context 的 setData 方法
      get () {
        return this.$mpxProxy.setData.bind(this.$mpxProxy)
      },
      configurable: true
    },
    __getInitialData: {
      get () {
        return () => context.data
      },
      configurable: false
    },
    __render: { // 小程序原生的 setData 方法
      get () {
        return rawSetData
      },
      configurable: false
    }
  })
  // context 绑定注入的render函数
  if (currentInject) {
    if (currentInject.render) { // 编译过程中生成的 render 函数
      Object.defineProperties(context, {
        __injectedRender: {
          get () {
            return currentInject.render.bind(context)
          },
          configurable: false
        }
      })
    }
    if (currentInject.getRefsData) {
      Object.defineProperties(context, {
        __getRefsData: {
          get () {
            return currentInject.getRefsData
          },
          configurable: false
        }
      })
    }
  }
}

接下来实例化一个 mpxProxy 实例并挂载至 context 上下文的 $mpxProxy 属性上,并调用 mpxProxy 的 created 方法完成这个代理对象的初始化的工作。在 created 方法内部主要是完成了以下的几个工作:

  1. initApi,在组件实例 this 上挂载$watch,$forceUpdate,$updated,$nextTick等方法,这样在你的业务代码当中即可直接访问实例上部署好的这些方法;
  2. initData
  3. initComputed,将 computed 计算属性字段全部代理至组件实例 this 上;
  4. 通过 Mobx observable 方法将 data 数据转化为响应式的数据;
  5. initWatch,初始化所有的 watcher 实例;
  6. initRender,初始化一个 renderWatcher 实例;

这里我们具体的来看下 initRender 方法内部是如何进行工作的:

export default class MPXProxy {
  ...
  initRender() {
    let renderWatcher
    let renderExcutedFailed = false
    if (this.target.__injectedRender) { // webpack 注入的有关这个 page/component 的 renderFunction
      renderWatcher = watch(this.target, () => {
        if (renderExcutedFailed) {
          this.render()
        } else {
          try {
            return this.target.__injectedRender() // 执行 renderFunction,获取渲染所需的响应式数据
          } catch(e) {
            ...
          }
        }
      }, {
        handler: (ret) => {
          if (!renderExcutedFailed) {
            this.renderWithData(ret) // 渲染页面
          }
        },
        immediate: true,
        forceCallback: true
      })
    }
  }
  ...
}

在 initRender 方法内部非常清楚的看到,首先判断这个 page/component 是否具有 renderFunction,如果有的话那么就直接实例化一个 renderWatcher:

export default class Watcher {
  constructor (context, expr, callback, options) {
    this.destroyed = false
    this.get = () => {
      return type(expr) === 'String' ? getByPath(context, expr) : expr()
    }
    const callbackType = type(callback)
    if (callbackType === 'Object') {
      options = callback
      callback = null
    } else if (callbackType === 'String') {
      callback = context[callback]
    }
    this.callback = typeof callback === 'function' ? action(callback.bind(context)) : null
    this.options = options || {}
    this.id = ++uid
    // 创建一个新的 reaction
    this.reaction = new Reaction(`mpx-watcher-${this.id}`, () => {
      this.update()
    })
    // 在调用 getValue 函数的时候,实际上是调用 reaction.track 方法,这个方法内部会自动执行 effect 函数,即执行 this.update() 方法,这样便会出发一次模板当中的 render 函数来完成依赖的收集
    const value = this.getValue()
    if (this.options.immediateAsync) { // 放置到一个队列里面去执行
      queueWatcher(this)
    } else { // 立即执行 callback
      this.value = value
      if (this.options.immediate) {
        this.callback && this.callback(this.value)
      }
    }
  }

  getValue () {
    let value
    this.reaction.track(() => {
      value = this.get() // 获取注入的 render 函数执行后返回的 renderData 的值,在执行 render 函数的过程中,就会访问响应式数据的值
      if (this.options.deep) {
        const valueType = type(value)
        // 某些情况下,最外层是非isObservable 对象,比如同时观察多个属性时
        if (!isObservable(value) && (valueType === 'Array' || valueType === 'Object')) {
          if (valueType === 'Array') {
            value = value.map(item => toJS(item, false))
          } else {
            const newValue = {}
            Object.keys(value).forEach(key => {
              newValue[key] = toJS(value[key], false)
            })
            value = newValue
          }
        } else {
          value = toJS(value, false)
        }
      } else if (isObservableArray(value)) {
        value.peek()
      } else if (isObservableObject(value)) {
        keys(value)
      }
    })
    return value
  }

  update () {
    if (this.options.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  run () {
    const immediateAsync = !this.hasOwnProperty('value')
    const oldValue = this.value
    this.value = this.getValue() // 重新获取新的 renderData 的值
    if (immediateAsync || this.value !== oldValue || isObject(this.value) || this.options.forceCallback) {
      if (this.callback) {
        immediateAsync ? this.callback(this.value) : this.callback(this.value, oldValue)
      }
    }
  }

  destroy () {
    this.destroyed = true
    this.reaction.getDisposer()()
  }
}

Watcher 观察者核心实现的工作流程就是:

  1. 构建一个 Reaction 实例;
  2. 调用 getValue 方法,即 reaction.track,在这个方法内部执行过程中会调用 renderFunction,这样在 renderFunction 方法的执行过程中便会访问到渲染所需要的响应式的数据并完成依赖收集;
  3. 根据 immediateAsync 配置来决定回调是放到下一帧还是立即执行;
  4. 当响应式数据发生变化的时候,执行 reaction 实例当中的回调函数,即this.update()方法来完成页面的重新渲染。

mpx 在构建这个响应式的系统当中,主要有2个大的环节,其一为在构建编译的过程中,将 template 模块转化为 renderFunction,提供了渲染模板时所需响应式数据的访问机制,并将 renderFunction 注入到运行时代码当中,其二就是在运行环节,mpx 通过构建一个小程序实例的代理对象,将小程序实例上的数据访问全部代理至 MPXProxy 实例上,而 MPXProxy 实例即 mpx 基于 Mobx 去构建的一套响应式数据对象,首先将 data 数据转化为响应式数据,其次提供了 computed 计算属性,watch 方法等一系列增强的拓展属性/方法,虽然在你的业务代码当中 page/component 实例 this 都是小程序提供的,但是最终经过代理机制,实际上访问的是 MPXProxy 所提供的增强功能,所以 mpx 也是通过这样一个代理对象去接管了小程序的实例。需要特别指出的是,mpx 将小程序官方提供的 setData 方法同样收敛至内部,这也是响应式系统提供的基础能力,即开发者只需要关注业务开发,而有关小程序渲染运行在 mpx 内部去帮你完成。