2024年最新深入了解 Vue3 响应式原理(1),2024年最新京东前端二面都问啥

最后:

总结来说,面试成功=基础知识+项目经验+表达技巧+运气。我们无法控制运气,但是我们可以在别的地方花更多时间,每个环节都提前做好准备。

面试一方面是为了找到工作,升职加薪,另一方面也是对于自我能力的考察。能够面试成功不仅仅是来自面试前的临时抱佛脚,更重要的是在平时学习和工作中不断积累和坚持,把每个知识点、每一次项目开发、每次遇到的难点知识,做好积累,实践和总结。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

return true

}

} else {

// in shallow mode, objects are set as-is regardless of reactive or not

}

const hadKey = hasOwn(target, key)

const result = Reflect.set(target, key, value, receiver)

// don’t trigger if target is something up in the prototype chain of original

if (target === toRaw(receiver)) {

if (!hadKey) {

// 如果 target 没有 key,就代表是新增操作,需要触发依赖

trigger(target, TriggerOpTypes.ADD, key, value)

} else if (hasChanged(value, oldValue)) {

// 如果新旧值不相等,才触发依赖

// 什么时候会有新旧值相等的情况?例如监听一个数组,执行 push 操作,会触发多次 setter

// 第一次 setter 是新加的值 第二次是由于新加的值导致 length 改变

// 但由于 length 也是自身属性,所以 value === oldValue

trigger(target, TriggerOpTypes.SET, key, value, oldValue)

}

}

return result

}

}

set() 的函数处理逻辑反而没那么难,看注释即可。track() 和 trigger() 将放在下面和 effect.ts 文件一起讲解。

deleteProperty、has、ownKeys

function deleteProperty(target: object, key: string | symbol): boolean {

const hadKey = hasOwn(target, key)

const oldValue = (target as any)[key]

const result = Reflect.deleteProperty(target, key)

// 如果删除结果为 true 并且 target 拥有这个 key 就触发依赖

if (result && hadKey) {

trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)

}

return result

}

function has(target: object, key: string | symbol): boolean {

const result = Reflect.has(target, key)

track(target, TrackOpTypes.HAS, key)

return result

}

function ownKeys(target: object): (string | number | symbol)[] {

track(target, TrackOpTypes.ITERATE, ITERATE_KEY)

return Reflect.ownKeys(target)

}

这三个函数比较简单,看代码即可。

effect.ts 文件


等把 effect.ts 文件讲解完,响应式模块基本上差不多结束了。

effect()

effect() 主要和响应式的对象结合使用。

export function effect<T = any>(

fn: () => T,

options: ReactiveEffectOptions = EMPTY_OBJ

): ReactiveEffect {

// 如果已经是 effect 函数,取得原来的 fn

if (isEffect(fn)) {

fn = fn.raw

}

const effect = createReactiveEffect(fn, options)

// 如果 lazy 为 false,马上执行一次

// 计算属性的 lazy 为 true

if (!options.lazy) {

effect()

}

return effect

}

真正创建 effect 的是 createReactiveEffect() 函数。

let uid = 0

function createReactiveEffect<T = any>(

fn: (…args: any[]) => T,

options: ReactiveEffectOptions

): ReactiveEffect {

// reactiveEffect() 返回一个新的 effect,这个新的 effect 执行后

// 会将自己设为 activeEffect,然后再执行 fn 函数,如果在 fn 函数里对响应式属性进行读取

// 会触发响应式属性 get 操作,从而收集依赖,而收集的这个依赖函数就是 activeEffect

const effect = function reactiveEffect(…args: unknown[]): unknown {

if (!effect.active) {

return options.scheduler ? undefined : fn(…args)

}

// 为了避免递归循环,所以要检测一下

if (!effectStack.includes(effect)) {

// 清空依赖

cleanup(effect)

try {

enableTracking()

effectStack.push(effect)

activeEffect = effect

return fn(…args)

} finally {

// track 将依赖函数 activeEffect 添加到对应的 dep 中,然后在 finally 中将 activeEffect

// 重置为上一个 effect 的值

effectStack.pop()

resetTracking()

activeEffect = effectStack[effectStack.length - 1]

}

}

} as ReactiveEffect

effect.id = uid++

effect._isEffect = true

effect.active = true // 用于判断当前 effect 是否激活,有一个 stop() 来将它设为 false

effect.raw = fn

effect.deps = []

effect.options = options

return effect

}

其中 cleanup(effect) 的作用是让 effect 关联下的所有 dep 实例清空 effect,即清除这个依赖函数。

function cleanup(effect: ReactiveEffect) {

const { deps } = effect

if (deps.length) {

for (let i = 0; i < deps.length; i++) {

deps[i].delete(effect)

}

deps.length = 0

}

}

从代码中可以看出来,真正的依赖函数是 activeEffect。执行 track() 收集的依赖就是 activeEffect。趁热打铁,现在我们再来看一下 track() 和 trigger() 函数。

track()

// 依赖收集

export function track(target: object, type: TrackOpTypes, key: unknown) {

// activeEffect 为空,代表没有依赖,直接返回

if (!shouldTrack || activeEffect === undefined) {

return

}

// targetMap 依赖管理中心,用于收集依赖和触发依赖

let depsMap = targetMap.get(target)

// targetMap 为每个 target 建立一个 map

// 每个 target 的 key 对应着一个 dep

// 然后用 dep 来收集依赖函数,当监听的 key 值发生变化时,触发 dep 中的依赖函数

// 类似于这样

// targetMap(weakmap) = {

// target1(map): {

// key1(dep): (fn1,fn2,fn3…)

// key2(dep): (fn1,fn2,fn3…)

// },

// target2(map): {

// key1(dep): (fn1,fn2,fn3…)

// key2(dep): (fn1,fn2,fn3…)

// },

// }

if (!depsMap) {

targetMap.set(target, (depsMap = new Map()))

}

let dep = depsMap.get(key)

if (!dep) {

depsMap.set(key, (dep = new Set()))

}

if (!dep.has(activeEffect)) {

dep.add(activeEffect)

activeEffect.deps.push(dep)

// 开发环境下会触发 onTrack 事件

if (DEV && activeEffect.options.onTrack) {

activeEffect.options.onTrack({

effect: activeEffect,

target,

type,

key

})

}

}

}

targetMap 是一个 WeakMap 实例。

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

弱引用是什么意思呢?

let obj = { a: 1 }

const map = new WeakMap()

map.set(obj, ‘测试’)

obj = null

当 obj 置为空后,对于 { a: 1 } 的引用已经为零了,下一次垃圾回收时就会把 weakmap 中的对象回收。

但如果把 weakmap 换成 map 数据结构,即使把 obj 置空,{ a: 1 } 依然不会被回收,因为 map 数据结构是强引用,它现在还被 map 引用着。

trigger()

// 触发依赖

export function trigger(

target: object,

type: TriggerOpTypes,

key?: unknown,

newValue?: unknown,

oldValue?: unknown,

oldTarget?: Map<unknown, unknown> | Set

) {

const depsMap = targetMap.get(target)

// 如果没有收集过依赖,直接返回

if (!depsMap) {

// never been tracked

return

}

// 对收集的依赖进行分类,分为普通的依赖或计算属性依赖

// effects 收集的是普通的依赖 computedRunners 收集的是计算属性的依赖

// 两个队列都是 set 结构,为了避免重复收集依赖

const effects = new Set()

const computedRunners = new Set()

const add = (effectsToAdd: Set | undefined) => {

if (effectsToAdd) {

effectsToAdd.forEach(effect => {

// effect !== activeEffect 避免重复收集依赖

if (effect !== activeEffect || !shouldTrack) {

// 计算属性

if (effect.options.computed) {

computedRunners.add(effect)

} else {

effects.add(effect)

}

} else {

// the effect mutated its own dependency during its execution.

// this can be caused by operations like foo.value++

// do not trigger or we end in an infinite loop

}

})

}

}

// 在值被清空前,往相应的队列添加 target 所有的依赖

if (type === TriggerOpTypes.CLEAR) {

// collection being cleared

// trigger all effects for target

depsMap.forEach(add)

} else if (key === ‘length’ && isArray(target)) { // 当数组的 length 属性变化时触发

depsMap.forEach((dep, key) => {

if (key === ‘length’ || key >= (newValue as number)) {

add(dep)

}

})

} else {

// schedule runs for SET | ADD | DELETE

// 如果不符合以上两个 if 条件,并且 key !== undefined,往相应的队列添加依赖

if (key !== void 0) {

add(depsMap.get(key))

}

// also run for iteration key on ADD | DELETE | Map.SET

const isAddOrDelete =

type === TriggerOpTypes.ADD ||

(type === TriggerOpTypes.DELETE && !isArray(target))

if (

isAddOrDelete ||

(type === TriggerOpTypes.SET && target instanceof Map)

) {

add(depsMap.get(isArray(target) ? ‘length’ : ITERATE_KEY))

}

if (isAddOrDelete && target instanceof Map) {

add(depsMap.get(MAP_KEY_ITERATE_KEY))

}

}

const run = (effect: ReactiveEffect) => {

if (DEV && effect.options.onTrigger) {

effect.options.onTrigger({

effect,

target,

key,

type,

newValue,

oldValue,

oldTarget

})

}

if (effect.options.scheduler) {

// 如果 scheduler 存在则调用 scheduler,计算属性拥有 scheduler

effect.options.scheduler(effect)

} else {

effect()

}

}

// Important: computed effects must be run first so that computed getters

// can be invalidated before any normal effects that depend on them are run.

computedRunners.forEach(run)

// 触发依赖函数

effects.forEach(run)

}

对依赖函数进行分类后,需要先运行计算属性的依赖,因为其他普通的依赖函数可能包含了计算属性。先执行计算属性的依赖能保证普通依赖执行时能得到最新的计算属性的值。

track() 和 trigger() 中的 type 有什么用?

这个 type 取值范围就定义在 operations.ts 文件中:

// track 的类型

export const enum TrackOpTypes {

GET = ‘get’, // get 操作

HAS = ‘has’, // has 操作

ITERATE = ‘iterate’ // ownKeys 操作

}

// trigger 的类型

export const enum TriggerOpTypes {

SET = ‘set’, // 设置操作,将旧值设置为新值

ADD = ‘add’, // 新增操作,添加一个新的值 例如给对象新增一个值 数组的 push 操作

DELETE = ‘delete’, // 删除操作 例如对象的 delete 操作,数组的 pop 操作

CLEAR = ‘clear’ // 用于 Map 和 Set 的 clear 操作。

}

type 主要用于标识 track() 和 trigger() 的类型。

trigger() 中的连续判断代码

if (key !== void 0) {

add(depsMap.get(key))

}

// also run for iteration key on ADD | DELETE | Map.SET

const isAddOrDelete =

type === TriggerOpTypes.ADD ||

(type === TriggerOpTypes.DELETE && !isArray(target))

if (

isAddOrDelete ||

(type === TriggerOpTypes.SET && target instanceof Map)

) {

add(depsMap.get(isArray(target) ? ‘length’ : ITERATE_KEY))

}

if (isAddOrDelete && target instanceof Map) {

add(depsMap.get(MAP_KEY_ITERATE_KEY))

}

在 trigger() 中有这么一段连续判断的代码,它们作用是什么呢?其实它们是用于判断数组/集合这种数据结构比较特别的操作。看个示例:

let dummy

const counter = reactive([])

effect(() => (dummy = counter.join()))

counter.push(1)

effect(() => (dummy = counter.join())) 生成一个依赖,并且自执行一次。在执行函数里的代码 counter.join() 时,会访问数组的多个属性,分别是 join 和 length,同时触发 track() 收集依赖。也就是说,数组的 join length 属性都收集了一个依赖。

当执行 counter.push(1) 这段代码时,实际上是将数组的索引 0 对应的值设为 1。这一点,可以通过打 debugger 从上下文环境看出来,其中 key 为 0,即数组的索引,值为 1。

设置值后,由于是新增操作,执行 trigger(target, TriggerOpTypes.ADD, key, value)。但由上文可知,只有数组的 key 为 join length 时,才有依赖,key 为 0 是没有依赖的。

从上面两个图可以看出来,只有 join length 属性才有对应的依赖。

这个时候,trigger() 的一连串 if 语句就起作用了,其中有一个 if 语句是这样的:

if (

isAddOrDelete ||

(type === TriggerOpTypes.SET && target instanceof Map)

) {

add(depsMap.get(isArray(target) ? ‘length’ : ITERATE_KEY))

}

如果 target 是一个数组,就添加 length 属性对应的依赖到队列中。也就是说 key 为 0 的情况下使用 length 对应的依赖。

另外,还有一个巧妙的地方。待执行依赖的队列是一个 set 数据结构。如果 key 为 0 有对应的依赖,同时 length 也有对应的依赖,就会添加两次依赖,但由于队列是 set,具有自动去重的效果,避免了重复执行。

示例

仅看代码和文字,是很难理解响应式数据和 track() trigger() 是怎么配合的。所以我们要配合示例来理解:

let dummy

const counter = reactive({ num: 0 })

effect(() => (dummy = counter.num))

console.log(dummy == 0)

counter.num = 7

console.log(dummy == 7)

上述代码执行过程如下:

1.对 { num: 0 } 进行监听,返回一个 proxy 实例,即 counter。2.effect(fn) 创建一个依赖,并且在创建时会执行一次 fn。3.fn() 读取 num 的值,并赋值给 dummy。4.读取属性这个操作会触发 proxy 的属性读取拦截操作,在拦截操作里会去收集依赖,这个依赖是步骤 2 产生的。5.counter.num = 7 这个操作会触发 proxy 的属性设置拦截操作,在这个拦截操作里,除了把新的值返回,还会触发刚才收集的依赖。在这个依赖里把 counter.num 赋值给 dummy(num 的值已经变为 7)。

用图来表示,大概这样的:

collectionHandlers.ts 文件


collectionHandlers.ts 文件包含了 Map WeakMap Set WeakSet 的处理器对象,分别对应完全响应式的 proxy 实例、浅层响应的 proxy 实例、只读 proxy 实例。这里只讲解对应完全响应式的 proxy 实例的处理器对象:

export const mutableCollectionHandlers: ProxyHandler = {

get: createInstrumentationGetter(false, false)

}

为什么只监听 get 操作,set has 等操作呢?不着急,先看一个示例:

const p = new Proxy(new Map(), {

get(target, key, receiver) {

console.log('get: ', key)

return Reflect.get(target, key, receiver)

},

set(target, key, value, receiver) {

console.log('set: ', key, value)

return Reflect.set(target, key, value, receiver)

}

})

p.set(‘ab’, 100) // Uncaught TypeError: Method Map.prototype.set called on incompatible receiver [object Object]

运行上面的代码会报错。其实这和 Map Set 的内部实现有关,必须通过 this 才能访问它们的数据。但是通过 Reflect 反射的时候,target 内部的 this 其实是指向 proxy 实例的,所以就不难理解为什么会报错了。

那怎么解决这个问题?通过源码可以发现,在 Vue3.0 中是通过代理的方式来实现对 Map Set 等数据结构监听的:

function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {

const instrumentations = shallow

? shallowInstrumentations
isReadonly
? readonlyInstrumentations
mutableInstrumentations

return (

target: CollectionTypes,

key: string | symbol,

receiver: CollectionTypes

) => {

// 这三个 if 判断和 baseHandlers 的处理方式一样

if (key === ReactiveFlags.isReactive) {

return !isReadonly

} else if (key === ReactiveFlags.isReadonly) {

return isReadonly

} else if (key === ReactiveFlags.raw) {

return target

}

return Reflect.get(

hasOwn(instrumentations, key) && key in target

? instrumentations
target,

key,

receiver

)

}

}

把最后一行代码简化一下:

target = hasOwn(instrumentations, key) && key in target? instrumentations : target

return Reflect.get(target, key, receiver);

其中 instrumentations 的内容是:

const mutableInstrumentations: Record<string, Function> = {

get(this: MapTypes, key: unknown) {

return get(this, key, toReactive)

},

get size() {

return size((this as unknown) as IterableCollections)

},

has,

add,

set,

delete: deleteEntry,

clear,

forEach: createForEach(false, false)

}

从代码可以看到,原来真正的处理器对象是 mutableInstrumentations。现在再看一个示例:

const proxy = reactive(new Map())

proxy.set(‘key’, 100)

生成 proxy 实例后,执行 proxy.set('key', 100)proxy.set 这个操作会触发 proxy 的属性读取拦截操作。

打断点可以看到,此时的 key 为 set。拦截了 set 操作后,调用 Reflect.get(target, key, receiver),这个时候的 target 已经不是原来的 target 了,而是 mutableInstrumentations 对象。也就是说,最终执行的是 mutableInstrumentations.set()

接下来再看看 mutableInstrumentations 的各个处理器逻辑。

get

// 如果 value 是对象,则返回一个响应式对象(reactive(value)),否则直接返回 value。

const toReactive = (value: T): T =>

isObject(value) ? reactive(value) : value

get(this: MapTypes, key: unknown) {

// this 指向 proxy

return get(this, key, toReactive)

}

function get(

target: MapTypes,

key: unknown,

wrap: typeof toReactive | typeof toReadonly | typeof toShallow

) {

target = toRaw(target)

const rawKey = toRaw(key)

// 如果 key 是响应式的,额外收集一次依赖

if (key !== rawKey) {

track(target, TrackOpTypes.GET, key)

}

track(target, TrackOpTypes.GET, rawKey)

// 使用 target 原型上的方法

const { has, get } = getProto(target)

// 原始 key 和响应式的 key 都试一遍

if (has.call(target, key)) {

// 读取的值要使用包装函数处理一下

return wrap(get.call(target, key))

} else if (has.call(target, rawKey)) {

return wrap(get.call(target, rawKey))

}

}

get 的处理逻辑很简单,拦截 get 之后,调用 get(this, key, toReactive)

set

function set(this: MapTypes, key: unknown, value: unknown) {

value = toRaw(value)

// 取得原始数据

const target = toRaw(this)

// 使用 target 原型上的方法

const { has, get, set } = getProto(target)

let hadKey = has.call(target, key)

if (!hadKey) {

key = toRaw(key)

hadKey = has.call(target, key)

} else if (DEV) {

checkIdentityKeys(target, has, key)

}

const oldValue = get.call(target, key)

const result = set.call(target, key, value)

// 防止重复触发依赖,如果 key 已存在就不触发依赖

if (!hadKey) {

trigger(target, TriggerOpTypes.ADD, key, value)

} else if (hasChanged(value, oldValue)) {

// 如果新旧值相等,也不会触发依赖

trigger(target, TriggerOpTypes.SET, key, value, oldValue)

}

return result

}

set 的处理逻辑也较为简单,配合注释一目了然。

还有剩下的 has add delete 等方法就不讲解了,代码行数比较少,逻辑也很简单,建议自行阅读。

ref.ts 文件


const convert = (val: T): T =>

isObject(val) ? reactive(val) : val

export function ref(value?: unknown) {

return createRef(value)

}

function createRef(rawValue: unknown, shallow = false) {

// 如果已经是 ref 对象了,直接返回原值

if (isRef(rawValue)) {

return rawValue

}

// 如果不是浅层响应并且 rawValue 是个对象,调用 reactive(rawValue)

let value = shallow ? rawValue : convert(rawValue)

const r = {

__v_isRef: true, // 用于标识这是一个 ref 对象,防止重复监听 ref 对象

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’,

DEV ? { newValue: newVal } : void 0

)

}

}

}

return r

}

在 Vue2.x 中,基本数值类型是不能监听的。但在 Vue3.0 中通过 ref() 可以实现这一效果。

const r = ref(0)

effect(() => console.log(r.value)) // 打印 0

r.value++ // 打印 1

ref() 会把 0 转成一个 ref 对象。如果给 ref(value) 传的值是个对象,在函数内部会调用 reactive(value) 将其转为 proxy 实例。

computed.ts 文件


export function computed(

options: WritableComputedOptions

): WritableComputedRef

export function computed(

getterOrOptions: ComputedGetter | WritableComputedOptions

) {

let getter: ComputedGetter

let setter: ComputedSetter

// 如果 getterOrOptions 是个函数,则是不可被配置的,setter 设为空函数

if (isFunction(getterOrOptions)) {

getter = getterOrOptions

setter = DEV

? () => {

console.warn(‘Write operation failed: computed value is readonly’)

}
NOOP

} else {

// 如果是个对象,则可读可写

getter = getterOrOptions.get

setter = getterOrOptions.set

}

// dirty 用于判断计算属性依赖的响应式属性有没有被改变

let dirty = true

let value: T

let computed: ComputedRef

const runner = effect(getter, {

lazy: true, // lazy 为 true,生成的 effect 不会马上执行

// mark effect as computed so that it gets priority during trigger

computed: true,

scheduler: () => { // 调度器

// trigger 时,计算属性执行的是 effect.options.scheduler(effect) 而不是 effect()

if (!dirty) {

dirty = true

trigger(computed, TriggerOpTypes.SET, ‘value’)

}

}

})

computed = {

__v_isRef: true,

// expose effect so computed can be stopped

effect: runner,

get value() {

if (dirty) {

value = runner()

dirty = false

}

track(computed, TrackOpTypes.GET, ‘value’)

return value

},

set value(newValue: T) {

setter(newValue)

}

} as any

return computed

}

下面通过一个示例,来讲解一下 computed 是怎么运作的:

const value = reactive({})

const cValue = computed(() => value.foo)

console.log(cValue.value === undefined)

value.foo = 1

console.log(cValue.value === 1)

1.生成一个 proxy 实例 value。2.computed() 生成计算属性对象,当对 cValue 进行取值时(cValue.value),根据 dirty 判断是否需要运行 effect 函数进行取值,如果 dirty 为 false,直接把值返回。3.在 effect 函数里将 effect 设为 activeEffect,并运行 getter(() => value.foo) 取值。在取值过程中,读取 foo 的值(value.foo)。4.这会触发 get 属性读取拦截操作,进而触发 track 收集依赖,而收集的依赖函数就是第 3 步产生的 activeEffect。5.当响应式属性进行重新赋值时(value.foo = 1),就会 trigger 这个 activeEffect 函数。6.然后调用 scheduler() 将 dirty 设为 true,这样 computed 下次求值时会重新执行 effect 函数进行取值。

index.ts 文件


基础学习:

前端最基础的就是 HTML , CSS 和 JavaScript 。

网页设计:HTML和CSS基础知识的学习

HTML是网页内容的载体。内容就是网页制作者放在页面上想要让用户浏览的信息,可以包含文字、图片、视频等。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

CSS样式是表现。就像网页的外衣。比如,标题字体、颜色变化,或为标题加入背景图片、边框等。所有这些用来改变内容外观的东西称之为表现。

动态交互:JavaScript基础的学习

JavaScript是用来实现网页上的特效效果。如:鼠标滑过弹出下拉菜单。或鼠标滑过表格的背景颜色改变。还有焦点新闻(新闻图片)的轮换。可以这么理解,有动画的,有交互的一般都是用JavaScript来实现的。

set value(newValue: T) {

setter(newValue)

}

} as any

return computed

}

下面通过一个示例,来讲解一下 computed 是怎么运作的:

const value = reactive({})

const cValue = computed(() => value.foo)

console.log(cValue.value === undefined)

value.foo = 1

console.log(cValue.value === 1)

1.生成一个 proxy 实例 value。2.computed() 生成计算属性对象,当对 cValue 进行取值时(cValue.value),根据 dirty 判断是否需要运行 effect 函数进行取值,如果 dirty 为 false,直接把值返回。3.在 effect 函数里将 effect 设为 activeEffect,并运行 getter(() => value.foo) 取值。在取值过程中,读取 foo 的值(value.foo)。4.这会触发 get 属性读取拦截操作,进而触发 track 收集依赖,而收集的依赖函数就是第 3 步产生的 activeEffect。5.当响应式属性进行重新赋值时(value.foo = 1),就会 trigger 这个 activeEffect 函数。6.然后调用 scheduler() 将 dirty 设为 true,这样 computed 下次求值时会重新执行 effect 函数进行取值。

index.ts 文件


基础学习:

前端最基础的就是 HTML , CSS 和 JavaScript 。

网页设计:HTML和CSS基础知识的学习

HTML是网页内容的载体。内容就是网页制作者放在页面上想要让用户浏览的信息,可以包含文字、图片、视频等。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

[外链图片转存中…(img-R7jlfTeH-1715754703137)]

CSS样式是表现。就像网页的外衣。比如,标题字体、颜色变化,或为标题加入背景图片、边框等。所有这些用来改变内容外观的东西称之为表现。

[外链图片转存中…(img-4nrR9jkZ-1715754703138)]

动态交互:JavaScript基础的学习

JavaScript是用来实现网页上的特效效果。如:鼠标滑过弹出下拉菜单。或鼠标滑过表格的背景颜色改变。还有焦点新闻(新闻图片)的轮换。可以这么理解,有动画的,有交互的一般都是用JavaScript来实现的。

[外链图片转存中…(img-kwUFXPeQ-1715754703138)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值