Обновил второй,Почему вы не можете понять исходный код Vue 3.0 [2]
唠 会 儿 儿
Я помню, давным-давно кто-то сказал, что чтение исходного кода похоже на изучение секретов боевых искусств в романах о боевых искусствах. Умение играть с ножами и мечами — это внешняя сила, а изучение секретов боевых искусств — внутренняя сила, чтобы вы могли быть лучше других, сражаясь с волнами или сражаясь перед войной.
В реальной разработке представьте такой сценарий:
- Сяо А: Я столкнулся с ошибкой, интересно, совместима ли она с браузером? Может это тоже проблема с сетью? Будет ли проблема с рамой!
- Вы: (с презрительным взглядом), эта часть, я стащил ее исходный код раньше, именно XX вызвал XX, а эта часть вашего кода имела эффект XX. (После этого он пошел за чашкой кофе, не оборачиваясь.)
Как навязчиво!
Реальность всегда несчастна
Чтение исходного кода фреймворка действительно может установить вышеуказанную силу, но...
Всякий раз, когда вы смотрите на исходный код, это все равно, что слушать урок математики в средней школе. Несколько минут назад вы были полны интереса. Вы опустили голову, чтобы взять ручку, а когда снова подняли голову, разные." Эйнштейн без бороды? ? !
Вы видите статьи об интерпретации исходного кода, присланные большим Vs, почему другие могут понять?
Вы можете подумать: не слишком ли я глуп? Исходный код слишком сложен для понимания? Я не подхожу для этой работы...
Не волнуйтесь, 99% людей такие же, как вы.
Эти большие буквы V говорят вам только о процессе чтения исходного кода, но не о подготовке к чтению исходного кода.
И я начну со стадии подготовки к чтению исходного кода.
Заточка ножей без ошибок рубит столяра
Прежде чем читать исходный код, ваши базовые навыки должны быть прочными.
Например, следующие два метода:
export const isObject = (val: any) =>
val !== null && typeof val === 'object'
export const isObject = (val: any) =>
val !== null && Object.prototype.toString.call(val) === '[object Object]'
То же имя функции, но другая функция, если вы понимаетеtypeof
и понять метод прототипированияtoString
так же какcall
Вы можете легко различить разницу и функцию двух.
Когда вы смотрите на исходный код, взгляните на тело функции, и вы знаете, что собирается сделать автор фреймворка.
Вы можете спросить меня, как посмотреть исходный код, если базовые навыки не очень хороши?
эммм мой совет не читать. Сначала заточите нож. Потому что самый быстрый способ стать лучше на вашем текущем этапе — это изучить основы.
Предусмотрите это заранее
Сяо А: Я отработал базовые навыки, могу я взглянуть на исходный код?
Я: Я вижу это, я не вижу этого.
Маленький А: Ты издеваешься?
Нет, видно, что мы сначала сканируем исходный код, этот процесс занимает менее 5 минут. Посмотрите, какие новые API или старые API вы не видели раньше.
Теперь нам нужно клонировать исходный код vue 3.0 и наслаждаться этими короткими пятью минутами.
Как видите, vue 3.0 использует много
Reflect\Proxy\Symbol\Map\Set
Вы должны убедиться, что хорошо знаете все эти API, а если нет, то должны знать, что делает каждый из них.
В этой статье я не буду приводить вам список их функций, расскажу только о подготовке перед чтением исходного кода. Надеюсь, вы сможете понять сами, а не подбирать чужую мудрость.
Поднять
Маленький А: Вы только что сказали выше, что не хотите ковыряться в зубах людей, и теперь вы хотите ковыряться в зубах людей.Что вы хотите, чтобы я сделал?
Я: Как упоминалось выше, при изучении новых методов нам нужно исследовать их самостоятельно и понимать их в соответствии с официальными стандартами. Здесь нам нужно читать исходный код. Мы можем стоять на плечах гигантов и немного понимать структура исходного кода. Позже будет легче читать самостоятельно.
Маленький А: Я тебе верю.
В этой статье примет основной реализацию связывания Vue данных в качестве примера, поэтому на этом этапе необходимо найти статьи, написанные Big VS в сообществе. Например, я думаю, что следующее хорошо написано.
Первый шаг — дать статье 👍, чтобы поддержать оригинального автора. При чтении статьи игнорируйте знания, которые вы уже знаете, такие как использование прокси и использование Reflect. Затем попробуйте набрать демо-код в статье вручную.
В этой статье вам лучше сначала реализовать прокси для проксирования глубоких объектов.
Ниже приведена демонстрация, которую я написал
<html>
<div id='array'></div>
<script>
const array= document.querySelector('#array')
const isObject = (val) => val !== null && typeof val === 'object'
function proxyMethod(obj) {
let p2 = new Proxy(obj, {
get(target, key, val) {
const res = Reflect.get(target, key, val)
if(isObject(res)) {
return proxyMethod(res)
}
return res
},
set(target, key, val, receiver) {
const res = Reflect.set(target, key, val)
array.innerHTML = receiver
console.log('set', receiver)
return true
}
});
return p2
}
let p = {
arr: [1,2,3],
c: 2
}
let p2 = proxyMethod(p)
p2.arr.push(8)
setTimeout(() => {
p2.arr.push(6)
}, 1000)
</script>
</html>
После прочтения статей сообщества и понимания основных функций исходного кода можно переходить к следующему шагу.
Паодин Цзе Ню
На данный момент вы знаете ядро реализации исходного кода, а остальное — это код, который реализует функции после того, как исходный код обертывает ядро. Например: теперь вы знаете, что делать с двигателем, дальше нужно поставить коробку передач, поставить шины, поставить раму...
Таким образом, вы должны иметь общее понимание большой картины кода, разделите его, вам нужно знать, где шины? Где дверь?
Этот процесс я называю «демонтаж архитектуры». Как это решить. Давайте сначала перечислим файловую структуру Vue.
Здесь рекомендуется инструмент, который может отображать древовидную структуру файлов в командной строке.
brew install tree
tree -L 1
├── compiler-core
├── compiler-dom
├── global.d.ts
├── reactivity
├── runtime-core
├── runtime-dom
├── runtime-test
├── server-renderer
├── shared
├── template-explorer
└── vue
Затем проанализируйте папки одну за другой. В этой статье анализируются толькоreactivity
папка.
.
├── README.md
├── __tests__
├── api-extractor.json
├── index.js
├── package.json
└── src
Если есть README, сначала посмотрите README.
# @vue/reactivity
## Usage Note
This package is inlined into Global & Browser ESM builds of user-facing renderers (e.g. `@vue/runtime-dom`), but also published as a package that can be used standalone. The standalone build should not be used alongside a pre-bundled build of a user-facing renderer, as they will have different internal storage for reactivity connections. A user-facing renderer should re-export all APIs from this package.
For full exposed APIs, see `src/index.ts`. You can also run `yarn build reactivity --types` from repo root, which will generate an API report at `temp/reactivity.api.md`.
## Credits
The implementation of this module is inspired by the following prior art in the JavaScript ecosystem:
- [Meteor Tracker](https://docs.meteor.com/api/tracker.html)
- [nx-js/reactivity-util](https://github.com/nx-js/reactivity-util)
- [salesforce/observable-membrane](https://github.com/salesforce/observable-membrane)
## Caveats
- Built-in objects are not observed except for `Map`, `WeakMap`, `Set` and `WeakSet`.
Как видно из предыдущего абзаца, этот пакет можно использовать самостоятельно. О том, как его использовать, см.index.ts
файл, при условии, что мы понимаем каждый объект, который он экспортирует.
Давайте взглянемsrc
Структура
.
├── baseHandlers.ts
├── collectionHandlers.ts
├── computed.ts
├── effect.ts
├── index.ts
├── lock.ts
├── operations.ts
├── reactive.ts
└── ref.ts
Следующее задание — обозначить роль каждого файла. Лучше всего сделать заметку. отindex.ts
читать, использовать深度优先
Путь.
Дальше буду читать построчно, можете идти по моим стопам...
Отслеживание секретного пути
index.ts
export { ref, isRef, toRefs, Ref, UnwrapRef } from './ref'
Ref ref файл в первый метод, чтобы увидеть, как получилось.
ref.ts
import { track, trigger } from './effect'
import { OperationTypes } from './operations'
import { isObject } from '@vue/shared'
import { reactive } from './reactive'
export const refSymbol = Symbol(__DEV__ ? 'refSymbol' : undefined)
export interface Ref<T> {
[refSymbol]: true
value: UnwrapNestedRefs<T>
}
export type UnwrapNestedRefs<T> = T extends Ref<any> ? T : UnwrapRef<T>
const convert = (val: any): any => (isObject(val) ? reactive(val) : val)
export function ref<T>(raw: T): Ref<T> {
// 如果是对象,则用 reactive 方法 包装 raw
raw = convert(raw)
// 返回一个 v 对象,在 取value 值时,调用 track 方法,在存 value 值时,调用 trigger方法
const v = {
[refSymbol]: true,
get value() {
track(v, OperationTypes.GET, '')
return raw
},
set value(newVal) {
raw = convert(newVal)
trigger(v, OperationTypes.SET, '')
}
}
return v as Ref<T>
}
Как видите, мы столкнулись с тремя неизвестными функциями.
- Метод преобразования называется
reactive
метод, который оборачивает параметр ref. - В объекте v, когда значение значения получено, вызывается метод отслеживания, а когда значение значения существует, вызывается метод триггера.
Продолжайте спускаться вниз и смотрите содержимое реактивного метода,Я написал свои мысли о чтении кода в заметки, вы можете найти Mark1 - Mark10, чтобы увидеть линию мыслей по линии. reactive.ts
import { isObject, toTypeString } from '@vue/shared'
import { mutableHandlers, readonlyHandlers } from './baseHandlers'
import {
mutableCollectionHandlers,
readonlyCollectionHandlers
} from './collectionHandlers'
import { UnwrapNestedRefs } from './ref'
import { ReactiveEffect } from './effect'
// WeakMap 主要是用来储存 {target -> key -> dep} 的链接,它更像是 依赖 Dep 的类,它包含了一组 Set,但我们只是简单的储存它们,以减少内存消耗
export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<string | symbol, Dep>
export const targetMap = new WeakMap<any, KeyToDepMap>()
// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()
// WeakSets for values that are marked readonly or non-reactive during
// observable creation.
const readonlyValues = new WeakSet<any>()
const nonReactiveValues = new WeakSet<any>()
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
const observableValueRE = /^\[object (?:Object|Array|Map|Set|WeakMap|WeakSet)\]$/
// MARK9: ->进去看
const canObserve = (value: any): boolean => {
return (
// 不是vue 对象
!value._isVue &&
// 不是 vNode
!value._isVNode &&
// 白名单: Object|Array|Map|Set|WeakMap|WeakSet
observableValueRE.test(toTypeString(value)) &&
// 没有代理过的
!nonReactiveValues.has(value)
)
}
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
// MARK1: 如果target 只读,则返回它
if (readonlyToRaw.has(target)) {
return target
}
// MARK2: 如果target被用户设置为只读,则让它只读,并返回
if (readonlyValues.has(target)) {
return readonly(target)
}
// MARK3: 貌似到重点了
return createReactiveObject(
target,
// MARK3: 底下这一堆是 weakMap,先不管它们
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}
export function readonly<T extends object>(
target: T
): Readonly<UnwrapNestedRefs<T>>
export function readonly(target: object) {
// value is a mutable observable, retrieve its original and return
// a readonly version.
if (reactiveToRaw.has(target)) {
target = reactiveToRaw.get(target)
}
return createReactiveObject(
target,
rawToReadonly,
readonlyToRaw,
readonlyHandlers,
readonlyCollectionHandlers
)
}
function createReactiveObject(
target: any,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// MARK4: 很明显,这里只 typeof判断,Array Date之类的也能通过
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// MARK5: 已经代理过的对象,不需要在代理
let observed = toProxy.get(target)
// MARK6: 这里用 void 0代替了undfined,防止 undfined 被重写。get 到新姿势了。
if (observed !== void 0) {
return observed
}
// MARK7: 目标本身就是一个 proxy 对象
if (toRaw.has(target)) {
return target
}
// MARK8: 只有加入白名单的对象才能被代理
if (!canObserve(target)) {
return target
}
// MARK10: collectionTypes 可以看到是一个Set结构,放了几个构造函数:[Set, Map, WeakMap, WeakSet],这个三目表达式就是区分了以上四种对象和其他对象的 handlers,我们先从 baseHandlers 看起
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
observed = new Proxy(target, handlers)
// 以下则是存储下已经代理过的对象,以作优化。
toProxy.set(target, observed)
toRaw.set(observed, target)
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
// 返回代理后的对象
return observed
}
export function isReactive(value: any): boolean {
return reactiveToRaw.has(value) || readonlyToRaw.has(value)
}
export function isReadonly(value: any): boolean {
return readonlyToRaw.has(value)
}
export function toRaw<T>(observed: T): T {
return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed
}
export function markReadonly<T>(value: T): T {
readonlyValues.add(value)
return value
}
export function markNonReactive<T>(value: T): T {
nonReactiveValues.add(value)
return value
}
reactive.ts, наконец, закончился, затем мы входим из MARK10 выше вhandler.ts
, старое правило, идея кода здесь начинается с MARK11.
import { reactive, readonly, toRaw } from './reactive'
import { OperationTypes } from './operations'
import { track, trigger } from './effect'
import { LOCKED } from './lock'
import { isObject, hasOwn } from '@vue/shared'
import { isRef } from './ref'
const builtInSymbols = new Set(
Object.getOwnPropertyNames(Symbol)
.map(key => (Symbol as any)[key])
.filter(value => typeof value === 'symbol')
)
function createGetter(isReadonly: boolean) {
return function get(target: any, key: string | symbol, receiver: any) {
const res = Reflect.get(target, key, receiver)
//MARK13: 防止key为Symbol的内置对象,比如 Symbol.iterator
if (typeof key === 'symbol' && builtInSymbols.has(key)) {
return res
}
//MARK14: 从上文知道,被ref 包装过,则返回
if (isRef(res)) {
return res.value
}
// 这里貌似是依赖收集的,暂且不深入
track(target, OperationTypes.GET, key)
// MARK15 对深层对象再次包装
// res 是深层对象,如果它不是只读对象,则调用 reactive 继续代理,上文自己实现过,这里很好理解
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}
function set(
target: any,
key: string | symbol,
value: any,
receiver: any
): boolean {
// MARK16 优化:之前有存过,直接拿就行了
value = toRaw(value)
// MARK17 key 是否为 taget 的属性,
/**
*
* export const hasOwn = (
val: object,
key: string | symbol
): key is keyof typeof val => hasOwnProperty.call(val, key)
这个方法是解决 数组push时,会调用两次 set 的情况,比如 arr.push(1)
第一次set,在数组尾部添加1
第二次set,给数组添加length属性
hasOwnProperty 方法用来判断目标对象是否含有指定属性。数组本身就有length的属性,所以这里是 true
*/
const hadKey = hasOwn(target, key)
const oldValue = target[key]
/**
* MARK18 如果 value 不是响应式数据,则需要将其赋值给 oldValue
*/
if (isRef(oldValue) && !isRef(value)) {
//MARK19 这将触发 oldValue 的 set value 方法,如果 isObject(value) ,则会经过 reactive 再包装一次,将其变成响应式数据
oldValue.value = value
return true
}
const result = Reflect.set(target, key, value, receiver)
// MARK20 target 如果只读 或者 存在于 reactiveToRaw 则不进入条件,reactiveToRaw 储存着代理后的对象
if (target === toRaw(receiver)) {
/* istanbul ignore else */
if (__DEV__) {
const extraInfo = { oldValue, newValue: value }
if (!hadKey) {
trigger(target, OperationTypes.ADD, key, extraInfo)
} else if (value !== oldValue) {
trigger(target, OperationTypes.SET, key, extraInfo)
}
// MARK21 只看生产环境
} else {
//MARK22 属性新增,触发 ADD 枚举
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (value !== oldValue) {
//MARK23 属性修改,触发 SET 枚举
trigger(target, OperationTypes.SET, key)
}
}
/** MARK24 对应 MARK17
* else {}
*/
}
return result
}
function deleteProperty(target: any, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
const oldValue = target[key]
const result = Reflect.deleteProperty(target, key)
if (hadKey) {
/* istanbul ignore else */
if (__DEV__) {
trigger(target, OperationTypes.DELETE, key, { oldValue })
} else {
trigger(target, OperationTypes.DELETE, key)
}
}
return result
}
function has(target: any, key: string | symbol): boolean {
const result = Reflect.has(target, key)
track(target, OperationTypes.HAS, key)
return result
}
function ownKeys(target: any): (string | number | symbol)[] {
track(target, OperationTypes.ITERATE)
return Reflect.ownKeys(target)
}
export const mutableHandlers: ProxyHandler<any> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
// MARK11 入口函数
export const readonlyHandlers: ProxyHandler<any> = {
// MARK12: 创建 getter
get: createGetter(true),
// MARK12: 创建 setter
set(target: any, key: string | symbol, value: any, receiver: any): boolean {
if (LOCKED) {
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
} else {
return set(target, key, value, receiver)
}
},
deleteProperty(target: any, key: string | symbol): boolean {
if (LOCKED) {
if (__DEV__) {
console.warn(
`Delete operation on key "${String(
key
)}" failed: target is readonly.`,
target
)
}
return true
} else {
return deleteProperty(target, key)
}
},
has,
ownKeys
}
Продолжение следует
Пока у вас есть смутное представление о коде в целом, но все же есть непонятные грани и углы, например:
- MARK4 эта серия
WeakMap
Для чего это? - Почему MARK10 установлен несколько раз ниже?
......
В следующих статьях я буду анализировать их один за другим. Чтение исходного кода - долгий процесс, продолжение следует...