面试:Vue相关

文章目录

Vue

简单介绍一下Vue

vue是一个渐进式的js框架,可以逐步采用,不必一下就通过框架去重构项目。vue的核心库只关注视图层,非常容易与其它库或已有项目整合。Vue.js是一个轻巧、高性能、可组件化的MVVM库,同时拥有非常容易上手的API。

特点:

  • 虚拟dom、diff算法
  • 响应式、双向数据绑定(数据劫持(拦截)结合发布订阅模式)、数据驱动
  • 组件化
  • 指令(v-if/v-show/v-for…v-开头的特殊属性,给HTML标签添加更多的特殊功能)

虚拟dom(virtual dom)

Web界面由DOM树(树的意思是数据结构)来构建,当其中一部分发生变化时,其实就是对应某个DOM节点发生了变化,

虚拟DOM就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性attch到DOM树上,再进行后续操作,避免大量无谓的计算量。所以,用JS对象模拟DOM节点的好处是,页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制

template转换成view

  • Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树
  • 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行DOM操作来更新视图。

简单点讲,在Vue的底层实现上,Vue将模板编译成虚拟DOM渲染函数。结合Vue自带的响应系统,在状态改变时,Vue能够智能地计算出重新渲染组件的最小代价并应到DOM操作上。

在这里插入图片描述

  • 渲染函数:渲染函数是用来生成Virtual DOM的。Vue推荐使用模板来构建我们的应用界面,在底层实现中Vue会将模板编译成渲染函数,当然我们也可以不写模板,直接写渲染函数,以获得更好的控制。
  • VNode 虚拟节点:它可以代表一个真实的 dom 节点。通过 createElement 方法能将 VNode 渲染成 dom 节点。简单地说,vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的DOM节点
  • patch(也叫做patching算法):虚拟DOM最核心的部分,它可以将vnode渲染成真实的DOM,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新。这点我们从单词含义就可以看出, patch本身就有补丁、修补的意思,其实际作用是在现有DOM上进行修改来实现更新视图的目的。

在这里插入图片描述

vdom作用

为了避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldVnode)做对比,找出真正需要更新的节点来进行DOM操作,从而避免操作其他无需改动的DOM。

其实虚拟DOM在Vue.js主要做了两件事:

  • 提供与真实DOM节点所对应的虚拟节点vnode
  • 将虚拟节点vnode和旧虚拟节点oldVnode进行对比,然后更新视图

为何需要Virtual DOM?

  1. 具备跨平台的优势
    由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。

  2. 操作 DOM 慢,js运行效率高。我们可以将DOM对比操作放在JS层,提高效率。
    因为DOM操作的执行速度远不如Javascript的运算速度快,因此,把大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。

Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)

  1. 提升渲染性能
    Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。

diff算法

在这里插入图片描述

在比较根节点的时候做的第一件事是先判断新旧两个节点有没有子节点,都有孩子则比较他们的孩子,进入孩子层级,若发现又有孩子则一直往下找孩子,如上图直接进入到第三层级,当发现往下都没有孩子时,则进入同层比较,同时在左侧两个橙色框内的孩子比较完后,也会返回上一级再按这种方式进行比较。

生命周期

详解vue生命周期

在这里插入图片描述

  1. 在beforeCreate和created钩子函数之间的生命周期
    在这个生命周期之间,进行初始化事件,进行数据的观测,可以看到在created的时候数据已经和data属性进行绑定(放在data中的属性当值发生改变的同时,视图也会改变)。
    注意看下:此时还是没有el选项

  2. created钩子函数和beforeMount间的生命周期
    首先会判断对象是否有el选项。如果有的话就继续向下编译,如果没有el选项,则停止编译,也就意味着停止了生命周期,直到在该vue实例上调用vm.$mount(el)。此时注释掉代码中:

el: '#app',

然后运行可以看到到created的时候就停止了。

如果我们在后面继续调用vm.$mount(el),可以发现代码继续向下执行了

vm.$mount(el) //这个el参数就是挂在的dom接点

然后,我们往下看,template参数选项的有无对生命周期的影响。
(1)如果vue实例对象中有template参数选项,则将其作为模板编译成render函数。
(2)如果没有template选项,则将外部HTML作为模板编译。
(3)可以看到template中的模板优先级要高于outer HTML的优先级。
修改代码如下, 在HTML结构中增加了一串html,在vue对象中增加了template选项:

<body>
  <div id="app">
    <!--html中修改的-->
    <h1>{{message + '这是在outer HTML中的'}}</h1>
  </div>
</body>
<script>
  var vm = new Vue({
    el: '#app',
    template: "<h1>{{message +'这是在template中的'}}</h1>", //在vue配置项中修改的
    data: {
      message: 'Vue的生命周期'
    }
</script>

执行后的结果可以看到在页面中显示的是:

这是在template中的

那么将vue对象中template的选项注释掉后打印如下信息:

这是在outer HTML中的

这下就可以想想什么el的判断要在template之前了:是因为vue需要通过el找到对应的outer HTML

在vue对象中还有一个render函数,它是以createElement作为参数,然后做渲染操作,而且我们可以直接嵌入JSX.

new Vue({
    el: '#app',
    render: function(createElement) {
        return createElement('h1', 'this is createElement')
    }
})

可以看到页面中渲染的是:

this is createElement

所以综合排名优先级:
render函数选项 > template选项 > outer HTML.

  1. mounted
    注意看下面截图:
    在这里插入图片描述

在mounted之前h1中还是通过{{message}}进行占位的,因为此时还没有挂在到页面上,还是JavaScript中的虚拟DOM形式存在的。在mounted之后可以看到h1中的内容发生了变化。

  1. beforeUpdate钩子函数和updated钩子函数间的生命周期
    当vue发现data中的数据发生了改变,会触发对应组件的重新渲染,先后调用beforeUpdate和updated钩子函数。我们在console中输入:
vm.message = '触发组件更新'

发现触发了组件的更新:
在这里插入图片描述

  1. beforeDestroy和destroyed钩子函数间的生命周期

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

mounted和created区别

在这里插入图片描述

  • 在created的时候,在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图,因此视图中的html并没有渲染出来,所以此时如果直接去操作html的dom节点,一定找不到相关的元素
    实例已经被初始化,但是还没有挂载至$el上,所以我们无法获取到对应的节点,但是此时我们是可以获取到vue中data与methods中的数据的
  • 而在mounted中,在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作,vue的template成功挂载在$el中,由于此时html已经渲染出来了,所以可以直接操作dom节点

Vue父子组件生命周期顺序

挂载阶段

父组件beforeCreated -> 父组件created -> 父组件beforeMounted -> 子组件beforeCreated -> 子组件created -> 子组件beforeMounted -> 子组件mounted -> 父组件mounted。

created与beforeMount之间,主要做了两步工作:

1、判断实例在dom中有没有挂载的元素(el:‘#app’),只有挂载了才能够继续。挂载好后,实例即与挂载dom元素进行了绑定(占坑),实例中也可以进行引用;

2、渲染dom模板。渲染dom模板只是在内存中,并非是在HTML中的DOM结构中渲染,所以前台在这个阶段时,组件对应的元素是没有显示的。(在调用 this.$el.outerHTML 后,控制台输出

—— 可以看到father组件的beforeMount时,child子组件的vue创建生命周期已经完成到mounted阶段。说明father在执行dom模板渲染的时候,会监测模板中是否有自定义的vue子组件。如果有,就进入子组件的生命周期的创建阶段,等到所有子组件的完成创建并挂载(mounted)到父组件的模板当中后,才能表明父组件在内存中的模板渲染完成。

—— 子组件的mounted阶段虽然完成,但父组件仍在beforeMounted阶段时,前台也看不见子组件渲染的效果,子组件只是完成了挂载到父组件的模板中了(控制台可以看到dom树中的元素并未变化)。因此此刻在子组件的mounted阶段直接调用一些方法(dom操作方法)可能会造成异常错误。为保险起见可在子组件中通过 $nextTick() 回调,等下一次DOM更新后再进行dom的操作。

更新阶段

从beforeUpdate阶段中可以看到,内部变量tData已经变化。页面中DOM的 tData绑定值还未变化。

子组件的数据更新,不会引起父组件的beforeUpdate和updated生命周期钩子。

beforeUpdate和updated阶段,vue根据变量更新后的数据在虚拟DOM中进行渲染(图中re-render)。而后进行页面相应组件的更新

销毁阶段
  • beforeDestroy:进入该阶段,表明实例已经接收到了被销毁的指令。在该阶段,实例的属性、方法、事件等仍然可以调用。
  • 在beforeDestroy与destroyed之间,组件开始注销自己的属性、方法、事件以及自己的子组件。只有等到所有都已注销完成(子组件达到destroyed阶段),父组件才能够进入destroyed阶段。

v-if与v-show的区别

相同点:v-if与v-show都可以动态控制dom元素显示隐藏

不同点:v-if显示隐藏是将dom元素整个添加或删除,而v-show隐藏则是为该元素添加css–display:none,dom元素还在

  • 区别
    (1)手段:v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐
    (2)编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;
    (3)编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译(编译被缓存?编译被缓存后,然后再切换的时候进行局部卸载); v-show是在任何条件下(首次条件是否为真)都被编译,然后被缓存,而且DOM元素保留
    (4)性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗
    (5)使用场景:v-if适合运行条件很少改变;v-show适合频繁切换

v-if实现原理

Vue进行了如下转化template —> ast —> render函数,最后根据生成的render函数来生成相应的DOM,这里就不拓展讲了。在生成ast和render函数的时候,Vue对v-if这一类指令进行了解析。

生成AST
这一部分是将template模板转换成相应的ast语法树:如果元素中有v-if的话,就将v-if的条件取出来加入到el.ifConditions里面,如果有 v-else,v-else-if属性的话就将相应的标志位置为成true。

因为我们只把v-if的条件挂在了el.ifConditions下面,但v-else,v-else-if都还未进行处理,所以接下来需要将它们也加入进去。

当遇到当前ele有v-else或者v-elseif属性的时候,需要处理if属性,在其上级兄弟元素中必然存在v-if属性,然后将当前元素的else-if上表达式添加到上个元素的ifConditions上。

生成render函数
genIf函数是对于v-if的处理:
genIfConditions函数将el上的ifConditions上收集的if,else,else-if属性进行递归处理,最后生成render函数
最终在执行render函数时,会根据vue中绑定变量的值,来决定创建哪一部分的template

小结
v-if 基于数据驱动的理念,当 v-if 指令对应的 value 为 false 的时候会预先创建一个注释节点在该位置,然后在 value 发生变化时,命中派发更新的逻辑,对新旧组件树进行 patch,从而完成使用 v-if 指令元素的动态显示隐藏。
在这里插入图片描述

v-for和v-if的优先级及其性能优化

  • 当 v-if 与 v-for 一起使用时,v-for 具有比 v-if 更高的优先级,这意味着 v-if 将分别重复运行于每个 v-for 循环中
  • 如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费了性能
  • 永远不要把 v-if 和 v-for 同时用在同一个元素上。

一般我们在两种常见的情况下会倾向于这样做:

  1. 为了过滤一个列表中的项目 (比如 v-for=“user in users” v-if=“user.isActive”)。在这种情形下,请将 users替换为一个计算属性 (比如 activeUsers),让其返回过滤后的列表。
  2. 为了避免渲染本应该被隐藏的列表 (比如 v-for=“user in users” v-if=“shouldShowUsers”)。这种情形下,请将 v-if 移动至容器元素上 (比如 ul, ol)。

初始化

https://segmentfault.com/q/1010000010364198
Props,methods,data和computed的初始化都是在beforeCreated和created之间完成的。

watch和computed区别

作用机制上

1.watch和computed都是以Vue的依赖追踪机制为基础的,它们都试图处理这样一件事情:当某一个数据(称它为依赖数据)发生变化的时候,所有依赖这个数据的“相关”数据“自动”发生变化,也就是自动调用相关的函数去实现数据的变动

2.对methods:methods里面是用来定义函数的,很显然,它需要手动调用才能执行。而不像watch和computed那样,“自动执行”预先定义的函数。

从性质上

1.methods里面定义的是函数,你显然需要像"fuc()"这样去调用它(假设函数为fuc)。

2.computed是计算属性,事实上和和data对象里的数据属性是同一类的(使用上)。

3.watch:类似于监听机制+事件机制。

watch和computed的对比

首先它们都是以Vue的依赖追踪机制为基础的,它们的共同点是:都是希望在依赖数据发生改变的时候,被依赖的数据根据预先定义好的函数,发生“自动”的变化

但watch和computed也有明显不同的地方:

watch和computed各自处理的数据关系场景不同

1.watch擅长处理的场景:一个数据影响多个数据,watch用于观察和监听页面上的vue实例,当你需要在数据变化响应时,执行异步操作,或高性能消耗的操作,那么watch为最佳选择

2.computed擅长处理的场景:一个数据受多个数据影响,可以关联多个实时计算的对象,当这些对象中的其中一个改变时都会触发这个属性;具有缓存能力,所以只有当数据再次改变时才会重新渲染,否则就会直接拿取缓存中的数据。

watch属性是一个对象,键是需要观察的表达式,值是对应回调函数,回调函数得到的参数为新值和旧值。值也可以是方法名,或者包含选项的对象。侦察器对于任何更新的东西都有用——无论是表单输入、异步更新还是动画。vue实例在实例化时调用$watch(),遍历watch对象的每一个属性。

相比于watch/computed,methods不处理数据逻辑关系,只提供可调用的函数

immediate/deep

immediate(立即处理 进入页面就触发)
new Vue({
        el: '#app',
        data: {
            num: 1
        },
        watch: {
            num: {
            	// 数据发生变化就会调用这个函数  
                handler(newVal, oldVal) {
                    console.log('oldVal:', oldVal)
                    console.log('newVal:', newVal)
                },
                // 立即处理 进入页面就触发
                immediate: true
            }
        }
    })
deep(深度监听)

对象和数组都是引用类型,引用类型变量存的是地址,地址没有变,所以不会触发watch。这时我们需要进行深度监听,就需要加上一个属性 deep,值为 true

new Vue({
        el: '#app',
        data: {
            food: {
                id: 1,
                name: '冰激凌'
            }
        },
        methods: {
            change() {
                this.food.name = '棒棒糖'
            }
        },
        watch: {
        	// 第一种方式:监听整个对象,每个属性值的变化都会执行handler
        	// 注意:属性值发生变化后,handler执行后获取的 newVal 值和 oldVal 值是一样的
            food: {
                // 每个属性值发生变化就会调用这个函数
                handler(newVal, oldVal) {
                    console.log('oldVal:', oldVal)
                    console.log('newVal:', newVal)
                },
                // 立即处理 进入页面就触发
                immediate: true,
                // 深度监听 属性的变化
                deep: true
            },
            // 第二种方式:监听对象的某个属性,被监听的属性值发生变化就会执行函数
            // 函数执行后,获取的 newVal 值和 oldVal 值不一样
            'food.name'(newVal, oldVal) {
                console.log('oldVal:', oldVal)   // 冰激凌
                console.log('newVal:', newVal)   // 棒棒糖
            }
        }
    })

v-model / 数据双向绑定

数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

在这里插入图片描述

  • 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者(Dep)
  • 实现一个指令解析器Compiler,对每个元素节点的指令进行扫描和解析根据指令模板替换数据,以及绑定相应的更新函数
  • 实现一个Watcher,作为连接Observer和Compiler的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
  • mvvm入口函数,整合以上三者

理解VUE双向数据绑定原理和实现

DocuemntFragment(碎片化文档)

你可以把他认为一个dom节点收容器,当你创造了10个节点,当每个节点都插入到文档当中都会引发一次浏览器的回流,也就是说浏览器要回流10次,十分消耗资源。
而使用碎片化文档,也就是说我把10个节点都先放入到一个容器当中,最后我再把容器直接插入到文档就可以了!浏览器只回流了1次。

什么是访问器属性

什么是访问器属性? - 望星muS的回答 - 知乎
https://www.zhihu.com/question/40648241/answer/155925352

在javaScript中,对象的属性分为两种类型:数据属性和访问器属性。

一、数据属性
1.数据属性:它包含的是一个数据值的位置,在这可以对数据值进行读写。

数据属性包含四个特性,分别是:

configurable:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或能否把属性修改为访问器属性,默认为true

enumerable:表示能否通过for-in循环返回属性

writable:表示能否修改属性的值

value:包含该属性的数据值。默认为undefined

二、访问器属性
1.访问器属性:这个属性不包含数据值,包含的是一对get和set方法,在读写访问器属性时,就是通过这两个方法来进行操作处理的。

2.访问器属性包含的四个特性:

configurable:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或能否把属性修改为访问器属性,默认为false

enumerable:表示能否通过for-in循环返回属性,默认为false

get:在读取属性时调用的函数,默认值为undefined

set:在写入属性时调用的函数,默认值为undefined

这里要注意下,访问器属性不能直接定义,要通过Object.defineProperty()这个方法来定义。

任务拆分

拆分任务可以让我们的思路更加清晰:

(1)将vue中的data中的内容绑定到输入文本框和文本节点中
写一个处理每一个节点的函数,如果有input绑定v-model属性或者有**{{ xxx }}的文本节点出现,就进行内容替换,替换为vm实例中的data中的内容**
然后,在向碎片化文档中添加节点时,每个节点都处理一下。

(2)当文本框的内容改变时,vue实例中的data也同时发生改变
通过事件监听器keyup,input等,来获取到最新的value,然后通过Object.defineProperty将获取的最新的value,赋值给实例vm的text,我们把vm实例中的data下的text通过Object.defineProperty设置为访问器属性,这样给vm.text赋值,就触发了set。set函数的作用一个是更新data中的text,另一个等到任务三再说。

实现一个响应式监听属性的函数。一旦有赋新值就发生变化;
实现一个观察者,对于一个实例、每一个属性值都进行观察;
改写编译函数,注意由于改成了访问器属性,访问的方法也产生变化,同时添加了事件监听器,把实例的text值随时更新

最终我们改变input中的内容能改变data中的数据,但页面却没有刷新

(3)当data中的内容发生改变时,输入框及文本节点的内容也发生变化
通过修改vm实例的属性 该改变输入框的内容 与 文本节点的内容。
这里涉及到一个问题 需要我们注意,当我们修改输入框,改变了vm实例的属性,这是1对1的。
但是,我们可能在页面中多处用到 data中的属性,这是1对多的。也就是说,改变1个model的值可以改变多个view中的值。
这就需要我们引入一个新的知识点:

订阅/发布者模式

订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。

发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应操作
在这里插入图片描述

之前提到的set函数的第二个作用,就是来提醒订阅者进行notify操作,告诉他们:“我的text变了!” 文本节点变成了订阅者,接到消息后,立马进行update操作。

回顾一下,每当 new 一个 Vue,主要做了两件事:第一个是监听数据:observe(data),第二个是编译 HTML:nodeToFragement(id)
在监听数据的过程中,我们会为 data 中的每一个属性生成一个主题对象 dep

在编译 HTML 的过程中,会为每个与数据绑定相关的节点生成一个订阅者 watcher,watcher 会将自己添加到相应属性的 dep 容器中。

我们已经实现:修改输入框内容 => 在事件回调函数中修改属性值 => 触发属性的 set 方法。

接下来我们要实现的是:发出通知 dep.notify() => 触发订阅者的 update 方法 => 更新视图
这里的关键逻辑是:如何将 watcher 添加到关联属性的 dep 中。

注意: 把直接赋值的操作改为了 添加一个 Watcher 订阅者
在这里插入图片描述
watcher:
在这里插入图片描述
首先,将自己(watcher生成的实例)赋给了一个全局变量 Dep.target;

其次,执行了 update 方法,进而执行了 get 方法,get 的方法读取了 vm 的访问器属性,从而触发了访问器属性的 get 方法,get 方法中将该 watcher 添加到了对应访问器属性的 dep 中;

再次,获取属性的值,然后更新视图。

最后,将 Dep.target 设为空。因为它是全局变量,也是 watcher 与 dep 关联的唯一桥梁,任何时刻都必须保证 Dep.target 只有一个值。
在这里插入图片描述
在这里插入图片描述

小结

(1)将vue中的data中的内容绑定到输入文本框和文本节点中
compile函数:获取和绑定v-model input和text显示节点,如果是input节点则添加input监听并更新节点,如果是文本节点则绑定一个订阅者

(2)当文本框的内容改变时,vue实例中的data也同时发生改变
事件监听器defineReactive:获取最新的value后,通过Object.defineProperty将获取的最新的value,赋值给实例vm的data的text,这样给vm.text赋值,就触发了set

最终我们改变input中的内容能改变data中的数据,但页面却没有刷新

(3)当data中的内容发生改变时,输入框及文本节点的内容也发生变化
注意是在set方法里进行主题对象(dep)通知(监听者/订阅者)活动,然后在notify方法中触发监听者者/订阅者(watcher/sub)的update方法,最后进行视图更新。

Vuex

在这里插入图片描述

什么时候用?

  1. 当一个组件需要多次触发事件时(多个视图依赖于同一状态)
  • 如果它多次触发事件,必然有其它组件进行接收并调用。 如果是一个组件进行接收和调用还好,但是如果两个?三个?甚至四个呢? 如果触发事件的组件只是触发一个事件,那还比较好管理,一旦进行多次触发那么维护的难度就会很高
  • 传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
  1. 跨组件共享数据、跨页面共享数据(来自不同视图的行为需要变更同一状态)
  • 我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

Vuex特点

  • 单向数据流。View 通过 store.dispatch() 调用 Action ,在 Action 执行完异步操作之后通过 store.commit() 调用 Mutation 更新 State ,通过 vue 的响应式机制进行视图更新
  • 单一数据源,和 Redux 一样全局只有一个 Store 实例
  • 只能应用于 Vue

mutation和action的详细区别

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。
  1. 流程顺序
    “相应视图—>修改State”:拆分成两部分,视图触发(dispatch)Action,Action再触发(commit)Mutation。
  2. 角色定位
    基于流程顺序,二者扮演不同的角色:
    Mutation:专注于修改State,理论上是修改State的唯一途径,并且这个过程是同步的。
    Action:业务代码、异步请求。
  3. 限制
    角色不同,二者有不同的限制。
    Mutation:必须同步执行,是可以直接修改state中状态;
    Action:可以异步,提交的是Mutation,但不能直接操作State。

Actions接受一个context对象参数,该参数具有和store实例相同的属性和方法,所以我们可以通过context.commit()提交mutations中的方法,或者可以通过context.state和context.getters去获取state和getters。

分发action:在组件中可以通过this.$store.dispatch分发action,或者使用mapActions辅助函数将methods映射为store.dispatch调用

Vuex和Event-Bus区别

本质上最大的区别:bus利用事件抛发(emit、on)的原理进行传递数据,由一个公共的vue实例专门处理emit和on事件(在父组件中创建eventBus,通过provide提供/inject注入)
而vuex通过数据劫持,做全局数据处理,是指限定了对公共数据的使用处理方法,统一管控,并且复制一份相同的_data来进行数据管理.

vuex 为什么要区分 actions 和 mutations

官方文档说明:“在 mutations 中混合异步调用会导致你的程序很难调试。例如,当你能调用了两个包含异步回调的 mutations 来改变状态,你怎么知道什么时候回调和哪个先回调呢?这就是为什么我们要区分这两个概念。在 Vuex 中,我们将全部的改变都用同步方式实现。我们将全部的异步操作都放在 Actions 中。”

区分 actions 和 mutations 并不是为了解决竞态问题,而是为了能用 devtools 追踪状态变化。
事实上在 vuex 里面 actions 只是一个架构性的概念,并不是必须的,说到底只是一个函数,你在里面想干嘛都可以,只要最后触发 mutation 就行。异步竞态怎么处理那是用户自己的事情。vuex 真正限制你的只有 mutation 必须是同步的这一点(在 redux 里面就好像 reducer 必须同步返回下一个状态一样)
同步的意义在于这样每一个 mutation 执行完成后都可以对应到一个新的状态(和 reducer 一样),这样 devtools 就可以打个 snapshot 存下来,然后就可以随便 time-travel 了。 如果你开着 devtool 调用一个异步的 action,你可以清楚地看到它所调用的 mutation 是何时被记录下来的,并且可以立刻查看它们对应的状态。

Vue2和Vue3区别

  1. 更小
  2. 更快
  3. 加强 TypeScript 支持
  4. 加强 API 设计一致性
  5. 提高自身可维护性
  6. 开放更多底层功能

Vue Function-based API RFC

【Vue3.0】尤雨溪 - 聊聊 Vue.js 3.0 Beta 官方直播完整版 2020-04-21

  1. 重构响应式系统,使用Proxy替换Object.defineProperty,使用Proxy优势:
    • 可直接监听数组类型的数据变化
    • 监听的目标为对象本身,不需要像Object.defineProperty一样遍历每个属性,有一定的性能提升
    • 可拦截apply、ownKeys、has等13种方法,而Object.defineProperty不行
    • 直接实现对象属性的新增/删除

  2. 新增Composition API,更好的逻辑复用和代码组织

  3. 重构 Virtual DOM
    • 模板编译时的优化,将一些静态节点编译成常量
    • slot优化,将slot编译为lazy函数,将slot的渲染的决定权交给子组件
    • 模板中内联事件的提取并重用(原本每次渲染都重新生成内联函数)

  4. 代码结构调整,更便于Tree shaking,使得体积更小

  5. 使用Typescript替换Flow

  • 为什么要新增Composition API,它能解决什么问题?
    Vue2.0中,随着功能的增加,组件变得越来越复杂,越来越难维护,而难以维护的根本原因是Vue的API设计迫使开发者使用watch,computed,methods选项组织代码,而不是实际的业务逻辑

另外Vue2.0缺少一种较为简洁的低成本的机制来完成逻辑复用,虽然可以minxis完成逻辑复用,但是当mixin变多的时候,会使得难以找到对应的data、computed或者method来源于哪个mixin,使得类型推断难以进行

所以Composition API的出现,主要是也是为了解决Option API带来的问题。

  • 第一个是代码组织问题,Compostion API可以让开发者根据业务逻辑组织自己的代码,让代码具备更好的可读性和可扩展性,也就是说当下一个开发者接触这一段不是他自己写的代码时,他可以更好的利用代码的组织反推出实际的业务逻辑,或者根据业务逻辑更好的理解代码。
  • 第二个是实现代码的逻辑提取与复用,当然mixin也可以实现逻辑提取与复用,但是像前面所说的,多个mixin作用在同一个组件时,很难看出property是来源于哪个mixin,来源不清楚,另外,多个mixin的property存在变量命名冲突的风险。而Composition API刚好解决了这两个问题。

Vue.nextTick()

Vue.nextTick 的原理和用途

原理

MutationObserver
MO是HTML5中的API,是一个用于监视DOM变动的接口,它可以监听一个DOM对象上发生的子节点删除、属性修改、文本内容修改等。

调用过程是要先给它绑定回调,得到MO实例,这个回调会在MO实例监听到变动时触发。这里MO的回调是放在microtask中执行的。

nextTick 源码主要分为两块:能力检测根据能力检测以不同方式执行回调队列

  • 能力检测
    由于宏任务耗费的时间是大于微任务的,所以在浏览器支持的情况下,优先使用微任务。如果浏览器不支持微任务,再使用宏任务。

  • 延迟调用优先级
    Promise > MutationObserver > setImmediate > setTimeout

next-tick.js 对外暴露了nextTick这一个参数,所以每次调用Vue.nextTick时会执行:

  1. 把传入的回调函数cb压入callbacks数组
  2. 执行timerFunc函数,延迟调用 flushCallbacks 函数
  3. 遍历执行 callbacks 数组中的所有函数

用法

下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM

Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。简单来说,Vue 在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。

简单总结事件循环:

同步代码执行 -> 查找异步队列,推入执行栈,执行Vue.nextTick[事件循环1] -> 查找异步队列,推入执行栈,执行Vue.nextTick[事件循环2]…

总之,异步是单独的一个tick,不会和同步在一个 tick 里发生,也是 DOM 不会马上改变的原因。

用途

应用场景:需要在视图更新之后,基于新的视图进行操作

elementui Loading

Loading 还可以以服务的方式调用。引入 Loading 服务:

import { Loading } from 'element-ui';

在需要调用时:

Loading.service(options);

其中 options 参数为 Loading 的配置项,具体见下表。LoadingService 会返回一个 Loading 实例,可通过调用该实例的 close 方法来关闭它:

let loadingInstance = Loading.service(options);
this.$nextTick(() => { // 以服务的方式调用的 Loading 需要异步关闭
  loadingInstance.close();
});

和node中nextTIck区别

process.nextTick()的意思就是定义出一个动作,并且让这个动作在下一个事件轮询的时间点上执行。

“为事件循环设置一项任务,node.js会在下次事件循环调响应时调用callback”

更精确的说,process.nextTick()定义的调用会创建一个新的子堆栈。在当前的栈里,你可以执行任意多的操作。但一旦调用netxTick,函数就必须返回到父堆栈。然后事件轮询机制又重新等待处理新的事件,如果发现nextTick的调用,就会创建一个新的栈。

vue渲染到页面流程

Vue组件的渲染更新原理解析
在这里插入图片描述

1.初始化

在 new Vue() 之后。 Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等。

2.把模板编译为render函数

在我们的主入口main.js

import Vue from 'vue'
import App from './App'

console.log(App)

new Vue({
render: h => h(App)
}).$mount('#app')

使用vue template complier(compile编译可以分成 parse、optimize 与 generate 三个阶段),将模板编译成render函数,执行render函数后,变成vnode

而使用Vue-cli进行组件化开发,在我们引入组件的后,其实会有一个解析器(vue-loader)对此模板进行了解析,生成了render函数

parse、optimize 与 generate 三个阶段
parse
parse 会用正则等方式解析 template 模板中的指令、class、style等数据,形成AST,就是with语法的过程。
optimize:优化AST
optimize 的主要作用是标记 static 静态节点,这是 Vue 在编译过程中的一处优化,后面当 update更新界面时,会有一个 patch 的过程, diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能
generate
generate 是将 AST 转化成 render function 字符串的过程,得到结果是 render 的字符串以及 staticRenderFns 字符串
在经历过 parse、optimize 与 generate 这三个阶段以后,组件中就会存在渲染 VNode 所需的 render function 了

template会被vue-loader编译成render函数并添加到组件选项对象里。组件通过components引用该组件选项,创建组件(Vue实例),渲染页面

Vue响应式原理

三、vue的响应式原理:

前置知识:

  • observer (value) ,其中 value(需要「响应式」化的对象)。
  • defineReactive ,这个方法通过 Object.defineProperty 来实现对对象的「响应式」化,入参是一个 obj(需要绑定的对象)、key(obj的某一个属性),val(具体的值)。
  • 对象被读,就是说,这个值已经在页面中使用或已经使用插值表达式插入。

正式知识:

  1. 首先我们一开始会进行响应式初始化,也即是我们开始前的哪个init过程,通过observer (value) 方法,然后通过defineReactive()方法遍历,对每个对象的每个属性进行setter和getter初始化。

  2. 依赖收集:我们在闭包中增加了一个 Dep 类的对象,用来收集 Watcher 对象。在对象被「读」的时候,会触发 reactiveGetter 函数把当前的 Watcher 对象,收集到 Dep 类中去。之后如果当该对象被「写」的时候,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图

附加知识点:object.defineproperty()的缺点

我们知道vue响应式主要使用的是object.defineproperty()这个api,那他也会带来一些缺点:
需要深度监听,需要递归到底,一次性计算量大(比如引用类型层级较深)

  • 无法监听新增属性/删除属性,需要使用Vue.set和Vue.delete才行
  • 无法监听原生数组,需要重写数组方法

3.虚拟节点VNode

我们把Vue的实例挂载到#app, 会调用实例里面的render方法,生成虚拟DOM。

new Vue({
	render: h => {
	let root = h(App)
	console.log('root:', root)
	return root
	}
}).$mount('#app')

组件渲染页面时会先调用render函数,render函数返回组件内标签节点(VNode实例)

  • vue在render的时候内部做什么处理
    每个标签(包括文本和组件标签等)会创建一个节点,先创建子标签的节点,在父节点创建时将它添加父节点的children数组中,形成与标签结构相同的树形结构

Vue实例进行挂载, 根据根节点render函数的调用,递归的生成虚拟dom

上面生成的root VNode就是虚拟节点,虚拟节点里面有一个属性elm, 这个属性指向真实的DOM节点。因为VNode指向了真实的DOM节点,那么虚拟节点经过对比后,生成的DOM节点就可以直接进行替换

这样有什么好处呢?

一个组件对象,如果内部的data发生变化,触发了render函数,重新生成了VNode节点。那么就可以直接找到所对应的节点,然后直接替换。那么这个过程只会在本组件内发生,不会影响其他的组件。于是组件与组件是隔离的。

patch函数/diff算法

前置知识:

  • insert:在父几点下插入节点,如果指定ref则插入到ref这个子节点的前面。
  • createElm:用来新建一些节点,tag节点存在创建一个标签节点,否则创建一个文本节点。
  • addVnodes:用来批量调用createElm新建节点。
  • removeNode:用来移除一个节点
  • removeVnodes:会批量调用removeNode移除节点
patch函数

patch的核心就是diff算法,diff算法通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有o(n),比较高效,我们看下图所示:
在这里插入图片描述

  • patch过程
function patch (oldVnode, vnode, parentElm) {
    if (!oldVnode) {
        addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
    } else if (!vnode) {
        removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
    } else {
        if (sameVnode(oldVNode, vnode)) {
            patchVnode(oldVNode, vnode);
        } else {
            removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
            addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
        }
    }
}
  1. 首先在 oldVnode(老 VNode 节点)不存在的时候,相当于新的 VNode 替代原本没有的节点,所以直接用 addVnodes 将这些节点批量添加到 parentElm 上。
  2. 如果 vnode(新 VNode 节点)不存在的时候,相当于要把老的节点删除,所以直接使用 removeVnodes 进行批量的节点删除即可。
  3. 当 oldVNode 与 vnode 都存在的时候,需要判断它们是否属于 sameVnode(相同的节点)。如果是则进行patchVnode(比对 VNode )操作,否则删除老节点,增加新节点
  • sameVnode函数
function sameVnode (a, b) {
  return (
    a.key === b.key &&  // key值
    a.tag === b.tag &&  // 标签名
    a.isComment === b.isComment &&  // 是否为注释节点
    // 是否都定义了data,data包含一些具体信息,例如onclick , style
    isDef(a.data) === isDef(b.data) &&  
    sameInputType(a, b) // 当标签是<input>的时候,type必须相同
  )
}

也就是说,判断两个节点是否为同一节点(也就是是否可复用),标准是key相同且tag相同

  • patchVnode函数
function patchVnode (oldVnode, vnode) {
    // 新老节点相同,直接return
    if (oldVnode === vnode) {
        return;
    }
    // 节点是否静态,并且新老节点的key相同,只要把老节点拿来用就好了
    if (vnode.isStatic && oldVnode.isStatic && vnode.key === oldVnode.key) {
        vnode.elm = oldVnode.elm;
        vnode.componentInstance = oldVnode.componentInstance;
        return;
    }
 	
 	// 找到对应真实的dom节点
    const elm = vnode.elm = oldVnode.elm;
    const oldCh = oldVnode.children;
    const ch = vnode.children;
    // 当VNode是文本节点,直接setTextContent来设置text
    if (vnode.text) {
        nodeOps.setTextContent(elm, vnode.text);
    // 不是文本节点
    } else {
        // oldch(老)与ch(新)存在且不同,使用updateChildren()
        if (oldCh && ch && (oldCh !== ch)) {
            updateChildren(elm, oldCh, ch);
        // 只有ch存在,若oldch(老)节点是文本节点,先删除,再将ch(新)节点插入elm节点下
        } else if (ch) {
            if (oldVnode.text) nodeOps.setTextContent(elm, '');
            addVnodes(elm, null, ch, 0, ch.length - 1);
        // 同理当只有oldch(老)节点存在,说明需要将oldch(老)节点通过removeVnode全部删除
        } else if (oldCh) {
            removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        // 当老节点是文本节点,清除其节点内容
        } else if (oldVnode.text) {
            nodeOps.setTextContent(elm, '')
        }
    }
}
  1. 新老节点相同,直接return
  2. 节点是否静态,并且新老节点的key相同,只要把老节点拿来用就好了(前面编译阶段的optimize操作)
  3. 当VNode是文本节点,直接setTextContent来设置text,即将el的文本节点设置为Vnode的文本节点。若不是文本节点者执行4-7
  4. oldch(老)与ch(新)存在且不同,使用updateChildren()(后面介绍)
  5. 只有ch存在,若oldch(老)节点是文本节点,先删除,再将ch(新)节点插入elm节点下
  6. 同理,当只有oldch(老)节点存在,说明需要将oldch(老)节点通过removeVnode全部删除
  7. 当老节点是文本节点,清除其节点内容
  • updateChildren函数
    在这里插入图片描述
updateChildren (parentElm, oldCh, newCh) {
    let oldStartIdx = 0, newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx
    let idxInOld
    let elmToMove
    let before
// sameVnode() 就是说key,tag,iscomment(注释节点),data四个同时定义
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  if (!oldStartVnode) {
      oldStartVnode = oldCh[++oldStartIdx];
  } else if (!oldEndVnode) {
      oldEndVnode = oldCh[--oldEndIdx];
  // 老节点的开头与新节点的开头对比
  } else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode);
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
  // 老节点的结尾与新节点的结尾对比
  } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
  // 老节点的开头与新节点的结尾
  } else if (sameVnode(oldStartVnode, newEndVnode)) {
      patchVnode(oldStartVnode, newEndVnode);
      nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
  // 老节点的结尾与新节点的开头
  } else if (sameVnode(oldEndVnode, newStartVnode)) {
      patchVnode(oldEndVnode, newStartVnode);
      nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
  // 如果上面的情况都没有满足
  } else {
      // 把老的元素进行移动
      let elmToMove = oldCh[idxInOld];
      // 如果老的节点找不到对应索引则创建
      if (!oldKeyToIdx) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      // 在新节点中的key值找到老节点索引
      idxInOld = newStartVnode.key ? oldKeyToIdx[newStartVnode.key] : null;
      // 如果没有找到相同的节点,则通过 createElm 创建一个新节点,并将 newStartIdx 向后移动一位。
      if (!idxInOld) {
          createElm(newStartVnode, parentElm);
          newStartVnode = newCh[++newStartIdx];
      // 否则如果找到了节点,同时它符合 sameVnode,则将这两个节点进行 patchVnode,将该位置的老节点赋值 undefined
      } else {
          // 这是是想把相同的节点进行移动
          elmToMove = oldCh[idxInOld];
          // 然后再进行对比
          if (sameVnode(elmToMove, newStartVnode)) {
              patchVnode(elmToMove, newStartVnode);
              oldCh[idxInOld] = undefined;
              nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm);
              newStartVnode = newCh[++newStartIdx];
              // 如果不符合 sameVnode,只能创建一个新节点插入到 parentElm 的子节点中,newStartIdx 往后移动一位。
          } else {
              createElm(newStartVnode, parentElm);
              newStartVnode = newCh[++newStartIdx];
          }
      }
  }
}
	// 当oldStartIdx > oldEndIdx 或oldStartIdx> oldEndIdx说明结束
	if (oldStartIdx > oldEndIdx) {
	  refElm = (newCh[newEndIdx + 1]) ? newCh[newEndIdx + 1].elm : null;
	  addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx);
	} else if (newStartIdx > newEndIdx) {
	  removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
	}
}

详解vue的diff算法

  • 将Vnode的子节点Ch和oldVnode的子节点oldCh提取出来
  • oldCh和Ch各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和vCh至少有一个已经遍历完了,就会结束比较。

diff算法是一个交叉对比的过程,大致可以简要概括为:头头比较、尾尾比较、头尾比较、尾头比较。具体过程可以参见这边博文,里面用例子讲的很清楚。

4.初次渲染

  1. 解析模板为render函数(或再开发环境已完成)
  2. 触发响应式,监听data属性的getter的依赖收集,也即是往dep里面添加watcher的过程
  3. 执行render函数,生成vnode(虚拟dom),patch

5.更新过程

在这里插入图片描述

  1. 修改data,setter(必需是初始渲染已经依赖过的)调用Dep.notify(),将通知它内部的所有的Watcher对象进行视图更新
  2. 重新执行rendern函数,生成newVnode
  3. 然后就是patch的过程(diff算法)

Vue3新特性

Vue3 的新特性

  1. 性能
  • 双向响应原理由Object.defineProperty改为基于ES6的Proxy,使其颗粒度更大,速度更快,且消除了之前存在的警告;
  • 重写了 Vdom ,突破了 Vdom 的性能瓶颈
  • 进行了模板编译的优化
  • 进行了更加高效的组件初始化
  1. Tree-Shaking 的支持
    支持了 tree-shaking (剪枝):像修剪树叶一样把不需要的东西给修剪掉,使 Vue3 的体积更小

需要的模块才会打入到包里,优化后的 Vue3.0 的打包体积只有原来的一半(13kb)。哪怕把所有的功能都引入进来也只有23kb,依然比 Vue2.x 更小。像 keep-alive 、 transition 甚至 v-for 等功能都可以按需引入。

  1. Composition API
    composition-api 是一个 Vue3 中新增的功能,它的灵感来自于 React Hooks ,是比 mixin 更强大的存在。

composition-api 可以提高代码逻辑的可复用性,从而实现与模板的无关性;同时使代码的可压缩性更强。另外,把 Reactivity 模块独立开来,意味着 Vue3.0 的响应式模块可以与其他框架相组合。

  1. Fragments
    不再限制 template 只有一个根节点。
    render函数也可以返回数组了,有点像 React.Fragments

  2. Better TypeScript Support
    更好的类型推导,使得 Vue3 把 TypeScript 支持得非常好

  3. Custom Renderer API
    实现用DOM的方式进行 WebGL 编程

Setup函数

setup()函数是Vue3.0中,专门为组件提供的新属性。它为基于Composition API的新特性提供了统一的入口。

在Vue3中,定义methods、watch、computed、data数据都放在了setup()函数中

  1. 执行时机
    setup()函数会在created()生命周期之前执行

diff算法优化

使用patch flag,在与上次虚拟节点进行对比时候,只对比带有patch flag的节点,并且可以通过flag的信息得知当前节点要对比的具体内容

Proxy和Object.defineProperty

Object.defineProperty是一个相对比较昂贵的操作,因为它直接操作对象的属性,颗粒度比较小。将它替换为es6的Proxy,在目标对象之上架了一层拦截,代理的是对象而不是对象的属性。这样可以将原本对对象属性的操作变为对整个对象的操作,颗粒度变大。

Proxy的优势如下:

  • 可直接监听数组类型的数据变化
  • 监听的目标为整个对象本身而非属性,不需要像Object.defineProperty一样遍历每个属性,有一定的性能提升
  • 可拦截apply、ownKeys、deleteProperty、has等13种方法,而Object.defineProperty不行
  • 直接实现对象属性的新增/删除
  • Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改

Object.defineProperty 不足在于:

  • Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,且必须深层遍历嵌套的对象
  • Object.defineProperty不能监听数组。是通过重写数据的那7个可以改变数据的方法来对数组进行监听的。
  • Object.defineProperty 也不能对 es6 新产生的 Map,Set 这些数据结构做出监听
  • Object.defineProperty也不能监听新增和删除操作,通过 Vue.set()和 Vue.delete()来实现响应式的。

重构Virtual DOM

  • 模板编译时的优化,将一些静态节点编译成常量
  • slot优化,将slot编译为lazy函数,将slot的渲染的决定权交给子组件
  • 模板中内联事件的提取并重用(原本每次渲染都重新生成内联函数)

使用Typescript替换Flow

为什么要新增Composition API,它能解决什么问题?

Vue2.0中,随着功能的增加,组件变得越来越复杂,越来越难维护,而难以维护的根本原因是Vue的API设计迫使开发者使用watch,computed,methods选项组织代码,而不是实际的业务逻辑

另外Vue2.0缺少一种较为简洁的低成本的机制来完成逻辑复用,虽然可以minxis完成逻辑复用,但是当mixin变多的时候,会使得难以找到对应的data、computed或者method来源于哪个mixin,使得类型推断难以进行

所以Composition API的出现,主要是也是为了解决Option API带来的问题:

  • 第一个是代码组织问题,Compostion API可以让开发者根据业务逻辑组织自己的代码,让代码具备更好的可读性和可扩展性,也就是说当下一个开发者接触这一段不是他自己写的代码时,他可以更好的利用代码的组织反推出实际的业务逻辑,或者根据业务逻辑更好的理解代码。
  • 第二个是实现代码的逻辑提取与复用,当然mixin也可以实现逻辑提取与复用,但是像前面所说的,多个mixin作用在同一个组件时,很难看出property是来源于哪个mixin,来源不清楚,另外,多个mixin的property存在变量命名冲突的风险。而Composition API刚好解决了这两个问题。

Proxy

Proxy可以理解成,在目标对象之前架设一层 “拦截”,当外界对该对象访问的时候,都必须经过这层拦截,而Proxy就充当了这种机制,类似于代理的含义,它可以对外界访问对象之前进行过滤和改写该对象。

proxy 不需要关心具体的 key,它去拦截的是 修改 data 上的任意 key 和 读取 data 上的任意 key
所以,不管是已有的 key 还是新增的 key,都会监听到

深入理解Proxy 及 使用Proxy实现vue数据双向绑定

vue的set和delete怎么实现的

[vue面试专问]Vue.set 和 Vue.delete 的实现

用法

直接修改/新增属性,可以修改/新增,但不会触发视图更新;

vue2.0 给data对象新增属性,并触发视图更新
原因是:受 ES5 的限制,Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue.js 转换它,才能让它是响应的。

要处理这种情况,我们可以使用$set()方法,既可以新增属性,又可以触发视图更新。

正确写法:this.$set(this.data,”key”,value’)

mounted () {
    this.$set(this.student,"age", 24)
}

实现

set 函数接收三个参数:第一个参数 target 是将要被添加属性的对象,第二个参数 key 以及第三个参数 val分别是要添加属性的键名和值

  1. if判断中isUndef函数用来判断一个值是否是 undefined 或 null,isPrimitive 函数用来判断一个值是否是原始类型值,ECMAScript 有 5 种原始类型(primitive type),即 Undefined、Null、Boolean、Number 和 String),如果是其中之一就报错;

  2. 判断是否为数组,且key是否为有效的数组索引,将数组的长度修改为 target.length 和 key 中的较大者,否则如果当要设置的元素的索引大于数组长度时 splice 无效;数组的 splice 变异方法能够完成数组元素的删除、添加、替换等操作。而 target.splice(key, 1, val) 就利用了替换元素的能力,将指定位置元素的值替换为新值,同时由于 splice 方法本身是能够触发响应的 (数组变异处理)

  3. 如果不是一个数组,那么必然是一个纯对象。

    • 假设该属性已经在对象上有定义了,那么只需要直接设置该属性的值即可,这将自动触发响应,因为已存在的属性是响应式的
    • 如果没有定义,就需要使用defineReactive 函数设置属性值,这是为了保证新添加的属性是响应式的;然后 ob.dep.notify() 从而触发响应。这就是添加全新属性触发响应的原理
  4. if (target._isVue || (ob && ob.vmCount)) {

    • Vue 实例对象拥有 _isVue 属性,所以当第一个条件成立时,那么说明你正在使用 Vue.set 函数为 Vue 实例对象添加属性,为了避免属性覆盖的情况出现,Vue.set/$set 函数不允许这么做,在非生产环境下会打印警告信息
    • 主要是观测一个数据对象是否为根数据对象,所以所谓的根数据对象就是 data 对象,当使用 Vue.set/$set 函数为根数据对象添加属性时,是不被允许的
      因为这样做是永远触发不了依赖的。原因就是根数据对象的 Observer 实例收集不到依赖(观察者)

v-for

key

key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素

有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

vue和react的虚拟DOM的Diff算法大致相同,其核心是基于两个简单的假设:

  1. 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构。
  2. 同一层级的一组节点,他们可以通过唯一的id进行区分。

vue中列表循环需加:key=“唯一标识”,唯一标识可以是item里面id等,因为vue组件高度复用,增加Key可以标识组件的唯一性,为了更好地区别各个组件 key的作用主要是为了高效的更新虚拟DOM

能否用index?

vue中使用v-for时为什么不能用index作为key?

v-for为什么要加key,能用index作为key么

:key 绑定的数据是index,删除数据后,index会重新赋值,index只有最后一个会被删除,其他的都保持从0到length-1递增(原来的0变成1,原来的1变成2…),由于用index做key导致点击事件发生后,新产生的newVDOM中元素的key被动态赋值,导致diff以为每个元素都发生了改变,于是页面重新渲染了所有的列表项。

不应该用 index 做为 key ,否则你增的0节点的key=0,而 oldNode 中的1的key也为0,实际上还是走了 sameNode 并且更新,和不写 key 一样效果

1)index作为key,其实就等于不加key
2)index作为key,只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出(这是vue官网的说明)

  • 总结:
  1. 更新DOM的时候会出现性能问题
  2. 会发生一些状态bug(select的options,selected由3变成4)

组件传值

props

$root,$parent,$children

子实例可以用 this.$parent 访问父实例,子实例被推入父实例的 $children 数组中。应当节制地使用它们,其只是作为访问组件的应急方法。更推荐用 props 和 events 实现父子组件通信.

  • provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的

provide/inject

vue提供了provide和inject帮助我们解决多层次嵌套嵌套通信问题在provide中指定要传递给子孙组件的数据,子孙组件通过inject注入祖父组件传递过来的数据。provide只需要将传递的值抛出,不需要知道使用哪一个子组件,子组件通过inject注入获取数据,也不需要知道父组件是谁,因此再封装组件库的时候很便利。

不推荐直接使用在应用程序代码中是因为数据追踪比较困难,不知道是哪一个层级声明了这个或者不知道哪一层级或若干个层级使用了

使用方式
  • provide是一个对象,或者是一个返回对象的函数。里面包含要给子孙后代的东西,也就是属性和属性值。注意:子孙层的provide会掩盖祖父层provide中相同key的属性值

  • inject一个字符串数组,或者是一个对象。属性值可以是一个对象,包含from和default默认值,from是在可用的注入内容中搜索用的 key (字符串或 Symbol),意思就是祖父多层provide提供了很多数据,from属性指定取哪一个key;default指定默认值。

在这里插入图片描述

中央事务总线EventBus

data为什么要用函数

在创建或注册模板的时候传入一个 data 属性作为用来绑定的数据。但是在组件中,data必须是一个函数,因为每一个 vue 组件都是一个 vue 实例,通过 new Vue() 实例化,引用同一个对象,如果 data 直接是一个对象的话,那么一旦修改其中一个组件的数据,其他组件相同数据就会被改变,而 data 是函数的话,每个 vue 组件的 data 都因为函数有了自己的作用域,互不干扰。

Object是引用数据类型,如果不用function返回,每个组件的data都是内存的同一个地址,一个数据改变了其他也改变了;

JavaScript只有函数构成作用域(注意理解作用域,只有函数{}构成作用域,对象的{}以及if(){}都不构成作用域),data是一个函数时,每个组件实例都有自己的作用域,每个实例相互独立,不会相互影响。

Vue-router

两种模式

深入理解前端中的 hash 和 history 路由

「前端进阶」彻底弄懂前端路由

hash模式

hash模式背后的原理是onhashchange事件,可以在window对象上监听这个事件;因为hash发生变化的url都会被浏览器记录下来,从而你会发现浏览器的前进后退都可以用了,同时点击后退时。这样一来,尽管浏览器没有请求服务器,但是页面状态和url一一关联起来,后来人们给它起了一个霸气的名字叫前端路由,成为了单页应用标配。

vue-router默认的是hash模式,使用URL的hash来模拟一个完整的URL,于是当URL改变的时候,页面不会重新加载,也就是单页应用了,当#后面的hash发生变化,不会导致浏览器向服务器发出请求,浏览器不发出请求就不会刷新页面,并且会触发hashChange这个事件,通过监听hash值的变化来实现更新页面部分内容的操作

对于hash模式会创建hashHistory对象,在访问不同的路由的时候,会发生两件事:

  1. HashHistory.push()将新的路由添加到浏览器访问的历史的栈顶
  2. HasHistory.replace()替换到当前栈顶的路由
    在这里插入图片描述
    在这里插入图片描述
  • url中的hash值只是客户端的一种状态,也就是说当向服务器发送请求时,hash部分不会被发送
  • hash值的改变,都会在浏览器的访问历史中增加一个记录,因此我们能通过浏览器的回退、前进按钮控制hash的切换;可以通过a标签,并设置href属性,当用户点击这个标签后,url的hash值会发生改变;或者使用JS来对location.hash进行赋值,改变url的hash值
  • 可以使用hashchange事件来监听hash值的变化,从而对页面进行跳转(渲染)
history模式

主要使用HTML5的pushState()replaceState()这两个api来实现的:

  • history.pushState() 和 history.replaceState() 均接收三个参数(state, title, url)
  • pushState()可以改变url地址且不会发送请求,在保留现有历史记录的同时,将 url 加入到历史记录中。
  • replaceState()可以读取历史记录栈,会将历史记录中的当前页面历史替换为 url。

如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。由于 history.pushState() 和 history.replaceState() 可以改变 url 同时,不会刷新页面,所以在 HTML5 中的 histroy 具备了实现前端路由的能力。
包括 back,forward,go三个方法,对应浏览器的前进,后退,跳转操作。

  • pushState和replaceState两个API来操作实现url的变化
  • 使用popState事件来监听url的变化,从而对页面进行跳转(渲染)
  • pushState和replaceState不会触发popState事件,这时我们需要手动触发页面跳转(渲染)

对于单页应用的 history 模式而言,url 的改变只能由下面四种方式引起:

  1. 点击浏览器的前进或后退按钮
  2. 点击 a 标签
  3. 在 JS 代码中触发 history.pushState 函数
  4. 在 JS 代码中触发 history.replaceState 函数

问题:
需要后台配置支持,如果后台没有正确的配置,当用户在访问某些url时会访问404;
可以在服务端增加一个覆盖所有情况的候选资源,如果url匹配不到任何静态资源,则应该返回同一个index.html页面,比如app的依赖页面

区别

  • 前面的hashchange,你只能改变#后面的url片段。而pushState设置的新URL可以是与当前URL同源的任意URL。

  • history模式则会将URL修改得就和正常请求后端的URL一样,如后端没有配置对应/user/id的路由处理,则会返回404错误

各自优势

  • history模式相比于直接修改 hash,存在以下优势:
  1. pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL
  2. pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中
  3. pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串
  4. pushState() 可额外设置 title 属性供后续使用。
  • hash 模式相比于 history 模式的优点:
  1. 兼容性更好,可以兼容到IE8
  2. 无需服务端配合处理非单页的url地址

综上所述,当我们不需要兼容老版本IE浏览器,并且可以控制服务端覆盖所有情况的候选资源时,我们可以愉快的使用 history 模式了。

Vue 路由钩子

用来在加载目标路由页面之前检验目标地址实际上是否存在。

Vue有几种不同的导航守卫,可以把它们看成是Vue的生命周期钩子,让我们可以在导航起效之前或之后执行一些代码。

  1. beforeEnter:单个路由钩子,路由独享的守卫

  2. beforeEach/afterEach/beforeResolve:全局钩子,可以使用 router.beforeEach 注册一个全局前置守卫,当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。

  • beforeResolve区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
  • afterEach不会接受 next 函数也不会改变导航本身
  1. beforeRouteEnter/beforeRouteUpdate/beforeRouteLeave:组件路由,在路由组件内直接定义以下路由导航守卫。
  • beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。**可以通过传一个回调给 next来访问组件实例。**在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
  • 对于 beforeRouteUpdate 和 beforeRouteLeave 来说,this 已经可用了
  • beforeRouteLeave通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。

每个守卫方法接收三个参数:

  • to: Route: 即将要进入的目标 路由对象
  • from: Route: 当前导航正要离开的路由
  • next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。

Vue插槽slot

我们经常需要向一个组件传递内容, Vue 自定义的 元素让这变得非常简单:只要在需要的地方加入插槽就行了
结合上面的例子来理解就是这样的:

  1. 父组件在引用子组件时希望向子组价传递模板内容<p>测试一下吧内容写在这里了能否显示</p>
  2. 子组件让父组件传过来的模板内容在所在的位置显示
  3. 子组件中的<slot>就是一个槽,可以接收父组件传过来的模板内容,<slot> 元素自身将被替换
  4. <myslot></myslot>组件没有包含一个 <slot> 元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃
  • 插槽的作用
    让用户可以拓展组件,去更好地复用组件和对其做定制化处理

keep-alive

keep-alive是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中;使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。 它提供了include与exclude属性,允许组件有条件地进行缓存,其中exclude的优先级比include高,max最多可以缓存多少组件实例。

用户在某个列表页面选择筛选条件过滤出一份数据列表,由列表页面进入数据详情页面,再返回该列表页面,我们希望:列表页面可以保留用户的筛选(或选中)状态。keep-alive就是用来解决这种场景。当然keep-alive不仅仅是能够保存页面/组件的状态这么简单,它还可以避免组件反复创建和渲染,有效提升系统性能。 总的来说,keep-alive用于保存组件的渲染状态。

  • keep-alive用法 在动态组件中的应用
<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
 <component :is="currentComponent"></component>
</keep-alive>

在vue-router中的应用

<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
 <router-view></router-view>
</keep-alive>
  • include 定义缓存白名单,keep-alive会缓存命中的组件;
  • exclude 定义缓存黑名单,被命中的组件将不会被缓存;
  • max 定义缓存组件上限,超出上限使用LRU的策略置换缓存数据。

对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。

在created钩子会创建一个cache对象,用来作为缓存容器,保存vnode节点。在需要重新渲染的时候再将vnode节点从cache对象中取出并渲染。在destroyed钩子则在组件被销毁的时候清除cache缓存中的所有组件实例。

Vue性能优化

编码优化

  1. 避免响应所有数据
    不要将所有的数据都放到data中,data中的数据都会增加getter和setter,并且会收集watcher,这样还占内存,不需要响应式的数据我们可以直接定义在实例上。
  2. 区分computed和watch使用场景
  3. v-for添加key(不要使用index),且避免同时使用v-if;每项元素绑定事件需要用事件代理,节约性能
  4. 区分v-if与v-show使用场景:v-if适合条件不太可能改变的情况,v-show适合条件频繁切换的情况。
  5. 长列表性能优化:可以通过Object.freeze方法来冻结一个对象,这样就不会增加getter和setter
  6. 仅渲染视窗可见的数据、使用防抖、节流进行优化,尽可能的少执行和不执行
  7. 路由懒加载
  8. 服务端渲染SSR
  9. 使用keep-alive组件

打包优化

  1. 配置splitChunksPlugins:专门用于提取多个Chunk中的公共部分的插件CommonsChunkPlugin,是用于提取公共代码的工具
  2. 使用treeShaking
  3. 第三方插件的按需引入:借助babel-plugin-component,然后可以只引入需要的组件,以达到减小项目体积的目的
  4. 使用 cdn 的方式加载第三方模块
  5. 多线程打包 happypack
  6. sourceMap 生成:在项目进行打包后,会将开发中的多个文件代码打包到一个文件中,并且经过压缩、去掉多余的空格、babel编译化后,最终将编译得到的代码会用于线上环境,那么这样处理后的代码和源代码会有很大的差别,当有bug的时候,我们只能定位到压缩处理后的代码位置,无法定位到开发环境中的代码,对于开发来说不好调式定位问题,因此sourceMap出现了,它就是为了解决不好调式代码问题的,在线上环境则需要关闭sourceMap。

缓存和压缩

  1. 客户端缓存、服务端缓存
  2. 服务端 gzip 压缩

React 和 Vue 的区别

在这里插入图片描述

  • 不同
  1. Vue 使用的是 web 开发者更熟悉的模板与特性,Vue的API跟传统web开发者熟悉的模板契合度更高,比如Vue的单文件组件是以模板+JavaScript+CSS的组合模式呈现,它跟web现有的HTML、JavaScript、CSS能够更好地配合。React 的特色在于函数式编程的理念和丰富的技术选型。

vue的主要特点:灵活易用的渐进式框架,进行数据拦截/代理,它对侦测数据的变化更敏感、更精确;Vue 推荐的做法是 template 的单文件组件格式(简单易懂,从传统前端转过来易于理解),即 html,css,JS 写在同一个文件(vue也支持JSX写法)

React推崇函数式编程(纯组件),数据不可变以及单向数据流,当然需要双向的地方也可以手动实现, 比如借助onChange和setState来实现;React推荐的做法是JSX + inline style, 也就是把 HTML 和 CSS 全都写进 JavaScript 中,即 all in js;

  1. 使用习惯和思维模式上考虑,对于一个没有任何Vue和React基础的web开发者来说, Vue会更友好,更符合他的思维模式。React对于拥有函数式编程背景的开发者以及一些并不是以web为主要开发平台的开发人员而言,React更容易接受

  2. 响应式原理
    实现上,Vue跟React的最大区别在于数据的reactivity,就是反应式系统上。Vue提供反应式的数据,当数据改动时,界面就会自动更新,而React里面需要调用方法SetState。我把两者分别称为Push-based和Pull-based。所谓Push-based就是说,改动数据之后,数据本身会把这个改动推送出去,告知渲染系统自动进行渲染。在React里面,它是一个Pull的形式,用户要给系统一个明确的信号说明现在需要重新渲染了,这个系统才会重新渲染。

Vue

  • Vue依赖收集,自动优化,数据可变。
  • Vue递归监听data的所有属性,直接修改。
  • 当数据改变时,自动找到引用组件重新渲染。

React
React基于状态机,手动优化,数据不可变,需要setState驱动新的state替换老的state当数据改变时,以组件为根目录,默认全部重新渲染, 所以 React 中会需要 shouldComponentUpdate 这个生命周期函数方法来进行控制

Vue源码编译过程图

在这里插入图片描述

React源码编译过程图

在这里插入图片描述

  • 相同:
    1… 数据驱动视图
    在jquery时代,我们需要频繁的操作DOM来实现页面效果与交互;而Vue和React 解决了这一痛点,采用数据驱动视图方式,隐藏操作DOM的频繁操作。所以我们在开发时,只需要关注数据变化即可,但是二者实现方式不尽相同。
  1. 使用 Virtual DOM.
    在这里插入图片描述

Vue与React都使用了 Virtual DOM + Diff算法, 不管是Vue的Template模板+options api 写法, 还是React的Class或者Function写法,最后都是生成render函数,而render函数执行返回VNode(虚拟DOM的数据结构,本质上是棵树)。

当每一次UI更新时,总会根据render重新生成最新的VNode,然后跟以前缓存起来老的VNode进行比对,再使用Diff算法(框架核心)去真正更新真实DOM(虚拟DOM是JS对象结构,同样在JS引擎中,而真实DOM在浏览器渲染引擎中,所以操作虚拟DOM比操作真实DOM开销要小的多)

  1. 组件化
    React与Vue都遵循组件化思想,它们把注意力放在UI层,将页面分成一些细块,这些块就是组件,组件之间的组合嵌套就形成最后的网页界面。

所以在开发时都有相同的套路,比如都有父子组件传递, 都有数据状态管理、前端路由、插槽等。

diff算法区别

关于 O(n³) 的由来。由于左树中任意节点都可能出现在右树,所以必须在对左树深度遍历的同时,对右树进行深度遍历,找到每个节点的对应关系,这里的时间复杂度是 O(n²),之后需要对树的各节点进行增删移的操作,这个过程简单可以理解为加了一层遍历循环,因此再乘一个 n。

vue和react的虚拟DOM的diff算法大致相同,其核心是基于两个简单的假设:

  1. 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构
  2. 同一层级的一组节点,他们可以通过唯一的id进行区分

(优化的)diff三点策略:

  • web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。
  • 拥有相同类型的两个组件将会生成相似的树形结构,拥有不同类型的两个组件将会生成不同树形结构。
  • 对于同一层级的一组自节点,他们可以通过唯一id进行区分。

即, 比较只会在同层级进行, 不会跨层级比较

只按层比较,就可以将时间复杂度降低为 O(n)。按层比较也不是广度遍历,其实就是判断某个节点的子元素间 diff,跨父节点的兄弟节点也不必比较

Vue 的 Dom diff

Vue diff的过程就是调用patch函数,就像打补丁一样修改真实dom

  • patchVnode
  • updateChildren

updateChildren是vue diff的核心
过程可以概括为:oldCh和newCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一个已经遍历完了,就会结束比较

Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。

在这里插入图片描述
Vue3.x借鉴了 ivi算法和 inferno算法。在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升。(实际的实现可以结合Vue3.x源码看

该算法中还运用了动态规划的思想求解最长递归子序列

React 的 Dom diff
  • tree diff
  • component diff
  • element diff
小结

相同点:
Vue和react的diff算法,都是不进行跨层级比较,只做同级比较。

不同点:

  1. vue比对节点,当节点元素类型相同,但是className不同,认为是不同类型元素,删除重建,而react会认为是同类型节点,只是修改节点属性
  2. vue的列表比对,采用从两端到中间的比对方式,旧集合和新集合两端各存在两个指针,两两进行比较,如果匹配上了就按照新集合去调整旧集合,每次对比结束后,指针向队列中间移动;
    react则采用从左到右依次比对的方式。当一个集合,只是把最后一个节点移动到了第一个,react会把前面的节点依次移动,利用元素的index和标识lastIndex进行比较,如果满足index < lastIndex就移动元素,删除和添加则各自按照规则调整;而vue只会把最后一个节点移动到第一个。

Vue的双端比较Diff算法相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅

组件通信的区别

在这里插入图片描述
Vue中有三种方式可以实现组件通信:父组件通过props向子组件传递数据或者回调,虽然可以传递回调,但是我们一般只传数据;子组件通过事件向父组件发送消息;通过V2.2.0中新增的provide/inject来实现父组件向子组件注入数据,可以跨越多个层级

React中也有对应的三种方式:父组件通过props可以向子组件传递数据或者回调可以通过 context 进行跨层级的通信,这其实和 provide/inject 起到的作用差不多。React 本身并不支持自定义事件,而Vue中子组件向父组件传递消息有两种方式:事件和回调函数,但Vue更倾向于使用事件。在React中我们都是使用回调函数的,这可能是他们二者最大的区别。

redux和vuex区别

redux流程图

在这里插入图片描述
整体流程为:Action Creator => action => store.dispatch(action) => reducer(state, action) => state = nextState

vuex流程图

在这里插入图片描述

核心概念对比

Redux 的组成

  • Store:存储应用的状态 – state 以及用于触发 state 更新的 dispatch 方法等,整个应用仅有单一的 Store。Store 中提供了几个管理 state 的 API:
    ** store.getState():获取当前 state
    ** store.dispatch(action):触发 state 改变(唯一途径)
    ** store.subscribe(listener):设置 state 变化的监听函数(若把视图更新函数作为 listener 传入,则可触发视图自动渲染)
  • Action:同 Flux ,Action 是用于更新 state 的消息对象,由 View 发出
    ** 有专门生成 Action 的 Action Creator,其本质上是一个返回 Action 对象的函数
  • Reducer:是一个根据 action.type 更新 state 并返回 nextState 替换原来的 state 的同步的纯函数(对于相同的参数返回相同的返回结果,不修改参数,不依赖外部变量)。即通过应用状态与 Action 推导出新的 state(previousState, action) => newState。Reducer 返回一个新的 state

Vuex 的核心概念

  • Store:Vuex 采用单一状态树每个应用仅有一个 Store 实例,在该实例下包含了 state, actions, mutations, getters, modules
  • State:Vuex 为单一数据源
    ** 可以通过 mapState 辅助函数将 state 作为计算属性访问,或者将通过 Storestate 注入全局之后使用 this.$store.state 访问
    ** State 更新视图是通过 vue 的双向绑定机制实现的
  • GetterGetter 的作用与 filters 有一些相似,可以将 State 进行过滤后输出
  • MutationMutaion 是 vuex 中改变 State 的唯一途径(严格模式下),并且只能是同步操作。Vuex 中通过 store.commit() 调用 Mutation
  • Action:一些对 State 的异步操作可以放在 Action 中,并通过在 Action 提交 Mutaion 变更状态
    ** Action 通过 store.dispatch() 方法触发
    ** 可以通过 mapActions 辅助函数将 vue 组件的 methods 映射成 store.dispatch 调用**(需要先在根节点注入 store)**
  • Module:当 Store 对象过于庞大时,可根据具体的业务需求分为多个 Module ,每个 Module 都具有自己的 state 、mutation 、action 、getter

从表面上来说,store 注入和使用方式有一些区别。
在 Vuex 中,$store 被直接注入到了组件实例中,因此可以比较灵活的使用:

  • 使用 dispatch 和 commit 提交更新
  • 通过 mapState 或者直接通过 this.$store 来读取数据

在 Redux 中,我们每一个组件都需要显式的用 connect 把需要的 props 和 dispatch 连接起来。
另外 Vuex 更加灵活一些,组件中既可以 dispatch action 也可以 commit updates,而 Redux 中只能进行 dispatch,并不能直接调用 reducer 进行修改。

从实现原理上来说,最大的区别是两点:

  • Redux使用的是不可变数据,而Vuex的数据是可变的,因此,Redux每次都是用新state替换旧state,而Vuex是直接修改
  • Redux在检测数据变化的时候,是通过diff的方式比较差异的,而Vuex其实和Vue的原理一样,是通过getter/setter来比较的

这两点的区别,也是因为React和Vue的设计理念不同。React更偏向于构建稳定大型的应用,非常的科班化。相比之下,Vue更偏向于简单迅速的解决问题,更灵活,不那么严格遵循条条框框。因此也会给人一种大型项目用React,小型项目用Vue的感觉。

react-router和vue-router区别

组件复用思路

在这里插入图片描述

论如何复用一个组件的逻辑

对比分析组件逻辑复用的三种方案

Mixin

HOC/高阶组件

Render Props/渲染属性/函数子组件

组件注入(Component Injection)

Hooks

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值