Vue面试须知

双向绑定的原理

好东西不能藏着,这个视频教学我看了不下3遍,思路清晰透明,看不懂的话多看两次!Vue MVVM原理


Vue的生命周期 及 每个阶段做了什么事情

官方的vue生命周期图,来自vue生命周期图
在这里插入图片描述

// vue生命周期
beforeCreate
created
beforeMount
mounted
updated
beforeUpdate
updated
beforeDestroy
destroyed
// 使用了keep-alive 后存在这两个生命周期
activated
deactivated

beforeCreate的时候,数据/事件还未初始化,无法访问到数据和真实到dom。

beforeCreated —> created 这一段时间内,进行初始化事件和数据。

created的时候,数据和事件已经初始化了,在这个阶段可以对数据进行更改,在这里更改数据不会触发updated函数。

created —> beforeMounted 这一段时间内,会先判断vue内有没有el这个元素,如果有则继续往下执行,如果没有则停止编译,直到在该vue实例上调用vm.$mount(el)。

然后会进行判断,在vue对象中有没有定义template,如果有的话则使用定义的template作为模版,如果没有则定义使用vue的el属性绑定的dom区域作为模版。

beforeMount —> mounted 这一段时间内,开始编译模板 (下面一个板块Vue的编译过程),挂载$el。替换真实节点。

mounted —> 挂载完成,模板中的html渲染到了页面中,mounted只会执行一次。这个时候页面的事件和数据都已经挂载了,真实dom也渲染好了。在这一步可以执行DOM操作。

在vue的对象中,当data的值发生改变时就会先调用beforeUpdate函数。

beforeUpdate --> updated vue的虚拟dom机制会重新构建虚拟dom与上一次的虚拟dom树利用diff算法进行对比之后重新渲染。渲染完以后就会执行updated钩子函数。

beforeDestroy钩子函数在实例销毁之前调用。在这一步,实例仍然完全可用。
destroyed钩子函数在Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。


Vue的编译过程

学习视频
参考: Vue.js的template编译过程
解析器(parse) - 将 模板字符串 转换成 element ASTs
优化器(optimize) - 对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化
代码生成器(generate) - 使用 element ASTs 生成 render 函数代码字符串

AST(abstract syntax tree 抽象语法树), 是源代码的抽象语法结构的树状表现形式。

解析器(parse)
parse 的目标是把 template 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。那么整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的

优化器(optimize)
通过 optimize 把整个 AST 树中的每一个 AST 元素节点标记了 static 和 staticRoot, optimize 的过程,就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点则它们生成 DOM 永远不需要改变

代码生成器(generate)
把优化后的 AST 树转换成render函数,render函数转换成虚拟dom,然后在打入真实节点


Vue的组件通信方式

参考: Vue组件通信

一、父子间通信 props / $emit

父组件

<template>
	<div>
		<sonComponent :msg="msg" @sonEmit="getSonEmit"></sonComponent>
	</div>
</template>
<script>
import sonComponent from './sonComponent.vue';
export default {
	name: 'FatherComponent',
	components: { sonComponent },
	data() {
		return {
			msg: '我来自父亲组件',
			sonValue: 'hi'
		}
	},
	methods: {
		getSonEmit(emitValue) {
			this.sonValue = emitValue;
		}
	}
}
</script>

子组件

<template>
	<div>
		<span>{{msg}}</span>
		<button @click="onClike()">click me!</button>
	</div>
</template>
<script>
export default {
	name: 'sonComponent',
	props: ['msg']
	data() {
		return {
			sonValue: '我来自子组件'
		}
	},
	methods: {
		onClike() {
			this.$emit('sonEmit', this.sonValue);
		}
	}
}
</script>

父亲组件通过 :值 的方式将值传递给子组件,子组件在props中定义并接住父组件传递的值。

子组件通过调用$emit传递一个名称和值,在父亲组件中用@名称的形式定义一个方法,并在方法中接住子组件传递过来的值

二、通过 $parent / $children

父组件

<template>
	<div>
		<sonComponent :msg="msg" />
		<button @click="updateSonValue">点击我修改子组件的sonValue值</button>
	</div>
</template>
<script>
import sonComponent from './sonComponent.vue';
export default {
	name: 'FatherComponent',
	components: { sonComponent },
	data() {
		return {
			msg: '我来自父亲组件'
		}
	},
	methods: {
		updateSonValue() {
			this.$children[0].sonValue = '子组件的值被父组件修改拉!'
		}
	}
}
</script>

子组件

<template>
	<div>
		<span>{{msg}}</span>
		<button @click="onClick()">点击修改父组件的值!</button>
	</div>
</template>
<script>
export default {
	name: 'sonComponent',
	props: ['msg']
	data() {
		return {
			sonValue: '我来自子组件'
		}
	},
	methods: {
		onClick() {
			this.$parent.msg = '父组件的值被子组件修改了!';
		}
	}
}
</script>

三、provide / inject
可以用于父子组件,隔代组件之间的通信

父组件

<template>
	<div>
		<sonComponent :msg="msg" />
	</div>
</template>
<script>
import sonComponent from './sonComponent.vue';
export default {
	components: { sonComponent },
	data() {
		return {
			msg: '我来自父亲组件'
		}
	},
	provide: {
		aaprovide: "aaprovide"
	}
}
</script>

子组件或者孙子组件

<template>
	<div>
		<span>{{provide}}</span>
	</div>
</template>
<script>
export default {
	data() {
		return {
			provide: this.aaprovide
		}
	},
	inject: ['aaprovide']
}
</script>

四、EventBus (参考自标题文章)
eventBus 又称为事件总线,在vue中可以使用它来作为沟通桥梁的概念, 就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件, 所以组件都可以通知其他组件。eventBus也有不方便之处, 当项目较大,就容易造成难以维护的灾难

// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
// 需要在组件中先引入EventBus
import {EventBus} from './event-bus.js'

// 在代码中调用即可
EventBus.$emit('addition', {
	num: 5 // 可以传入当前组件的变量
})

// 响应
EventBus.$on('addition', param => {
	this.count = this.count + param.num;
})

// 移除该事件
EventBus.$off('addition', {})

五、Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。分为五个模块:

state类似与vue的data,用于数据的存储。
getter类似与computed,计算属性。基于state数据的二次包装,常用于数据的筛选和多个数据的相关性计算。
mutation中定义了同步的方法,通过它来修改state,是修改state数据的唯一途径。
action中定义了异步的方法,一般用于请求,可以通过调用mutation来修改state。
modules用于项目中将各个模块的状态分开定义和操作,便于维护。

将需要进行通信的数据存储在store中,需要数据的组件通过连接store获取state的值即可实现组件间的通信。该方式可实现全部组件的通信。

六、localStorage / sessionStorage
该方法是最常见的组件通信方法之一了。在不同的组件,跨页面都可以进行通信的一种方式。

// 存储的两种方式
localStorage.setItem("isConfirm", "true");
localStorage.isConfirm= "true";

// 读取
localStorage.getItem("isConfirm");
localStorage.isConfirm; 
// 存储的两种方式
sessionStorage.setItem("isConfirm", "true");
sessionStorage.isConfirm= "true";

// 读取
sessionStorage.getItem("isConfirm");
sessionStorage.isConfirm; 

keep-alive

参考:keep-alive
通过vue cli 创建了一个工程,在该工程的app.vue中

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link>
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>

是这个样子的,点击了router-link会根据路由,在router-view中显示对应路由的组件内容,想要添加缓存,则需要修改成以下样子

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link>
      <router-link to="/about">About</router-link>
    </div>
    <keep-alive>
      <router-view v-if="$route.meta.keepAlive" />
    </keep-alive>
    <router-view v-if="!$route.meta.keepAlive" />
  </div>
</template>

意思是如果我们在router配置了keepAlive为true,则会使用keep-alive区域内的router-view,从而数据就会进行缓存了!相反则使用keep-alive外的router-view。

当然,前提是我们必须先在router中配置好keepAlive

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
    meta: {
      keepAlive: true // 需要被缓存
    }
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../views/About.vue'),
    meta: {
      keepAlive: true // 需要被缓存
    }
  }
]

这样子的话,两个页面之间互相切换,都可以进行值的缓存了!

当然在组件中的部分区域也是可以实现缓存的。看以下例子:

// router
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../views/About.vue'),
    children: [    //需要执行缓存有关联操作的页面,都要写在children下
      {
        path: '/pageFirst',
        name: 'pageFirst',
        component: () => import('../components/pageFirst.vue')
      },
      {
        path: '/pageSecond',
        name: 'pageSecond',
        component: () => import('../components/pageSecond.vue')
      }
    ]
  }
]

我们在about页面下配置了children,即为about页面配置了两个子路由。

about页面:

<template>
  <div class="about">
    <router-link to="/pageFirst">goFrist</router-link>
    <router-link to="/pageSecond">goSecond</router-link>
    <h1>This is an about page</h1>
    <keep-alive>
      <router-view />
    </keep-alive>
  </div>
</template>

点击router-link,下面的router-view区域就会跳转到对应的组件,用keep-alive包裹住就可以将两个组件的数据缓存起来了!


v-if 和 v-show 的区别

v-if:添加了vif的标签,类似于给标签定义了display 的css属性,当值为true时正常显示,当值为false就类似diaplay: none。它带来的影响就是会影响到当前的文档流排布,该dom元素会被隐藏不会被渲染,它不占据空间。如果是定义在组件标签上,vif的值在true和false来回切换时,相当于是重渲染,是会执行该组件的正常生命周期函数的。因为影响到了文档流排布,会引起了回流的现象。

v-show:添加了vshow的标签,类似于给标签定义了visibility 的css属性,当值为true时正常显示,当值为false就类似visilibity: hidden。设置了该值的区域会隐藏起来,但是它还会占据原本的空间,不影响文档流的排布。就只是单纯的“看不见”。当值在true和false来回切换时,是不会执行该组件的正常生命周期函数的。但是会触发重绘的现象。

当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。
当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。


Vue Router

Vue Router 官方文档

我们用 vue cli 创建完工程以后,都会有一个router文件夹,里面有一个index.js文件,配置了基本的路由。

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../views/About.vue')
  }
]
基本路由

如上配置了两个页面的基本路由,当我们启动工程的时候会默认加载app.vue页面,app.vue页面内的<router-view />区域则会默认加载路径为’/'的页面,即为引入home组件。

我们可以在页面上定义<router-link to="/home" />标签,用来点击后跳转到’/home’路由。也可以在页面的其他标签绑定click方法,在方法中调用this.$router.push('/home')来跳转。

子路由

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../views/About.vue'),
    children: [    //需要执行缓存有关联操作的页面,都要写在children下
      {
        path: '/pageFirst',
        name: 'pageFirst',
        component: () => import('../components/pageFirst.vue'),
      },
      {
        path: '/pageSecond',
        name: 'pageSecond',
        component: () => import('../components/pageSecond.vue'),
      }
    ]
  }
]

如上,在About路由中配置了两个子路由,所以在about页面中的<router-view />区域则会显示对应子路由组件的内容。

路由传参数
const routes = [
  {
    path: '/home/:id/:name',
    name: 'Home',
    component: Home
  }
]

如上,可以在一个路由中设置多段“路径参数”,对应的值都会设置到 $route.params 中。


Vuex

参考:Vuex 官方文档

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

  2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

State
// 最简单的store例子
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 0,
    name: 'tom'
  }
})
template: `<div id="app">{{ count }}</div>`,
computed: {
	count() {
		return this.$store.state.count
	}
}
mapState 辅助函数

我们可以通过computed计算属性讲store中state的值绑定在vue对象中进行使用,但是当这样的变量变多了一个一个绑定就变得不太方便了。所以就出现了mapState辅助函数。

首先我们需要将他引入到我们的组件中。import { mapState } from 'vuex'

然后在computed中配合上…扩展运算符
当映射的计算属性的名称与 state 的子节点名称相同时,我们可以给 mapState 传一个字符串数组。

computed: {
	...mapState([ 'count', 'name' ]),
}

在…mapStete([])数组中写入要引入的state值,这样子就绑定好了。

如果想要自定义变量的名称,可以传入一个对象,有多种方式可以获取state值

data: {
	return {
		number: 2
	}
},
computed: {
	...mapState({
		countNumber: state => state.count, // 第一种方式用箭头函数返回state中的count 箭头函数中的this会出问题
		countNumber2: 'count' // 第二种方式 直接写字符串与state中的变量同名即可
		countNumber3(state) { // 第三种方式 ,此方式的this即是vue对象
			return state.count + this.number
		}
	}),
}

使用这种方式也可以绑定。

Getter

getter其实就是store的计算属性,在官方文档中是这样定义的:Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

Getter 接受 state 作为其第一个参数:

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})

Getter 也可以接受其他 getter 作为第二个参数:

getters: {
  doneTodosCount: (state, getters) => {
    return getters.doneTodos.length
  }
}
mapGetters 辅助函数

它的用法跟mapState是类似的

import { mapGetters } from 'vuex'

export default {
  computed: {
  // 使用对象展开运算符将 getter 混入 computed 对象中
    ...mapGetters([
      'doneTodos',
      'doneTodosCount',
      // ...
    ])
  }
}

当然也能自定义变量名称

...mapGetters({
  doneTodosFake: 'doneTodos',
  doneTodosCount: 'doneTodosCountFake'
})
Mutation

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

// 最简单的store例子
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 0,
    name: 'tom'
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

我们可以调用mutations中的increment方法来使state中的count值得到修改。在组件中,如果我们需要进行获取state或者调用mutation的方法时,首先我们得绑定store。

new Vue({
  el: '#app',
  store,
})

绑定了以后就可以在页面中展示store中的变量或者调用方法了!

template: `<div id="app">{{ count }}</div>`,
computed: {
	count() {
		return this.$store.state.count
	}
},
methods: {
  increment() {
    this.$store.commit('increment')
    console.log(this.$store.state.count)
  }
}

官方:你可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload),其实就是可以传递参数拉,真的太官方了。。

// ...
mutations: {
  increment (state, n) {
    state.count += n
  }
}

调用的时候将参数传递进去

store.commit('increment', 10)

官方提示:!
Mutation 需遵守 Vue 的响应规则

既然 Vuex 的 store 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:

最好提前在你的 store 中初始化好所有所需属性。

当需要在对象上添加新属性时,你应该

使用 Vue.set(obj, ‘newProp’, 123), 或者

以新对象替换老对象。例如,利用对象展开运算符 (opens new window)我们可以这样写:

state.obj = { ...state.obj, newProp: 123 }
mapMutation
import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

      // `mapMutations` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
  }
}
Action

因为修改state的方式只能通过mutation,所以在action中如果想要修改state,需要提交mutation。

Action 类似于 mutation,不同在于:
Action 提交的是 mutation,而不是直接变更状态。
Action 可以包含任意异步操作。

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象。其实第一个参数就是store本身,类似于我们在组件中的 this.$store。所以自然的可以调用context.commit('increment'),当前还可以context.state 或者context.getter

实践中,我们会经常用到 ES2015 的 参数解构 (opens new window)来简化代码(特别是我们需要调用 commit 很多次的时候):

actions: {
  increment ({ commit, state, getter }) {
    commit('increment')
  }
}

在组件中调用mutation的方式是store.commit ,调用action的话就是store.dispatch

mapAction
import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

      // `mapActions` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
  }
}

Action 通常是异步的,store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise:

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}
store.dispatch('actionA').then(() => {
  // ...
})

在另外一个 action 中也可以:

actions: {
  // ...
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

如果使用 async / await

// 假设 getData() 和 getOtherData() 返回的是 Promise

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

module 我基本没怎么用到过,想要学习可以看官方的文档


Vue 虚拟DOM

真的是一篇好文章啊!vue虚拟dom

虚拟DOM

Vue源码中虚拟DOM构建经历 template编译成AST语法树 -> 再转换为render函数 最终返回一个VNode(VNode就是Vue的虚拟DOM节点)


nextTick

简单理解Vue中的nextTick


computed 和 watch 的区别

computed是计算属性,需要依赖一个数据,基于data中声明过或者父组件传递的props中的数据通过计算得到的值,当依赖当数据发生改变时,会重新进行计算得到新值。是不支持异步的。如果computed的属性值是函数,那么默认会调用get方法,函数的返回值就是属性值。

watch是一个监听函数,如下,watch是支持异步的。当他监听的属性发生改变时则会调用这个监听方法handler,该监听方法接收两个参数,一个值属性改变后的值,一个是修改前的值。还可以为该监听方法定义两个属性,deep和immediate。immediate为true时,则组件一加载就立即触发handler函数的执行。deep为true时,进行深度监听,为了发现对象内部值的变化,复杂类型的数据时使用。

watch: {
	cityName: {
		handler(newName, oldName) {},
		deep: true,
		immediate: true
	}
} 

自定义指令

参考:vue自定义指令

全局注册
<div id="app" class="demo">
    <!-- 全局注册 -->
    <input type="text" placeholder="我是全局自定义指令" v-focus>
</div>
<script>
    Vue.directive("focus", {
        inserted: function(el){
            el.focus();
        }
    })
    new Vue({
        el: "#app"
    })
</script>
局部注册

即在vue对象内注册自定义指令

<div id="app" class="demo">
    <!-- 局部注册 -->
    <input type="text" placeholder="我是局部自定义指令" v-focus2>
</div>
<script>
    new Vue({
        el: "#app",
        directives: {
            focus2: {
                inserted: function(el){
                    el.focus();
                }
            }
        }
    })
</script>
自定义指令钩子函数

bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update:所在组件的 VNode 更新时调用。
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind:只调用一次,指令与元素解绑时调用。

以上的钩子函数会被传入以下参数:

el: 指令所绑定的元素,可以用来直接操作 DOM,就是放置指令的那个元素。
binding: 一个对象,里面包含了几个属性,这里不多展开说明,官方文档上都有很详细的描述。
vnode:Vue 编译生成的虚拟节点。
oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

举例:

Vue.directive('demo', function (el, binding) {
	console.log(binding.value.color) // "white"
	console.log(binding.value.text)  // "hello!"
})

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值