尤川豪   ·  3年前
445 貼文  ·  275 留言

現代前端:各家技術是如何做到 reactive programming 的效果?所謂 data binding 的背後實作?

現代前端開發,已經沒有人會在更新完 state 之後,再把跟 state 相關的 UI 逐一更新了。

也就是不會更新完 state object 之後,還一個一個去 update HTML element 內容。

都是更新完 state object 之後,讓 HTML element 自動就跟著更新,這是如何做到的?

甚至連跟核心 state 相關的 state,都能自動跟著核心 state 更新而一併自動更新。

純 JavaScript

舉例來說,這樣寫就有現代前端框架的基本效果

// define the state object
var state = {
  name: "Tony Default",
  age: 20
};
// define the view
<div>
  <div>my name is <span id="state-name"></span></div>
  <div>my age is <span id="state-age"></span></div>
</div>
let stateHandler = {
  set: function(obj, prop, value) {
    if (prop === 'name') {
      obj[prop] = value;

      document.getElementById('state-name').textContent = value;

      return true;
    }

    if (prop === 'age') {
      obj[prop] = value;

      document.getElementById('state-age').textContent = value;

      return true;
    }
  }
};

var reactiveState = new Proxy(state, stateHandler);

reactiveState.name = "Tony Updated";

reactiveState.age = 30;

最後幾行實際操作 state object 的時候,會發現 UI 會自動跟著變化!很神!

如果你不想使用 reactive state object,古代那種直接操作 state object 的寫法,會需要記得去更新相關的 html DOM element,畫面一複雜,就很容易出錯、漏掉元素更新!所以現代 reactive programming 的寫法比較好!

我們常看到的以下模板語法,背後就是透過 framework 本身的 template engine 再轉成以上寫法而已

// define the view
<div>
  <div>my name is {{ state.name }}</div>
  <div>my age is {{ state.age }}</div>
</div>

我估計 React or Vue,hook 或非 hook 的寫法(functional style or not),背後也只是如同以上巧妙使用 Proxy 而已吧?

React

this.setState

結果查了一下原始碼,並沒有出現 new Proxy 這幾個字,所以不是用這個 feature 做到的,而是用更複雜的方法達成,囧。

/packages/react/src/ReactBaseClasses.js
enqueueSetState

接著有四種實作

ReactNoopUpdateQueue.js

ReactFiberClassComponent.old.js

ReactFiberClassComponent.new.js

ReactPartialRenderer.js

後面有 virtualDOM、有 diff 演算法、有合併多個更新為一次更新的機制...等等的,相當複雜,囧

在背後可能還是用 proactive 的方法去更新 DOM tree。

useState

packages/react/src/ReactHooks.js
export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}
function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  invariant(
    dispatcher !== null,
    'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
      ' one of the following reasons:\n' +
      '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
      '2. You might be breaking the Rules of Hooks\n' +
      '3. You might have more than one copy of React in the same app\n' +
      'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
  );
  return dispatcher;
}
import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';

/**
 * Keeps track of the current dispatcher.
 */
const ReactCurrentDispatcher = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: (null: null | Dispatcher),
};

export default ReactCurrentDispatcher;

好吧後面就很複雜了,開始 react-reconciler,大概是跟 this.setState 開始會做一樣的事情,只是使用方式不同而已,囧。

反正後面就是 https://reactjs.org/docs/reconciliation.html

Reconciliation

React provides a declarative API so that you don’t have to worry about exactly what changes on every update. This makes writing applications a lot easier, but it might not be obvious how this is implemented within React. This article explains the choices we made in React’s “diffing” algorithm so that component updates are predictable while being fast enough for high-performance apps.

Vue

查了一下原始碼,也是沒有出現 new Proxy,但是有出現以下這段:

/src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
export function defineReactive (
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val

看來有用到 Object.defineProperty 這個 JS 功能來擴充,並在背後做到 reactive 效果。

如何使用 Object.defineProperty 做到呢?大概後面又是 proactive 去更新吧?

不過根據這邊的說明,看到 watcher notify addSub 可知道這邊的實作是 observer pattern。從抽象化的外面來看,就是 reactive data 啦!

開發者使用上不管是 data computed watchers 還是 vue3 的 composition api 的 value function 還是怎麼跟資料模型互動,反正後面核心就是這組 reactivity system囉!

所以不用把 reactive programming 想得太複雜。開發的時候有紀律地去跟一組能夠 subscribe/notify 的資料模型互動(比如說簡單地實作 observer pattern)就是 reactive programming 囉!

data/computed

data 是上面提到的 reactive data 啦。computed 跟一般拿 data 內容當作參數的函式沒兩樣,只是宣告在這邊方便多加快取功能

Instead of a computed property, we can define the same function as a method. For the end result, the two approaches are indeed exactly the same. However, the difference is that computed properties are cached based on their reactive dependencies. A computed property will only re-evaluate when some of its reactive dependencies have changed.

至於原始碼的話大概就是

/src/core/instance/state.js
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

這附近吧!有去找物件的 datacomputed 屬性,還有 watch 屬性之類的,找出來放進 reactivity data 模型裡面,準備後面的互動吧!

後面的 initData 也是有看到 proxy observe 之類的單字出現囉!

value function

因為 vue 3 還沒發佈,要去這邊看原始碼 https://github.com/vuejs/vue-next

背後是不是也是跟同樣的 reactivity system 互動而已呢?

查看原始碼會發現,檔案結構跟前面寫到的完全不一樣了,然後程式碼拆分方式與模組命名也有很大變化,然後全面使用 TypeScript 重寫,算是很大程度的翻新重寫。

/packages/reactivity/src/reactive.ts
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

前面一直說 value function 算是說錯了,其中一個 RFC 是叫 value 但後面又刪掉改名稱為 reactive 囉。

最後會使用 createReactiveObject 來實作 reactive data 哦

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  if (
    hasOwn(target, isReadonly ? ReactiveFlags.READONLY : ReactiveFlags.REACTIVE)
  ) {
    return isReadonly
      ? target[ReactiveFlags.READONLY]
      : target[ReactiveFlags.REACTIVE]
  }
  // only a whitelist of value types can be observed.
  if (!canObserve(target)) {
    return target
  }
  const observed = new Proxy(
    target,
    collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
  )
  def(
    target,
    isReadonly ? ReactiveFlags.READONLY : ReactiveFlags.REACTIVE,
    observed
  )
  return observed
}

終於看到 new Proxy 關鍵字囉!可見 vue 3 背後有使用原生的 Proxy 功能來實作哦!

終於找到用原生 Proxy 實作 reactive programming 的地方囉!


ref:

  分享   共 4,058 次點閱
按了喜歡:
共有 1 則留言
Morris-Chen   ·  3年前
1 貼文  ·  3 留言

讚喔 這篇也是在講類似的事: 不会Object.defineProperty你就out了

 
您的留言
尤川豪
445 貼文  ·  275 留言

Devs.tw 是讓工程師寫筆記、網誌的平台。隨手紀錄、寫作,方便日後搜尋!

歡迎您一起加入寫作與分享的行列!

查看所有文章