网站页面建设需要ps吗,韩国美食网站建设目的,wordpress播放本地mp4,价格低廉什么是 mixin #xff1f;
Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。如果希望在多个组件之间重用一组组件选项#xff0c;例如生命周期 hook、 方法等#xff0c;则可以将其编写为 mixin#xff0c;并在组件中简单的引用它。然后将 mixin 的内容合并到组件中…什么是 mixin
Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。如果希望在多个组件之间重用一组组件选项例如生命周期 hook、 方法等则可以将其编写为 mixin并在组件中简单的引用它。然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook那么它在执行时将优化于组件自已的 hook。
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 langless
/style2. data数据的使用
由于 setup 不需写 return 所以直接声明数据即可
script setup
import {ref,reactive,toRefs,
} from vueconst data reactive({patternVisible: false,debugVisible: false,aboutExeVisible: false,
})const content ref(content)
//使用toRefs解构
const { patternVisible, debugVisible, aboutExeVisible } toRefs(data)
/script3. method方法的使用
template button clickonClickHelp帮助/button
/template
script setup
import {reactive} from vueconst data reactive({aboutExeVisible: false,
})
// 点击帮助
const onClickHelp () {console.log(帮助)data.aboutExeVisible true
}
/script4. watchEffect的使用
script setup
import {ref,watchEffect,
} from vuelet sum ref(0)watchEffect((){const x1 sum.valueconsole.log(watchEffect所指定的回调执行了)
})
/script5. 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}) /script6. 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]}
})
/script7. props父子传值的使用
父组件代码如下示例
templatechild :namename/
/templatescript setupimport {ref} from vue// 引入子组件import child from ./child.vuelet name ref(poetry)
/script子组件代码如下示例
templatespan{{props.name}}/span
/templatescript setup
import { defineProps } from vue
// 声明props
const props defineProps({name: {type: String,default: poetries}
})
// 或者
//const props defineProps([name])
/script8. emit子父传值的使用
父组件代码如下示例
templateAdoutExe aboutExeVisibleaboutExeHandleCancel /
/template
script setup
import { reactive } from vue
// 导入子组件
import AdoutExe from ../components/AdoutExeComconst data reactive({aboutExeVisible: false,
})
// content组件ref// 关于系统隐藏
const aboutExeHandleCancel () {data.aboutExeVisible false
}
/script子组件代码如下示例
templatea-button clickisOk确定/a-button
/template
script setup
import { defineEmits } from vue;// emit
const emit defineEmits([aboutExeVisible])
/*** 方法*/
// 点击确定按钮
const isOk () {emit(aboutExeVisible);
}
/script9. 获取子组件ref变量和defineExpose暴露
即vue2中的获取子组件的ref直接在父组件中控制子组件方法和变量的方法
父组件代码如下示例
templatebutton clickonClickSetUp点击/buttonContent refcontent /
/templatescript setup
import {ref} from vue// content组件ref
const content ref(content)
// 点击设置
const onClickSetUp ({ key }) {content.value.modelVisible true
}
/script
style scoped langless
/style子组件代码如下示例
templatep{{data }}/p
/templatescript setup
import {reactive,toRefs
} from vue/*** 数据部分
* */
const data reactive({modelVisible: false,historyVisible: false, reportVisible: false,
})defineExpose({...toRefs(data),
})
/script10. 路由useRoute和useRouter的使用
script setupimport { useRoute, useRouter } from vue-router// 声明const route useRoute()const router useRouter()// 获取queryconsole.log(route.query)// 获取paramsconsole.log(route.params)// 路由跳转router.push({path: /index})
/script11. store仓库的使用
script setupimport { useStore } from vueximport { num } from ../store/indexconst store useStore(num)// 获取Vuex的stateconsole.log(store.state.number)// 获取Vuex的gettersconsole.log(store.state.getNumber)// 提交mutationsstore.commit(fnName)// 分发actions的方法store.dispatch(fnName)
/script12. await的支持
setup语法糖中可直接使用await不需要写asyncsetup会自动变成async setup
script setupimport api from ../api/Apiconst data await Api.getData()console.log(data)
/script13. provide 和 inject 祖孙传值
父组件代码如下示例
templateAdoutExe /
/templatescript setupimport { ref,provide } from vueimport AdoutExe from /components/AdoutExeComlet name ref(py)// 使用provideprovide(provideState, {name,changeName: () {name.value poetries}})
/script子组件代码如下示例
script setupimport { inject } from vueconst provideState inject(provideState)provideState.changeName()
/scriptv-show 与 v-if 有什么区别
v-if 是真正的条件渲染因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建也是惰性的如果在初始渲染时条件为假则什么也不做——直到条件第一次变为真时才会开始渲染条件块。
v-show 就简单得多——不管初始条件是什么元素总是会被渲染并且只是简单地基于 CSS 的 “display” 属性进行切换。
所以v-if 适用于在运行时很少改变条件不需要频繁切换条件的场景v-show 则适用于需要非常频繁切换条件的场景。
vue3中 watch、watchEffect区别
watch是惰性执行也就是只有监听的值发生变化的时候才会执行但是watchEffect不同每次代码加载watchEffect都会执行忽略watch第三个参数的配置如果修改配置项也可以实现立即执行watch需要传递监听的对象watchEffect不需要watch只能监听响应式数据ref定义的属性和reactive定义的对象如果直接监听reactive定义对象中的属性是不允许的会报警告除非使用函数转换一下。其实就是官网上说的监听一个getterwatchEffect如果监听reactive定义的对象是不起作用的只能监听对象中的属性
看一下watchEffect的代码
template
div请输入firstNameinput typetext v-modelfirstName
/div
div请输入lastNameinput typetext v-modellastName
/div
div请输入obj.textinput typetext v-modelobj.text
/divdiv【obj.text】 {{obj.text}}/div
/templatescript
import {ref, reactive, watch, watchEffect} from vue
export default {name: HelloWorld,props: {msg: String,},setup(props,content){let firstName ref()let lastName ref()let obj reactive({text:hello})watchEffect((){console.log(触发了watchEffect);console.log(组合后的名称为${firstName.value}${lastName.value})})return{obj,firstName,lastName}}
};
/script改造一下代码
watchEffect((){console.log(触发了watchEffect);// 这里我们不使用firstName.value/lastName.value 相当于是监控整个ref,对应第四点上面的结论console.log(组合后的名称为${firstName}${lastName})
})watchEffect((){console.log(触发了watchEffect);console.log(obj);
})稍微改造一下
let obj reactive({text:hello
})
watchEffect((){console.log(触发了watchEffect);console.log(obj.text);
})再看一下watch的代码验证一下
let obj reactive({text:hello
})
// watch是惰性执行 默认初始化之后不会执行只有值有变化才会触发可通过配置参数实现默认执行
watch(obj, (newValue, oldValue) {// 回调函数console.log(触发监控更新了new, newValue);console.log(触发监控更新了old, oldValue);
},{// 配置immediate参数立即执行以及深层次监听immediate: true,deep: true
})监控整个reactive对象从上面的图可以看到 deep 实际默认是开启的就算我们设置为false也还是无效。而且旧值获取不到。要获取旧值则需要监控对象的属性也就是监听一个getter看下图 总结
如果定义了reactive的数据想去使用watch监听数据改变则无法正确获取旧值并且deep属性配置无效自动强制开启了深层次监听。如果使用 ref 初始化一个对象或者数组类型的数据会被自动转成reactive的实现方式生成proxy代理对象。也会变得无法正确取旧值。用任何方式生成的数据如果接收的变量是一个proxy代理对象就都会导致watch这个对象时,watch回调里无法正确获取旧值。所以当大家使用watch监听对象时如果在不需要使用旧值的情况可以正常监听对象没关系但是如果当监听改变函数里面需要用到旧值时只能监听 对象.xxx属性 的方式才行
watch和watchEffect异同总结
体验
watchEffect立即运行一个函数然后被动地追踪它的依赖当这些依赖改变时重新执行该函数
const count ref(0)
watchEffect(() console.log(count.value))
// - logs 0
count.value
// - logs 1watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数
const state reactive({ count: 0 })
watch(() state.count,(count, prevCount) {/* ... */}
)回答范例
watchEffect立即运行一个函数然后被动地追踪它的依赖当这些依赖改变时重新执行该函数。watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数watchEffect(effect)是一种特殊watch传入的函数既是依赖收集的数据源也是回调函数。如果我们不关心响应式数据变化前后的值只是想拿这些数据做些事情那么watchEffect就是我们需要的。watch更底层可以接收多种数据源包括用于依赖收集的getter函数因此它完全可以实现watchEffect的功能同时由于可以指定getter函数依赖可以控制的更精确还能获取数据变化前后的值因此如果需要这些时我们会使用watchwatchEffect在使用时传入的函数会立刻执行一次。watch默认情况下并不会执行回调函数除非我们手动设置immediate选项从实现上来说watchEffect(fn)相当于watch(fn,fn,{immediate:true})
watchEffect定义如下
export function watchEffect(effect: WatchEffect,options?: WatchOptionsBase
): WatchStopHandle {return doWatch(effect, null, options)
}watch定义如下
export function watchT any, Immediate extends Readonlyboolean false(source: T | WatchSourceT,cb: any,options?: WatchOptionsImmediate
): WatchStopHandle {return doWatch(source as any, cb, options)
}很明显watchEffect就是一种特殊的watch实现。
那vue中是如何检测数组变化的呢
数组就是使用 object.defineProperty 重新定义数组的每一项那能引起数组变化的方法我们都是知道的 pop 、 push 、 shift 、 unshift 、 splice 、 sort 、 reverse 这七种只要这些方法执行改了数组内容我就更新内容就好了是不是很好理解。
是用来函数劫持的方式重写了数组方法具体呢就是更改了数组的原型更改成自己的用户调数组的一些方法的时候走的就是自己的方法然后通知视图去更新。数组里每一项可能是对象那么我就是会对数组的每一项进行观测且只有数组里的对象才能进行观测观测过的也不会进行观测
vue3改用 proxy 可直接监听对象数组的变化。
了解nextTick吗
异步方法异步渲染最后一步与JS事件循环联系紧密。主要使用了宏任务微任务setTimeout、promise那些定义了一个异步方法多次调用nextTick会将方法存入队列通过异步方法清空当前队列。
参考 前端进阶面试题详细解答
Vue中的key到底有什么用
key是为Vue中的vnode标记的唯一id,通过这个key,我们的diff操作可以更准确、更快速
diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行比对,然后超出差异. diff程可以概括为oldCh和newCh各有两个头尾的变量StartIdx和EndIdx它们的2个变量相互比较一共有4种比较方式。如果4种比较都没匹配如果设置了key就会用key进行比较在比较的过程中变量会往中间靠一旦StartIdxEndIdx表明oldCh和newCh至少有一个已经遍历完了就会结束比较,这四种比较方式就是首、尾、旧尾新头、旧头新尾. 准确: 如果不加key,那么vue会选择复用节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug.快速: key的唯一性可以被Map数据结构充分利用,相比于遍历查找的时间复杂度O(n),Map的时间复杂度仅仅为O(1).
Vue 组件间通信有哪几种方式
Vue 组件间通信是面试常考的知识点之一这题有点类似于开放题你回答出越多方法当然越加分表明你对 Vue 掌握的越熟练。Vue 组件间通信只要指以下 3 类通信父子组件通信、隔代组件通信、兄弟组件通信下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。
1props / $emit 适用 父子组件通信 这种方法是 Vue 组件的基础相信大部分同学耳闻能详所以此处就不举例展开介绍。
2ref 与 $parent / $children 适用 父子组件通信
ref如果在普通的 DOM 元素上使用引用指向的就是 DOM 元素如果用在子组件上引用就指向组件实例$parent / $children访问父 / 子实例
3EventBus $emit / $on 适用于 父子、隔代、兄弟组件通信 这种方法通过一个空的 Vue 实例作为中央事件总线事件中心用它来触发事件和监听事件从而实现任何组件间的通信包括父子、隔代、兄弟组件。
4$attrs/$listeners 适用于 隔代组件通信
$attrs包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时这里会包含所有父作用域的绑定 ( class 和 style 除外 )并且可以通过 v-bind$attrs 传入内部组件。通常配合 inheritAttrs 选项一起使用。$listeners包含了父作用域中的 (不含 .native 修饰器的) v-on事件监听器。它可以通过 v-on$listeners 传入内部组件
5provide / inject 适用于 隔代组件通信 祖先组件中通过 provider 来提供变量然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题不过它的使用场景主要是子组件获取上级组件的状态跨级组件间建立了一种主动提供与依赖注入的关系。 6Vuex 适用于 父子、隔代、兄弟组件通信 Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store仓库。“store” 基本上就是一个容器它包含着你的应用中大部分的状态 ( state )。
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候若 store 中的状态发生变化那么相应的组件也会相应地得到高效更新。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
Vue.extend 作用和原理
官方解释Vue.extend 使用基础 Vue 构造器创建一个“子类”。参数是一个包含组件选项的对象。
其实就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并
相关代码如下
export default function initExtend(Vue) {let cid 0; //组件的唯一标识// 创建子类继承Vue父类 便于属性扩展Vue.extend function (extendOptions) {// 创建子类的构造函数 并且调用初始化方法const Sub function VueComponent(options) {this._init(options); //调用Vue初始化方法};Sub.cid cid;Sub.prototype Object.create(this.prototype); // 子类原型指向父类Sub.prototype.constructor Sub; //constructor指向自己Sub.options mergeOptions(this.options, extendOptions); //合并自己的options和父类的optionsreturn Sub;};
}mixin 和 mixins 区别
mixin 用于全局混入会影响到每个组件实例通常插件都是这样做初始化的。
Vue.mixin({beforeCreate() {// ...逻辑 // 这种方式会影响到每个组件的 beforeCreate 钩子函数},
});
虽然文档不建议在应用中直接使用 mixin但是如果不滥用的话也是很有帮助的比如可以全局混入封装好的 ajax 或者一些工具函数等等。
mixins 应该是最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑就可以将这些逻辑剥离出来通过 mixins 混入代码比如上拉下拉加载数据这种逻辑等等。 另外需要注意的是 mixins 混入的钩子函数会先于组件内的钩子函数执行并且在遇到同名选项的时候也会有选择性的进行合并。
为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组 push();
pop();
shift();
unshift();
splice();
sort();
reverse();
由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。 Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。 Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。 如何理解Vue中模板编译原理 Vue 的编译过程就是将 template 转化为 render 函数的过程 解析生成AST树 将template模板转化成AST语法树使用大量的正则表达式对模板进行解析遇到标签、文本的时候都会执行对应的钩子进行相关处理标记优化 对静态语法做静态标记 markup(静态节点如div下有p标签内容不会变化) diff来做优化 静态节点跳过diff操作 Vue的数据是响应式的但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化对应的DOM也不会变化。那么优化过程就是深度遍历AST树按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对对运行时的模板起到很大的优化作用等待后续节点更新如果是静态的不会在比较children了 代码生成 编译的最后一步是将优化后的AST树转换为可执行的代码
回答范例
思路
引入vue编译器概念说明编译器的必要性阐述编译器工作流程
回答范例
Vue中有个独特的编译器模块称为compiler它的主要作用是将用户编写的template编译为js中可执行的render函数。之所以需要这个编译过程是为了便于前端能高效的编写视图模板。相比而言我们还是更愿意用HTML来编写视图直观且高效。手写render函数不仅效率底下而且失去了编译期的优化能力。在Vue中编译器会先对template进行解析这一步称为parse结束之后会得到一个JS对象我们称为 抽象语法树AST 然后是对AST进行深加工的转换过程这一步成为transform最后将前面得到的AST生成为JS代码也就是render函数
可能的追问
Vue中编译器何时执行 在 new Vue()之后。 Vue 会调用 _init 函数进行初始化也就是这里的 init 过程它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch等。其中最重要的是通过 Object.defineProperty 设置 setter 与 getter 函数用来实现「响应式」以及「依赖收集」 初始化之后调用 $mount 会挂载组件如果是运行时编译即不存在 render function 但是存在 template 的情况需要进行「编译」步骤 compile编译可以分成 parse、optimize 与 generate 三个阶段最终需要得到 render function
React有没有编译器
react 使用babel将JSX语法解析
div idapp/div
scriptlet vm new Vue({el: #app,template: div// spanhello world/span 是静态节点spanhello world/span // p{{name}}/p 是动态节点p{{name}}/p/div,data() {return { name: test }}});
/script源码分析
export function compileToFunctions(template) {// 我们需要把html字符串变成render函数// 1.把html代码转成ast语法树 ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法// 很多库都运用到了ast 比如 webpack babel eslint等等let ast parse(template);// 2.优化静态节点对ast树进行标记,标记静态节点if (options.optimize ! false) {optimize(ast, options);}// 3.通过ast 重新生成代码// 我们最后生成的代码需要和render函数一样// 类似_c(div,{id:app},_c(div,undefined,_v(hello_s(name)),_c(span,undefined,_v(world))))// _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本let code generate(ast);// 使用with语法改变作用域为this 之后调用render函数可以使用call改变this 方便code里面的变量取值let renderFn new Function(with(this){return ${code}});return renderFn;
}为什么Vue采用异步渲染呢
Vue 是组件级更新如果不采用异步更新那么每次更新数据都会对当前组件进行重新渲染所以为了性能 Vue 会在本轮数据更新后在异步更新视图。核心思想 nextTick 。 dep.notify 通知 watcher进行更新 subs[i].update 依次调用 watcher 的 update queueWatcher 将watcher 去重放入队列 nextTick flushSchedulerQueue 在下一tick中刷新watcher队列异步。
Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题
受现代 JavaScript 的限制 Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value) 来实现为对象添加响应式属性那框架本身是如何实现的呢
我们查看对应的 Vue 源码vue/src/core/instance/index.js
export function set (target: Arrayany | Object, key: any, val: any): any {// target 为数组 if (Array.isArray(target) isValidArrayIndex(key)) {// 修改数组的长度, 避免索引数组长度导致splcie()执行有误target.length Math.max(target.length, key)// 利用数组的splice变异方法触发响应式 target.splice(key, 1, val)return val}// key 已经存在直接修改属性值 if (key in target !(key in Object.prototype)) {target[key] valreturn val}const ob (target: any).__ob__// target 本身就不是响应式数据, 直接赋值if (!ob) {target[key] valreturn val}// 对属性进行响应式处理defineReactive(ob.value, key, val)ob.dep.notify()return val
}我们阅读以上源码可知vm.$set 的实现原理是
如果目标是数组直接使用数组的 splice 方法触发相应式如果目标是对象会先判读属性是否存在、对象是否是响应式最终如果要对属性进行响应式处理则是通过调用 defineReactive 方法进行响应式处理 defineReactive 方法就是 Vue 在初始化对象时给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法
谈谈Vue和React组件化的思想
1.我们在各个页面开发的时候会产生很多重复的功能比如element中的xxxx。像这种纯粹非页面的UI便成为我们常用的UI组件最初的前端组件也就仅仅指的是UI组件2.随着业务逻辑变得越来多是我们就想要我们的组件可以处理很多事这就是我们常说的组件化这个组件就不是UI组件了而是包具体业务的业务组件3.这种开发思想就是分而治之。最大程度的降低开发难度和维护成本的效果。并且可以多人协作每个人写不同的组件最后像撘积木一样的把它构成一个页面
Vue2.x 响应式数据原理
整体思路是数据劫持观察者模式
对象内部通过 defineReactive 方法使用 Object.defineProperty 来劫持各个属性的 setter、getter只会劫持已经存在的属性数组则是通过重写数组7个方法来实现。当页面使用对应属性时每个属性都拥有自己的 dep 属性存放他所依赖的 watcher依赖收集当属性变化后会通知自己对应的 watcher 去更新(派发更新)
Object.defineProperty基本使用
function observer(value) { // proxy reflectif (typeof value object typeof value ! null)for (let key in value) {defineReactive(value, key, value[key]);}
}function defineReactive(obj, key, value) {observer(value);Object.defineProperty(obj, key, {get() { // 收集对应的key 在哪个方法组件中被使用return value;},set(newValue) {if (newValue ! value) {observer(newValue);value newValue; // 让key对应的方法组件重新渲染重新执行}}})
}
let obj1 { school: { name: poetry, age: 20 } };
observer(obj1);
console.log(obj1)源码分析 class Observer {// 观测值constructor(value) {this.walk(value);}walk(data) {// 对象上的所有属性依次进行观测let keys Object.keys(data);for (let i 0; i keys.length; i) {let key keys[i];let value data[key];defineReactive(data, key, value);}}
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {observe(value); // 递归关键// --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止// 思考如果Vue数据嵌套层级过深 性能会受影响Object.defineProperty(data, key, {get() {console.log(获取值);//需要做依赖收集过程 这里代码没写出来return value;},set(newValue) {if (newValue value) return;console.log(设置值);//需要做派发更新过程 这里代码没写出来value newValue;},});
}
export function observe(value) {// 如果传过来的是对象或者数组 进行属性劫持if (Object.prototype.toString.call(value) [object Object] ||Array.isArray(value)) {return new Observer(value);}
}说一说你对vue响应式理解回答范例
所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制MVVM框架中要解决的一个核心问题是连接数据层和视图层通过数据驱动应用数据变化视图更新要做到这点的就需要对数据做响应式处理这样一旦数据发生变化就可以立即做出更新处理以vue为例说明通过数据响应式加上虚拟DOM和patch算法开发人员只需要操作数据关心业务完全不用接触繁琐的DOM操作从而大大提升开发效率降低开发难度vue2中的数据响应式会根据数据类型来做不同处理如果是 对象则采用Object.defineProperty()的方式定义数据拦截当数据被访问或发生变化时我们感知并作出响应如果是数组则通过覆盖数组对象原型的7个变更方法 使这些方法可以额外的做更新通知从而作出响应。这种机制很好的解决了数据响应化的问题但在实际使用中也存在一些缺点比如初始化时的递归遍历会造成性能损失新增或删除属性时需要用户使用Vue.set/delete这样特殊的api才能生效对于es6中新产生的Map、Set这些数据结构不支持等问题为了解决这些问题vue3重新编写了这一部分的实现利用ES6的Proxy代理要响应化的数据它有很多好处编程体验是一致的不需要使用特殊api初始化性能和内存消耗都得到了大幅改善另外由于响应化的实现代码抽取为独立的reactivity包使得我们可以更灵活的使用它第三方的扩展开发起来更加灵活了
computed和watch有什么区别?
computed:
computed是计算属性,也就是计算值,它更多用于计算值的场景computed具有缓存性,computed的值在getter执行后是会缓存的只有在它依赖的属性值改变之后下一次获取computed的值时才会重新调用对应的getter来计算computed适用于计算比较消耗性能的计算场景
watch:
更多的是「观察」的作用,类似于某些数据的监听回调,用于观察props $emit或者本组件的值,当数据变化时来执行回调进行后续操作无缓存性页面重新渲染时值不变化也会执行
小结:
当我们要进行数值计算,而且依赖于其他数据那么把这个数据设计为computed如果你需要在某个数据变化时做一些事情使用watch来观察这个数据变化
Vue.js的template编译
简而言之就是先转化成AST树再得到的render函数返回VNodeVue的虚拟DOM节点详细步骤如下 首先通过compile编译器把template编译成AST语法树abstract syntax tree 即 源代码的抽象语法结构的树状表现形式compile是createCompiler的返回值createCompiler是用以创建编译器的。另外compile还负责合并option。 然后AST会经过generate将AST语法树转化成render funtion字符串的过程得到render函数render的返回值是VNodeVNode是Vue的虚拟DOM节点里面有标签名、子节点、文本等等 父组件可以监听到子组件的生命周期吗
比如有父组件 Parent 和子组件 Child如果父组件监听到子组件挂载 mounted 就做一些逻辑处理可以通过以下写法实现
// Parent.vue
Child mounteddoSomething/// Child.vue
mounted() {
this.$emit(mounted);
}以上需要手动通过 $emit 触发父组件的事件更简单的方式可以在父组件引用子组件时通过 hook 来监听即可如下所示
// Parent.vue
Child hook:mounteddoSomething /ChilddoSomething() {console.log(父组件监听到 mounted 钩子函数 ...);
},// Child.vue
mounted(){console.log(子组件触发 mounted 钩子函数 ...);
}, // 以上输出顺序为
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ... 当然 hook 方法不仅仅是可以监听 mounted其它的生命周期事件例如createdupdated 等都可以监听
Vue的diff算法详细分析
1. 是什么
diff 算法是一种通过同层的树节点进行比较的高效算法
其有两个特点
比较只会在同层级进行, 不会跨层级比较在diff比较的过程中循环从两边向中间比较
diff 算法在很多场景下都有应用在 vue 中作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较
2. 比较方式
diff整体策略为深度优先同层比较
比较只会在同层级进行, 不会跨层级比较 比较的过程中循环从两边向中间收拢 下面举个vue通过diff算法更新的例子
新旧VNode节点如下图所示 第一次循环后发现旧节点D与新节点D相同直接复用旧节点D作为diff后的第一个真实节点同时旧节点endIndex移动到C新节点的 startIndex 移动到了 C 第二次循环后同样是旧节点的末尾和新节点的开头(都是 C)相同同理diff 后创建了 C 的真实节点插入到第一次创建的 D 节点后面。同时旧节点的 endIndex 移动到了 B新节点的 startIndex 移动到了 E 第三次循环中发现E没有找到这时候只能直接创建新的真实节点 E插入到第二次创建的 C 节点之后。同时新节点的 startIndex 移动到了 A。旧节点的 startIndex 和 endIndex 都保持不动 第四次循环中发现了新旧节点的开头(都是 A)相同于是 diff 后创建了 A 的真实节点插入到前一次创建的 E 节点后面。同时旧节点的 startIndex 移动到了 B新节点的startIndex 移动到了 B 第五次循环中情形同第四次循环一样因此 diff 后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex移动到了 C新节点的 startIndex 移动到了 F 新节点的 startIndex 已经大于 endIndex 了需要创建 newStartIdx 和 newEndIdx 之间的所有节点也就是节点F直接创建 F 节点对应的真实节点放到 B 节点后面 3. 原理分析
当数据发生改变时set方法会调用Dep.notify通知所有订阅者Watcher订阅者就会调用patch给真实的DOM打补丁更新相应的视图
源码位置src/core/vdom/patch.js
function patch(oldVnode, vnode, hydrating, removeOnly) {if (isUndef(vnode)) { // 没有新节点直接执行destory钩子函数if (isDef(oldVnode)) invokeDestroyHook(oldVnode)return}let isInitialPatch falseconst insertedVnodeQueue []if (isUndef(oldVnode)) {isInitialPatch truecreateElm(vnode, insertedVnodeQueue) // 没有旧节点直接用新节点生成dom元素} else {const isRealElement isDef(oldVnode.nodeType)if (!isRealElement sameVnode(oldVnode, vnode)) {// 判断旧节点和新节点自身一样一致执行patchVnodepatchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)} else {// 否则直接销毁及旧节点根据新节点生成dom元素if (isRealElement) {if (oldVnode.nodeType 1 oldVnode.hasAttribute(SSR_ATTR)) {oldVnode.removeAttribute(SSR_ATTR)hydrating true}if (isTrue(hydrating)) {if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {invokeInsertHook(vnode, insertedVnodeQueue, true)return oldVnode}}oldVnode emptyNodeAt(oldVnode)}return vnode.elm}}
}patch函数前两个参数位为oldVnode 和 Vnode 分别代表新的节点和之前的旧节点主要做了四个判断
没有新节点直接触发旧节点的destory钩子没有旧节点说明是页面刚开始初始化的时候此时根本不需要比较了直接全是新建所以只调用 createElm旧节点和新节点自身一样通过 sameVnode 判断节点是否一样一样时直接调用 patchVnode去处理这两个节点旧节点和新节点自身不一样当两个节点不一样的时候直接创建新节点删除旧节点
下面主要讲的是patchVnode部分
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {// 如果新旧节点一致什么都不做if (oldVnode vnode) {return}// 让vnode.el引用到现在的真实dom当el修改时vnode.el会同步变化const elm vnode.elm oldVnode.elm// 异步占位符if (isTrue(oldVnode.isAsyncPlaceholder)) {if (isDef(vnode.asyncFactory.resolved)) {hydrate(oldVnode.elm, vnode, insertedVnodeQueue)} else {vnode.isAsyncPlaceholder true}return}// 如果新旧都是静态节点并且具有相同的key// 当vnode是克隆节点或是v-once指令控制的节点时只需要把oldVnode.elm和oldVnode.child都复制到vnode上// 也不用再有其他操作if (isTrue(vnode.isStatic) isTrue(oldVnode.isStatic) vnode.key oldVnode.key (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {vnode.componentInstance oldVnode.componentInstancereturn}let iconst data vnode.dataif (isDef(data) isDef(i data.hook) isDef(i i.prepatch)) {i(oldVnode, vnode)}const oldCh oldVnode.childrenconst ch vnode.childrenif (isDef(data) isPatchable(vnode)) {for (i 0; i cbs.update.length; i) cbs.update[i](oldVnode, vnode)if (isDef(i data.hook) isDef(i i.update)) i(oldVnode, vnode)}// 如果vnode不是文本节点或者注释节点if (isUndef(vnode.text)) {// 并且都有子节点if (isDef(oldCh) isDef(ch)) {// 并且子节点不完全一致则调用updateChildrenif (oldCh ! ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)// 如果只有新的vnode有子节点} else if (isDef(ch)) {if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, )// elm已经引用了老的dom节点在老的dom节点上添加子节点addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)// 如果新vnode没有子节点而vnode有子节点直接删除老的oldCh} else if (isDef(oldCh)) {removeVnodes(elm, oldCh, 0, oldCh.length - 1)// 如果老节点是文本节点} else if (isDef(oldVnode.text)) {nodeOps.setTextContent(elm, )}// 如果新vnode和老vnode是文本节点或注释节点// 但是vnode.text ! oldVnode.text时只需要更新vnode.elm的文本内容就可以} else if (oldVnode.text ! vnode.text) {nodeOps.setTextContent(elm, vnode.text)}if (isDef(data)) {if (isDef(i data.hook) isDef(i i.postpatch)) i(oldVnode, vnode)}}patchVnode主要做了几个判断
新节点是否是文本节点如果是则直接更新dom的文本内容为新节点的文本内容新节点和旧节点如果都有子节点则处理比较更新子节点只有新节点有子节点旧节点没有那么不用比较了所有节点都是全新的所以直接全部新建就好了新建是指创建出所有新DOM并且添加进父节点只有旧节点有子节点而新节点没有说明更新后的页面旧节点全部都不见了那么要做的就是把所有的旧节点删除也就是直接把DOM 删除
子节点不完全一致则调用updateChildren
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {let oldStartIdx 0 // 旧头索引let newStartIdx 0 // 新头索引let oldEndIdx oldCh.length - 1 // 旧尾索引let newEndIdx newCh.length - 1 // 新尾索引let oldStartVnode oldCh[0] // oldVnode的第一个childlet oldEndVnode oldCh[oldEndIdx] // oldVnode的最后一个childlet newStartVnode newCh[0] // newVnode的第一个childlet newEndVnode newCh[newEndIdx] // newVnode的最后一个childlet oldKeyToIdx, idxInOld, vnodeToMove, refElm// removeOnly is a special flag used only by transition-group// to ensure removed elements stay in correct relative positions// during leaving transitionsconst canMove !removeOnly// 如果oldStartVnode和oldEndVnode重合并且新的也都重合了证明diff完了循环结束while (oldStartIdx oldEndIdx newStartIdx newEndIdx) {// 如果oldVnode的第一个child不存在if (isUndef(oldStartVnode)) {// oldStart索引右移oldStartVnode oldCh[oldStartIdx] // Vnode has been moved left// 如果oldVnode的最后一个child不存在} else if (isUndef(oldEndVnode)) {// oldEnd索引左移oldEndVnode oldCh[--oldEndIdx]// oldStartVnode和newStartVnode是同一个节点} else if (sameVnode(oldStartVnode, newStartVnode)) {// patch oldStartVnode和newStartVnode 索引左移继续循环patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)oldStartVnode oldCh[oldStartIdx]newStartVnode newCh[newStartIdx]// oldEndVnode和newEndVnode是同一个节点} else if (sameVnode(oldEndVnode, newEndVnode)) {// patch oldEndVnode和newEndVnode索引右移继续循环patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)oldEndVnode oldCh[--oldEndIdx]newEndVnode newCh[--newEndIdx]// oldStartVnode和newEndVnode是同一个节点} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right// patch oldStartVnode和newEndVnodepatchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)// 如果removeOnly是false则将oldStartVnode.eml移动到oldEndVnode.elm之后canMove nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))// oldStart索引右移newEnd索引左移oldStartVnode oldCh[oldStartIdx]newEndVnode newCh[--newEndIdx]// 如果oldEndVnode和newStartVnode是同一个节点} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left// patch oldEndVnode和newStartVnodepatchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)// 如果removeOnly是false则将oldEndVnode.elm移动到oldStartVnode.elm之前canMove nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)// oldEnd索引左移newStart索引右移oldEndVnode oldCh[--oldEndIdx]newStartVnode newCh[newStartIdx]// 如果都不匹配} else {if (isUndef(oldKeyToIdx)) oldKeyToIdx createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)// 尝试在oldChildren中寻找和newStartVnode的具有相同的key的VnodeidxInOld isDef(newStartVnode.key)? oldKeyToIdx[newStartVnode.key]: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)// 如果未找到说明newStartVnode是一个新的节点if (isUndef(idxInOld)) { // New element// 创建一个新VnodecreateElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)// 如果找到了和newStartVnodej具有相同的key的Vnode叫vnodeToMove} else {vnodeToMove oldCh[idxInOld]/* istanbul ignore if */if (process.env.NODE_ENV ! production !vnodeToMove) {warn(It seems there are duplicate keys that is causing an update error. Make sure each v-for item has a unique key.)}// 比较两个具有相同的key的新节点是否是同一个节点//不设keynewCh和oldCh只会进行头尾两端的相互比较设key后除了头尾两端的比较外还会从用key生成的对象oldKeyToIdx中查找匹配的节点所以为节点设置key可以更高效的利用dom。if (sameVnode(vnodeToMove, newStartVnode)) {// patch vnodeToMove和newStartVnodepatchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)// 清除oldCh[idxInOld] undefined// 如果removeOnly是false则将找到的和newStartVnodej具有相同的key的Vnode叫vnodeToMove.elm// 移动到oldStartVnode.elm之前canMove nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)// 如果key相同但是节点不相同则创建一个新的节点} else {// same key but different element. treat as new elementcreateElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)}}// 右移newStartVnode newCh[newStartIdx]}}while循环主要处理了以下五种情景
当新老 VNode 节点的 start 相同时直接 patchVnode 同时新老 VNode 节点的开始索引都加 1当新老 VNode 节点的 end相同时同样直接 patchVnode 同时新老 VNode 节点的结束索引都减 1当老 VNode 节点的 start 和新 VNode 节点的 end 相同时这时候在 patchVnode 后还需要将当前真实 dom 节点移动到 oldEndVnode 的后面同时老 VNode 节点开始索引加 1新 VNode 节点的结束索引减 1当老 VNode 节点的 end 和新 VNode 节点的 start 相同时这时候在 patchVnode 后还需要将当前真实 dom 节点移动到 oldStartVnode 的前面同时老 VNode 节点结束索引减 1新 VNode 节点的开始索引加 1如果都不满足以上四种情形那说明没有相同的节点可以复用则会分为以下两种情况 从旧的 VNode 为 key 值对应 index 序列为 value 值的哈希表中找到与 newStartVnode 一致 key 的旧的 VNode 节点再进行patchVnode同时将这个真实 dom移动到 oldStartVnode 对应的真实 dom 的前面调用 createElm 创建一个新的 dom 节点放到当前 newStartIdx 的位置
小结
当数据发生改变时订阅者watcher就会调用patch给真实的DOM打补丁通过isSameVnode进行判断相同则调用patchVnode方法patchVnode做了以下操作 找到对应的真实dom称为el如果都有都有文本节点且不相等将el文本节点设置为Vnode的文本节点如果oldVnode有子节点而VNode没有则删除el子节点如果oldVnode没有子节点而VNode有则将VNode的子节点真实化后添加到el如果两者都有子节点则执行updateChildren函数比较子节点 updateChildren主要做了以下操作 设置新旧VNode的头尾指针新旧头尾指针进行比较循环向中间靠拢根据情况调用patchVnode进行patch重复流程、调用createElem创建一个新节点从哈希表寻找 key一致的VNode 节点再分情况操作