После прочтения этой статьи Vue3 совсем не сложен!

Vue.js

предисловие

Такое ощущение, что время летит так быстро, а я уже больше месяца не обновлял текст. Эта картина все еще производится в 2020 году ~

Начав путешествие в 2021 году, эта статья впервые будет опубликована в 2021 году... Надеюсь, читатели поддержат вас~

Разобравшись с Vue3, который я изучил за эти дни, я думаю, что Vue3 станет тенденцией в будущем. Тогда поторопитесь и научитесь~

setup

Время вызова этого API: создайте экземпляр компонента, затем инициализируйте свойства, а затем вызовитеsetupфункция. С точки зрения крючков жизненного цикла это будетbeforeCreate Хук был вызван раньше.

В этой функции мы можем написать большую часть бизнес-логики.В Vue2 мы разделяем логику кода в виде каждой опции, напримерmethods,data,watchопции. Vue3 сейчасизмененныйТакой режим (ps: не могу сказать, чтобы изменить, потому что он также совместим с методом записи Vue2)

Разве это не теория Троецарствия?分久必合,合久并分Хорошо

установка с 2 необязательными параметрами.

  • props --- свойства (реактивные объекты и наблюдаемые)
  • объект контекста контекста - используйте этот объект и больше не нужно беспокоиться о том, куда он указывает

Создание реактивных объектов

Как понять реактивные объекты? Я использовал пример для описания сценария в другой статье, проверьте его:[Vue.js Advanced] Кратко о том, что я узнал из исходного кода Vue (часть 1)

Теперь давайте посмотрим, как Vue3 генерирует адаптивные данные.

reactive

Функция принимает объект в качестве параметра и возвращает прокси-объект.reactiveОбъекты, созданные функциями, потеряют отзывчивость, если они не используются должным образом.

Давайте сначала посмотрим, при каких обстоятельствах будет терять отзывчивость:

setup(){
    const obj=reactive({
      name:'金毛',
      age:10
    });
    return{
      ...obj, //失去响应式了 因为obj失去了引用了
    }
  },

Как исправить эту потерю отзывчивости?

return {
	 obj, //这样写那么模板就要都基于obj来调取, 类型{{obj.age}}
 	...toRefs(obj)   //用toRefs包装,必须是reactive生成的对象, 普通对象不可以, 它把每一项都拿出来包了一下, 这样就可以在模板中使用 {{age}}。
}

О том, что такое toRefs, я расскажу дальше, а пока знание его может решить проблему потери отзывчивости.

ref

потому чтоreactiveФункции могут проксировать объект, но не могут проксировать базовые типы данных, поэтому вам нужно использоватьrefфункции для косвенного управления примитивными типами данных. Эта функция выполняет операции упаковки данных примитивного типа данных, чтобы сделать их реактивным объектом, который может отслеживать изменения данных.

 <span @click="addN">快来点击我呗~~</span>
 <span>{{n}}</span> 
setup(){
    
    const n=ref(1); //生成的n是一个对象, 这样方便vue去监控它
    function addN(){
      console.log(n.value,n,'.....')
      n.value++;  //注意这里要使用.value的形式, 因为n是对象, value才是他的值
    }
    return {
      n,      //返回出去页面的template才可以使用它, {{n}} 不需要.value
      addN,
    }
 }

Как реализовать реф?

Прежде чем реализовать ref, давайте сначала разберемся, что содержится в исходном коде Vue3.trackа такжеtriggerэти два аписа.

трек и триггер являются ядром сбора зависимостей

trackИспользуется для отслеживания зависимостей коллекций (эффектов коллекций): принимает три параметра. ЭтоtriggerИспользуется для запуска ответа (выполнения эффекта). (ps: В продолжении статьи будет описано, как реализовать эти два API, так что давайте пока оставим вопрос...)

Вот использование js для однопоточности, поэтому вы можете перехватывать сбор зависимостей при получении значения и запускать обновление зависимости при установке значения обновления. Следовательно, реализацию ref можно грубо записать так:

function myRef(val: any) {
  let value = val
  const ref = {
    get value() {
      // 收集依赖
      track(r, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newVal: any) {
      if (newVal !== value) {
        value = newVal
        // 触发响应
        trigger(r, TriggerOpTypes.SET, 'value')
      }
    }
  }
  return ref
}

toRef и toRefs

как указано вышеreactiveсодержимое модуля с помощьюtoRefsчтобы обернуть сгенерированный объект, то сгенерированный объект указывает на соответствующее свойство объектаref.

Используйте 🌰 на официальном сайте Vue3, чтобы углубить свое понимание:

const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)
// ref 和 原始property “链接”
state.foo++
console.log(stateAsRefs.foo.value) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

Принципы реализации toRef и toRefs одинаковы. toRef используется для преобразования значения ключа реактивного объекта в ссылку. Функция toRefs преобразует все ключи отзывчивого объекта в ссылки.

Как реализовать toRef

Поскольку целевой объект сам по себе является реагирующими данными, он уже подвергся сбору зависимостей, и срабатывает перехват, вызванный ответом, поэтому он не нужен при реализации toRef.

Здесь реализован toRef, тогда toRefs может быть реализован естественным образом... (ps: можно получить путем обхода)

function toRef(target, key) {
    return {
        get value() {
            return target[key]
        },
        set value(newVal){
            target[key] = newVal
        }
    }
}

думать

Работая с вещами, связанными с ref, мы увидели, что в шаблонах нет .value для получения данных, но .value используется в блоках кода js для доступа к свойствам.

Это можно охарактеризовать как автоматическую распаковку Vue3:

JS: необходимо получить доступ к объекту-оболочке через .value

Шаблоны: автоматическая распаковка, то есть нет необходимости использовать доступ .value в шаблонах

побочный эффект

В Vue3 есть концепция побочных эффектов, так что же такое побочные эффекты?

Описываемый далее API связан с этим побочным эффектом!

effect & watchEffect

effectЭта функция используется для определения побочных эффектов, а ее параметром является функция побочных эффектов, которая может вызывать побочные эффекты. По умолчанию этот побочный эффект выполняется первым.

Как понять этот побочный эффект?

Его можно узнать по следующему коду:

import { effect,reactive } from '@vue/reactivity';
// import {watchEffect} from '@vue/runtime-core';
// 使用 reactive() 函数定义响应式数据
const obj = reactive({ text: 'hello' })
// 使用 effect() 函数定义副作用函数
effect(() => {
     document.body.innerText = obj.text
})

// watchEffect(() => {
//      document.body.innerText = obj.text
// })

// 一秒后修改响应式数据,这会触发副作用函数重新执行
setTimeout(() => {
  obj.text += ' world'
}, 1000)

То есть функция обратного вызова cb, полученная эффектом, является побочным эффектом, при изменении данных будет срабатывать cb...

думать

import {effect,reactive } from '@vue/reactivity';
const obj = reactive({ a: 1 })
effect(() => {
   console.log(obj.foo)
}
obj.a++
obj.a++
obj.a++
//结果:2,3,4

Когда функция эффекта связана с ответными данными, как только ответные данные изменятся, будет выполнен обратный вызов функции эффекта. То есть он изменяется несколько раз и выполняется несколько раз. Плох ли этот спектакль? ?

эффекты могут быть переданы вторым параметром{ scheduler: XXX }, Укажите планировщик: XXX.

Так называемый планировщик используется для указания того, как запускать функцию побочного эффекта.

а такжеwatchEffectФункция основана на принципе этого планировщика для оптимизации реализации побочных эффектов.

import {reactive } from '@vue/reactivity';
 import {watchEffect} from '@vue/runtime-core';
const obj = reactive({ a: 1 })
watchEffect(() => {
   console.log(obj.foo)
}
obj.a++
obj.a++
obj.a++
//结果:4

Так что же это за идея реализации? Это эквивалентно использованию очереди очереди для сбора этих CB и вынесения суждения о том, существует ли уже CB в очереди перед сбором. Затем выполните cb в очереди через цикл while. Поддельный код:

const queue=  [];
let dirty = false;
function queueHandle(job) {
  if (!queue.includes(job)) queue.push(job)
  if (!dirty) {
    dirty = true
    Promise.resolve().then(() => {
      let fn;
      while(fn = queue.shift()) {
        fn();
      }
    })
  }
}

Как правило, в среде разработки не используйте эффект, а используйте watchEffect.

Асинхронные побочные эффекты

Только что упомянутое выше заключается в том, что в случае синхронизации асинхронные побочные эффекты, такие как изменение данных, вызовут другой запрос ajax. Мы не можем судить, какой запрос быстрее, что вносит в нас неопределенность...

Так как же разрешить эту неопределенность?

Чтобы найти способ очистить обратный вызов, когда он недействителен, я думаю, что сделать ранее приостановленную асинхронную операцию недействительной, очистив последний асинхронный побочный эффект, когда выполняется побочный эффект этого времени.

Vue3 может получать функцию обратного вызова, переданную с помощью watchEffect.onInvalidateфункционировать как входной параметр.

Общий принцип может быть реализован на основе эффекта:

import { effect } from '@vue/reactivity'

function watchEffect(fn: (onInvalidate: (fn: () => void) => void) => void) {
  let cleanup: Function
  function onInvalidate(fn: Function) {
    cleanup = fn
  }
  // 封装一下 effect
  // 在执行副作用函数之前,先使上一次无作用无效
  effect(() => {
    cleanup && cleanup();
    fn(onInvalidate)
  })
}

Как остановить побочные эффекты

Vue3 предоставляет функцию остановки для прекращения побочных эффектов.

Функция эффекта возвращает значение, которое на самом деле является самим эффектом. Передайте это возвращаемое значение в функцию остановки, тогда функция обратного вызова эффекта не может быть вызвана при последующем изменении данных.

разница

watchEffectБудет поддерживать связь с экземпляром компонента и состоянием компонента (удален ли он и т. д.), если компонент удален, тоwatchEffectтакже будетstop,ноeffectНе будет.

effectНам необходимо очистить побочные эффекты, иначе он не будет активно очищаться.

watch

давай поговоримwatch, что эквивалентно слушателю компонента.watchНеобходимо прослушивать определенный источник данных и выполнять побочные эффекты в функции обратного вызова. Он также ленив по умолчанию, т. е. обратный вызов выполняется только тогда, когда прослушиваемый источник изменяется.

Реализация этого API не сильно отличается от Vue 2. Ключевым моментом является то, что Vue 3 расширяет некоторые функции на основе Vue 2, который более совершенен, чем Vue 2...

// 特定响应式对象监听
// 也可开启immediate: true, 这个和2.0没什么区别
watch(
  text,
  () => {
    console.log("watch text:");
  }
);

// 特定响应式对象监听 可以获取新旧值
watch(
  text,
 (newVal, oldVal) => {
    console.log("watch text:", newVal, oldVal);
  },
);

// 多响应式对象监听
watch(
  [firstName,lastName],
 ([newFirst,newLast], [oldFirst,oldlast]) => {
   console.log(newFirst,'新的first值',newLast,'新的last值')
  },
  
);

По сравнению с watchEffect, watch позволяет нам:

  • ленивое выполнение побочных эффектов;
  • Уточните, какое состояние должно инициировать повторный запуск слушателя;
  • Получите доступ к значениям до и после прослушивания изменений состояния.

triggerRef

Помните Vue2$forceUpdateЭто обязательное обновление? В Vue3 также есть API, который вызывает побочные эффекты. Давайте посмотрим на следующий код:

//shallowRef它只代理 ref 对象本身,也就是说只有 .value 是被代理的,而 .value 所引用的对象并没有被代理
const shallow = shallowRef({
  greet: 'Hello, world'
})

// 第一次运行时记录一次 "Hello, world"
watchEffect(() => {
  console.log(shallow.value.greet)
})

// 这不会触发作用,因为 ref 很浅层
shallow.value.greet = 'Hello, universe'

// 手动触发,记录 "Hello, universe"
triggerRef(shallow)

жизненный цикл вещь

2.x против 3.0

  beforeCreate -> 使用 setup()
  created -> 使用 setup()
  beforeMount -> onBeforeMount  ---只能在setup里面使用
  mounted -> onMounted		---只能在setup里面使用
  beforeUpdate -> onBeforeUpdate	---只能在setup里面使用
  updated -> onUpdated		---只能在setup里面使用
  beforeDestroy -> onBeforeUnmount		---只能在setup里面使用
  destroyed -> onUnmounted		---只能在setup里面使用
  errorCaptured -> onErrorCaptured		---只能在setup里面使用

Получите настоящий элемент Дом

Ссылка Vue2 получает настоящий элемент domthis.$refs.XXX, а Vue3 также получает реальный элемент dom через ref, но метод записи изменился.

 <div v-for="item in list" :ref="setItemRef"></div>
  <p ref="content"></p>
import { ref, onBeforeUpdate, onUpdated } from 'vue'

export default {
setup() {
	//定义一个变量接收dom
  let itemRefs = [];
  let content=ref(null);
  const setItemRef = el => {
    itemRefs.push(el)
  }
  onBeforeUpdate(() => {
    itemRefs = []
  })
  onUpdated(() => {
    console.log(itemRefs)
  })
  //返出去的名称要与dom的ref相同, 这样就可以接收到dom的回调
  return {
    itemRefs,
    setItemRef,
    content
  }
}
}

обеспечить/внедрить -- разработать плагины

Раньше при разработке общедоступного компонента или инкапсуляции общедоступного плагина функция навешивалась на прототип или использовала примеси.

На самом деле это нехорошо, зависание на прототипе делает Vue раздутым и есть вероятность конфликтов имен, примеси заставят код прыгать, и логика читалки будет прыгать.

Теперь есть новый планCompositionAPI, реализовать разработку плагинов

1. Публичные функции плагина могут быть обернуты функцией предоставления и функцией ввода.

import {provide, inject} from 'vue';
// 这里使用symbol就不会造成变量名的冲突了, 这个命名权交给用户才是真正合理的架构设计
const StoreSymbol = Symbol()

export function provideString(store){
  provide(StoreSymbol, store)  //
}

//目标插件
export function useString() {
  const store = inject(StoreSymbol)  
  /*拿到具体的值了,可以做相关的功能了*/
  return store
}

2. Инициализировать данные в корневом компоненте, ввести функцию ProvideString и выполнить передачу данных

export default {
  setup(){
    // 一些初始化'配置/操作'可以在这里进行
    // 需要放在对应的根节点, 因为依赖provide 和 inject
     provideString({
       a:'可能我是axios',
       b:'可能我是一个message弹框'
     })
  }
}

3. Внедрите плагины в нужные компоненты, чтобы завершить использование связанных функций

import { useString } from '../插件';

export default {
  setup(){
    const store = useString(); //可以使用这个插件了
  }
}

Принцип отзывчивости Vue3

Отзывчивые недостатки vue2:

  • Регенерация будет возвращена по умолчанию
  • Не поддерживает изменение длины массива, чтобы реагировать
  • Свойства, которых нет у объекта, не будут перехвачены

Как добиться

Вышеупомянутые оставшиеся сомнения теперь реализованы здесь.

Давайте сначала проанализируем, как реализован принцип адаптивности Vue3:

  • Vue3 не используетсяObject.definePropertyПерехвачен, вместо альтернативы ES6Proxy.
  • Сбор зависимостей не выполняется черезDepкласс иWatchкласс, но черезtrackФункция связывает целевой объект с побочными эффектами черезtriggerДелайте зависимые ответы.
  • Побочные эффекты хранятся в виде стеков, а принцип «первым пришел — последним вышел».

Это только общая идея.В процессе реализации есть много подробного анализа.Далее я объясню это шаг за шагом...

первый шаг:

Во-первых, давайте реализуем реактивность Проблемы, которые необходимо учитывать при реализации этой функции:

1. {a:{b:2}} такой многоуровневый вложенный объект, как реагировать на обновление?

2. Объект несколько раз вызывает функцию реактивности, что делать?

3. Что делать, когда прокси-объект объекта вызывает функцию реактивности?

4. Как определить, является ли объект новым атрибутом или измененным атрибутом?

//工具类函数
function isObject(obj){
  return typeof obj==='object' && obj!==null?true:false;
}
function isOwnKey(target,key){
  return Object.hasOwnProperty(target,key)?true:false;
}

function reactivity(target){
 return  createReactivity(target);
}
let toProxy=new WeakMap();   //用来连接目标对象(key)与代理对象(value)的关系
let toRaw=new WeakMap();   //用来连接代理对象(key)与目标对象(value)的关系

function createReactivity(target){
  if(!isObject(target)){
    return ;
  }
  let mapProxy=toProxy.get(target); //处理目标对象被响应式处理多次的情况
  if(mapProxy){
    return mapProxy;
  }
  if(toRaw.has(target)){  //处理目标对象的代理对象被响应式处理  let proxy=reactivity(obj);reactivity(proxy);
    return target;
  }
  let proxy=new Proxy(target,{
    get(target,key,receiver){
      let res=Reflect.get(target,key);
      //在获取属性的时候,收集依赖
      track(target,key);  ++++
      return isObject(res)?reactivity(res):res;  //递归实现多层嵌套的对象
    },
    set(target,key,value,receiver){
      let res=Reflect.set(target, name, value,receiver);  
      let oldValue=target[key];  //获取老值,用于比较新值和老值的变化,改变了才修改
      
      /**通过判断key是否已经存在来判断是新增属性还是修改属性,并且在新增的时候可能会改变原有老的属性,这一点大多数人都不会被考虑到 */
      if(!isOwnKey(target,key)){
        console.log('新增属性');
        //在设置属性的时候发布
        trigger(target,'add',key); +++
      }else if(oldValue!==value){
        console.log('修改属性');
        trigger(target,'set',key); +++
      }
      return res;
    },
  })
  toProxy.set(target,proxy);
  toRaw.set(proxy,target);
  return proxy;
}
Шаг 2:

Теперь давайте реализуем функцию побочного эффекта.Самый простой способ для этой функции — передать fn в качестве входного параметра.

Мы используем глобальную очередь для хранения эффектов, способ хранения этой очереди — это то, как я только что сказал, это стековая очередь.

Тогда эффект будет выполняться один раз по умолчанию, поэтому сначала соберите эффект, а затем используйте принцип js single thread, чтобы связать целевой объект с эффектом.

let effectStack=[];  //栈队列,先进后出

function effect(fn){
  let effect=createEffect(fn);
  effect();  //默认先执行一遍
}
function createEffect(){
  let effect=function(){
    run(effect,fn);
  }
  return effect;
}
//run执行函数,功能1.收集effect,2.执行fn
function run(effect,fn){
  //利用try-finally来防止当发生错误的时候也会执行finally的代码
  //利用js是单线程执行的。先收集再关联
  try{
    effectStack.push(effect);
    fn();
  }finally{
    effectStack.pop();
  }
}
третий шаг:

Ключом к этому шагу является то, как связать ключ целевого объекта с эффектом при сборе зависимостей. Здесь есть специальная структура данных:

 {
   target:{
     key1:[effect1,effect12],
     key2:[effect3,effect4],
     key3:[effect5],
   }
 }

Каждый целевой объект (как ключ) имеет соответствующее значение (которое является объектом), а затем объект (значение) отображает ключ и эффект.

Затем при ответе на зависимости, поскольку мы получили связь между эффектом и ключом, соответствующим целевому объекту, можно запустить обход.

let targetMap=new WeakMap();
//收集依赖
function track(target,key){
  let effect=effectStack[effectStack.length-1]; //从栈获取effect看看是否有副作用
  if(effect){  //有关系才创建依赖
    let depMap=targetMap.get(target);
    if(!mapRarget){
      targetMap.set(target,(depMap=new Map()));
    }
    let deps=depMap.get(key);
    if(!deps){
      mapDep.set(key,(deps=new Set()));
    }
    if(!deps.has(effect)){
      deps.add(effect)
    }
  }
}
//响应依赖
function trigger(target,type,key){
  let depMap=targetMap.get(target);
  if(depMap){
   let deps= depMap.get(key);  //当前的key所对应的effect
   if(deps){
     deps.forEach(effect=>effect());
   }
  }
}

Что ж, здесь в основном реализован адаптивный принцип Vue3.Конечно, вы можете прочитать исходный код после прочтения этой статьи.Я думаю, вам будет легче понять исходный код~

Суммировать

В процессе обучения просматривал исходники Vue3 дня 3. Честно говоря, это была довольно головная боль. Позже я решил не смотреть сначала исходный код, сначала узнать, как использовать Vue3 для начала, потом подумать, зачем он используется, и, наконец, как его реализовать.

Дорога к знаниям действительно路漫漫其修远兮,吾将上下而求索

использованная литература

Документация Vue3 на китайском языке

【Официальное руководство по Vue3】🎄 4D Notes | Синхронизированное обучающее видео