1.说说对双向绑定的理解
1.1、双向绑定的原理是什么
我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成
数据层(Model):应用的数据及业务逻辑
视图层(View):应用的展示效果,各类UI组件
业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理
理解ViewModel
它的主要职责就是:
数据变化后更新视图
视图变化后更新数据
当然,它还有两个主要部分组成
监听器(Observer):对所有数据的属性进行监听
解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
1.2、实现双向绑定
我们还是以Vue为例,先来看看Vue中的双向绑定流程是什么的
new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe中
同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile中
同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher
将来data中数据⼀旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数
2.单页应用与多页应用的区别
单页应用 ---SPA(single-page application),翻译过来就是单页应用SPA是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换打断用户体验在单页应用中,所有必要的代码(HTML、JavaScript和CSS)都通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面页面在任何时间点都不会重新加载,也不会将控制转移到其他页面举个例子来讲就是一个杯子,早上装的牛奶,中午装的是开水,晚上装的是茶,我们发现,变的始终是杯子里的内容,而杯子始终是那个杯子
单页面应用(SPA) | 多页面应用(MPA)
组成 | 一个主页面和多个页面片段 | 多个主页面 |
刷新方式 | 局部刷新 | 整页刷新 |
url模式 | 哈希模式 | 历史模式 |
SEO搜索引擎优化 | 难实现,可使用SSR方式改善 | 容易实现 |
数据传递 | 容易 | 通过url、cookie、localStorage等传递 |
页面切换 | 速度快,用户体验良好 | 切换加载资源,速度慢,用户体验差 |
维护成本 | 相对容易 | 相对复杂 |
1.1单页应用优缺点
优点:
具有桌面应用的即时性、网站的可移植性和可访问性
用户体验好、快,内容的改变不需要重新加载整个页面
良好的前后端分离,分工更明确
缺点:
不利于搜索引擎的抓取
首次渲染速度相对较慢
3.v-show与v-if的区别
控制手段不同
编译过程不同
编译条件不同
控制手段:v-show隐藏则是为该元素添加css--display:none,dom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删除
编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换
编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染
v-show 由false变为true的时候不会触发组件的生命周期
v-if由false变为true的时候,触发组件的beforeCreate、create、beforeMount、mounted钩子,由true变为false的时候触发组件的beforeDestory、destoryed方法
性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
4.vue挂载都干了什么
1.1 分析
首先找到vue的构造函数
源码位置:src\core\instance\index.js
functionVue(options){if(process.env.NODE_ENV!=='production'&&!(thisinstanceofVue)){warn('Vue is a constructor and should be called with the `new` keyword')}this._init(options)}
options是用户传递过来的配置项,如data、methods等常用的方法
vue构建函数调用_init方法,但我们发现本文件中并没有此方法,但仔细可以看到文件下方定定义了很多初始化方法
initMixin(Vue);// 定义 _initstateMixin(Vue);// 定义 $set $get $delete $watch 等eventsMixin(Vue);// 定义事件 $on $once $off $emitlifecycleMixin(Vue);// 定义 _update $forceUpdate $destroyrenderMixin(Vue);// 定义 _render 返回虚拟dom
首先可以看initMixin方法,发现该方法在Vue原型上定义了_init方法
源码位置:src\core\instance\init.js
Vue.prototype._init=function(options?:Object){constvm:Component=this// a uidvm._uid=uid++letstartTag,endTag/* istanbul ignore if */if(process.env.NODE_ENV!=='production'&&config.performance&&mark){startTag=`vue-perf-start:${vm._uid}`endTag=`vue-perf-end:${vm._uid}`mark(startTag)}// a flag to avoid this being observedvm._isVue=true// merge options// 合并属性,判断初始化的是否是组件,这里合并主要是 mixins 或 extends 的方法if(options&&options._isComponent){// optimize internal component instantiation// since dynamic options merging is pretty slow, and none of the// internal component options needs special treatment.initInternalComponent(vm,options)}else{// 合并vue属性vm.$options=mergeOptions(resolveConstructorOptions(vm.constructor),options||{},vm)}/* istanbul ignore else */if(process.env.NODE_ENV!=='production'){// 初始化proxy拦截器initProxy(vm)}else{vm._renderProxy=vm}// expose real selfvm._self=vm// 初始化组件生命周期标志位initLifecycle(vm)// 初始化组件事件侦听initEvents(vm)// 初始化渲染方法initRender(vm)callHook(vm,'beforeCreate')// 初始化依赖注入内容,在初始化data、props之前initInjections(vm)// resolve injections before data/props// 初始化props/data/method/watch/methodsinitState(vm)initProvide(vm)// resolve provide after data/propscallHook(vm,'created')/* istanbul ignore if */if(process.env.NODE_ENV!=='production'&&config.performance&&mark){vm._name=formatComponentName(vm,false)mark(endTag)measure(`vue${vm._name}init`,startTag,endTag)}// 挂载元素if(vm.$options.el){vm.$mount(vm.$options.el)}}
仔细阅读上面的代码,我们得到以下结论:
在调用beforeCreate之前,数据初始化并未完成,像data、props这些属性无法访问到
到了created的时候,数据已经初始化完成,能够访问data、props这些属性,但这时候并未完成dom的挂载,因此无法访问到dom元素
挂载方法是调用vm.$mount方法
initState方法是完成props/data/method/watch/methods的初始化
源码位置:src\core\instance\state.js
exportfunctioninitState(vm:Component){// 初始化组件的watcher列表vm._watchers=[]constopts=vm.$options// 初始化propsif(opts.props)initProps(vm,opts.props)// 初始化methods方法if(opts.methods)initMethods(vm,opts.methods)if(opts.data){// 初始化data initData(vm)}else{observe(vm._data={},true/* asRootData */)}if(opts.computed)initComputed(vm,opts.computed)if(opts.watch&&opts.watch!==nativeWatch){initWatch(vm,opts.watch)}}
我们和这里主要看初始化data的方法为initData,它与initState在同一文件上
functioninitData(vm:Component){letdata=vm.$options.data// 获取到组件上的datadata=vm._data=typeofdata==='function'?getData(data,vm):data||{}if(!isPlainObject(data)){data={}process.env.NODE_ENV!=='production'&&warn('data functions should return an object:\n'+'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',vm)}// proxy data on instanceconstkeys=Object.keys(data)constprops=vm.$options.propsconstmethods=vm.$options.methodsleti=keys.lengthwhile(i--){constkey=keys[i]if(process.env.NODE_ENV!=='production'){// 属性名不能与方法名重复if(methods&&hasOwn(methods,key)){warn(`Method "${key}" has already been defined as a data property.`,vm)}}// 属性名不能与state名称重复if(props&&hasOwn(props,key)){process.env.NODE_ENV!=='production'&&warn(`The data property "${key}" is already declared as a prop. `+`Use prop default value instead.`,vm)}elseif(!isReserved(key)){// 验证key值的合法性// 将_data中的数据挂载到组件vm上,这样就可以通过this.xxx访问到组件上的数据proxy(vm,`_data`,key)}}// observe data// 响应式监听data是数据的变化observe(data,true/* asRootData */)}
仔细阅读上面的代码,我们可以得到以下结论:
初始化顺序:props、methods、data
data定义的时候可选择函数形式或者对象形式(组件只能为函数形式)
关于数据响应式在这就不展开详细说明
上文提到挂载方法是调用vm.$mount方法
源码位置:
Vue.prototype.$mount=function(el?:string|Element,hydrating?:boolean):Component{// 获取或查询元素el=el&&query(el)/* istanbul ignore if */// vue 不允许直接挂载到body或页面文档上if(el===document.body||el===document.documentElement){process.env.NODE_ENV!=='production'&&warn(`Do not mount Vue to or
- mount to normal elements instead.`)returnthis}constoptions=this.$options// resolve template/el and convert to render functionif(!options.render){lettemplate=options.template// 存在template模板,解析vue模板文件if(template){if(typeoftemplate==='string'){if(template.charAt(0)==='#'){template=idToTemplate(template)/* istanbul ignore if */if(process.env.NODE_ENV!=='production'&&!template){warn(`Template element not found or is empty:${options.template}`,this)}}}elseif(template.nodeType){template=template.innerHTML}else{if(process.env.NODE_ENV!=='production'){warn('invalid template option:'+template,this)}returnthis}}elseif(el){// 通过选择器获取元素内容template=getOuterHTML(el)}if(template){/* istanbul ignore if */if(process.env.NODE_ENV!=='production'&&config.performance&&mark){mark('compile')}/** * 1.将temmplate解析ast tree * 2.将ast tree转换成render语法字符串 * 3.生成render方法 */const{render,staticRenderFns}=compileToFunctions(template,{outputSourceRange:process.env.NODE_ENV!=='production',shouldDecodeNewlines,shouldDecodeNewlinesForHref,delimiters:options.delimiters,comments:options.comments},this)options.render=renderoptions.staticRenderFns=staticRenderFns/* istanbul ignore if */if(process.env.NODE_ENV!=='production'&&config.performance&&mark){mark('compile end')measure(`vue${this._name}compile`,'compile','compile end')}}}returnmount.call(this,el,hydrating)}阅读上面代码,我们能得到以下结论:
不要将根元素放到body或者html上
可以在对象中定义template/render或者直接使用template、el表示元素选择器
最终都会解析成render函数,调用compileToFunctions,会将template解析成render函数
对template的解析步骤大致分为以下几步:
将html文档片段解析成ast描述符
将ast描述符解析成字符串
生成render函数
生成render函数,挂载到vm上后,会再次调用mount方法
源码位置:src\platforms\web\runtime\index.js
// public mount methodVue.prototype.$mount=function(el?:string|Element,hydrating?:boolean):Component{el=el&&inBrowser?query(el):undefined// 渲染组件returnmountComponent(this,el,hydrating)}
调用mountComponent渲染组件
exportfunctionmountComponent(vm:Component,el: ?Element,hydrating?:boolean):Component{vm.$el=el// 如果没有获取解析的render函数,则会抛出警告// render是解析模板文件生成的if(!vm.$options.render){vm.$options.render=createEmptyVNodeif(process.env.NODE_ENV!=='production'){/* istanbul ignore if */if((vm.$options.template&&vm.$options.template.charAt(0)!=='#')||vm.$options.el||el){warn('You are using the runtime-only build of Vue where the template '+'compiler is not available. Either pre-compile the templates into '+'render functions, or use the compiler-included build.',vm)}else{// 没有获取到vue的模板文件warn('Failed to mount component: template or render function not defined.',vm)}}}// 执行beforeMount钩子callHook(vm,'beforeMount')letupdateComponent/* istanbul ignore if */if(process.env.NODE_ENV!=='production'&&config.performance&&mark){updateComponent=()=>{constname=vm._nameconstid=vm._uidconststartTag=`vue-perf-start:${id}`constendTag=`vue-perf-end:${id}`mark(startTag)constvnode=vm._render()mark(endTag)measure(`vue${name}render`,startTag,endTag)mark(startTag)vm._update(vnode,hydrating)mark(endTag)measure(`vue${name}patch`,startTag,endTag)}}else{// 定义更新函数updateComponent=()=>{// 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_rendervm._update(vm._render(),hydrating)}}// we set this to vm._watcher inside the watcher's constructor// since the watcher's initial patch may call $forceUpdate (e.g. inside child// component's mounted hook), which relies on vm._watcher being already defined// 监听当前组件状态,当有数据变化时,更新组件newWatcher(vm,updateComponent,noop,{before(){if(vm._isMounted&&!vm._isDestroyed){// 数据更新引发的组件更新callHook(vm,'beforeUpdate')}}},true/* isRenderWatcher */)hydrating=false// manually mounted instance, call mounted on self// mounted is called for render-created child components in its inserted hookif(vm.$vnode==null){vm._isMounted=truecallHook(vm,'mounted')}returnvm}
阅读上面代码,我们得到以下结论:
会触发boforeCreate钩子
定义updateComponent渲染页面视图的方法
监听组件数据,一旦发生变化,触发beforeUpdate生命钩子
updateComponent方法主要执行在vue初始化时声明的render,update方法
render的作用主要是生成vnode
源码位置:src\core\instance\render.js
// 定义vue 原型上的render方法Vue.prototype._render=function():VNode{constvm:Component=this// render函数来自于组件的optionconst{render,_parentVnode}=vm.$optionsif(_parentVnode){vm.$scopedSlots=normalizeScopedSlots(_parentVnode.data.scopedSlots,vm.$slots,vm.$scopedSlots)}// set parent vnode. this allows render functions to have access// to the data on the placeholder node.vm.$vnode=_parentVnode// render selfletvnodetry{// There's no need to maintain a stack because all render fns are called// separately from one another. Nested component's render fns are called// when parent component is patched.currentRenderingInstance=vm// 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNodevnode=render.call(vm._renderProxy,vm.$createElement)}catch(e){handleError(e,vm,`render`)// return error render result,// or previous vnode to prevent render error causing blank component/* istanbul ignore else */if(process.env.NODE_ENV!=='production'&&vm.$options.renderError){try{vnode=vm.$options.renderError.call(vm._renderProxy,vm.$createElement,e)}catch(e){handleError(e,vm,`renderError`)vnode=vm._vnode}}else{vnode=vm._vnode}}finally{currentRenderingInstance=null}// if the returned array contains only a single node, allow itif(Array.isArray(vnode)&&vnode.length===1){vnode=vnode[0]}// return empty vnode in case the render function errored outif(!(vnodeinstanceofVNode)){if(process.env.NODE_ENV!=='production'&&Array.isArray(vnode)){warn('Multiple root nodes returned from render function. Render function '+'should return a single root node.',vm)}vnode=createEmptyVNode()}// set parentvnode.parent=_parentVnodereturnvnode}
_update主要功能是调用patch,将vnode转换为真实DOM,并且更新到页面中
源码位置:src\core\instance\lifecycle.js
Vue.prototype._update=function(vnode:VNode,hydrating?:boolean){constvm:Component=thisconstprevEl=vm.$elconstprevVnode=vm._vnode// 设置当前激活的作用域constrestoreActiveInstance=setActiveInstance(vm)vm._vnode=vnode// Vue.prototype.__patch__ is injected in entry points// based on the rendering backend used.if(!prevVnode){// initial render// 执行具体的挂载逻辑vm.$el=vm.__patch__(vm.$el,vnode,hydrating,false/* removeOnly */)}else{// updatesvm.$el=vm.__patch__(prevVnode,vnode)}restoreActiveInstance()// update __vue__ referenceif(prevEl){prevEl.__vue__=null}if(vm.$el){vm.$el.__vue__=vm}// if parent is an HOC, update its $el as wellif(vm.$vnode&&vm.$parent&&vm.$vnode===vm.$parent._vnode){vm.$parent.$el=vm.$el}// updated hook is called by the scheduler to ensure that children are// updated in a parent's updated hook.}
new Vue的时候调用会调用_init方法
定义 $set、 $get 、$delete、$watch 等方法
定义 $on、$off、$emit、$off 等事件
定义 _update、$forceUpdate、$destroy生命周期
调用$mount进行页面的挂载
挂载的时候主要是通过mountComponent方法
定义updateComponent更新函数
执行render生成虚拟DOM
_update将虚拟DOM生成真实DOM结构,并且渲染到页面中
5.vue 生命周期
1.1生命周期是什么
生命周期(Life Cycle)的概念应用很广泛,特别是在政治、经济、环境、技术、社会等诸多领域经常出现,其基本涵义可以通俗地理解为“从摇篮到坟墓”(Cradle-to-Grave)的整个过程在Vue中实例从创建到销毁的过程就是生命周期,即指从创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等一系列过程我们可以把组件比喻成工厂里面的一条流水线,每个工人(生命周期)站在各自的岗位,当任务流转到工人身边的时候,工人就开始工作PS:在Vue生命周期钩子会自动绑定 this 上下文到实例中,因此你可以访问数据,对 property 和方法进行运算这意味着你不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos())
1.2生命周期有哪些
Vue生命周期总共可以分为8个阶段:创建前后, 载入前后,更新前后,销毁前销毁后,以及一些特殊场景的生命周期
生命周期描述
beforeCreate组件实例被创建之初
created组件实例已经完全创建
beforeMount组件挂载之前
mounted组件挂载到实例上去之后
beforeUpdate组件数据发生变化,更新之前
updated数据数据更新之后
beforeDestroy组件实例销毁之前
destroyed组件实例销毁之后
activatedkeep-alive 缓存的组件激活时
deactivatedkeep-alive 缓存的组件停用时调用
errorCaptured捕获一个来自子孙组件的错误时被调用
1.3生命周期整体流程
Vue生命周期流程图
具体分析
beforeCreate -> created
初始化vue实例,进行数据观测
created
完成数据观测,属性与方法的运算,watch、event事件回调的配置
可调用methods中的方法,访问和修改data数据触发响应式渲染dom,可通过computed和watch完成数据计算
此时vm.$el 并没有被创建
created -> beforeMount
判断是否存在el选项,若不存在则停止编译,直到调用vm.$mount(el)才会继续编译
优先级:render > template > outerHTML
vm.el获取到的是挂载DOM的
beforeMount
在此阶段可获取到vm.el
此阶段vm.el虽已完成DOM初始化,但并未挂载在el选项上
beforeMount -> mounted
此阶段vm.el完成挂载,vm.$el生成的DOM替换了el选项所对应的DOM
mounted
vm.el已完成DOM的挂载与渲染,此刻打印vm.$el,发现之前的挂载点及内容已被替换成新的DOM
beforeUpdate
更新的数据必须是被渲染在模板上的(el、template、render之一)
此时view层还未更新
若在beforeUpdate中再次修改数据,不会再次触发更新方法
updated
完成view层的更新
若在updated中再次修改数据,会再次触发更新方法(beforeUpdate、updated)
beforeDestroy
实例被销毁前调用,此时实例属性与方法仍可访问
destroyed
完全销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器
并不能清除DOM,仅仅销毁实例
使用场景分析
生命周期描述
beforeCreate执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
created组件初始化完毕,各种数据可以使用,常用于异步数据获取
beforeMount未执行渲染、更新,dom未创建
mounted初始化结束,dom已创建,可用于获取访问数据和dom元素
beforeUpdate更新前,可用于获取更新前各种状态
updated更新后,所有状态已是最新
beforeDestroy销毁前,可用于一些定时器或订阅的取消
destroyed组件已销毁,作用同上
1.4题外话:数据请求在created和mouted的区别
created是在组件实例一旦创建完成的时候立刻调用,这时候页面dom节点并未生成mounted是在页面dom节点渲染完毕之后就立刻执行的触发时机上created是比mounted要更早的两者相同点:都能拿到实例对象的属性和方法讨论这个问题本质就是触发的时机,放在mounted请求有可能导致页面闪动(页面dom结构已经生成),但如果在页面加载前完成则不会出现此情况建议:放在create生命周期当中
6.v-if 和v-for
1.1优先级
v-if与v-for都是vue模板系统中的指令
在vue模板编译的时候,会将指令系统转化成可执行的render函数
示例
编写一个p标签,同时使用v-if与 v-for