Vue的核心是什么
-
数据驱动
专注于View 层。它让开发者省去了操作DOM的过程,只需要改变数据。
-
组件响应原理
数据(model)改变驱动视图(view)自动更新
-
组件化
扩展HTML元素,封装可重用的代码。
MVC和MVVM的区别
MVC全名是 Model View Controller,时模型 - 视图 - 控制器的缩写,一种软件设计典范。
- Model(模型):是用于处理应用程序数据逻辑部分。通常模型对象负责在数据库中存取数据。
- View(视图):是应用程序中处理数据显示的本分。通常视图是依据模型数据创建的。
- Controller(控制器):是应用程序处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。
MVC的思想:一句话描述就是Controller负责将Model的数据用View显示出来,换句话说就是在Controller里面把Model的数据赋值给View。
MVVM Model-View-ViewModel
新增了VM类
ViewModel层:做了两件事达到了数据的双向绑定,一是将模型转化成视图(数据绑定,数据劫持),二是将视图转化为模型,即将所看到的页面转化为后端的数据,实现方式是DOM事件监听
MVVM和MVC最大的区别就是:实现了View和Model的自动同步,也就是当Model的属性改变时,我们不用再手动操作Dom元素来改变View的显示,而是改变属性后该属性对应的View层会自动改变。整体来看,MVVM比MVC精简很多,不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器选择DOM元素。因为在MVVM中,View不知道Model的存在,Model和ViewModel也察觉不到View,这种低耦合模式提高了代码的可重复性
!!!!Vue并未完全遵循MVVM的思想,严格的MVVM要求View不能和Model直接通信,而Vue提供了$refs这个属性,让Model可以直接操作View
为什么data是一个函数
组件的data写成一个函数,数据以函数返回值形式定义,这也每复用一次组件,就会返回一分新的的data,类似给每个组件实例创建了一个私有的数据空间,让各个组件实例维护各自的数据。如果写成对象形式,这就使得所有组件实例共用一份data,就会造成一个变了全都变得结果
new vue的时候发生了什么
Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等,最后初始化完成检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM;
Vue组件通信有哪些方式
1、props 和 emit触发事件来做到的。
2、children 获取单签组件的父组件和当前组件的子组件。
3、listeners A -> B -> C。Vue2.4开始提供了listeners来解决这个问题。
4、父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量。(官方不推荐在实际业务中适用,但是写组件库时很常用。)
5、$refs 获取组件实例。
6、envetBus 兄弟组件数据传递,这种情况下可以使用事件总线的方式。
7、vuex 状态管理。
Vue的生命周期方法有哪些?一般在哪一步发送请求?
beforeCreate 在实例初始化之后,此阶段为实例初始化之后,this指向创建的实例,此时数据观察事件机制都未形成,不能获得DOM节点,在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问。
created 实例已经创建完成之后被调用。在这一步,实例已经完成以下的配置:数据观测(data observe ),属性和方法的运算,watch/event 事件回调。这里没有 nextTick 来访问 DOM。
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。vue实例的$el和data都已初始化,此时为虚拟的DOM节点,data.message未替换(此阶段生成虚拟DOM)
mounted 在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom节点。
beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁 (patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
updated 发生在更新完成之后,当前阶段组件 Dom 已经完成更新。要注意的是避免在此期间更新数据,因为这个可能导致无限循环的更新,该钩子在服务器渲染期间不被调用。
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。我们可以在这时进行 善后收尾工作,比如清除定时器。
destroy 组件销毁之后调用,对data的改变不会再触发周期函数,vue实例已解除事件监听和DOM绑定,但DOM结构依然存在
activated keep-alive 专属,组件被激活时调用
deactivated keep-alive 专属,组件被销毁时调用
第一次页面加载时触发:beforeCreate、created、beforeMount、mounted
异步请求在哪一步发起?
可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data已经创建,可以将服务器端返回的数据进行赋值。
如果有DOM操作,那么肯定在mounted时候才能操作
如果异步请求不需要依赖 DOM 推荐加载 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面loading时间;
- ssr 不支持 beforeMount、mounted 钩子函数,所以放在 created 中有助于一致性。
Vue从后端获得的数据写在created里面比较好
- created是发生在mounted之前的,template还没有被渲染成html,但是在这个阶段已经可以获取到props和data了,所以在这一步进行数据赋值是比较恰当的。
- mounted就是页面已经渲染完成了,template已经被渲染成html,这个时候再进行数据请求,很有可能会造成页面闪屏。
$set的用法
受 ES5 的限制,Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue.js 转换它,才能让它是响应的。
要处理这种情况,我们可以使用$set()方法,既可以给data对象新增属性,又可以触发视图更新。
用法:this.$set(this.data,”key”,value’)
因为响应式数据 我们给对象和数组本身新增了ob属性,代表的是 Observer 实例。当给对象新增不存在的属性,首先会把新的属性进行响应式跟踪 然后会触发对象 ob 的dep收集到的 watcher 去更新,当修改数组索引时我们调用数组本身的 splice 方法去更新数组。
v-if和v-show的区别
v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。
v-show 会被编译成指令,条件不满足时控制样式将此节点隐藏(display:none)
扩展补充:display:none 、 visibility:hidden 和 opacity:0 之间的区别?
三者公共点都是隐藏。不同点:
- 一、是否占据空间。 display:none,隐藏之后不占位置;visibility:hidden、opacity:0,隐藏后任然占据位置。
- 二、子元素是否继承。 display:none --- 不会被子元素继承,父元素都不存在了,子元素也不会显示出来。 visibility:hidden --- 会被子元素继承,通过设置子元素 visibility:visible 来显示子元素。 opacity:0 --- 会被子元素继承,但是不能设置子元素 opacity:0 来先重新显示。
- 三、事件绑定。 display:none 的元素都已经不存在了,因此无法触发他绑定的事件。 visibility:hidden 不会触发他上面绑定的事件。 opacity:0 元素上面绑定的事件时可以触发的。
- 四、过度动画。 transition对于display是无效的。 transition对于visibility是无效的。 transition对于opacity是有效的。
说说vue内置指令
- v-once - 定义它的元素或组件只渲染一次,包括元素或组件的所有节点,首次渲染后,不再随数据的变化重新渲染,将被视为静态内容。
- v-cloak - 这个指令保持在元素上直到关联实例结束编译 -- 解决初始化慢到页面闪动的最佳实践。
- v-bind - 绑定属性,动态更新HTML元素上的属性。例如 v-bind:class。
- v-on - 用于监听DOM事件。例如 v-on:click v-on:keyup
- v-html - 赋值就是变量的innerHTML -- 注意防止xss攻击
- v-text - 更新元素的textContent
- v-model - 1、在普通标签。变成value和input的语法糖,并且会处理拼音输入法的问题。2、再组件上。也是处理value和input语法糖。
- v-if / v-else / v-else-if。可以配合template使用;在render函数里面就是三元表达式。
- v-show - 使用指令来实现 -- 最终会通过display来进行显示隐藏
- v-for - 循环指令编译出来的结果是 -L 代表渲染列表。优先级比v-if高最好不要一起使用,尽量使用计算属性去解决。注意增加唯一key值,不要使用index作为key。
- v-pre - 跳过这个元素以及子元素的编译过程,以此来加快整个项目的编译速度。
怎样理解Vue的单项数据流
数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会防止从子组件意外改变父组件的状态,从而导致你的应用的数据流向难以理解。
注意:在子组件直接用 v-model 绑定父组件传过来的 props 这样是不规范的写法,开发环境会报警告。
如果实在要改变父组件的 props 值可以再data里面定义一个变量,并用 prop 的值初始化它,之后用$emit 通知父组件去修改。
什么是SPA,优缺点是什么
SPA(single-page application),单页面应用,即一个web项目只有一个页面(即一个HTML文件,HTML内容的变换是利用路由机制实现的)
仅在Web页面初始化时加载相应的HTML、JavaScript和CSS,一旦页面加载完成,SPA不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现HTML内容的变换,UI与用户的的交互,避免页面的重新加载
优点:
- 用户体验好、块,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染
- SPA相对来说对服务器压力小
- 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理
缺点:
- 初次加载耗时多:为实现单页面web应用功能及显示效果,需要在加载页面的时候将js,css统一加载,部分页面按需加载
- 前进后退路由管理:由于单页面应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退,开发的时候肯定可以,但是单页面一般放在移动端在中,页面的切换需要自己简历堆栈管理
- SEO难度较大,由于所有的内容都在一个页面中动态替换显示,所以在SEO上其有着天然的弱势
computed和watch的区别和运用的场景
computed 是计算属性,依赖其它属性计算值,并且 computed 的值有缓存,只有计算值变化才会返回内容,他可以设置getter和setter。
watch 监听到值的变化就会执行回调,在回调中可以进行一系列的操作。
计算属性一般用在模板渲染中,某个值是依赖其它响应对象甚至是计算属性而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。
v-if和v-for为什么不建议一起使用
v-for和v-if不要在同一标签中使用,因为解析时先解析v-for在解析v-if。如果遇到需要同时使用时可以考虑写成计算属性的方式。
eg:
有一个数组,只渲染里面大于0的项,如果用v-for+v-if的话,v-for的优先级更高,会造成每渲染一个dom之前都要判断一次浪费性能,可以将其用cumpute来进行过滤
Vue3.0用过吗?了解多少?
- 响应式原理的改变 Vue3.x 使用 Proxy 取代 Vue2.x 版本的 Object.defineProperty。
- 组件选项声明方式 Vue3.x 使用 Composition API setup是Vue3.x新增的一个选项,他是组件内使用Composition API 的入口。
- 模板语法变化 slot 具名插槽语法,自定义指令v-model升级。
- 其他方面的更改 Suspense支持Fragment(多个根节点)和 Protal(在dom其他部分渲染组件内容)组件,针对一些特殊的场景做了处理。基于 treeShaking 优化,提供了更多的内置功能。
Vue3.0和2.0的响应式原理区别
Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达13种拦截方法。
虚拟DOM是什么?有什么优缺点?
由于在浏览器中操作DOM是很昂贵的。频繁操作DOM,会产生一定性能问题。这就是虚拟Dom的产生原因。Vue2的Virtual DOM 借鉴了开源库 snabbdom 的实现。Virtual DOM本质就是用一个原生的JS对象去描述一个DOM节点,是对真实DOM的一层抽象。
优点: 1、保证性能下限:框架的虚拟DOM需要适配任何上层API可能产生的操作,他的一些DOM操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的DOM操作性能要好很多,因此框架的虚拟DOM至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,既保证性能的下限。 2、无需手动操作DOM:我们不需手动去操作DOM,只需要写好 View-Model的 代码逻辑,框架会根据虚拟DOM和数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率。 3、跨平台:虚拟DOM本质上是JavaScript对象,而DOM与平台强相关,相比之下虚拟DOM可以进行更方便地跨平台操作,例如服务器端渲染、weex开发等等。 缺点: 1、无法进行极致优化:虽然虚拟DOM + 合理的优化,足以应对大部分应用的性能需要,但在一些性能要求极高的应用中虚拟DOM无法进行针对性的极致优化。 2、首次渲染大量DOM时,由于多了一层DOM计算,会比innerHTML插入慢。
Vue事件绑定原理
原生事件绑定是通过 addEventListener 绑定给真实元素的,组件事件绑定是通过Vue自定义的on、$emit 是基于发布订阅模式的,维护一个事件中心,on的时候将事件按名称存在事件中心里,称之为订阅者,然后emit将对应的事件进行发布,去执行事件中心里的对应的监听器。
vue-router路由钩子函数是什么?执行顺序是什么?
路由钩子的执行流程,钩子函数种类有:全局守卫、路由守卫、组件守卫
全局路由:前置钩子
const router = new VueRouter({ ... });
router.beforeEach((to, from, next) => {
// do someting
});
后置钩子,没有next
router.afterEach((to, from) => {
// do someting
});
单独路由守卫,在路由配置上定义,其参数的使用和全局的前置守卫是一样的
const router = new VueRouter({
routes: [
{
path: '/file',
component: File,
beforeEnter: (to, from ,next) => {
// do someting
}
}
]
});
组件内的守卫
组件内的导航钩子主要有这三种:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave。他们是直接在路由组件内部直接进行定义的
const File = {
template: `<div>This is file</div>`,
beforeRouteEnter(to, from, next) {
// do someting
// 在渲染该组件的对应路由被 confirm 前调用
},
beforeRouteUpdate(to, from, next) {
// do someting
// 在当前路由改变,但是依然渲染该组件是调用
},
beforeRouteLeave(to, from ,next) {
// do someting
// 导航离开该组件的对应路由时被调用
}
}
谈一下对vuex的个人理解
vuex 是专门为 vue 提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部内心原理是通过创造一个全局实例 new Vue)
主要包括以下几个模块:
- State:定义了应用状态的数据结构,可以在这里设置默认的初始化状态。
- Getter:允许组件从Store中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
- Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
- Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步请求。
- Module:允许将单一的 Store 拆分更多个 store 且同时保存在单一的状态树中。
vuex页面刷新数据丢失怎么解决
需要做 vuex 数据持久化,一般使用本地储存的方案来保存数据,可以自己设计存储方案,也可以使用第三方插件。 推荐使用 vuex-persist 插件,它是为 Vuex 持久化储存而生的一个插件。不需要你手动存取 storage,而是直接将状态保存至 cookie 或者 localStorage中。
vuex为什么要分模块并且加命名空间?
模块: 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能会变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。
命名空间: 默认情况下,模块内部的 action、mutation、getter是注册在全局命名空间的 --- 这样使得多个模块能够对同一 mutation 或 action 做出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced:true 的方式使其成为带命名的模块。当模块被注册后,他所有 getter、action、及 mutation 都会自动根据模块注册的路径调整命名。
使用过Vue SSR吗?说说SSR
SSR 也就是服务端渲染,也就是将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端完成,然后再把 html 直接返回给客户端。
优点: SSR 有着更好的 SEO、并且首屏加载速度更快。 缺点: 开发条件会受限制,服务器端渲染只支持 beforeCreate 和 created 两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境。 服务器会有更大的负载需求
Vue中使用了哪些设计模式
1、工厂模式 - 传入参数即可创建实例 虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode。
eg
//通过工厂函数动态创建对象并返回。返回一个对象的函数就是工厂函数
funtion createPerson(name, age){
var obj = {name: name, age: age, setName: funtion(){}}
return obj
}
var p = createPerson('tom', 12)
2、单例模式 - 整个程序有且仅有一个实例 vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉。
3、发布-订阅模式。(vue 事件机制)
4、观察者模式。(响应式数据原理)
5、装饰器模式(@装饰器的用法)
6、策略模式,策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案 - 比如选项的合并策略
你都做过哪些Vue的性能优化?
这里只列举针对 Vue 的性能优化,整个项目的性能优化是一个大工程。
- 对象层级不要过深,否则性能就会差。
- 不需要响应式的数据不要放在 data 中(可以使用 Object.freeze() 冻结数据)www.jianshu.com/p/a4c85bc51…
- v-if 和 v-show 区分使用场景
- computed 和 watch 区分场景使用
- v-for 遍历必须加 key,key最好是id值,且避免同时使用 v-if
- 大数据列表和表格性能优化 - 虚拟列表 / 虚拟表格
- 防止内部泄露,组件销毁后把全局变量和时间销毁
- 图片懒加载
- 路由懒加载
- 异步路由
- 第三方插件的按需加载
- 适当采用 keep-alive 缓存组件
- 防抖、节流的运用
- 服务端渲染 SSR or 预渲染
nextTick的使用场景和原理
nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法
考虑兼容问题,vue 做了 microtask 向 macrotask 的降级方案
keep-alive使用场景和原理
keep-alive 是 Vue 内置的一个组件,可以实现组件缓存,当组件切换时不会对当前组件进行卸载。
- 常用的两个属性 include/exclude,允许组件有条件的进行缓存。
- 两个生命周期 activated/deactivated,用来得知当前组件是否处理活跃状态。
- keep-alive 运用了 LRU 算法,选择最近最久未使用的组件予以淘汰。
扩展补充:LRU 算法是什么?
一个队列,将数据从尾部插入到this.keys中
每当缓存命中,则将数据移动到this.keys的尾部
当this.keys满的时候将头部的数据丢弃
Vue修饰符
事件修饰符
- .stop 阻止事件继续传播
- .prevent 阻止标签默认行为
- .capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理
- .self 只当在 event.target 是当前元素自身时触发处理函数
- .once 事件只会触发一次
- .passive 告诉浏览器你不想阻止事件的默认行为
Vue.set方法原理
了解 Vue 响应式原理的同学都知道在两种情况下修改 Vue 是不会触发视图更新的。 1、在实例创建之后添加新的属性到实例上(给响应式对象新增属性) 2、直接更改数组下标来修改数组的值。
Vue.set 或者说是 $set 原理如下
因为响应式数据 我们给对象和数组本身新增了ob属性,代表的是 Observer 实例。当给对象新增不存在的属性,首先会把新的属性进行响应式跟踪 然后会触发对象 ob 的dep收集到的 watcher 去更新,当修改数组索引时我们调用数组本身的 splice 方法去更新数组。
v-model的修饰符
- .lazy 通过这个修饰符,转变为在 change 事件再同步
- .number 自动将用户输入值转化为数值类型
- .trim 自动过滤用户输入的收尾空格
Vue命名插槽
ListCard.vue
<template>
<swiper>
<swiper-slide v-for="(category,i) in categories" :key="i">
<slot name="items" :category="category"></slot>
</swiper-slide>
</swiper>
</template>
向组件外暴露了category变量,在父组件中可以使用暴露的变量,父组件中我们将ListCard命名为m-list-card
父组件
<template>
<m-list-card>
<template #items="{category}">
<router-link v-for="(news,index) in category.newsList" :key="index">
</router-link>
</template>
</m-list-card>
</template>
父组件可以使用子组件暴露出来的数据,然后在父组件中定义这些数据的显示方式
为什么要用这个,比如在项目中,我们需要多次复用这个组件,但是组件中数据格式相似呈现的格式不一样,我们就需要通过这个命名插槽来暴露组件中传入的数据
键盘修饰符
- .enter
- .tab
- .delete (捕获“删除”和“退格”键)
- .esc
- .space
- .up
- .down
- .left
- .right
监听图片加载完成
原生js监听:img.onload=function(){}
vue中监听图片:<img @load='方法名称'>
所有组件都有一个属性$el:用于获取组件中的元素
this.el.offsetTop(tabControl是一个组件对象)
能说一下vue-router中常用的路由模式和实现原理吗
hash 模式
1、location.hash 的值实际就是 URL 中 # 后面的东西。它的特点在于:hash虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。
2、可以为 hash 的改变添加监听事件 window.addEventListener("hashchange",funcRef,false) 每一次改变 hash (window.location.hash),都会在浏览器的访问历史中增加一个记录,利用hash的以上特点,就可以实现前端路由“更新视图但不重新请求页面”的功能了 特点:兼容性好但是不美观
history 模式
利用 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。
这两个方法应用于浏览器的历史记录站,在当前已有的 back、forward、go 的基础上,他们提供了对历史记录进行修改的功能。这两个方法有个共同点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页面应用前端路由“更新视图但不重新请求页面”提供了基础
触发场景:项目打包之后前端资源部署到了5000端口下,直接允许5000端口,会找到index.html,这个index是vue项目最终打包之后生成的首页,然后点击index.html进行相应跳转,注意:这里跳转并未触发对5000端口的请求,只是根据前端路由进行页面的跳转。比如路径变成了localhost:5000/user/info/1,直接点击这个是静态的跳转,如果一刷新,GET404,由于history会将整个地址栏的信息发往服务器去请求这个资源。。。。根本就没有这个资源,所以hitstory模式需要后端进行配合,后端对于前端传过来的请求进行解析,区分哪些路由是前端路由哪些是后端路由
后端如何实现:正则表达式验证/借助第三方插件/nginx反向代理
特点:虽然美观,但是刷新会出现 404 需要后端进行配置。
Vue中的虚拟DOM树
虚拟DOM树本质上就是一个普通的js对象,它是用来描述一段HTML片段的
01 当页面渲染的时候Vue会创建一颗虚拟DOM树
02 当页面发生改变Vue会再创建一颗新的虚拟DOM树
03 diff算法:前后两颗新旧虚拟DOM树进行对比,Vue通过diff算法,去记录差异的地方
04 patch算法:将有差异的地方更新到真实的DOM树中
虚拟DOM树的作用:
vue中的虚拟DOM树只会重新渲染页面修改的地方,大大减少了对真实DOM树的操作。
diff算法了解吗
diff算法采用同级比较。
-
1、tag 标签不一致直接新节点替换旧节点。
-
2、tag 标签一样。
先替换属性
对比子元素
-
新老都有子元素,采用双指针方式进行对比
sameVnode 判断tag和key完全相同为同一节点,进行节点复用
指针比较过程:
头和头相等对比
尾和尾相等对比
头和尾相等对比
详细对比过程
老节点数组:oldStartIndex(指向老节点数组头部) oldEndIndex 指向老节点数组尾部
新节点数组: newStartIndex newEndIndex
循环 :
执行上面指针比较过程,End指针如果匹配到了就前移,Start指针匹配到了就后移,然后执行下一轮循环
如果newEndIndex指向的元素在旧节点数组中找不到,那么dom队列新增一个,然后当oldStartIndex<oldEndIndex的时候或者newStartIndex<newEndIndex的时候退出循环
这也是为什么列表要加上key了,通过key可以直接通过key去比较两个元素看看两个元素是否值得比较,如果用index的话,比如删除了队首的一个元素,新元素队列全部都要渲染,就因为key对应的元素不一样,而其实改动的只有一个
sameVnode 的时候传入两个新老子节点patch(oldChild,newChild)
乱序情况 -- 上面的都不符合,先遍历旧子节点数组形成 key值映射的map对象。
然后根据新子节点数组循环 按照key值和位置关系移动以及新增节点 最后删除多余的旧子节点 如果移动旧节点同样需要patch(oldChild,newChild)
-
新的有子元素,老的没有子元素。-- 直接将子元素虚拟节点转化成真实节点插入即可。
-
新的没有子元素,老的有子元素。 -- 直接清空 innerHtml
-
-
3、无 tag 标签 -- 文本节点直接比较内容是否一致
vue双向数据绑定原理
reduce
应用场景:下次操作的初始值,依赖于上一次操作的返回值
1.数组的累加计算
const newarr = arr.reduce((pre,cur)=>{return pre+cur},0)
2.链式获取对象属性的值
const obj = {
name:'zs',
info:{
address:{
location:'北京'
}
}
}
const arrs = ['info','address','location']
const zs_location = arrs.reduce((pre,cur)=>{return pre[cur]},obj)
console.log(zs_location)
升级操作
const obj = {
name:'zs',
info:{
address:{
location:'北京'
}
}
}
const arrStr = 'info.address.location'
const zs_location = arrStr.split('.').reduce((pre,cur)=>pre[cur],obj)
console.log(zs_location)
watch 监听对象的时候,不能直接使用变量,要加上引号
'testdata.count'(){
console.log(this.testdata.count)
this.$store.dispatch('SetCount',this.testdata.count)
this.$store.dispatch('PrintCount')
},
发布订阅模式
1.Dep类:负责进行依赖收集
- 首先,有一个数组,专门来存放所有的订阅信息
- 其次,还要提供一个向数组中追加订阅信息的方法
- 然后,还要提供一个循环,循环触发数组中的每个订阅信息
2.Watch类:负责订阅一些事件
dom相当于订阅者,当数据发生改变的时候,发布者会将消息发布给订阅者,订阅者拿到消息之后进行重新渲染
Object.defineProperty
const obj = {
name:'zs'
}
Object.defineProperty(obj,'name',{
enumerable:true,//当前属性允许被循环
configurable:true,//当前属性,允许被配置 delete
get(){
return '我不是zs'
},
set(newVal){
console.log(newVal)
}
})
console.log(obj.name)//我不是zs,说明结果被拦截了,访问name会被get所拦截
obj.name='ls'//给name赋值的时候会被set所拦截
需要3个类
class Vue{}//通过实例化Vue创建vm对象
class Dep{}//依赖收集的类/收集watcher订阅者的类
class Watcher{}//订阅者的类
两个方法
function Observe(obj){}//定义一个数据劫持的方法
function Compile(el,vm){}//对HTML结构进行模板编译的方法
双向绑定实现:单向数据绑定(能够将vm实例中数据渲染到页面),单向数据动态绑定(更改vm实例中数据会重新渲染对应页面),双向数据(在单项数据绑定的基础上,实现修改页面对应vm的数据能够影响vm实例中的数据)
首先我们来考虑如何实现将vm实例中的数据渲染到页面中,像vue那样,在模板表达式中使用obj.name就可以访问vm实例中的数据,那我们就需要一个模板编译函数,来实现vm实例中数据到页面的渲染
function Compile(el, vm) {
// 获取 el 对应的 DOM 元素
vm.$el = document.querySelector(el)
// 创建文档碎片,提高 DOM 操作的性能
const fragment = document.createDocumentFragment()
while ((childNode = vm.$el.firstChild)) {
fragment.appendChild(childNode)
}
// 进行模板编译
replace(fragment)
vm.$el.appendChild(fragment)
// 负责对 DOM 模板进行编译的方法
function replace(node) {
// 定义匹配插值表达式的正则
const regMustache = /{{\s*(\S+)\s*}}/
// 证明当前的 node 节点是一个文本子节点,需要进行正则的替换
if (node.nodeType === 3) {
// 注意:文本子节点,也是一个 DOM 对象,如果要获取文本子节点的字符串内容,需要调用 textContent 属性获取
const text = node.textContent
// 进行字符串的正则匹配与提取
const execResult = regMustache.exec(text)
console.log(execResult)
if (execResult) {
const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
node.textContent = text.replace(regMustache, value)
}
// 终止递归的条件
return
}
}
// 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
node.childNodes.forEach((child) => replace(child))
}
}
但是这样我们只能第一次刷新页面的时候才可以使用vm中的数据,后面我们修改vm实例中数据,页面并不会变化,也就是说我们只是实现了单项数据的渲染,但是没有动态的进行绑定,如何绑定喃?
这个时候就需要使用到上面所说的订阅发布机制了,当我们创建一个具有数据的DOM的时候,DOM对象可以理解为一个订阅者,我将我的信息存到发布者那里,当发布者监听到DOM对应的vm数据发生变化的时候就发送信息告诉DOM进行页面重新渲染,所以我们在创建一个DOM的时候就要创建一个Watcher类(订阅者类,里面需要拿到对应的vm、自己dom对应的数据key,以及如何更新的回调函数),每次创建一个DOM的时候就实例化一个Watcher类,并将其存储在发布者类里面,所以我们还需要有一个依赖收集类Dep类,里面存放着订阅的信息,这个类的功能:存放订阅信息(watcher实例)、一个添加watcher实例的方法、一个通知每个watcher进行更新的方法,这么看好像Dep要简单点,所以我们首先来实现Dep类 Dep
// 依赖收集的类/收集 watcher 订阅者的类
class Dep {
constructor() {
// 今后,所有的 watcher 都要存到这个数组中
this.subs = []
}
// 向 subs 数组中,添加 watcher 的方法
addSub(watcher) {
this.subs.push(watcher)
}
// 负责通知每个 watcher 的方法
notify() {
this.subs.forEach((watcher) => watcher.update())
}
}
为了方便起见,我们没有做到非常详细只更新某一部分,而是只要数据有更新我们就更新全部,中心在理解数据绑定上面 接下来是Watcher类
// 订阅者的类
class Watcher {
// cb 回调函数中,记录着当前 Watcher 如何更新自己的文本内容
// 但是,只知道如何更新自己还不行,还必须拿到最新的数据,
// 因此,还需要在 new Watcher 期间,把 vm 也传递进来(因为 vm 中保存着最新的数据)
// 除此之外,还需要知道,在 vm 身上众多的数据中,哪个数据,才是当前自己所需要的数据,
// 因此,必须在 new Watcher 期间,指定 watcher 对应的数据的名字
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
// ↓↓↓↓↓↓ 下面三行代码,负责把创建的 Watcher 实例存到 Dep 实例的 subs 数组中 ↓↓↓↓↓↓
Dep.target = this
key.split('.').reduce((newObj, k) => newObj[k], vm)
Dep.target = null
}
// watcher 的实例,需要有 update 函数,从而让发布者能够通知我们进行更新!
update() {
const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
this.cb(value)
}
}
同时我们还要有一个数据劫持的方法,我们需要去劫持vm实例上data数据,比如某个数据访问的时候我们该做什么操作,修改某个数据的时候该进行什么操作,递归给data中每一个数据都进行数据劫持,我们编译类中只要使用到了vm中的数据,对应操作为get,那么我们就需要实例化一个watcher类,然后存在Dep里面,一旦vm数据发生变化,即触发set,然后dep通知watcher进行更新
// 定义一个数据劫持的方法
function Observe(obj) {
// 这是递归的终止条件
if (!obj || typeof obj !== 'object') return
const dep = new Dep()
// 通过 Object.keys(obj) 获取到当前 obj 上的每个属性
Object.keys(obj).forEach((key) => {
// 当前被循环的 key 所对应的属性值
let value = obj[key]
// 把 value 这个子节点,进行递归
Observe(value)
// 需要为当前的 key 所对应的属性,添加 getter 和 setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 只要执行了下面这一行,那么刚才 new 的 Watcher 实例,
// 就被放到了 dep.subs 这个数组中了
Dep.target && dep.addSub(Dep.target)
return value
},
set(newVal) {
value = newVal
Observe(value)
// 通知每一个订阅者更新自己的文本
dep.notify()
},
})
})
}
当前还只是实现了单项数据绑定,即vm中data数据变化会引发页面中对应dom重新渲染,但是我们还没有实现双向数据绑定,双向数据绑定其实还较为简单,在编译方法中进行修改,在具有v-model属性的文本框上添加监听函数,即node.addEventListener,数据变化的时候把最新的数据更新到vm上即可
// 对 HTML 结构进行模板编译的方法
function Compile(el, vm) {
// 获取 el 对应的 DOM 元素
vm.$el = document.querySelector(el)
// 创建文档碎片,提高 DOM 操作的性能
const fragment = document.createDocumentFragment()
while ((childNode = vm.$el.firstChild)) {
fragment.appendChild(childNode)
}
// 进行模板编译
replace(fragment)
vm.$el.appendChild(fragment)
// 负责对 DOM 模板进行编译的方法
function replace(node) {
// 定义匹配插值表达式的正则
const regMustache = /{{\s*(\S+)\s*}}/
// 证明当前的 node 节点是一个文本子节点,需要进行正则的替换
if (node.nodeType === 3) {
// 注意:文本子节点,也是一个 DOM 对象,如果要获取文本子节点的字符串内容,需要调用 textContent 属性获取
const text = node.textContent
// 进行字符串的正则匹配与提取
const execResult = regMustache.exec(text)
console.log(execResult)
if (execResult) {
const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
node.textContent = text.replace(regMustache, value)
// 在这个时候,创建 Watcher 类的实例
new Watcher(vm, execResult[1], (newValue) => {
node.textContent = text.replace(regMustache, newValue)
})
}
// 终止递归的条件
return
}
// 判断当前的 node 节点是否为 input 输入框
if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
// 得到当前元素的所有属性节点
const attrs = Array.from(node.attributes)
const findResult = attrs.find((x) => x.name === 'v-model')
if (findResult) {
// 获取到当前 v-model 属性的值 v-model="name" v-model="info.a"
const expStr = findResult.value
const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)
node.value = value
// 创建 Watcher 的实例
new Watcher(vm, expStr, (newValue) => {
node.value = newValue
})
// 监听文本框的 input 输入事件,拿到文本框最新的值,把最新的值,更新到 vm 上即可
node.addEventListener('input', (e) => {
const keyArr = expStr.split('.')
const obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, k) => newObj[k], vm)
const leafKey = keyArr[keyArr.length - 1]
obj[leafKey] = e.target.value
})
}
}
// 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
node.childNodes.forEach((child) => replace(child))
}
}
完整代码
<div id="app">
{{name}}
<input type="text" v-model='name'>
</div>
<script>
const vm = new Vue({
el:'#app',
data:{
name:'zs',
age:20,
info:{
a:'a1'
}
}
})
</script>
vue.js
class Vue {
constructor(options) {
this.$data = options.data
// 调用数据劫持的方法
Observe(this.$data)
// 属性代理
Object.keys(this.$data).forEach((key) => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return this.$data[key]
},
set(newValue) {
this.$data[key] = newValue
},
})
})
// 调用模板编译的函数
Compile(options.el, this)
}
}
// 定义一个数据劫持的方法
function Observe(obj) {
// 这是递归的终止条件
if (!obj || typeof obj !== 'object') return
const dep = new Dep()
// 通过 Object.keys(obj) 获取到当前 obj 上的每个属性
Object.keys(obj).forEach((key) => {
// 当前被循环的 key 所对应的属性值
let value = obj[key]
// 把 value 这个子节点,进行递归
Observe(value)
// 需要为当前的 key 所对应的属性,添加 getter 和 setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 只要执行了下面这一行,那么刚才 new 的 Watcher 实例,
// 就被放到了 dep.subs 这个数组中了
Dep.target && dep.addSub(Dep.target)
return value
},
set(newVal) {
value = newVal
Observe(value)
// 通知每一个订阅者更新自己的文本
dep.notify()
},
})
})
}
// 对 HTML 结构进行模板编译的方法
function Compile(el, vm) {
// 获取 el 对应的 DOM 元素
vm.$el = document.querySelector(el)
// 创建文档碎片,提高 DOM 操作的性能
const fragment = document.createDocumentFragment()
while ((childNode = vm.$el.firstChild)) {
fragment.appendChild(childNode)
}
// 进行模板编译
replace(fragment)
vm.$el.appendChild(fragment)
// 负责对 DOM 模板进行编译的方法
function replace(node) {
// 定义匹配插值表达式的正则
const regMustache = /{{\s*(\S+)\s*}}/
// 证明当前的 node 节点是一个文本子节点,需要进行正则的替换
if (node.nodeType === 3) {
// 注意:文本子节点,也是一个 DOM 对象,如果要获取文本子节点的字符串内容,需要调用 textContent 属性获取
const text = node.textContent
// 进行字符串的正则匹配与提取
const execResult = regMustache.exec(text)
console.log(execResult)
if (execResult) {
const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
node.textContent = text.replace(regMustache, value)
// 在这个时候,创建 Watcher 类的实例
new Watcher(vm, execResult[1], (newValue) => {
node.textContent = text.replace(regMustache, newValue)
})
}
// 终止递归的条件
return
}
// 判断当前的 node 节点是否为 input 输入框
if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
// 得到当前元素的所有属性节点
const attrs = Array.from(node.attributes)
const findResult = attrs.find((x) => x.name === 'v-model')
if (findResult) {
// 获取到当前 v-model 属性的值 v-model="name" v-model="info.a"
const expStr = findResult.value
const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)
node.value = value
// 创建 Watcher 的实例
new Watcher(vm, expStr, (newValue) => {
node.value = newValue
})
// 监听文本框的 input 输入事件,拿到文本框最新的值,把最新的值,更新到 vm 上即可
node.addEventListener('input', (e) => {
const keyArr = expStr.split('.')
const obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, k) => newObj[k], vm)
const leafKey = keyArr[keyArr.length - 1]
obj[leafKey] = e.target.value
})
}
}
// 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
node.childNodes.forEach((child) => replace(child))
}
}
// 依赖收集的类/收集 watcher 订阅者的类
class Dep {
constructor() {
// 今后,所有的 watcher 都要存到这个数组中
this.subs = []
}
// 向 subs 数组中,添加 watcher 的方法
addSub(watcher) {
this.subs.push(watcher)
}
// 负责通知每个 watcher 的方法
notify() {
this.subs.forEach((watcher) => watcher.update())
}
}
// 订阅者的类
class Watcher {
// cb 回调函数中,记录着当前 Watcher 如何更新自己的文本内容
// 但是,只知道如何更新自己还不行,还必须拿到最新的数据,
// 因此,还需要在 new Watcher 期间,把 vm 也传递进来(因为 vm 中保存着最新的数据)
// 除此之外,还需要知道,在 vm 身上众多的数据中,哪个数据,才是当前自己所需要的数据,
// 因此,必须在 new Watcher 期间,指定 watcher 对应的数据的名字
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
// ↓↓↓↓↓↓ 下面三行代码,负责把创建的 Watcher 实例存到 Dep 实例的 subs 数组中 ↓↓↓↓↓↓
Dep.target = this
key.split('.').reduce((newObj, k) => newObj[k], vm)
Dep.target = null
}
// watcher 的实例,需要有 update 函数,从而让发布者能够通知我们进行更新!
update() {
const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
this.cb(value)
}
}
伪代码
//vue数据双向绑定伪代码
class Vue{
constructor(){
//定义data,存放vm数据
//调用Observe函数对vm数据进行劫持
//调用Compile函数对vm数据进行编译
}
}
class Dep{
constructor(){
//定义一个数组,存放watcher
this.subs = []
}
//定义一个向数组中添加watcher的方法
addSub(watcher){}
notify(){//通知自己数组中watcher去更新
//调用每个watcher的update进行更新
}
}
class Watcher{
constructor(vm,key,cb){//知道数据从哪拿,拿那个,怎么更新
//创建一个Watcher实例,就应该将其放入Dep中,所以应该在构造函数中将这个实例加入到Dep中
}
//需要有一个update函数,方便让发布者能够在通知函数中调用这个函数来对其进行更新
update(){
}
}
function Oberve(obj){
//递归给传入的对象中每一个属性都进行数据劫持
//通过Object.defineProperty来进行实现
Object.defineProperty(obj,{
get(){
//数据被访问的时候即创建一个watcher实例放入Dep的数组中
},
set(){
//数据被修改的时候新值赋给vm,发布修改信息,通知订阅者进行修改
dep.notify()
}
})
}
function Compile(el,vm){//将el与vm进行对应,渲染vm数据到el上
//一旦创建一个带数据的dom就实例化一个watcher
//如果是input输入框,添加addEventListener,监听其变化,如果有变化就将新数据重新赋给vm
}