Эта статья синхронизирована в личном блогеshymean.comвверх, добро пожаловать, чтобы следовать
Vue3.0 выпустил бета-версию, давайте посмотрим на исходный код. В этой серии будет две статьи, включая общий процесс подачи заявки, новую адаптивную систему и API композиции.
- Анализ исходного кода Vue3 — обнаружение данных
- Vue3 Анализ исходного кода - общий процесс и комбинированные API
Одной из особенностей Vue является система реагирования на данные.Поскольку это относительно независимая система, давайте начнем здесь и посмотрим, как новый прокси-сервер реализует сбор зависимостей данных и уведомление.
Содержание этой статьи немного длинное и может быть прочитано главами по оглавлению.
Ссылаться на:
Предварительные знания
Прежде чем учиться, вам нужно знать некоторые предпосылки JavaScript:Reflect
а такжеProxy
.
Reflect
Reflect
Инкапсулировать операции некоторых объектов, в основном для интеграции некоторых нестандартных мест, существовавших в JS раньше
Я чувствую, что роль Reflect в основном состоит в том, чтобы интегрировать некоторые разрозненные инструментальные функции вReflect
на этом объекте, напримерin
,delete
равный оператор иFunction.prototype.apply
и т.д. API
Более интересной особенностью являетсяReflect.get
третий параметрreceiver
var a = {
name: 'a',
get greet(){
return 'hello ' + this.name
}
}
console.log(a.greet); // "hi, I am a"
var receiver = {
name: 'receiver'
}
// receiver 修改类似于 通过函数的apply修改内部this指向一样,不过这里修改的是访问对象
// 通过这个功能,可以实现新对象借助原对象部分属性和方法的功能
var res = Reflect.get(a, 'greet', receiver); // "hi, I am receiver"
console.log(res)
JavaScript Proxy
Для того, чтобы собирать зависимости и обновления данных, когда изменения данных, Vue2 пропускаетdefineProperty
Перехватите набор и получите дескрипторы доступа к свойствам.
defineProperty
Есть некоторые проблемы, самая распространенная проблема в том, что нельзя контролировать объект и динамически добавляемые свойства массива, даже если Vue2 переписывает методы, связанные с прототипом массива, его все равно нельзя отслеживать.arr[1]=xxx
эта форма.
Vue3 использует новые возможности ES6.Proxy
вместо этого интерфейсdefineProperty
. В этой главе мы в основном разбираемся в основном использовании прокси, некоторых специальных применениях и недостатках самого прокси.
Прокси-объекты используются для определения пользовательского поведения для основных операций.
const p = new Proxy(target, handler)
вhandler
Объект — это объект-заполнитель, который содержит набор определенных свойств и содержит различные ловушки прокси-сервера.trap,如
set、
получить и т. д.
Некоторые детали, на которые следует обратить внимание
-
setМетод должен возвращать логическое значение в строгом режиме, если
set
метод возвращаетfalsish
(в том числе undefined, false и т. д.), будет выброшено исключение, эти детали более хлопотны, вы можете пройтиReflect
иметь дело с -
Если прокси-объект является массивом, при вызове
push
,pop
Когда вы ждете метод, не только элементы массива будут изменены, но иlength
и другие атрибуты, в это время, если проксиset
, он будет запущен несколько раз.
let arr = [100,200,300]
let p = new Proxy(arr, {
get(target, key) {
return target[key]
},
set(target, key, value, receiver) {
console.log('set value', key)
target[key] = value
return true
}
})
p.push(400)
// set value 3 第一次data[3] = 400
// set value length 第二次 data.length = 4
Эта функция многократного запуска обработчика является избыточной в некоторых сценариях.Если представление повторно отображается после набора, несколько наборов могут привести к многократному отображению (без учета записи flushQueue и дедупликации).
- Прокси могут только агенты
let o = {x:{y:100},z:10}
let p = new Proxy(o, {
get(target, key) {
console.log('get value', key)
return target[key]
},
set(target, key, value, receiver) {
console.log('set value', key)
target[key] = value
return true
}
})
p.x.y =100 // 只输出了get value x, 无法监听到set value
Поэтому, когда прокси-объект представляет собой многоуровневую вложенную структуру, разработчику необходимо реализовать преобразование вложенного атрибутивного объекта вProxy
объект
let handler = {
get(target, key) {
console.log('get value', key)
return target[key]
},
set(target, key, value, receiver) {
console.log('set value', key)
target[key] = value
return true
}
}
let x = {y:100}
let o = {x:new Proxy(x, handler),z:10}
let p = new Proxy(o, handler)
p.x.y =100
// get value x
// set value y
Таким образом, есть некоторые проблемы с прокси
- При проксировании массива ловушка может срабатывать несколько раз, и необходимо реализовать дедупликацию.
- Вложенные объекты необходимо вручную преобразовать в прокси, что можно реализовать рекурсивно.
TypeScript
Использование Vue2flow
Обнаружение типов, полностью поддерживаемое Vue3TypeScript
, поэтому чтение исходного кода требует некоторых знаний, связанных с TS, обратитесь к предыдущему
lerna
Vue3 принимаетlerna
Организуйте исходный код проекта (сейчас многие крупные проекты используют lerna), я писал статью раньшеИспользование verdaccio и lerna и управление пакетами npmЗдесь не вводить слишком много.
среда разработки
Затем создайте среду разработки исходного кода.
# 如果下载比较慢的话可以从gitee上面克隆备份仓库
git clone git+https://github.com/vuejs/vue-next.git
# 安装依赖,需要一会
yarn install
# 开始rollup watch
npm run dev
cd packages/vue/examples/
существуетexamples
Вы можете просмотреть различные демо-коды. Здесь мы напрямую выбираем код в каталоге композиции для просмотра нового синтаксиса. Настоятельно рекомендуется прочитать исходный код перед чтением исходного кода.документация по составу API, что очень важно для понимания дизайна API в Vue3! !
Затем измените исходный кодvue/src/index.ts
, обновите страницу, вы увидите эффект изменения, а затем начните счастливое время исходного кода~
rective
Документация показываетrective
Базовый вариант использования для
const { reactive, watchEffect, computed } = Vue
const state = reactive({
count: 0
})
function render() {
document.body.innerHTML = `count is ${state.count}`
}
watchEffect(render) // 初始化会调用一次render
setTimeout(() => {
state.count = 100 // state.count发生变化,会通过watchEffect重新触发render
}, 1000)
выглядитreactive
Метод будет прокси, чтобы вернуть прокси-объект, иwatchEffect
Зарегистрированная функция обратного вызова будет выполняться повторно при изменении свойства объекта.render
.
Далее, давайте начнем с этих двух методов
function reactive(target: object) {
return createReactiveObject(target,false,mutableHandlers,mutableCollectionHandlers)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 检测target是否是对象,是否已经设置__v_isReactive、__v_skip、__v_isReadonly等
// ...
const observed = new Proxy(
target,
// 根据target是否是Set, Map, WeakMap, WeakSet对象来判断使用哪一种handler
collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
)
return observed
}
Давайте сначала посмотрим на обычные объектыbaseHandlers
, то есть входящийmutableHandlers
элемент конфигурации
export const mutableHandlers: ProxyHandler<object> = {
get: createGetter(),
set: createSetter(),
// 下面三个方法均是通过`Reflect`来实现相关操作
deleteProperty,
has,
ownKeys
}
createGetter собирает зависимости
const targetMap = new WeakMap<any, KeyToDepMap>()
function createGetter(isReadonly = false, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
// ... 根据 key与ReactiveFlags 返回特殊值
// ... 处理target为数组时的get操作
const res = Reflect.get(target, key, receiver)
// ... 处理res是Ref类型的操作
if (isRef(res)) {
if (targetIsArray) {
!isReadonly && track(target, TrackOpTypes.GET, key)
return res
} else {
// ref unwrapping, only for Objects, not for Arrays.
return res.value
}
}
// 收集依赖
!isReadonly && track(target, TrackOpTypes.GET, key)
// 判断属性值类型,递归调用reactive处理,返回新的Proxy
return isObject(res) ? reactive(res) : res
}
}
видно только при включенииget
Когда тип значения атрибута обнаружен, а затем значение атрибута обрабатываетсяreactive
Операция, общая производительность должна иметь много улучшения с точки зрения рекурсивно угона, все получилось, когда Vue2 инициализируется.
Мы знаем, что нам нужно собирать зависимости при запуске get, вы можете видетьtrack
Вот что справляется с работой
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 初始化target的依赖列表,通过Map保存,每个依赖可能依赖target某个或某些属性,因此该Map的键值是target的每个属性
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
// 对于每个属性key而言,通过Set保存依赖该key的activeEffect
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// activeEffect是一个全局变量
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
При рендеринге представления в методе рендеринга передатьget
вызыватьtrack
,ПотомactiveEffect
Добавьте в список зависимостей свойства данных, и все готово.Коллекция зависимостей.
Абстракция изменений эффектов
Мы столкнулись с новой концепцией здесьeffect
, включая предыдущийeffect
,watchEffect
и другие методы. похожий наWatcher
Объект, используемый для инкапсуляции различных изменений.
На примереwatchEffect
Зарегистрированную функцию обратного вызова можно понимать как эффект.
// 从类型声明可以看出,effect是一个包含如下属性的函数
export interface ReactiveEffect<T = any> {
(...args: any[]): T
_isEffect: true
id: number
active: boolean
raw: () => T
deps: Array<Dep>
options: ReactiveEffectOptions
}
Тогда взглянитеwatchEffect
реализация
export function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase
): WatchStopHandle {
return doWatch(effect, null, options)
}
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
const instance = currentInstance
// 根据source的类型封装getter,内部执行source的调用
let getter: () => any = () => {return callWithErrorHandling(source, instance) }
// 根据 flush字符串初始化调度器,决定何时调用getter
let scheduler: (job: () => any) => void = job => queuePostRenderEffect(job, instance && instance.suspense)
// 调用effect注册
const runner = effect(getter, {
lazy: true, // 由于下面会直接调用runner,因此lazy传入了true
computed: true,
onTrack,
onTrigger,
scheduler: applyCb ? () => scheduler(applyCb) : scheduler
})
recordInstanceBoundEffect(runner)
runner()
// 返回一个取消effect的方法
return () => {
stop(runner)
if (instance) {
remove(instance.effects!, runner)
}
}
}
Здесь мы видимmountComponent
серединаinstance.update
похожийrunner
методы, они на самом деле являются проходомeffect
Функция упаковки.
const effectStack: ReactiveEffect[] = []
let activeEffect: ReactiveEffect | undefined
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
// 创建effect对象(函数)
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
// 在前面初始化instance.update时会先调用一次 componentEffect,从而完成页面的初始化渲染
effect()
}
return effect
}
// 下面这段源码基本是原样copy过来的,没有做删减
function createReactiveEffect<T = any>(
fn: (...args: any[]) => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn(...args)
}
if (!effectStack.includes(effect)) {
cleanup(effect) // 清除effect.deps
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect // 将全局变量activeEffect 设置为当前运行的effect,然后调用effect
return fn(...args)
} finally {
// finally中的代码始终都能执行
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1] // 将activeEffect重置为上一个effect
}
}
} as ReactiveEffect
effect.id = uid++
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
прибратьсяwatchEffect(cb)
обработать
- существует
doWatch
инкапсулировать cb вgetter
середина, - передача
effect(getter)
,пройти черезcreateReactiveEffect
возвращает истинуReactiveEffect
функция, возложенная наrunner
- назад
doWatch
Средний, напрямую звонитьrunner
воплощать в жизнь- пройдет при запуске ReactiveEffect
effectStack
Установить текущую глобальную переменнуюactiveEffect
- пройдет при запуске ReactiveEffect
activeEffectочень важная глобальная переменная, в предыдущемwatchEffect(render)
, запустит рендеринг при инициализации
function render() {
document.body.innerHTML = `count is ${state.count}`
}
Поскольку внутренний метод имеет внутренний доступstate.count
, вызоветstate
Операция get прокси, чтобы ее можно было использовать вtrack
при прохождении черезactiveEffect
доступ к пакетуrender
Эффект метода, так что когдаstate.count
Когда происходит изменение, соответствующий эффект можно запустить снова.Давайте изменим метод рендеринга.
function render() {
// document.body.innerHTML = `count is ${state.count}`
console.log('render') // 只有第一次初始化的时候回打印render
document.body.innerHTML = '123'
}
watchEffect(render)
state.count // 触发一个get,然而并没有activeEffect,因此不会收集到相关的依赖
setTimeout(() => {
state.count = 100 // state.count更新时也不会触发render
}, 1000)
Как видите, если он не срабатывает в обратном вызовеstate.count
, зависимости не могут быть правильно отслежены. комбинироватьget
а такжеactiveEffect
, который может точно собирать соответствующий эффект при изменении каждого свойства, что очень эффективно.
Также вwatchEffect
Как видно из исходного кодаrunner
выполняется синхронно, после завершения выполненияactiveEffect
сбросить, если мы находимся вrender
Асинхронный доступ к методуstate.count
, и не может правильно отслеживать зависимости.
function render() {
setTimeout(()=>{
document.body.innerHTML = `count is ${state.count}` // 放在回调里面
})
}
watchEffect(render)
setTimeout(() => {
state.count = 100 // 也不会更新视图
}, 1000)
createSetter уведомляет об изменениях
Наконец, давайте посмотрим на установленный прокси
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
const oldValue = (target as any)[key]
// 处理 ... ref
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// 判断target === toRaw(receiver) ,不处理target原型链更新的情况
if (target === toRaw(receiver)) {
if (!hadKey) {
// 动态添加属性的情况
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 属性更新的情况
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
а такжеget
серединаtrack
По аналогии,trigger
Должна быть логика обновления данных и уведомления зависимостей для обработки
export function trigger(
target: object,
type: TriggerOpTypes, // 表示不同的变化,如ADD、SET等
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
// 找到target某个key的依赖
const depsMap = targetMap.get(target)
const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()
// 将对应key的变化添加到effects或者computedRunners中
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || !shouldTrack) {
if (effect.options.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
}
})
}
}
// 根据type和key找到需要处理的depsMap,这里处理了各种特殊情况
add(depsMap.get(key))
// ...
// 遍历effects和computedRunners
const run = (effect: ReactiveEffect) => {
// 如果effect自己配置了scheduler,则使用调度器运行effect
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect() // 可以看见effect实际上是一个函数
}
}
// 计算属性先运行,这样可以保证其他属性在运行时能够获取到计算属性的值
computedRunners.forEach(run)
effects.forEach(run)
}
Резюме: Реализуйте минималистскую версию реактивного
Предыдущий код игнорируетshallow
неглубокий реактивный,readonly
В других случаях мы можем реализовать 50-строчный кодreactive
,
let activeEffect
let targetMap = new Map()
function reactive(obj){
return new Proxy(obj, {
get(target, key){
track(target, key)
return target[key]
},
set(target, key, value){
target[key] = value
trigger(target, key)
return true
}
})
}
function track(target, key){
let depMap = targetMap.get(target)
if(!depMap) {
targetMap.set(target, (depMap = new Map()))
}
let dep = depMap.get(key)
if(!dep) {
depMap.set(key, ( dep = new Set()))
}
if(!dep.has(activeEffect)){
dep.add(activeEffect)
}
}
function watchEffect(cb){
activeEffect = cb
cb()
}
function trigger(target, key){
let depMap = targetMap.get(target)
if(!depMap) return
let effects = depMap.get(key)
if(!effects) return
effects.forEach((effect)=>{
effect()
})
}
затем проверить
let {reactive, watchEffect} = require('./reactive')
let state = reactive({
x: 100
})
function render(){
let msg = `render template with state.x = ${state.x}`
console.log(msg)
}
watchEffect(render)
setTimeout(()=>{
state.x = 200
}, 1000)
Приведенный выше код намеренно игнорирует многие детали, такие как вложенность свойств и многократное выполнение набора массивов, в основном для того, чтобы показать самую базовую структуру реактивного.
Избегайте повторного выполнения эффектов
Мы также упустили из виду более важную особенность: когда состояние зависимостей постоянно меняется, как избежать триггеров, которые не сравниваются в середине? Например, в демонстрационном коде выше
setTimeout(() => {
state.x = 100
console.log(state.x) // 100
state.x = 200
console.log(state.x) // 200
}, 1000)
// 会连续打印两次 render template with state.x = 100 | 200
В некоторых случаях, например при рендеринге представления, запуск метода рендеринга в первом наборе совершенно не нужен и приводит к потере производительности.
В Vue2 Watcher будет добавлен в очередь и дедуплицирован при добавлении вnextTick
в единой очереди выполнения. В приведенной выше демонстрации, поскольку activeEffectrender
метод, мы можем вызвать рендеринг только один раз с помощью debounce
Так как же реализован Vue3? Давайте посмотрим на это
Это видно при срабатывании
const run = (effect: ReactiveEffect) => {
// 如果effect自己配置了scheduler,则使用调度器运行effect
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect() // 可以看见effect实际上是一个函数
}
}
существуетdoWatch
Вы можете увидеть код ниже
scheduler = job => queuePostRenderEffect(job, instance && instance.suspense)
const runner = effect(getter, {
//... 其他配置
scheduler
})
Вы можете видеть, что в эффекте, созданном doWatch,scheduler
Конфигурация, которая вызывается при запуске эффектаscheduler
, он будет выполнен здесьqueuePostRenderEffect
// 使用了两个全局队列来维护
const queue: (Job | null)[] = []
const postFlushCbs: Function[] = []
export const queuePostRenderEffect = __FEATURE_SUSPENSE__
? queueEffectWithSuspense
: queuePostFlushCb
export function queuePostFlushCb(cb: Function | Function[]) {
// 将effect放入postFlushCbs队列
if (!isArray(cb)) {
postFlushCbs.push(cb)
} else {
postFlushCbs.push(...cb)
}
queueFlush()
}
export function queueJob(job: Job) {
if (!queue.includes(job)) {
queue.push(job)
queueFlush()
}
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
nextTick(flushJobs) // nextTick直接使用的Promise.then,在nextTick中执行flushJobs
}
}
function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
let job
// 组件由父组件向子组件更新,父组件的id始终比子组件小(先构造)
queue.sort((a, b) => getId(a!) - getId(b!))
while ((job = queue.shift()) !== undefined) {
if (job === null) {
continue
}
// 依次运行effect
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
flushPostFlushCbs(seen)
isFlushing = false
// 如果在运行过程中调用了queueJob或者queuePostRenderEffect,则继续执行flushJobs
if (queue.length || postFlushCbs.length) {
flushJobs(seen)
}
}
// 依次运行postFlushCbs中的回调,在前面scheduler中添加的job就会通过queuePostRenderEffect放在postFlushCbs中
export function flushPostFlushCbs(seen?: CountMap) {
if (postFlushCbs.length) {
// 关键的异步,对postFlushCbs进行去重,这意味着即使postFlushCbs存在多个相同的effect,也只会被执行一次
const cbs = [...new Set(postFlushCbs)]
postFlushCbs.length = 0
for (let i = 0; i < cbs.length; i++) {
cbs[i]()
}
}
}
Код здесь относительно прост, чтобы показать, как выполняется наш эффект.
- Во-первых, зарегистрировав планировщик элемента конфигурации, вызывайте планировщик (эффект) при запуске
- Планировщик вызывает queuePostFlushCb, чтобы поместить эффект в глобальную очередь postFlushCbs, и одновременно регистрирует flushJobs в nextTick.
- В клубнах очереди и PostFlushcbs будут опустошены.
postFlushCbs
Раньше postFlushCbs будет дедуплицироваться через Set - Таким образом, один и тот же эффект будет выполняться только один раз в одних и тех же flushJobs, независимо от того, сколько раз планировщик (эффект) вызывался в предыдущем триггере.
Ref
Предыдущий анализ исходного кода позволил нам понять, как прокси-объекты Vue3 через прокси-сервер, и отследить эффект при получении в Vue3.set
Триггерный эффект.
В какой-то момент вам нужен кусок данных, который зависит от других состояний, которые можно сделать черезвычисляемое свойствоЧтобы получить, вычисляемое свойство используется как функция, которая может возвращать различные типы значений, и при изменении состояния зависимости вычисляемого свойства оно автоматически пересчитывает и уведомляет место, которое зависит от реализации вычислений.
Официальный сайтИзлагает простейшую схему реализации через замыкание иwatchEffect
выполнитьcomputed
(Настоятельно рекомендуется сначала прочитать эту статью на официальном сайте, что очень полезно для понимания дизайна Vue3)
function computed(getter) {
let value
watchEffect(() => {
value = getter()
})
return value
}
Затем возникает проблема, когда вычисляемое свойство возвращает значение базового типа, хотя его можно перезапустить.watchEffect
обратный вызов и обновить значение, но не может уведомить предыдущую зависимость oldValue,
Это связано с тем, что обычные типы в JavaScript передаются по значению, а не по ссылке.
Решение этой проблемы состоит в том, чтобы вернуть объект, а затем проксировать значение ответа для сбора зависимостей.Ref
.
function computed(getter) {
const ref = {
value: null,
}
watchEffect(() => {
ref.value = getter()
})
return ref
}
Далее начнем с вычисляемого и посмотрим на роль и реализацию Ref
Ниже приведен базовый демонстрационный код
const state = reactive({
count: 1
})
const doubleCount = computed(() => {
return state.count * 2
})
function render() {
document.body.innerHTML = `count is ${doubleCount.value}`
}
watchEffect(render)
setTimeout(() => {
state.count = 100
}, 1000)
computed
Нижеcomputed
исходный код
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set // 处理set computed
}
let dirty = true // 判断getter是否需要重新运算
let value: T
let computed: ComputedRef<T>
const runner = effect(getter, {
lazy: true, // 只有当用到computed的时候才运行求职
computed: true, // 将effect.computed标志为true,这样会在普通的effect之前运行
// 自定义调度器,在trigger时调用
scheduler: () => {
// 通知使用了computed.value的effect,只有当计算属性的getter已经被运行过才进行通知
if (!dirty) {
dirty = true
trigger(computed, TriggerOpTypes.SET, 'value')
}
}
})
computed = {
__v_isRef: true,
effect: runner,
get value() {
if (dirty) {
value = runner() // 调用runner 完成activeEffect设置,这样当计算属性依赖的其他状态发生变化时,可以重新触发getter,
dirty = false // 缓存已经计算过的值
}
// runner运行完毕后会重置activeEffect为上一个effect,然后将依赖该computed的activeEffect添加到依赖中
track(computed, TrackOpTypes.GET, 'value')
return value
},
set value(newValue: T) {
setter(newValue)
}
} as any
// 可以看见,计算属性返回的是一个特殊的对象
return computed
}
Хорошо, это выглядит яснее, в сочетании с приведенным выше примером
const doubleCount = computed(() => {
return state.count * 2
})
function render() {
document.body.innerHTML = `count is ${doubleCount.value}`
}
watchEffect(render)
Упростите весь процесс, чтобы
上游数据 -> computed -> 下游数据
конкретный процесс
- передача
computed(getter)
Обертка будет инициализирована, когдаgetter
эффект - Когда запускается вычисляемое свойство
get value
, эффект, который будет работать- Триггер в это время
getter
Получение восходящих данных, от которых зависит вычисляемое свойство, и в то же время путем вызоваtrack
Соберите данные вниз по течению, которые зависит от текущей вычисленной собственности - При изменении исходных данных эффект будет запущен повторно, потому что он настраивается здесь.
scheduler
, поэтому будем использоватьscheduler(effect)
способ запустить эффект, - существует
scheduler(effect)
сбросить грязный в , то звонитеtrigger
Уведомлять нисходящие данные
- Триггер в это время
Ref
Точно так же мы можем посмотреть на реализацию обычного Ref
export function ref(value?: unknown) {
return createRef(value)
}
function createRef(rawValue: unknown, shallow = false) {
if (isRef(rawValue)) {
return rawValue
}
let value = shallow ? rawValue : convert(rawValue)
const r = {
__v_isRef: true,
get value() {
track(r, TrackOpTypes.GET, 'value')
return value
},
set value(newVal) {
if (hasChanged(toRaw(newVal), rawValue)) {
rawValue = newVal
value = shallow ? newVal : convert(newVal)
trigger(
r,
TriggerOpTypes.SET,
'value'
)
}
}
}
return r
}
имеютcomputed
опыта, кажется легче здесь и здесь, вget value
когдаtrack
собрать activeEffect, вset value
, если значение изменится, передатьtrigger
Эффект уведомления.
резюме
В этой статье в основном организована новая адаптивная система в Vue3,
- пройти через
effect
Изменения инкапсуляции в Vue2 черезWatcher
Ответственный - Через набор и получение каждого атрибута прокси-объекта активный эффект собирается через дорожку во время получения, и все обновления зависимостей соответствующего атрибута уведомляются через триггер во время набора.
- Сохраните зависимости каждого свойства в каждом реактивном объекте через Map
- Равномерно запускайте эффект в nextTick через очередь эффектов и дедуплицируйте эффект через Set
- Чтобы решить проблему передачи общих типов по значению, Vue3 реализует объект типа Ref,
computed
Его также можно рассматривать как особый вид Ref.
Пока у меня есть определенное понимание адаптивной системы Vue3, пойдем посмотримСоставной APIДолжно быть удобнее.