Vue中封装的数组办法有哪些,其如何实现页面更新
在Vue中,对响应式解决利用的是Object.defineProperty对数据进行拦挡,而这个办法并不能监听到数组外部变动,数组长度变动,数组的截取变动等,所以须要对这些操作进行hack,让Vue能监听到其中的变动。 那Vue是如何实现让这些数组办法实现元素的实时更新的呢,上面是Vue中对这些办法的封装:
// 缓存数组原型const arrayProto = Array.prototype;// 实现 arrayMethods.__proto__ === Array.prototypeexport const arrayMethods = Object.create(arrayProto);// 须要进行性能拓展的办法const methodsToPatch = [ "push", "pop", "shift", "unshift", "splice", "sort", "reverse"];/** * Intercept mutating methods and emit events */methodsToPatch.forEach(function(method) { // 缓存原生数组办法 const original = arrayProto[method]; def(arrayMethods, method, function mutator(...args) { // 执行并缓存原生数组性能 const result = original.apply(this, args); // 响应式解决 const ob = this.__ob__; let inserted; switch (method) { // push、unshift会新增索引,所以要手动observer case "push": case "unshift": inserted = args; break; // splice办法,如果传入了第三个参数,也会有索引退出,也要手动observer。 case "splice": inserted = args.slice(2); break; } // if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听 // notify change ob.dep.notify();// 告诉依赖更新 // 返回原生数组办法的执行后果 return result; });});
简略来说就是,重写了数组中的那些原生办法,首先获取到这个数组的__ob__,也就是它的Observer对象,如果有新的值,就调用observeArray持续对新的值察看变动(也就是通过target__proto__ == arrayMethods
来扭转了数组实例的型),而后手动调用notify,告诉渲染watcher,执行update。
Vue.js的template编译
简而言之,就是先转化成AST树,再失去的render函数返回VNode(Vue的虚构DOM节点),具体步骤如下:
首先,通过compile编译器把template编译成AST语法树(abstract syntax tree 即 源代码的形象语法结构的树状表现形式),compile是createCompiler的返回值,createCompiler是用以创立编译器的。另外compile还负责合并option。
而后,AST会通过generate(将AST语法树转化成render funtion字符串的过程)失去render函数,render的返回值是VNode,VNode是Vue的虚构DOM节点,外面有(标签名、子节点、文本等等)
怎么了解 Vue 的单向数据流
数据总是从父组件传到子组件,子组件没有权力批改父组件传过来的数据,只能申请父组件对原始数据进行批改。这样会 避免从子组件意外扭转父级组件的状态 ,从而导致你的利用的数据流向难以了解
留神 :在子组件间接用 v-model
绑定父组件传过来的 prop
这样是不标准的写法 开发环境会报正告
如果切实要扭转父组件的 prop
值,能够在 data
外面定义一个变量 并用 prop
的值初始化它 之后用$emit
告诉父组件去批改
有两种常见的试图扭转一个 prop 的情景 :
- 这个
prop
用来传递一个初始值;这个子组件接下来心愿将其作为一个本地的prop
数据来应用。 在这种状况下,最好定义一个本地的data
属性并将这个prop
用作其初始值
props: ['initialCounter'],data: function () { return { counter: this.initialCounter }}
- 这个
prop
以一种原始的值传入且须要进行转换。 在这种状况下,最好应用这个prop
的值来定义一个计算属性
props: ['size'],computed: { normalizedSize: function () { return this.size.trim().toLowerCase() }}
虚构DOM真的比实在DOM性能好吗
- 首次渲染大量DOM时,因为多了一层虚构DOM的计算,会比innerHTML插入慢。
- 正如它能保障性能上限,在实在DOM操作的时候进行针对性的优化时,还是更快的。
Vue 是如何实现数据双向绑定的
Vue
数据双向绑定次要是指:数据变动更新视图,视图变动更新数据,如下图所示:
- 输入框内容变动时,
Data
中的数据同步变动。即View => Data
的变动。 Data
中的数据变动时,文本节点的内容同步变动。即Data => View
的变动
Vue 次要通过以下 4 个步骤来实现数据双向绑定的
- 实现一个监听器 Observer :对数据对象进行遍历,包含子属性对象的属性,利用
Object.defineProperty()
对属性都加上setter
和getter
。这样的话,给这个对象的某个值赋值,就会触发setter
,那么就能监听到了数据变动 - 实现一个解析器 Compile :解析
Vue
模板指令,将模板中的变量都替换成数据,而后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,增加监听数据的订阅者,一旦数据有变动,收到告诉,调用更新函数进行数据更新 - 实现一个订阅者 Watcher :
Watcher
订阅者是Observer
和Compile
之间通信的桥梁 ,次要的工作是订阅Observer
中的属性值变动的音讯,当收到属性值变动的音讯时,触发解析器Compile
中对应的更新函数 - 实现一个订阅器 Dep :订阅器采纳 公布-订阅 设计模式,用来收集订阅者
Watcher
,对监听器Observer
和 订阅者Watcher
进行对立治理
Vue 数据双向绑定原理图
子组件能够间接扭转父组件的数据么,阐明起因
这是一个实际知识点,组件化开发过程中有个单项数据流准则
,不在子组件中批改父组件是个常识问题
思路
- 讲讲单项数据流准则,表明为何不能这么做
- 举几个常见场景的例子说说解决方案
- 联合实际讲讲如果须要批改父组件状态应该如何做
答复范例
- 所有的
prop
都使得其父子之间造成了一个单向上行绑定:父级prop
的更新会向下流动到子组件中,然而反过来则不行。这样会避免从子组件意外变更父级组件的状态,从而导致你的利用的数据流向难以了解。另外,每次父级组件产生变更时,子组件中所有的prop
都将会刷新为最新的值。这意味着你不应该在一个子组件外部扭转prop
。如果你这样做了,Vue
会在浏览器控制台中收回正告
const props = defineProps(['foo'])// ❌ 上面行为会被正告, props是只读的!props.foo = 'bar'
- 理论开发过程中有两个场景会想要批改一个属性:
这个 prop 用来传递一个初始值;这个子组件接下来心愿将其作为一个本地的 prop 数据来应用。 在这种状况下,最好定义一个本地的 data
,并将这个 prop
用作其初始值:
const props = defineProps(['initialCounter'])const counter = ref(props.initialCounter)
这个 prop 以一种原始的值传入且须要进行转换。 在这种状况下,最好应用这个 prop
的值来定义一个计算属性:
const props = defineProps(['size'])// prop变动,计算属性自动更新const normalizedSize = computed(() => props.size.trim().toLowerCase())
- 实际中如果的确想要扭转父组件属性应该
emit
一个事件让父组件去做这个变更。留神尽管咱们不能间接批改一个传入的对象或者数组类型的prop
,然而咱们还是可能间接改内嵌的对象或属性
参考 前端进阶面试题具体解答
怎么实现路由懒加载呢
这是一道应用题。当打包利用时,JavaScript 包会变得十分大,影响页面加载。如果咱们能把不同路由对应的组件宰割成不同的代码块,而后当路由被拜访时才加载对应组件,这样就会更加高效
// 将// import UserDetails from './views/UserDetails'// 替换为const UserDetails = () => import('./views/UserDetails')const router = createRouter({ // ... routes: [{ path: '/users/:id', component: UserDetails }],})
答复范例
- 当打包构建利用时,JavaScript 包会变得十分大,影响页面加载。利用路由懒加载咱们能把不同路由对应的组件宰割成不同的代码块,而后当路由被拜访的时候才加载对应组件,这样会更加高效,是一种优化伎俩
- 一般来说,对所有的路由都应用动静导入是个好主见
- 给
component
选项配置一个返回Promise
组件的函数就能够定义懒加载路由。例如:{ path: '/users/:id', component: () => import('./views/UserDetails') }
- 联合正文
() => import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
能够做webpack
代码分块
Vue.set的实现原理
- 给对应和数组自身都减少了
dep
属性 - 当给对象新增不存在的属性则触发对象依赖的
watcher
去更新 - 当批改数组索引时,咱们调用数组自身的
splice
去更新数组(数组的响应式原理就是从新了splice
等办法,调用splice
就会触发视图更新)
根本应用
以下办法调用会扭转原始数组:push()
,pop()
,shift()
,unshift()
,splice()
,sort()
,reverse()
,Vue.set( target, key, value )
调用办法:
Vue.set(target, key, value )
target
:要更改的数据源(能够是对象或者数组)key
:要更改的具体数据value
:从新赋的值
<div id="app">{{user.name}} {{user.age}}</div><div id="app"></div><script> // 1. 依赖收集的特点:给每个属性都减少一个dep属性,dep属性会进行收集,收集的是watcher // 2. vue会给每个对象也减少一个dep属性 const vm = new Vue({ el: '#app', data: { // vm._data user: {name:'poetry'} } }); // 对象的话:调用defineReactive在user对象上定义一个age属性,减少到响应式数据中,触发对象自身的watcher,ob.dep.notify()更新 // 如果是数组 通过调用 splice办法,触发视图更新 vm.$set(vm.user, 'age', 20); // 不能给根属性增加,因为给根增加属性 性能耗费太大,须要做很多解决 // 批改必定是同步的 -> 更新都是一步的 queuewatcher</script>
相干源码
// src/core/observer/index.js 44export class Observer { // new Observer(value) value: any; dep: Dep; vmCount: number; // number of vms that have this object as root $data constructor (value: any) { this.value = value this.dep = new Dep() // 给所有对象类型减少dep属性 }}
// src/core/observer/index.js 201export function set (target: Array<any> | Object, key: any, val: any): any { // 1.是开发环境 target 没定义或者是根底类型则报错 if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`) } // 2.如果是数组 Vue.set(array,1,100); 调用咱们重写的splice办法 (这样能够更新视图) if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) // 利用数组的splice变异办法触发响应式 target.splice(key, 1, val) return val } // 3.如果是对象自身的属性,则间接增加即可 if (key in target && !(key in Object.prototype)) { target[key] = val // 间接批改属性值 return val } // 4.如果是Vue实例 或 根数据data时 报错,(更新_data 无意义) const ob = (target: any).__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ) return val } // 5.如果不是响应式的也不须要将其定义成响应式属性 if (!ob) { target[key] = val return val } // 6.将属性定义成响应式的 defineReactive(ob.value, key, val) // 告诉视图更新 ob.dep.notify() return val}
咱们浏览以上源码可知,vm.$set 的实现原理是:
- 如果指标是数组 ,间接应用数组的
splice
办法触发相应式; - 如果指标是对象 ,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式解决,则是通过调用
defineReactive
办法进行响应式解决(defineReactive
办法就是Vue
在初始化对象时,给对象属性采纳Object.defineProperty
动静增加getter
和setter
的性能所调用的办法)
你感觉vuex有什么毛病
剖析
相较于redux
,vuex
曾经相当简便好用了。但模块的应用比拟繁琐,对ts
反对也不好。
体验
应用模块:用起来比拟繁琐,应用模式也不对立,基本上得不到类型零碎的任何反对
const store = createStore({ modules: { a: moduleA }})store.state.a // -> 要带上 moduleA 的key,内嵌模块的话会很长,不得不配合mapState应用store.getters.c // -> moduleA里的getters,没有namespaced时又变成了全局的store.getters['a/c'] // -> 有namespaced时要加path,应用模式又和state不一样store.commit('d') // -> 没有namespaced时变成了全局的,能同时触发多个子模块中同名mutationstore.commit('a/d') // -> 有namespaced时要加path,配合mapMutations应用感觉也没简化
答复范例
vuex
利用响应式,应用起来曾经相当方便快捷了。然而在应用过程中感觉模块化这一块做的过于简单,用的时候容易出错,还要常常查看文档- 比方:拜访
state
时要带上模块key
,内嵌模块的话会很长,不得不配合mapState
应用,加不加namespaced
区别也很大,getters
,mutations
,actions
这些默认是全局,加上之后必须用字符串类型的path来匹配,应用模式不对立,容易出错;对ts的反对也不敌对,在应用模块时没有代码提醒。 - 之前
Vue2
我的项目中用过vuex-module-decorators
的解决方案,尽管类型反对上有所改善,但又要学一套新货色,减少了学习老本。pinia
呈现之后应用体验好了很多,Vue3 + pinia
会是更好的组合
原理
上面咱们来看看vuex
中store.state.x.y
这种嵌套的门路是怎么搞进去的
首先是子模块装置过程:父模块状态parentState
下面设置了子模块名称moduleName
,值为以后模块state
对象。放在下面的例子中相当于:store.state['x'] = moduleX.state
。此过程是递归的,那么store.state.x.y
装置时就是:store.state['x']['y'] = moduleY.state
//源码地位 https://github1s.com/vuejs/vuex/blob/HEAD/src/store-util.js#L102-L115if (!isRoot && !hot) { // 获取父模块state const parentState = getNestedState(rootState, path.slice(0, -1)) // 获取子模块名称 const moduleName = path[path.length - 1] store._withCommit(() => { // 把子模块state设置到父模块上 parentState[moduleName] = module.state })}
Vue-router 除了 router-link 怎么实现跳转
申明式导航
<router-link to="/about">Go to About</router-link>
编程式导航
// literal string pathrouter.push('/users/1')// object with pathrouter.push({ path: '/users/1' })// named route with params to let the router build the urlrouter.push({ name: 'user', params: { username: 'test' } })
答复范例
vue-router
导航有两种形式:申明式导航和编程形式导航- 申明式导航形式应用
router-link
组件,增加to
属性导航;编程形式导航更加灵便,可传递调用router.push()
,并传递path
字符串或者RouteLocationRaw
对象,指定path
、name
、params
等信息 - 如果页面中简略示意跳转链接,应用
router-link
最快捷,会渲染一个a标签;如果页面是个简单的内容,比方商品信息,能够增加点击事件,应用编程式导航 - 实际上外部两者调用的导航函数是一样的
vue-router 有哪几种导航守卫
- 全局守卫
- 路由独享守卫
- 路由组件内的守卫
全局守卫
vue-router全局有三个守卫
router.beforeEach
全局前置守卫 进入路由之前router.beforeResolve
全局解析守卫(2.5.0+) 在beforeRouteEnter
调用之后调用router.afterEach
全局后置钩子 进入路由之后
// main.js 入口文件import router from './router'; // 引入路由router.beforeEach((to, from, next) => { next();});router.beforeResolve((to, from, next) => { next();});router.afterEach((to, from) => { console.log('afterEach 全局后置钩子');});
路由独享守卫
如果你不想全局配置守卫的话,你能够为某些路由独自配置守卫
const router = new VueRouter({ routes: [ { path: '/foo', component: Foo, beforeEnter: (to, from, next) => { // 参数用法什么的都一样,调用程序在全局前置守卫前面,所以不会被全局守卫笼罩 // ... } } ]})
路由组件内的守卫
beforeRouteEnter
进入路由前, 在路由独享守卫后调用 不能 获取组件实例this
,组件实例还没被创立beforeRouteUpdate
(2.2
) 路由复用同一个组件时, 在以后路由扭转,然而该组件被复用时调用 能够拜访组件实例this
beforeRouteLeave
来到以后路由时, 导航来到该组件的对应路由时调用,能够拜访组件实例this
Vue组件之间通信形式有哪些
Vue 组件间通信是面试常考的知识点之一,这题有点相似于凋谢题,你答复出越多办法当然越加分,表明你对 Vue 把握的越纯熟。 Vue 组件间通信只有指以下 3 类通信 :父子组件通信
、隔代组件通信
、兄弟组件通信
,上面咱们别离介绍每种通信形式且会阐明此种办法可实用于哪类组件间通信
组件传参的各种形式
组件通信罕用形式有以下几种
props / $emit
实用 父子组件通信- 父组件向子组件传递数据是通过
prop
传递的,子组件传递数据给父组件是通过$emit
触发事件来做到的
- 父组件向子组件传递数据是通过
ref
与$parent / $children(vue3废除)
实用 父子组件通信ref
:如果在一般的DOM
元素上应用,援用指向的就是DOM
元素;如果用在子组件上,援用就指向组件实例$parent / $children
:拜访拜访父组件的属性或办法 / 拜访子组件的属性或办法
EventBus ($emit / $on)
实用于 父子、隔代、兄弟组件通信- 这种办法通过一个空的
Vue
实例作为地方事件总线(事件核心),用它来触发事件和监听事件,从而实现任何组件间的通信,包含父子、隔代、兄弟组件
- 这种办法通过一个空的
$attrs / $listeners(vue3废除)
实用于 隔代组件通信$attrs
:蕴含了父作用域中不被prop
所辨认 (且获取) 的个性绑定 (class
和style
除外 )。当一个组件没有申明任何prop
时,这里会蕴含所有父作用域的绑定 (class
和style
除外 ),并且能够通过v-bind="$attrs"
传入外部组件。通常配合inheritAttrs
选项一起应用$listeners
:蕴含了父作用域中的 (不含.native
润饰器的)v-on
事件监听器。它能够通过v-on="$listeners"
传入外部组件
provide / inject
实用于 隔代组件通信- 先人组件中通过
provider
来提供变量,而后在子孙组件中通过inject
来注入变量。provide / inject
API 次要解决了跨级组件间的通信问题, 不过它的应用场景,次要是子组件获取下级组件的状态 ,跨级组件间建设了一种被动提供与依赖注入的关系
- 先人组件中通过
$root
实用于 隔代组件通信 拜访根组件中的属性或办法,是根组件,不是父组件。$root
只对根组件有用Vuex
实用于 父子、隔代、兄弟组件通信Vuex
是一个专为Vue.js
利用程序开发的状态管理模式。每一个Vuex
利用的外围就是store
(仓库)。“store” 基本上就是一个容器,它蕴含着你的利用中大部分的状态 (state
)Vuex
的状态存储是响应式的。当Vue
组件从store
中读取状态的时候,若store
中的状态发生变化,那么相应的组件也会相应地失去高效更新。- 扭转
store
中的状态的惟一路径就是显式地提交 (commit
)mutation
。这样使得咱们能够不便地跟踪每一个状态的变动。
依据组件之间关系探讨组件通信最为清晰无效
- 父子组件:
props
/$emit
/$parent
/ref
- 兄弟组件:
$parent
/eventbus
/vuex
- 跨层级关系:
eventbus
/vuex
/provide+inject
/$attrs + $listeners
/$root
上面演示组件之间通信三种状况: 父传子、子传父、兄弟组件之间的通信
1. 父子组件通信
应用props
,父组件能够应用props
向子组件传递数据。
父组件vue
模板father.vue
:
<template> <child :msg="message"></child></template><script>import child from './child.vue';export default { components: { child }, data () { return { message: 'father message'; } }}</script>
子组件vue
模板child.vue
:
<template> <div>{{msg}}</div></template><script>export default { props: { msg: { type: String, required: true } }}</script>
回调函数(callBack)
父传子:将父组件里定义的method
作为props
传入子组件
// 父组件Parent.vue:<Child :changeMsgFn="changeMessage">methods: { changeMessage(){ this.message = 'test' }}
// 子组件Child.vue:<button @click="changeMsgFn">props:['changeMsgFn']
子组件向父组件通信
父组件向子组件传递事件办法,子组件通过$emit
触发事件,回调给父组件
父组件vue
模板father.vue
:
<template> <child @msgFunc="func"></child></template><script>import child from './child.vue';export default { components: { child }, methods: { func (msg) { console.log(msg); } }}</script>
子组件vue
模板child.vue
:
<template> <button @click="handleClick">点我</button></template><script>export default { props: { msg: { type: String, required: true } }, methods () { handleClick () { //........ this.$emit('msgFunc'); } }}</script>
2. provide / inject 跨级拜访先人组件的数据
父组件通过应用provide(){return{}}
提供须要传递的数据
export default { data() { return { title: '我是父组件', name: 'poetry' } }, methods: { say() { alert(1) } }, // provide属性 可能为前面的后辈组件/嵌套的组件提供所须要的变量和办法 provide() { return { message: '我是先人组件提供的数据', name: this.name, // 传递属性 say: this.say } }}
子组件通过应用inject:[“参数1”,”参数2”,…]
接管父组件传递的参数
<template> <p>曾孙组件</p> <p>{{message}}</p></template><script>export default { // inject 注入/接管先人组件传递的所须要的数据即可 //接管到的数据 变量 跟data外面的变量一样 能够间接绑定到页面 {{}} inject: [ "message","say"], mounted() { this.say(); },};</script>
3. $parent + $children 获取父组件实例和子组件实例的汇合
this.$parent
能够间接拜访该组件的父实例或组件- 父组件也能够通过
this.$children
拜访它所有的子组件;须要留神$children
并不保障程序,也不是响应式的
<!-- parent.vue --><template><div> <child1></child1> <child2></child2> <button @click="clickChild">$children形式获取子组件值</button></div></template><script>import child1 from './child1'import child2 from './child2'export default { data(){ return { total: 108 } }, components: { child1, child2 }, methods: { funa(e){ console.log("index",e) }, clickChild(){ console.log(this.$children[0].msg); console.log(this.$children[1].msg); } }}</script>
<!-- child1.vue --><template> <div> <button @click="parentClick">点击拜访父组件</button> </div></template><script>export default { data(){ return { msg:"child1" } }, methods: { // 拜访父组件数据 parentClick(){ this.$parent.funa("xx") console.log(this.$parent.total); } }}</script>
<!-- child2.vue --><template> <div> child2 </div></template><script>export default { data(){ return { msg: 'child2' } }}</script>
4. $attrs + $listeners多级组件通信
$attrs
蕴含了从父组件传过来的所有props
属性
// 父组件Parent.vue:<Child :name="name" :age="age"/>// 子组件Child.vue:<GrandChild v-bind="$attrs" />// 孙子组件GrandChild<p>姓名:{{$attrs.name}}</p><p>年龄:{{$attrs.age}}</p>
$listeners
蕴含了父组件监听的所有事件
// 父组件Parent.vue:<Child :name="name" :age="age" @changeNameFn="changeName"/>// 子组件Child.vue:<button @click="$listeners.changeNameFn"></button>
5. ref 父子组件通信
// 父组件Parent.vue:<Child ref="childComp"/><button @click="changeName"></button>changeName(){ console.log(this.$refs.childComp.age); this.$refs.childComp.changeAge()}// 子组件Child.vue:data(){ return{ age:20 }},methods(){ changeAge(){ this.age=15 }}
6. 非父子, 兄弟组件之间通信
vue2
中废除了broadcast
播送和散发事件的办法。父子组件中能够用props
和$emit()
。如何实现非父子组件间的通信,能够通过实例一个vue
实例Bus
作为媒介,要互相通信的兄弟组件之中,都引入Bus
,而后通过别离调用Bus事件触发和监听来实现通信和参数传递。Bus.js
能够是这样:
// Bus.js// 创立一个地方工夫总线类 class Bus { constructor() { this.callbacks = {}; // 寄存事件的名字 } $on(name, fn) { this.callbacks[name] = this.callbacks[name] || []; this.callbacks[name].push(fn); } $emit(name, args) { if (this.callbacks[name]) { this.callbacks[name].forEach((cb) => cb(args)); } } } // main.js Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上 // 另一种形式 Vue.prototype.$bus = new Vue() // Vue曾经实现了Bus的性能
<template> <button @click="toBus">子组件传给兄弟组件</button></template><script>export default{ methods: { toBus () { this.$bus.$emit('foo', '来自兄弟组件') } }}</script>
另一个组件也在钩子函数中监听on
事件
export default { data() { return { message: '' } }, mounted() { this.$bus.$on('foo', (msg) => { this.message = msg }) }}
7. $root 拜访根组件中的属性或办法
- 作用:拜访根组件中的属性或办法
- 留神:是根组件,不是父组件。
$root
只对根组件有用
var vm = new Vue({ el: "#app", data() { return { rootInfo:"我是根元素的属性" } }, methods: { alerts() { alert(111) } }, components: { com1: { data() { return { info: "组件1" } }, template: "<p>{{ info }} <com2></com2></p>", components: { com2: { template: "<p>我是组件1的子组件</p>", created() { this.$root.alerts()// 根组件办法 console.log(this.$root.rootInfo)// 我是根元素的属性 } } } } }});
8. vuex
- 实用场景: 简单关系的组件数据传递
- Vuex作用相当于一个用来存储共享变量的容器
state
用来寄存共享变量的中央getter
,能够减少一个getter
派生状态,(相当于store
中的计算属性),用来取得共享变量的值mutations
用来寄存批改state
的办法。actions
也是用来寄存批改state的办法,不过action
是在mutations
的根底上进行。罕用来做一些异步操作
小结
- 父子关系的组件数据传递抉择
props
与$emit
进行传递,也可抉择ref
- 兄弟关系的组件数据传递可抉择
$bus
,其次能够抉择$parent
进行传递 - 先人与后辈组件数据传递可抉择
attrs
与listeners
或者Provide
与Inject
- 简单关系的组件数据传递能够通过
vuex
寄存共享的变量
实现双向绑定
咱们还是以Vue
为例,先来看看Vue
中的双向绑定流程是什么的
new Vue()
首先执行初始化,对data
执行响应化解决,这个过程产生Observe
中- 同时对模板执行编译,找到其中动静绑定的数据,从
data
中获取并初始化视图,这个过程产生在Compile
中 - 同时定义⼀个更新函数和
Watcher
,未来对应数据变动时Watcher
会调用更新函数 - 因为
data
的某个key
在⼀个视图中可能呈现屡次,所以每个key
都须要⼀个管家Dep
来治理多个Watcher
- 未来data中数据⼀旦发生变化,会首先找到对应的
Dep
,告诉所有Watcher
执行更新函数
流程图如下:
先来一个构造函数:执行初始化,对data
执行响应化解决
class Vue { constructor(options) { this.$options = options; this.$data = options.data; // 对data选项做响应式解决 observe(this.$data); // 代理data到vm上 proxy(this); // 执行编译 new Compile(options.el, this); } }
对data
选项执行响应化具体操作
function observe(obj) { if (typeof obj !== "object" || obj == null) { return; } new Observer(obj); } class Observer { constructor(value) { this.value = value; this.walk(value); } walk(obj) { Object.keys(obj).forEach((key) => { defineReactive(obj, key, obj[key]); }); } }
编译Compile
对每个元素节点的指令进行扫描跟解析,依据指令模板替换数据,以及绑定相应的更新函数
class Compile { constructor(el, vm) { this.$vm = vm; this.$el = document.querySelector(el); // 获取dom if (this.$el) { this.compile(this.$el); } } compile(el) { const childNodes = el.childNodes; Array.from(childNodes).forEach((node) => { // 遍历子元素 if (this.isElement(node)) { // 判断是否为节点 console.log("编译元素" + node.nodeName); } else if (this.isInterpolation(node)) { console.log("编译插值⽂本" + node.textContent); // 判断是否为插值文本 {{}} } if (node.childNodes && node.childNodes.length > 0) { // 判断是否有子元素 this.compile(node); // 对子元素进行递归遍历 } }); } isElement(node) { return node.nodeType == 1; } isInterpolation(node) { return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent); } }
依赖收集
视图中会用到data
中某key
,这称为依赖。同⼀个key
可能呈现屡次,每次都须要收集进去用⼀个Watcher
来保护它们,此过程称为依赖收集多个Watcher
须要⼀个Dep
来治理,须要更新时由Dep
统⼀告诉
实现思路
defineReactive
时为每⼀个key
创立⼀个Dep
实例- 初始化视图时读取某个
key
,例如name1
,创立⼀个watcher1
- 因为触发
name1
的getter
办法,便将watcher1
增加到name1
对应的Dep
中 - 当
name1
更新,setter
触发时,便可通过对应Dep
告诉其治理所有Watcher
更新
// 负责更新视图 class Watcher { constructor(vm, key, updater) { this.vm = vm this.key = key this.updaterFn = updater // 创立实例时,把以后实例指定到Dep.target动态属性上 Dep.target = this // 读一下key,触发get vm[key] // 置空 Dep.target = null } // 将来执行dom更新函数,由dep调用的 update() { this.updaterFn.call(this.vm, this.vm[this.key]) } }
申明Dep
class Dep { constructor() { this.deps = []; // 依赖治理 } addDep(dep) { this.deps.push(dep); } notify() { this.deps.forEach((dep) => dep.update()); } }
创立watcher
时触发getter
class Watcher { constructor(vm, key, updateFn) { Dep.target = this; this.vm[this.key]; Dep.target = null; } }
依赖收集,创立Dep
实例
function defineReactive(obj, key, val) { this.observe(val); const dep = new Dep(); Object.defineProperty(obj, key, { get() { Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例 return val; }, set(newVal) { if (newVal === val) return; dep.notify(); // 告诉dep执行更新办法 }, }); }
Vue组件为什么只能有一个根元素
vue3
中没有问题
Vue.createApp({ components: { comp: { template: ` <div>root1</div> <div>root2</div> ` } }}).mount('#app')
vue2
中组件的确只能有一个根,但vue3
中组件曾经能够多根节点了。- 之所以须要这样是因为
vdom
是一颗单根树形构造,patch
办法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom
vue3
中之所以能够写多个根节点,是因为引入了Fragment
的概念,这是一个形象的节点,如果发现组件是多根的,就创立一个Fragment
节点,把多个根节点作为它的children
。未来patch
的时候,如果发现是一个Fragment
节点,则间接遍历children
创立或更新
Vue3.2 setup 语法糖汇总
提醒:vue3.2
版本开始能力应用语法糖!
在 Vue3.0
中变量必须 return
进去, template
中能力应用;而在 Vue3.2
中只须要在 script
标签上加上 setup
属性,无需 return
, template
便可间接应用,十分的香啊!
1. 如何应用setup语法糖
只需在 script
标签上写上 setup
<template></template><script setup></script><style scoped lang="less"></style>
2. data数据的应用
因为 setup
不需写 return
,所以间接申明数据即可
<script setup>import { ref, reactive, toRefs,} from 'vue'const data = reactive({ patternVisible: false, debugVisible: false, aboutExeVisible: false,})const content = ref('content')//应用toRefs解构const { patternVisible, debugVisible, aboutExeVisible } = toRefs(data)</script>
3. method办法的应用
<template > <button @click="onClickHelp">帮忙</button></template><script setup>import {reactive} from 'vue'const data = reactive({ aboutExeVisible: false,})// 点击帮忙const onClickHelp = () => { console.log(`帮忙`) data.aboutExeVisible = true}</script>
4. watchEffect的应用
<script setup>import { ref, watchEffect,} from 'vue'let sum = ref(0)watchEffect(()=>{ const x1 = sum.value console.log('watchEffect所指定的回调执行了')})</script>
5. watch的应用
<script setup>import { reactive, watch,} from 'vue'//数据let sum = ref(0)let msg = ref('hello')let person = reactive({ name:'张三', age:18, job:{ j1:{ salary:20 } }})// 两种监听格局watch([sum,msg],(newValue,oldValue)=>{ console.log('sum或msg变了',newValue,oldValue) }, {immediate:true})watch(()=>person.job,(newValue,oldValue)=>{ console.log('person的job变动了',newValue,oldValue)},{deep:true}) </script>
6. computed计算属性的应用
computed
计算属性有两种写法(简写和思考读写的残缺写法)
<script setup>import { reactive, computed,} from 'vue'// 数据let person = reactive({ firstName:'poetry', lastName:'x'})// 计算属性简写person.fullName = computed(()=>{ return person.firstName + '-' + person.lastName})// 残缺写法person.fullName = computed({ get(){ return person.firstName + '-' + person.lastName }, set(value){ const nameArr = value.split('-') person.firstName = nameArr[0] person.lastName = nameArr[1] }})</script>
7. props父子传值的应用
父组件代码如下(示例):
<template> <child :name='name'/> </template><script setup> import {ref} from 'vue' // 引入子组件 import child from './child.vue' let name= ref('poetry')</script>
子组件代码如下(示例):
<template> <span>{{props.name}}</span></template><script setup>import { defineProps } from 'vue'// 申明propsconst props = defineProps({ name: { type: String, default: 'poetries' }}) // 或者//const props = defineProps(['name'])</script>
8. emit子父传值的应用
父组件代码如下(示例):
<template> <AdoutExe @aboutExeVisible="aboutExeHandleCancel" /></template><script setup>import { reactive } from 'vue'// 导入子组件import AdoutExe from '../components/AdoutExeCom'const data = reactive({ aboutExeVisible: false, })// content组件ref// 对于零碎暗藏const aboutExeHandleCancel = () => { data.aboutExeVisible = false}</script>
子组件代码如下(示例):
<template> <a-button @click="isOk"> 确定 </a-button></template><script setup>import { defineEmits } from 'vue';// emitconst emit = defineEmits(['aboutExeVisible'])/** * 办法 */// 点击确定按钮const isOk = () => { emit('aboutExeVisible');}</script>
9. 获取子组件ref变量和defineExpose裸露
即vue2
中的获取子组件的ref
,间接在父组件中管制子组件办法和变量的办法
父组件代码如下(示例):
<template> <button @click="onClickSetUp">点击</button> <Content ref="content" /></template><script setup>import {ref} from 'vue'// content组件refconst content = ref('content')// 点击设置const onClickSetUp = ({ key }) => { content.value.modelVisible = true}</script><style scoped lang="less"></style>
子组件代码如下(示例):
<template> <p>{{data }}</p></template><script setup>import { reactive, toRefs} from 'vue'/** * 数据局部* */const data = reactive({ modelVisible: false, historyVisible: false, reportVisible: false, })defineExpose({ ...toRefs(data),})</script>
10. 路由useRoute和useRouter的应用
<script setup> import { useRoute, useRouter } from 'vue-router' // 申明 const route = useRoute() const router = useRouter() // 获取query console.log(route.query) // 获取params console.log(route.params) // 路由跳转 router.push({ path: `/index` })</script>
11. store仓库的应用
<script setup> import { useStore } from 'vuex' import { num } from '../store/index' const store = useStore(num) // 获取Vuex的state console.log(store.state.number) // 获取Vuex的getters console.log(store.state.getNumber) // 提交mutations store.commit('fnName') // 散发actions的办法 store.dispatch('fnName')</script>
12. await的反对
setup
语法糖中可间接应用await
,不须要写async
,setup
会主动变成async setup
<script setup> import api from '../api/Api' const data = await Api.getData() console.log(data)</script>
13. provide 和 inject 祖孙传值
父组件代码如下(示例):
<template> <AdoutExe /></template><script setup> import { ref,provide } from 'vue' import AdoutExe from '@/components/AdoutExeCom' let name = ref('py') // 应用provide provide('provideState', { name, changeName: () => { name.value = 'poetries' } })</script>
子组件代码如下(示例):
<script setup> import { inject } from 'vue' const provideState = inject('provideState') provideState.changeName()</script>
Vue组件渲染和更新过程
渲染组件时,会通过Vue.extend
办法构建子组件的构造函数,并进行实例化。最终手动调用$mount()
进行挂载。更新组件时会进行patchVnode
流程,外围就是diff
算法
怎么监听vuex数据的变动
剖析
vuex
数据状态是响应式的,所以状态变视图跟着变,然而有时还是须要晓得数据状态变了从而做一些事件。- 既然状态都是响应式的,那天然能够
watch
,另外vuex
也提供了订阅的API:store.subscribe()
答复范例
- 我晓得几种办法:
- 能够通过
watch
选项或者watch
办法监听状态 - 能够应用
vuex
提供的API:store.subscribe()
watch
选项形式,能够以字符串模式监听$store.state.xx
;subscribe
形式,能够调用store.subscribe(cb)
,回调函数接管mutation
对象和state
对象,这样能够进一步判断mutation.type
是否是期待的那个,从而进一步做后续解决。watch
形式简略好用,且能获取变动前后值,首选;subscribe
办法会被所有commit
行为触发,因而还须要判断mutation.type
,用起来略繁琐,个别用于vuex
插件中
实际
watch
形式
const app = createApp({ watch: { '$store.state.counter'() { console.log('counter change!'); } }})
subscribe
形式:
store.subscribe((mutation, state) => { if (mutation.type === 'add') { console.log('counter change in subscribe()!'); }})
Vue为什么没有相似于React中shouldComponentUpdate的生命周期
- 考点:
Vue
的变动侦测原理 - 前置常识: 依赖收集、虚构
DOM
、响应式零碎
根本原因是Vue
与React
的变动侦测形式有所不同
- 当React晓得发生变化后,会应用
Virtual Dom Diff
进行差别检测,然而很多组件实际上是必定不会发生变化的,这个时候须要shouldComponentUpdate
进行手动操作来缩小diff
,从而进步程序整体的性能 Vue
在一开始就晓得那个组件产生了变动,不须要手动管制diff
,而组件外部采纳的diff
形式实际上是能够引入相似于shouldComponentUpdate
相干生命周期的,然而通常正当大小的组件不会有适量的diff,手动优化的价值无限,因而目前Vue
并没有思考引入shouldComponentUpdate
这种手动优化的生命周期
Class 与 Style 如何动静绑定
Class
能够通过对象语法和数组语法进行动静绑定
对象语法:
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>data: { isActive: true, hasError: false}
数组语法:
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>data: { activeClass: 'active', errorClass: 'text-danger'}
Style
也能够通过对象语法和数组语法进行动静绑定
对象语法:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>data: { activeColor: 'red', fontSize: 30}
数组语法:
<div v-bind:style="[styleColor, styleSize]"></div>data: { styleColor: { color: 'red' }, styleSize:{ fontSize:'23px' }}
v-once的应用场景有哪些
剖析
v-once
是Vue
中内置指令,很有用的API
,在优化方面常常会用到
体验
仅渲染元素和组件一次,并且跳过将来更新
<!-- single element --><span v-once>This will never change: {{msg}}</span><!-- the element have children --><div v-once> <h1>comment</h1> <p>{{msg}}</p></div><!-- component --><my-component v-once :comment="msg"></my-component><!-- `v-for` directive --><ul> <li v-for="i in list" v-once>{{i}}</li></ul>
答复范例
v-once
是vue
的内置指令,作用是仅渲染指定组件或元素一次,并跳过将来对其更新- 如果咱们有一些元素或者组件在初始化渲染之后不再须要变动,这种状况下适宜应用
v-once
,这样哪怕这些数据变动,vue
也会跳过更新,是一种代码优化伎俩 - 咱们只须要作用的组件或元素上加上
v-once
即可 vue3.2
之后,又减少了v-memo
指令,能够有条件缓存局部模板并管制它们的更新,能够说控制力更强了- 编译器发现元素下面有
v-once
时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而防止再次计算
原理
上面例子应用了v-once
:
<script setup>import { ref } from 'vue'const msg = ref('Hello World!')</script><template> <h1 v-once>{{ msg }}</h1> <input v-model="msg"></template>
咱们发现v-once
呈现后,编译器会缓存作用元素或组件,从而防止当前更新时从新计算这一部分:
// ...return (_ctx, _cache) => { return (_openBlock(), _createElementBlock(_Fragment, null, [ // 从缓存获取vnode _cache[0] || ( _setBlockTracking(-1), _cache[0] = _createElementVNode("h1", null, [ _createTextVNode(_toDisplayString(msg.value), 1 /* TEXT */) ]), _setBlockTracking(1), _cache[0] ),// ...