谈谈你对vue的理解
一套构建用户界面的 渐进式框架,vue的核心库只关注视图层。 渐进式框架: 声明式渲染 组件系统 客户端路由 大规模状态管理 构建工具命令式和声明式的区别 越接近现实的表达就越“声明式”,越接近计算机的执行过程就越“命令式”。
声明式:这种风格强调程序应该描述做什么,而不是如何去做。好的命名能够提高代码的可读性
模板变函数性能不高,一个负责编译 render函数(打包),运行时调用函数生成虚拟节点。
- 声明式框架
- MVVM 模式
- 采用虚拟 DOM
- 区分编译时(打包)和运行时(浏览器)
- 组件化
总结: vue是一个渐进式框架,主要核心是命名式渲染,特点是简单方便。 而且数据绑定采用的是mvvm模式,但不是完全的mvvm模式,只是借鉴了它里面的一些思想,实现了这样的功能。MVVM是简化MVC的。 vue采用的是模板的语法,最后编译成虚拟DOM,虚拟DOM在vue中的角色,一是实现跨平台,二是减少操作真实dom的频率, 用diff算法比较差异,最终去更新。 同时view还区分了编译时和运行时。 vue支持组件化,组件化里面有一个叫组件级更新。
谈谈你对 SPA 的理解
- 基本概念 SPA 单页应用。默认情况下vue 只有一个html页面,并提供一个挂载点,由此页面引入对应的资源(页面的渲染全部是由js动态进行渲染的)。切换页面时,通过监听路由变化,渲染对应的页面。Client Side Rendering MPA 多页应用。多个html服务端返回完整的html,同时数据也可以再后端进行获取一并返回“模板引擎”。多页应用跳转要整页刷新。 Server Side Rendering-
优缺点 刷新方式 局部刷新 整页刷新 SEO搜索引擎优化 无法实现 容易实现 页面切换 速度快,用户体验号(服务端压力小) 切换加载资源,速度慢,用户体验差 维护成本 相对容易 相对复杂 首次渲染 较慢(第一次返回html,需要再次请求首屏数据)白屏时间长
-
解决方案 -静态页面预渲染Static Site Generation,在构建时生成完整的html页面(打包后,将页面放到浏览器中运行一下,将html保存起来)。仅适和静态页面网站,变化率不高的网站。 SSR+CSR,首屏采用服务端渲染的方式,后续交互采用客户单渲染方式。Nuxtjs
Vue 为什么需要使用 虚拟 DOM
1.1 基本概念 - virtual DOM 使用js对象来描述真实DOM,是对真实DOM的抽象,由于直接操作DOM性能低但是js层的操作效率高,可以将DOM操作转化成对象操作,最终通过diff算法对比差异进行更新DOM(减少了对真实DOM的操作)。 - 虚拟DOM 不依赖真实平台环境从而可以实现跨平台。1.2 补充: VDOM 是如何生成的 初渲染
- 在vue中我们常常在组件中编写模板 -template
- 这个模板会被编译为渲染函数 -render
- 在接下来的挂载过程中会调用render函数,返回的对象就是虚拟dom
- 会在后续的patch过程中进一步转化为真实dom
1.3 再次补充: VDOM 如何做 diff
- 挂载结束后,会记录第一次生成的 vdom-oldVnode
- 当响应式数据发生变化时,将会引起组件重新render,此时就会生成新的vdom-newVnode
- 使用oldVnode与newVnode做diff操作,将更改的部分应用到真实DOM上,从而转换为最小量的dom操作,高效更新视图。
谈一谈对 vue 组件化的理解
优点:高内聚、可重用、可组合 组件化开发能大幅度提高应用开发效率、测试性、复用性等; 降低更新范围,只重新渲染变化的组件;vue中的每个组件都有一个渲染函数watcher、effect 数据是响应式的,数据变化后会执行watcher或者effect 组件要合理的划分,如果不拆分组件,那更新的时候整个页面都要重新更新 如果过分的拆分组件会导致watcher、effect产生过多也会造成性能浪费
组件化是对ui的封装 模块化是业务逻辑的封装。 组件核心组成:模板、属性、事件、生命周期(暴露一些钩子)、插槽
既然 Vue 通过数据劫持可以精准探测数据变化,为什么还要虚拟 DOM 进行 diff 检测差异
Vue 内部设计原因导致,vue设计的是每个组件一个watcher(渲染watcher),没有采用一个属性对应一个watcher。这样会导致大量watcher的产生而浪费内存,如果粒度过低也无法精准检查变化。所以采用 diff算法 + 组件级watcher。是个折中的方案。
当Vue程序初始化的时候就会对数据data进行依赖的收集,一但数据发生变化,响应式系统就会立刻得知。因此Vue是一开始就明确知道在哪发生变化了。 每个数据在使用的时候都需要一个更新函数。在表单页面上有100个数据,就需要给100个数据都一一对应一个更新函数。就有100个watcher。
响应式数据的理解
vue2源码 https://github1s.com/vuejs/vue/blob/HEAD/src/core/observer/index.tsvue3源码 github1s.com/vuejs/core/…
1.1 如何实现响应式数据 数组和对象类型当值变化时如何劫持到。对象内部通过 defineReactive 方法,使用 Object.defineProperty 将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。多层对象是通过递归来实现劫持。vue3则采用proxy
1.2 vue2处理缺陷
- 性能差
- 新增属性、删除属性无法监控变化。通过delete实现
- 需要对数组单独处理
- 不支持 ES6 中新产生的Map、Set
Vue 如何检测数组变化
考虑性能,重写数组方法 push shift unshift pop reverse splice sort; 数组中如果是对象类型也会进行递归劫持; 缺点:数组的索引和长度变化时无法监控到的;Vue 中如何进行依赖收集
1.1 依赖收集流程 - 每个属性都会拥有自己的dep属性,存放在他所依赖的watcher,当属性变化后会通知自己对应的watcher去更新 - 默认在初始化时会调用render函数,此时会触发属性依赖收集 dep.depend - 当属性发生修改时会触发 watcher 更新 dep.notify()1.2 vue3 依赖收集
- vue3中会通过Map结构将属性和 effect 映射起来
- 默认在初始化时会调用render函数,此时会触发属性依赖收集track,
- 当属性发生修改时,会找到对应的effect列表依次执行trigger
总结: 把当前渲染逻辑放到一个全局变量上,当渲染的时候会发生取值操作,当取值的时候就会把把取值变量和属性关联在一起,等去更新页面的时候或者更新数据的时候对应刚才收集的那个渲染函数watcher或者effect,最后都会让它重新去执行。这样就达到了响应式数据变化。数据变了可以去更新视图这样的操作。
Vue.set 方法是如何实现的
已经创建的实例 支持 动态添加新的响应式属性。- 是 开发环境 target 没定义或者是基础类型则报错
- 如果是数组 Vue.set(arrray,1,100) 调用重写的splice方法(这样可以更新视图)
- 如果是对象本身的属性,则直接添加即刻
- 如果是Vue实例 或 根数据data时,报错 (更新 _data 无意义)
- 如果不是响应式的也不需要将其定义成响应式 ob 标识 响应式对象(响应式对象有__ob__)
- 将属性定义成响应式的 defineReactive,通知视图更新 ob.dep.notify()
当我们选择新增属性时,可以考虑使用对象合并的方式实现 this.info = {...this.info, ...{newProperty1:1,newProperty2:2}}
v-if 和 v-show 的优先级
vue2模板编译 https://v2.template-explorer.vuejs.org/ vue3模板编译 https://template-explorer.vuejs.org/v-if 如果条件不成立不会渲染当前指令所在的节点的 dom元素 v-show 只是切换当前 dom 的显示或者隐藏 display (opacity visibility 占位置)
- v-if可以阻断内部代码是否执行,如果条件不成立不会执行内部逻辑
- 如果页面逻辑在第一次加载的时候已经被确认后续不会频繁更改则采用 v-if
v-if 优先。 v-if先编译成三元表达式,再编译v-show指令
watch & computed
1.1 computed - 计算属性仅当用户取值时才会执行对应的方法。 - computed属性是具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会 重新执行。 - 计算属性可以简化模板中的复杂表达式 - 计算属性不支持异步逻辑 - computed 属性可以在模板中使用过程: 每一个计算属性内部维护一个dirty属性 dirty:true 当取值的时候dirty为true就执行用户的方法,拿到值缓存起来 this.value 并且将dirty = false 再次取值的时候 dirty为false,直接返回缓存的 this.value
整个流程:
- 计算属性会创建一个计算属性watcher,这个watcher(lazy:true)不会即刻执行
- 通过Object.defineProperty 将计算属性定义到实例上(用的时候写的是get方法,最终变成了属性)
- 当用户取值时会触发 getter, 拿到计算属性对应的watcher,看dirty是否为true,如果为true则求值 createComputedGetter(key)
- 并且让计算属性watcher中依赖的属性收集最外层的渲染watcher,可以做到依赖的属性变化了,触发计算属性更新dirty并且触发页面更新
- 如果依赖的值没有发生变化,则采用缓存
vue3中和vue2不一样的是 计算属性会收集当前组件渲染产生effect(计算属性本身拥有收集能力,而不是让它里面的属性去收集外层的effect) 依赖的值变化后,会通知计算属性effect 更新dirty,并且计算属性会触发自己收集的渲染effect执行。
1.2 watch watch 监控值得变化,当值发生变化时对应的回调函数。经常用于监控某个值的变化,进行一些操作。(异步要注意竞态问题) vue3提供了 onCleanup 函数,让用户更加方便使用也解决了清理问题。
总结: 内部都是基于watcher或者effect实现的。 计算属性的特点是具备缓存功能,通过dirty变量来控制是否重新执行,依赖的值变化了才会将dirty变为true,dirty为true才会重新取执行函数。 计算属性可以通过已有的数据进行衍生,衍生的数据可以在模板中使用,简化模板中的逻辑。但是它不支持异步逻辑,因为它要求同步有返回结果 watch主要功能是检测数据,变化了,可以去请求或者修改一些其他数据。watch在vue3多了onCleanup函数,让用户更加方便地解决清理问题(更加方便地进行清理操作)。
解释 ref 和 reactive 区别
ref和reactive是vue3数据响应式中非常重要的两个概念 - reactive 用于处理对象类型的数据响应式。底层采用的是 new Proxy() - ref 通常用于处理单值得响应式,ref主要解决原始值的响应式问题。底层采用的是Object.defineProperty()实现总结:reacitve通常用于对象格式,ref通常用于基本类型。reactive如果解构的话会失去响应式,可以在reactive里去包裹ref,去取reactive的属性,会有一个拆包操作,ref也是一样。ref可以放上对象类型,我们可以更新这个对象的引用,内部采用的是new Proxy 帮我们把传过去的对象进行代理,主要还是用单值得形式
watch 和 watchEffect 区别
- watchEffect 立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。 立即执行: 与 watch 相反,watchEffect 在组件初始化时就会执行一遍,因为它是对数据的“效应”进行监听。 深度追踪: watchEffect 会追踪所有在回调中访问到的新数据,这意味着只要数据在组件的任何地方被访问,它都会被视为依赖并进行更新。- watch 侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数。 惰性(lazy): watch 具有惰性,意味着在首次页面展示时,它不会立即执行。只有当数据发生变化时,才会触发回调。 明确侦听的数据源: watch 只追踪明确定义的数据源,它不会追踪任何在回调中访问到的新数据。 参数: 可以获取到当前值和原始值。
总结: watch允许用户写一些额外的操作;watchEffect是数据变了作为参数传进去的函数。
如何将template转换render函数
vue中包含模板编译的功能,主要作用是将用户编写的template编译为js中可执行的render函数。 - 将template模板转换成ast语法树(抽象语法树) - parserHTML - 对静态语法做静态编辑 - markup diff 来做优化的 静态节点跳过diff操作 - 重新生成代码 - codeGenvue3中模板转换做了更多的优化操作。vue2仅是标记了静态节点。 AST Explorer astexplorer.net/
AST和vdom之间没有直接的关系,但它们都是前端开发中常用的技术。AST可以用于代码分析、语法检查、代码转换等场景,而vdom则主要用于优化页面渲染性能。在某些情况下,可以通过将AST转换为vdom来实现对源代码的解析和处理,从而进一步提高开发效率和代码质量。
new Vue() 过程中做了什么
-
在new Vue的时候,内部会进行初始化操作
-
内部会初始化组件绑定的事件,初始化组件的父子关系children $root
-
初始化响应式数据 data computed props watch method。 同时也初始化 provide和inject方法。内部会对数据进行劫持,对象采用defineProperty数组采用方法重写。
-
在看一下用户是否传入了 el 属性 和template或者render。render的优先级更高,如果用户写的是template,会做模板编译(三部曲),最终就拿到了render函数
-
内部挂载的时候会产生一个watcher,会调用render函数触发依赖收集。内部还会给所有的响应式数据增加dep属性,让属性记录当前的watcher(用户后续修改的时候可以触发watcher重新渲染)
-
vue 更新的时候采用虚拟DOM 的方式进行diff算法更新
用户是否提供了el属性,没有的话调用mount?都会调用$mount??) 在挂载的时候,它呢就会看用户有没有传入这样一个模板属性。因为要把模板变成render函数。怎么去变的呢?第一步要去解析AST语法树,第二步要去优化这个树,第三步是代码生成。就是渲染的流程。
mount)在 src/platforms/web/runtime-with-compiler.ts 带编译的。 如果没有render函数,检查用户是否写模板,有模板会进行编译(模板的写法很多:指定id的方式去选择模板;给一个节点,模板的类型是一个DOM元素,去dom里的内容;)没有模板,给了el,可以拿元素里面放着的内容当做模板。 有模板之后,把模板编译成render函数。
lifecycle.ts _update 里调用的 vm.__patch__方法。比较先后节点的差异,用diff算法做对比,找到差异去更新页面。 watcherOptions callHook beforUpdated。
lifecycle.ts 如果用户调用destroy方法,就会触发销毁操作,调用beforeDestroy,把当前组件从父组件移除,停止响应式的处理,包括移除一些事件。最终整个组件销毁完毕了,此时会调用一个方法 destroyed。
总结:
在new Vue的时候,内部会进行初始化操作。内部会初始化组件绑定的事件,包括父子关系$parent $children $root。响应式数的处理——这是vue的核心。 如果用户传的是模板,还要把模板变成render函数,如果render函数用户传了,它的优先级是最高的。用户没有传才会选择去编译模板。一般编译是在构建工具中做的。有了render函数之后,就要考虑渲染流程。靠watcher去渲染,这里面有依赖收集。在创建watcher的时候回去渲染模板,渲染模板的时候回去取值,这些属性就会去收集watcher,稍后,属性变化了就可以让watcher重新去执行,数据变化就会产生新的节点,比较两个虚拟节点,找出差异再更新页面。最终如果用destroy方法会帮当前实例移除调,并解绑组件上的所有事件。