从 0 到 1 手写 Vue3 响应式
前言
本文源自个人笔记,文笔一般,摘抄过多,重在分享,主打详细。
对比vue2
首先,Vue3中使用 Proxy对象 来代替 Vue 2 中基于 Object.defineProperty, 消除了 Vue 2 中基于 Object.defineProperty 所存在的一些局限,比如无法监听数组索引,length属性。默认监听动态添加属性和属性的删除操作,就很方便。
Reflect
ES6出现的新特性,代码运行期间用来设置或获取对象成员(操作对象成员),
Reflect没有出现前使用Object的一些方法比如 Object.getPrototypeOf,
Reflect也有对应的方法 Reflect.getPrototypeOf
,两者都是一样的,不过Reflect更有语义。
Reflect.get(obj, prop) === obj[prop]
Reflect.get(target, key, receiver)
中的receiver
参数修改了this
指向,
不加this
指向target
, 加了后指向receiver
,
也就是说,谁调用它,this就指向谁。
const target = {
name: '小浪',
age: 22,
}
const handler = {
get(target, key, receiver) {
console.log(`获取对象属性${key}值`)
// Reflect.get(target, key) == target[key], 不加 receiver
// this指向 永远是 被代理的对象
// 即使 Object.setPrototypeOf( 新对象 , 被代理的对象),this 依然指向 被代理的对象
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(`设置对象属性${key}值`)
return Reflect.set(target, key, value, receiver)
},
deleteProperty(target, key) {
console.log(`删除对象属性${key}值`)
return Reflect.deleteProperty(target, key)
},
}
const proxy = new Proxy(target, handler)
console.log(proxy.age)
proxy.age = 21
console.log(delete proxy.age)
/**
output:
获取对象属性age值
22
设置对象属性age值
删除对象属性age值
true
*/
reactive本质
返回 一个 proxy对象,可实现深层次递归,也就是说,子元素存在引用类型会递归处理。
副作用函数与响应式数据
副作用函数,就是会产生副作用的函数,也就是副作用函数的执行会直接或者间接影响到其他函数,比如函数修改了一个全局变量,也算是副作用。
响应式数据,在副作用函数执行时,会触发到变量的读取操作。当你修改变量的值时,会触发到变量的设置操作。
实现原理
- 先基于 监控对象属性更加完善 的 Proxy 和 可在代码运行期间操控对象成员的 Reflect,实现一个最基础版的响应式系统。
响应式系统-初级版
const isObject = val => val !== null && typeof val === 'object'
const hasOwn = (target,key) => Object.prototype.hasOwnProperty.call(target,key)
// 基础版 响应式代码
function reactive(target){
if(!isObject(target)){
return target;
}
const handler = {
get(target,key,receiver){
console.log('获取对象属性key',key);
const result = Reflect.get(target,key,receiver);
if(isObject(result)){
return reactive(result);
}else{
return result;
}
},
set(target,key,value,receiver){
console.log('设置对象属性key',key);
const old = Reflect.get(target,key,receiver)
let flag = true;
if(old !== value){
flag = Reflect.set(target,key,value,receiver)
}
return flag;
},
deleteProperty(target,key){
console.log('删除对象属性key',key)
return Reflect.deleteProperty(target, key)
}
}
return new Proxy(target,handler);
}
let p = reactive({name:'1',age: 20})
p.age += 1;
console.log(p.name);
delete p.name;
- 一个响应式数据,也就是一个对象会有很多的属性key,每个 key 都有关联的一系列 effect 副作用函数,可用 集合 Set 存储。很多的key可以放Map里维护,这个 Map 是在对象存在的时候它就存在,对象销毁的时候它也要跟着销毁,而 js中 WeakMap 正好就有这样的特性,WeakMap 的 key 必须是一个对象,value 可以是任意数据,key 的对象销毁的时候,value 也会销毁。
结论: vue3 的响应式数据会用 WeakMap 来保存,key 为原对象,value为 n 个 属性key 和 n个 副作用函数集合Set 组成的 map。
-
基于以上理论,在 Proxy 代理 状态的 get 中 可 实现 自动收集 每个 key值 的 副作用依赖, set 中 可 循环触发 之前收集到的 副作用函数的执行。
track函数:收集副作用依赖。trigger函数: 触发副作用函数的执行。
// targetMap 表里每个key都是一个普通对象 对应他们的 depsMap
let targetMap = new WeakMap()
let activeEffect
function track(target, key) {
// 如果当前没有effect就不执行追踪
if (!activeEffect) return
// 获取当前对象的依赖图
let depsMap = targetMap.get(target)
// 不存在就新建
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 根据key 从 依赖图 里获取到到 effect 集合
let dep = depsMap.get(key)
// 不存在就新建
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 如果当前effect不存在,才注册到 dep里
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
}
}
// trigger 响应式触发
function trigger(target, key) {
// 拿到 依赖图
const depsMap = targetMap.get(target)
if (!depsMap) {
// 没有被追踪,直接 return
return
}
// 拿到了 视图渲染effect 就可以进行排队更新 effect 了
const dep = depsMap.get(key)
// 遍历 dep 集合执行里面 effect 副作用方法
if (dep) {
dep.forEach(effect => {
effect()
})
}
}
- 对于effect函数而言,除了执行放入的副作用函数本身,我们还需要考虑到 很多情况。
-
添加新的依赖前要先清除(cleanup) 之前的依赖。
-
值得注意的是,在 Proxy 代理 状态的set中 不能 直接遍历执行 收集到的副作用依赖,因为执行前会清除依赖,执行后又产生依赖,所以这会造成 副作用的依赖数组 的 无限增删过程。
方案: 就是根据 获取到的副作用集合 创建一个新的集合,只用于遍历执行。
- 最后,我们需要支持effect函数的嵌套,因为vue组件本身是可以嵌套的,而且组件里会写 effect,那也就是 effect 嵌套了,所以必须支持嵌套。
方案: 利用栈的思想,执行 effect 函数前把当前 effectFn 入栈,执行完以后出栈,修改 activeEffect 为栈顶的 effectFn。
effect函数代码
const data = {
a: 1,
b: 2
}
let activeEffect
const effectStack = [];
function effect(fn) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn);
fn()
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
effectFn.deps = []
effectFn()
}
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
- 综上所述,即可完成一个比较完善的响应式系统。
响应式系统-中级版
// 判断是否为对象 ,注意 null 也是对象
const isObject = val => val !== null && typeof val === 'object'
// 判断key是否存在
const hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key)
function reactive(target) {
// 首先先判断是否为对象
if (!isObject(target)) return target
const handler = {
get(target, key, receiver) {
console.log(`获取对象属性${key}值`)
// 这里还需要收集依赖,先空着
track(target, key)
const result = Reflect.get(target, key, receiver)
// 递归判断的关键, 如果发现子元素存在引用类型,递归处理。
if (isObject(result)) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
console.log(`设置对象属性${key}值`)
// 首先先获取旧值
const oldValue = Reflect.get(target, key, reactive)
// set 是需要返回 布尔值的
let result = true
// 判断新值和旧值是否一样来决定是否更新setter
if (oldValue !== value) {
result = Reflect.set(target, key, value, receiver)
trigger(target, key)
}
return result
},
deleteProperty(target, key) {
console.log(`删除对象属性${key}值`)
// 先判断是否有key
const hadKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
// 删除时,是否需要 响应式触发trigger
trigger(target, key)
}
return result
},
}
return new Proxy(target, handler)
}
// activeEffect 表示当前正在走的 effect
let activeEffect = null
const effectStack = [];
function effect(fn) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn);
fn()
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
effectFn.deps = []
effectFn()
}
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
// targetMap 表里每个key都是一个普通对象 对应他们的 depsMap
let targetMap = new WeakMap()
function track(target, key) {
// 如果当前没有effect就不执行追踪
if (!activeEffect) return
// 获取当前对象的依赖图
let depsMap = targetMap.get(target)
// 不存在就新建
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 根据key 从 依赖图 里获取到到 effect 集合
let dep = depsMap.get(key)
// 不存在就新建
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 如果当前effectc 不存在,才注册到 dep里
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
}
}
// trigger 响应式触发
function trigger(target, key) {
// 拿到 依赖图
const depsMap = targetMap.get(target)
if (!depsMap) {
// 没有被追踪,直接 return
return
}
// 拿到了 视图渲染effect 就可以进行排队更新 effect 了
const dep = depsMap.get(key)
// 遍历 dep 集合执行里面 effect 副作用方法
// 避免 副作用的依赖数组 无限增删依赖问题
const effectsToRun = new Set(dep);
if (dep) {
effectsToRun.forEach(effect => {
effect()
})
}
}
let a = reactive({b : 1, c: 'name'})
effect(()=>{
console.log('effect1',a.c);
a.b = 2;
delete a.c;
effect(()=>{
// delete a.b;
console.log('effect2',a.b);
})
})
computed 实现
计算属性 computed :本质就是 effect 函数的返回值。
当然,这需要我们先简单改造下之前写的effect函数,也就是把fn执行的结果返回出来。
function computed(fn) {
const value = effect(fn);
return value
}
const value = computed(() => {
return obj.a + obj.b;
});
对比下 我们平时用的computed,我们会发现 初步实现的computed 会有以下问题。
- computed 这里的 effectFn 每次都是重新执行。
- 每次访问变量,执行了 所有的 effectFn 函数。
- 返回的结果,不是响应式数据。
为了解决问题1,可以给effect 加个options参数,让 effect 函数 有 返回函数 | 执行并返回函数 两种选项。
然后 computed 里创建一个对象,在 value 属性的 get 触发时,才重新执行effectFn函数,拿到最新的值。
function effect(fn,options) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn);
const res = fn()
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
return res;
}
effectFn.deps = []
effectFn.options = []
if(!options.lazy){
effectFn()
}
// computed
return effectFn
}
function computed(fn) {
const effectFn = effect(fn,{
lazy: true
});
const obj = {
get value(){
return effectFn()
}
}
return obj
}
为了解决问题2,我们依旧是需要升级老代码,因为这个问题的本质是我们实现的trigger函数没有所谓的调度算法,
不能精准判断 哪些副作用是该重新执行的。当 trigger 响应式触发可以根据我们自定义的逻辑来调度后,可设置一个 dirty 变量 ,当 scheduler 被调用的时候就说明数据变了,这时候 dirty 设置为 true,然后取 value 的时候就重新计算,之后再改为 false,下次取 value 就直接拿计算好的值了。
至于问题3,其实很好解决,遵循之前的规则。
在访问属性时,track函数收集依赖,在修改属性值,trigger函数触发依赖的执行。
响应式系统-高级版
// 判断是否为对象 ,注意 null 也是对象
const isObject = val => val !== null && typeof val === 'object'
// 判断key是否存在
const hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key)
function reactive(target) {
// 首先先判断是否为对象
if (!isObject(target)) return target
const handler = {
get(target, key, receiver) {
console.log(`获取对象属性${key}值`)
// 这里还需要收集依赖,先空着
track(target, key)
const result = Reflect.get(target, key, receiver)
// 递归判断的关键, 如果发现子元素存在引用类型,递归处理。
if (isObject(result)) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
console.log(`设置对象属性${key}值`)
// 首先先获取旧值
const oldValue = Reflect.get(target, key, reactive)
// set 是需要返回 布尔值的
let result = true
// 判断新值和旧值是否一样来决定是否更新setter
if (oldValue !== value) {
result = Reflect.set(target, key, value, receiver)
trigger(target, key)
}
return result
},
deleteProperty(target, key) {
console.log(`删除对象属性${key}值`)
// 先判断是否有key
const hadKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
// 删除时,是否需要 响应式触发trigger
trigger(target, key)
}
return result
},
}
return new Proxy(target, handler)
}
// activeEffect 表示当前正在走的 effect
let activeEffect = {}
const effectStack = [];
function effect(fn,options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn);
const res = fn()
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
return res;
}
effectFn.deps = []
effectFn.options = options;
if(!options.lazy){
effectFn()
}
// computed
return effectFn
}
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
// targetMap 表里每个key都是一个普通对象 对应他们的 depsMap
let targetMap = new WeakMap()
function track(target, key) {
if (!activeEffect) return
// 获取当前对象的依赖图
let depsMap = targetMap.get(target)
// 不存在就新建
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 根据key 从 依赖图 里获取到到 effect 集合
let dep = depsMap.get(key)
// 不存在就新建
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 如果当前effectc 不存在,才注册到 dep里
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
// trigger 响应式触发
function trigger(target, key) {
// 拿到 依赖图
const depsMap = targetMap.get(target)
if (!depsMap) {
// 没有被追踪,直接 return
return
}
// 拿到了 视图渲染effect 就可以进行排队更新 effect 了
const dep = depsMap.get(key)
// 遍历 dep 集合执行里面 effect 副作用方法
// 避免 副作用的依赖数组 无限增删依赖问题
const effectsToRun = new Set(dep);
effectsToRun.forEach(effectFn => {
if(effectFn.options.scheduler){
effectFn.options.scheduler(effectFn)
}else{
effectFn()
}
})
}
function computed(fn) {
let dirty = true;
let val;
const effectFn = effect(fn,{
lazy: true,
scheduler(fn){
if(!dirty) {
dirty = true
trigger(obj, 'value');
}
}
});
const obj = {
get value(){
if(dirty){
val = effectFn();
dirty = false;
// console.log('重新计算',val);
}else{
// console.log('旧值',val);
}
track(obj, 'value');
return val;
}
}
return obj
}
let obj = reactive({a : 1, b : 2})
effect(()=>{
let res = computed(()=>{
return obj.a + obj.b ;
},{lazy: true})
console.log('computed1',res.value);
// console.log('computed2',res.value);
obj.a = 3;
})
watch实现
watch,本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数。
与 computed 类似, 通过effect设置scheduler选项,即可在数据发生变化时不是直接执行副作用函数,而是触发我们自定义的scheduler函数。
也就是说,在scheduler里 执行传入的回调函数,一个最简版watch实现也就写好了。
与vue3的watch相比,这个最简版的watch还缺了一些东西。
- vue3的watch,不仅支持响应式数据(基本数据类型 ,引用数据类型),还支持 getter函数 形式 。
- vue3的watch,可以在回调函数中拿到新值和旧值。
- vue3的watch,immediate选项为true时,立即执行一次。
针对问题1,其实很简单,只要封装一个通用的遍历读取属性的函数即可, 如果入参是函数,则 getter函数返回值 就是 读取函数的返回值。
// 遍历读取对象的每个值
function traverse(value,seen = new Set()){
if(value !== 'object' || value === null || seen.has(value)){
return;
}
seen.add(value);
for(const k in value){
traverse(value[k],seen);
}
return value;
}
function watch(source,cb){
let getter;
if(typeof source === 'function'){
getter = source
}else{
getter = traverse(source)
}
effect(()=> getter(),{
scheduler(){
// 执行回调函数
cb()
}
})
}
针对问题2,其实可以先回想下上文 介绍 computed 通过设置一个 dirty 变量 来 判断数据是否更新时的做法,
还是在effect的scheduler里处理,设置oldValue,newValue两个变量,重新执行effect函数的结果就是newValue,
执行回调函数后,曾经的新值newValue也就变成了旧值oldValue。
function watch(source,cb){
let getter;
let newValue,oldValue;
if(typeof source === 'function'){
getter = source;
}else{
getter = traverse(source);
}
const effectFn = effect(()=> getter(),{
lazy: true,
scheduler(){
// 执行回调函数
newValue = effectFn();
cb(oldValue,newValue);
oldValue = newValue;
}
})
// 第一次调用时的值,也就是旧值
oldValue = effectFn();
}
watch(()=>obj.a,(oldValue,newValue)=>{
console.log('old',oldValue);
console.log('new',newValue);
})
obj.a = 12;
针对问题3,其实就是一个调度时机的判定问题。
当 immediate选项为true时,新值 newValue 对应的 是 读取变量 执行的副作用函数结果,oldValue 在watch中还没赋过值,自然是undefined。
当 immediate选项为false时,我们想在变量第一次发生变化时获取旧值,就需要oldValue在watch函数创建之初就变为副作用函数结果。
至此,watch函数的主要功能已经完成,代码如下。
响应式系统-专业版
// 判断是否为对象 ,注意 null 也是对象
const isObject = val => val !== null && typeof val === 'object'
// 判断key是否存在
const hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key)
function reactive(target) {
// 首先先判断是否为对象
if (!isObject(target)) return target
const handler = {
get(target, key, receiver) {
console.log(`获取对象属性${key}值`)
// 这里还需要收集依赖,先空着
track(target, key)
const result = Reflect.get(target, key, receiver)
// 递归判断的关键, 如果发现子元素存在引用类型,递归处理。
if (isObject(result)) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
console.log(`设置对象属性${key}值`)
// 首先先获取旧值
const oldValue = Reflect.get(target, key, reactive)
// set 是需要返回 布尔值的
let result = true
// 判断新值和旧值是否一样来决定是否更新setter
if (oldValue !== value) {
result = Reflect.set(target, key, value, receiver)
trigger(target, key)
}
return result
},
deleteProperty(target, key) {
console.log(`删除对象属性${key}值`)
// 先判断是否有key
const hadKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
// 删除时,是否需要 响应式触发trigger
trigger(target, key)
}
return result
},
}
return new Proxy(target, handler)
}
// activeEffect 表示当前正在走的 effect
let activeEffect = {}
const effectStack = [];
function effect(fn,options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn);
const res = fn()
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
return res;
}
effectFn.deps = []
effectFn.options = options;
if(!options.lazy){
effectFn()
}
// computed
return effectFn
}
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
// targetMap 表里每个key都是一个普通对象 对应他们的 depsMap
let targetMap = new WeakMap()
function track(target, key) {
if (!activeEffect) return
// 获取当前对象的依赖图
let depsMap = targetMap.get(target)
// 不存在就新建
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 根据key 从 依赖图 里获取到到 effect 集合
let dep = depsMap.get(key)
// 不存在就新建
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 如果当前effectc 不存在,才注册到 dep里
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
// trigger 响应式触发
function trigger(target, key) {
// 拿到 依赖图
const depsMap = targetMap.get(target)
if (!depsMap) {
// 没有被追踪,直接 return
return
}
// 拿到了 视图渲染effect 就可以进行排队更新 effect 了
const dep = depsMap.get(key)
// 遍历 dep 集合执行里面 effect 副作用方法
// 避免 副作用的依赖数组 无限增删依赖问题
const effectsToRun = new Set(dep);
effectsToRun.forEach(effectFn => {
if(effectFn.options.scheduler){
effectFn.options.scheduler(effectFn)
}else{
effectFn()
}
})
}
function computed(fn) {
let dirty = true;
let val;
const effectFn = effect(fn,{
lazy: true,
scheduler(fn){
if(!dirty) {
dirty = true
trigger(obj, 'value');
}
}
});
const obj = {
get value(){
if(dirty){
val = effectFn();
dirty = false;
// console.log('重新计算',val);
}else{
// console.log('旧值',val);
}
track(obj, 'value');
return val;
}
}
return obj
}
// 遍历读取对象的每个值
function traverse(value,seen = new Set()){
if(value !== 'object' || value === null || seen.has(value)){
return;
}
seen.add(value);
for(const k in value){
traverse(value[k],seen);
}
return value;
}
function watch(source,cb,options={}){
let getter;
let newValue,oldValue;
if(typeof source === 'function'){
getter = source;
}else{
getter = traverse(source);
}
const job = ()=>{
newValue = effectFn();
cb(oldValue,newValue);
oldValue = newValue;
}
const effectFn = effect(()=> getter(),{
lazy: true,
scheduler: job
})
if(options.immediate){
job()
}else{
oldValue = effectFn();
}
}
简单总结
- 一个响应式数据的最基本实现依赖于对数据读取和设置操作的拦截,vue3是基于 监控对象属性更加完善 的 Proxy 和 可在代码运行期间操控对象成员的 Reflect来完成的,并且使用weakMap、Map、Set 将 源对象,属性key,每个属性key对应的一系列副作用函数建立起联系。
- 在 Proxy 代理 状态的 get 中 实现 自动收集 每个 key值 的 副作用依赖,封装成track函数。在 set 中循环触发之前收集到的副作用函数的执行,封装成trigger函数。
- 为了避免副作用函数进行不必要的更新,我们会在副作用函数执行前清除上一次建立起的响应联系,并注意到trigger函数直接遍历执行副作用函数集合会产生无限循环问题。
- 利用副作用函数栈结构,实现了副作用的嵌套,使得一个响应式数据只会收集读取其值的副作用,而不会相互影响。
- 为了实现计算属性computed ,effect函数加了lazy和scheduler两个选项,让响应系统具有调度性,而不是每次检测到依赖变动就会立即触发副作用函数,计算属性结合了这点,通过用一个变量标记,当计算属性依赖的响应式数据发生变化时,才会重新计算。
- watch属性,本质也是利用effect函数执行的可调度性,根据回调函数是否执行区分新值和旧值,通过自定义的scheduler函数可以灵活控制回调函数的执行时机。
主要参考文章
/post/713428…
/post/722246…
/post/711274…
…………
其实主要还是看《vue.js设计与实现》。
本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!