Devs.tw 是讓工程師寫筆記、網誌的平台。歡迎您隨手紀錄、寫作,方便日後搜尋!
現代前端開發,已經沒有人會在更新完 state 之後,再把跟 state 相關的 UI 逐一更新了。
也就是不會更新完 state object 之後,還一個一個去 update HTML element 內容。
都是更新完 state object 之後,讓 HTML element 自動就跟著更新,這是如何做到的?
甚至連跟核心 state 相關的 state,都能自動跟著核心 state 更新而一併自動更新。
Object.observe -> 以前是用這個,目前已 deprecated
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/observe
現在要用 Proxy
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
反正就是,監聽物件的變化,去做出對應的動作. 只要做出對應的修改,就有 reactive programming 的效果
// 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 而已吧?
結果查了一下原始碼,並沒有出現 new Proxy
這幾個字,所以不是用這個 feature 做到的,而是用更複雜的方法達成,囧。
/packages/react/src/ReactBaseClasses.js
enqueueSetState
接著有四種實作
ReactNoopUpdateQueue.js
ReactFiberClassComponent.old.js
ReactFiberClassComponent.new.js
ReactPartialRenderer.js
後面有 virtualDOM、有 diff 演算法、有合併多個更新為一次更新的機制...等等的,相當複雜,囧
在背後可能還是用 proactive 的方法去更新 DOM tree。
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.
查了一下原始碼,也是沒有出現 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
是上面提到的 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 */)
}
這附近吧!有去找物件的 data
跟 computed
屬性,還有 watch
屬性之類的,找出來放進 reactivity data 模型裡面,準備後面的互動吧!
後面的 initData 也是有看到 proxy
observe
之類的單字出現囉!
因為 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:
讚喔 這篇也是在講類似的事: 不会Object.defineProperty你就out了