共计 37793 个字符,预计需要花费 95 分钟才能阅读完成。
vue2.x 具体
1. 剖析
首先找到 vue
的构造函数
源码地位:src\core\instance\index.js
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
options
是用户传递过去的配置项,如 data、methods
等罕用的办法
vue
构建函数调用 _init
办法,但咱们发现本文件中并没有此办法,但认真能够看到文件下方定定义了很多初始化办法
initMixin(Vue); // 定义 _init
stateMixin(Vue); // 定义 $set $get $delete $watch 等
eventsMixin(Vue); // 定义事件 $on $once $off $emit
lifecycleMixin(Vue);// 定义 _update $forceUpdate $destroy
renderMixin(Vue); // 定义 _render 返回虚构 dom
首先能够看 initMixin
办法,发现该办法在 Vue
原型上定义了 _init
办法
源码地位:src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, 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 observed
vm._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 self
vm._self = vm
// 初始化组件生命周期标记位
initLifecycle(vm)
// 初始化组件事件侦听
initEvents(vm)
// 初始化渲染办法
initRender(vm)
callHook(vm, 'beforeCreate')
// 初始化依赖注入内容,在初始化 data、props 之前
initInjections(vm) // resolve injections before data/props
// 初始化 props/data/method/watch/methods
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(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
export function initState (vm: Component) {
// 初始化组件的 watcher 列表
vm._watchers = []
const opts = vm.$options
// 初始化 props
if (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
在同一文件上
function initData (vm: Component) {
let data = vm.$options.data
// 获取到组件上的 data
data = vm._data = typeof data === '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 instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {const key = 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
)
} else if (!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 <html> or <body> - mount to normal elements instead.`)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
// 存在 template 模板,解析 vue 模板文件
if (template) {if (typeof template === '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
)
}
}
} else if (template.nodeType) {template = template.innerHTML} else {if (process.env.NODE_ENV !== 'production') {warn('invalid template option:' + template, this)
}
return this
}
} else if (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 = render
options.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')
}
}
}
return mount.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 method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {el = el && inBrowser ? query(el) : undefined
// 渲染组件
return mountComponent(this, el, hydrating)
}
调用 mountComponent
渲染组件
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 如果没有获取解析的 render 函数,则会抛出正告
// render 是解析模板文件生成的
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (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')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = 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 中定义的_render
vm._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
// 监听以后组件状态,当有数据变动时,更新组件
new Watcher(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 hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
浏览下面代码,咱们失去以下论断:
- 会触发
boforeCreate
钩子 - 定义
updateComponent
渲染页面视图的办法 - 监听组件数据,一旦发生变化,触发
beforeUpdate
生命钩子
updateComponent
办法次要执行在 vue
初始化时申明的 render
,update
办法
render的作用次要是生成
vnode
源码地位:src\core\instance\render.js
// 定义 vue 原型上的 render 办法
Vue.prototype._render = function (): VNode {
const vm: Component = this
// render 函数来自于组件的 option
const {render, _parentVnode} = vm.$options
if (_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 self
let vnode
try {
// 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 参数,生成 vNode
vnode = 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 it
if (Array.isArray(vnode) && vnode.length === 1) {vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {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 parent
vnode.parent = _parentVnode
return vnode
}
_update
次要性能是调用 patch
,将vnode
转换为实在DOM
,并且更新到页面中
源码地位:src\core\instance\lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
// 设置以后激活的作用域
const restoreActiveInstance = 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 {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {prevEl.__vue__ = null}
if (vm.$el) {vm.$el.__vue__ = vm}
// if parent is an HOC, update its $el as well
if (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.
}
2. 论断
-
new Vue
的时候调用会调用_init
办法- 定义
$set
、$get
、$delete
、$watch
等办法 - 定义
$on
、$off
、$emit
、$off
等事件 - 定义
_update
、$forceUpdate
、$destroy
生命周期
- 定义
- 调用
$mount
进行页面的挂载 - 挂载的时候次要是通过
mountComponent
办法 - 定义
updateComponent
更新函数 - 执行
render
生成虚构DOM
_update
将虚构DOM
生成实在DOM
构造,并且渲染到页面中
Vue 中如何扩大一个组件
此题属于实际题,考查大家对 vue 罕用 api 应用熟练度,答题时不仅要列出这些解决方案,同时最好说出他们异同
答题思路:
-
依照逻辑扩大和内容扩大来列举
- 逻辑扩大有:
mixins
、extends
、composition api
- 内容扩大有
slots
;
- 逻辑扩大有:
- 别离说出他们应用办法、场景差别和问题。
- 作为扩大,还能够说说
vue3
中新引入的composition api
带来的变动
答复范例:
- 常见的组件扩大办法有:
mixins
,slots
,extends
等 - 混入
mixins
是散发Vue
组件中可复用性能的非常灵活的形式。混入对象能够蕴含任意组件选项。当组件应用混入对象时,所有混入对象的选项将被混入该组件自身的选项
// 复用代码:它是一个配置对象,选项和组件外面一样
const mymixin = {
methods: {dosomething(){}}
}
// 全局混入:将混入对象传入
Vue.mixin(mymixin)
// 部分混入:做数组项设置到 mixins 选项,仅作用于以后组件
const Comp = {mixins: [mymixin]
}
- 插槽次要用于
vue
组件中的内容散发,也能够用于组件扩大
子组件 Child
<div>
<slot> 这个内容会被父组件传递的内容替换 </slot>
</div>
父组件 Parent
<div>
<Child> 来自父组件内容 </Child>
</div>
如果要准确散发到不同地位能够应用 具名插槽
,如果要应用子组件中的数据能够应用作用域插槽
- 组件选项中还有一个不太罕用的选项
extends
,也能够起到扩大组件的目标
// 扩大对象
const myextends = {
methods: {dosomething(){}}
}
// 组件扩大:做数组项设置到 extends 选项,仅作用于以后组件
// 跟混入的不同是它只能扩大单个对象
// 另外如果和混入发生冲突,该选项优先级较高,优先起作用
const Comp = {extends: myextends}
- 混入的数据和办法不能明确判断起源且可能和以后组件内变量产生命名抵触,
vue3
中引入的composition api
,能够很好解决这些问题,利用独立进去的响应式模块能够很不便的编写独立逻辑并提供响应式的数据,而后在setup
选项中组合应用,加强代码的可读性和维护性。例如
// 复用逻辑 1
function useXX() {}
// 复用逻辑 2
function useYY() {}
// 逻辑组合
const Comp = {setup() {const {xx} = useXX()
const {yy} = useYY()
return {xx, yy}
}
}
Vue-router 跳转和 location.href 有什么区别
- 应用
location.href= /url
来跳转,简略不便,然而刷新了页面; - 应用
history.pushState(/url)
,无刷新页面,动态跳转; - 引进 router,而后应用
router.push(/url)
来跳转,应用了diff
算法,实现了按需加载,缩小了 dom 的耗费。其实应用 router 跳转和应用history.pushState()
没什么差异的,因为 vue-router 就是用了history.pushState()
,尤其是在 history 模式下。
watch 原理
watch
实质上是为每个监听属性 setter
创立了一个 watcher
,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deep
和 immediate
,对应原理如下
deep
:深度监听对象,为对象的每一个属性创立一个watcher
,从而确保对象的每一个属性更新时都会触发传入的回调函数。次要起因在于对象属于援用类型,单个属性的更新并不会触发对象setter
,因而引入deep
可能很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,防止性能节约。immediate
:在初始化时间接调用回调函数,能够通过在created
阶段手动调用回调函数实现雷同的成果
Vue computed 实现
- 建设与其余属性(如:
data
、Store
)的分割; - 属性扭转后,告诉计算属性从新计算
实现时,次要如下
- 初始化
data
,应用Object.defineProperty
把这些属性全副转为getter/setter
。 - 初始化
computed
, 遍历computed
里的每个属性,每个computed
属性都是一个watch
实例。每个属性提供的函数作为属性的getter
,应用Object.defineProperty
转化。 Object.defineProperty getter
依赖收集。用于依赖发生变化时,触发属性从新计算。- 若呈现以后
computed
计算属性嵌套其余computed
计算属性时,先进行其余的依赖收集
说说 vue 内置指令
参考 前端进阶面试题具体解答
Vue 中常见性能优化
编码优化:
- 应用
v-show
复用DOM
:防止反复创立组件
<template>
<div class="cell">
<!-- 这种状况用 v -show 复用 DOM,比 v -if 成果好 -->
<div v-show="value" class="on">
<Heavy :n="10000"/>
</div>
<section v-show="!value" class="off">
<Heavy :n="10000"/>
</section>
</div>
</template>
- 正当应用路由懒加载、异步组件,无效拆分
App
尺寸,拜访时才异步加载
const router = createRouter({
routes: [// 借助 webpack 的 import()实现异步组件
{path: '/foo', component: () => import('./Foo.vue') }
]
})
keep-alive
缓存页面:防止反复创立组件实例,且能保留缓存组件状态
<router-view v-slot="{Component}">
<keep-alive>
<component :is="Component"></component>
</keep-alive>
</router-view>
v-once
和v-memo
:不再变动的数据应用v-once
<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
<h1>comment</h1>
<p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
<li v-for="i in list" v-once>{{i}}</li>
</ul>
按条件跳过更新时应用v-momo
:上面这个列表只会更新选中状态变动项
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
<p>ID: {{item.id}} - selected: {{item.id === selected}}</p>
<p>...more child nodes</p>
</div>
- 长列表性能优化:如果是大数据长列表,可采纳虚构滚动,只渲染少部分区域的内容
<recycle-scroller
class="items"
:items="items"
:item-size="24"
>
<template v-slot="{item}">
<FetchItemView
:item="item"
@vote="voteItem(item)"
/>
</template>
</recycle-scroller>
- 避免外部透露,组件销毁后把全局变量和事件销毁:
Vue
组件销毁时,会主动解绑它的全副指令及事件监听器,然而仅限于组件自身的事件
export default {created() {this.timer = setInterval(this.refresh, 2000)
},
beforeUnmount() {clearInterval(this.timer)
}
}
- 图片懒加载
对于图片过多的页面,为了减速页面加载速度,所以很多时候咱们须要将页面内未呈现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载
<!-- 参考 https://github.com/hilongjw/vue-lazyload -->
<img v-lazy="/static/img/1.png">
- 滚动到可视区域动静加载
https://tangbc.github.io/vue-virtual-scroll-list(opens new window)
- 第三方插件按需引入:(
babel-plugin-component
)
像 element-plus
这样的第三方组件库能够按需引入防止体积太大
import {createApp} from 'vue';
import {Button, Select} from 'element-plus';
const app = createApp()
app.use(Button)
app.use(Select)
- 服务端渲染:SSR
如果 SPA
利用有首屏渲染慢的问题,能够思考SSR
以及上面的其余办法
- 不要将所有的数据都放在
data
中,data
中的数据都会减少getter
和setter
,会收集对应的watcher
v-for
遍历为item
增加key
v-for
遍历防止同时应用v-if
- 辨别
computed
和watch
的应用 - 拆分组件(进步复用性、减少代码的可维护性, 缩小不必要的渲染)
- 防抖、节流
用户体验
app-skeleton
骨架屏pwa
serviceworker
SEO 优化
- 预渲染插件
prerender-spa-plugin
- 服务端渲染
ssr
打包优化
Webpack
对图片进行压缩- 应用
cdn
的形式加载第三方模块 - 多线程打包
happypack
splitChunks
抽离公共文件- 优化
SourceMap
- 构建后果输入剖析,利用
webpack-bundle-analyzer
可视化剖析工具
根底的 Web 技术的优化
- 服务端
gzip
压缩 - 浏览器缓存
CDN
的应用- 应用
Chrome Performance
查找性能瓶颈
构建的 vue-cli 工程都到了哪些技术,它们的作用别离是什么
vue.js
:vue-cli
工程的外围,次要特点是 双向数据绑定 和 组件零碎。vue-router
:vue
官网举荐应用的路由框架。vuex
:专为Vue.js
利用我的项目开发的状态管理器,次要用于保护vue
组件间共用的一些 变量 和 办法。axios
(或者fetch
、ajax
):用于发动GET
、或POST
等http
申请,基于Promise
设计。vuex
等:一个专为vue
设计的挪动端 UI 组件库。- 创立一个
emit.js
文件,用于vue
事件机制的治理。 webpack
:模块加载和vue-cli
工程打包器。
vue 初始化页面闪动问题
应用 vue 开发时,在 vue 初始化之前,因为 div 是不归 vue 管的,所以咱们写的代码在还没有解析的状况下会容易呈现花屏景象,看到相似于 {{message}} 的字样,尽管个别状况下这个工夫很短暂,然而还是有必要让解决这个问题的。
首先:在 css 里加上以下代码:
[v-cloak] {display: none;}
如果没有彻底解决问题,则在根元素加上style="display: none;" :style="{display:'block'}"
Vue 组件之间通信形式有哪些
Vue 组件间通信是面试常考的知识点之一,这题有点相似于凋谢题,你答复出越多办法当然越加分,表明你对 Vue 把握的越纯熟。Vue 组件间通信只有指以下 3 类通信 :
父子组件通信
、隔代组件通信
、兄弟组件通信
,上面咱们别离介绍每种通信形式且会阐明此种办法可实用于哪类组件间通信
组件传参的各种形式
组件通信罕用形式有以下几种
-
props / $emit
实用 父子组件通信- 父组件向子组件传递数据是通过
prop
传递的,子组件传递数据给父组件是通过$emit
触发事件来做到的
- 父组件向子组件传递数据是通过
-
ref
与$parent / $children(vue3 废除)
实用 父子组件通信ref
:如果在一般的DOM
元素上应用,援用指向的就是DOM
元素;如果用在子组件上,援用就指向组件实例$parent / $children
:拜访拜访父组件的属性或办法 / 拜访子组件的属性或办法
-
EventBus($emit / $on)
实用于 父子、隔代、兄弟组件通信- 这种办法通过一个空的
Vue
实例作为地方事件总线(事件核心),用它来触发事件和监听事件,从而实现任何组件间的通信,包含父子、隔代、兄弟组件
- 这种办法通过一个空的
-
$attrs / $listeners(vue3 废除)
实用于 隔代组件通信$attrs
:蕴含了父作用域中不被prop
所辨认 (且获取) 的个性绑定 (class
和style
除外 )。当一个组件没有申明任何prop
时,这里会蕴含所有父作用域的绑定 (class
和style
除外 ),并且能够通过v-bind="$attrs"
传入外部组件。通常配合inheritAttrs
选项一起应用$listeners
:蕴含了父作用域中的 (不含.native
润饰器的)v-on
事件监听器。它能够通过v-on="$listeners"
传入外部组件
-
provide / inject
实用于 隔代组件通信- 先人组件中通过
provider
来提供变量,而后在子孙组件中通过inject
来注入变量。provide / inject
API 次要解决了跨级组件间的通信问题,不过它的应用场景,次要是子组件获取下级组件的状态,跨级组件间建设了一种被动提供与依赖注入的关系
- 先人组件中通过
$root
实用于 隔代组件通信 拜访根组件中的属性或办法,是根组件,不是父组件。$root
只对根组件有用-
Vuex
实用于 父子、隔代、兄弟组件通信Vuex
是一个专为Vue.js
利用程序开发的状态管理模式。每一个Vuex
利用的外围就是store
(仓库)。“store”基本上就是一个容器,它蕴含着你的利用中大部分的状态 (state
)Vuex
的状态存储是响应式的。当Vue
组件从store
中读取状态的时候,若store
中的状态发生变化,那么相应的组件也会相应地失去高效更新。- 扭转
store
中的状态的惟一路径就是显式地提交 (commit
)mutation
。这样使得咱们能够不便地跟踪每一个状态的变动。
依据组件之间关系探讨组件通信最为清晰无效
- 父子组件:
props
/$emit
/$parent
/ref
- 兄弟组件:
$parent
/eventbus
/vuex
- 跨层级关系:
eventbus
/vuex
/provide+inject
/$attrs + $listeners
/$root
上面演示组件之间通信三种状况: 父传子、子传父、兄弟组件之间的通信
1. 父子组件通信
应用
props
,父组件能够应用props
向子组件传递数据。
父组件 vue
模板father.vue
:
<template>
<child :msg="message"></child>
</template>
<script>
import child from './child.vue';
export default {
components: {child},
data () {
return {message: 'father message';}
}
}
</script>
子组件 vue
模板child.vue
:
<template>
<div>{{msg}}</div>
</template>
<script>
export default {
props: {
msg: {
type: String,
required: true
}
}
}
</script>
回调函数(callBack)
父传子:将父组件里定义的 method
作为 props
传入子组件
// 父组件 Parent.vue:<Child :changeMsgFn="changeMessage">
methods: {changeMessage(){this.message = 'test'}
}
// 子组件 Child.vue:<button @click="changeMsgFn">
props:['changeMsgFn']
子组件向父组件通信
父组件向子组件传递事件办法,子组件通过
$emit
触发事件,回调给父组件
父组件 vue
模板father.vue
:
<template>
<child @msgFunc="func"></child>
</template>
<script>
import child from './child.vue';
export default {
components: {child},
methods: {func (msg) {console.log(msg);
}
}
}
</script>
子组件 vue
模板child.vue
:
<template>
<button @click="handleClick"> 点我 </button>
</template>
<script>
export default {
props: {
msg: {
type: String,
required: true
}
},
methods () {handleClick () {
//........
this.$emit('msgFunc');
}
}
}
</script>
2. provide / inject 跨级拜访先人组件的数据
父组件通过应用 provide(){return{}}
提供须要传递的数据
export default {data() {
return {
title: '我是父组件',
name: 'poetry'
}
},
methods: {say() {alert(1)
}
},
// provide 属性 可能为前面的后辈组件 / 嵌套的组件提供所须要的变量和办法
provide() {
return {
message: '我是先人组件提供的数据',
name: this.name, // 传递属性
say: this.say
}
}
}
子组件通过应用 inject:[“参数 1”,”参数 2”,…]
接管父组件传递的参数
<template>
<p> 曾孙组件 </p>
<p>{{message}}</p>
</template>
<script>
export default {
// inject 注入 / 接管先人组件传递的所须要的数据即可
// 接管到的数据 变量 跟 data 外面的变量一样 能够间接绑定到页面 {{}}
inject: ["message","say"],
mounted() {this.say();
},
};
</script>
3. $parent + $children 获取父组件实例和子组件实例的汇合
this.$parent
能够间接拜访该组件的父实例或组件- 父组件也能够通过
this.$children
拜访它所有的子组件;须要留神$children
并不保障程序,也不是响应式的
<!-- parent.vue -->
<template>
<div>
<child1></child1>
<child2></child2>
<button @click="clickChild">$children 形式获取子组件值 </button>
</div>
</template>
<script>
import child1 from './child1'
import child2 from './child2'
export default {data(){
return {total: 108}
},
components: {
child1,
child2
},
methods: {funa(e){console.log("index",e)
},
clickChild(){console.log(this.$children[0].msg);
console.log(this.$children[1].msg);
}
}
}
</script>
<!-- child1.vue -->
<template>
<div>
<button @click="parentClick"> 点击拜访父组件 </button>
</div>
</template>
<script>
export default {data(){
return {msg:"child1"}
},
methods: {
// 拜访父组件数据
parentClick(){this.$parent.funa("xx")
console.log(this.$parent.total);
}
}
}
</script>
<!-- child2.vue -->
<template>
<div>
child2
</div>
</template>
<script>
export default {data(){
return {msg: 'child2'}
}
}
</script>
4. $attrs + $listeners 多级组件通信
$attrs
蕴含了从父组件传过来的所有props
属性
// 父组件 Parent.vue:<Child :name="name" :age="age"/>
// 子组件 Child.vue:<GrandChild v-bind="$attrs" />
// 孙子组件 GrandChild
<p> 姓名:{{$attrs.name}}</p>
<p> 年龄:{{$attrs.age}}</p>
$listeners
蕴含了父组件监听的所有事件
// 父组件 Parent.vue:<Child :name="name" :age="age" @changeNameFn="changeName"/>
// 子组件 Child.vue:<button @click="$listeners.changeNameFn"></button>
5. ref 父子组件通信
// 父组件 Parent.vue:<Child ref="childComp"/>
<button @click="changeName"></button>
changeName(){console.log(this.$refs.childComp.age);
this.$refs.childComp.changeAge()}
// 子组件 Child.vue:data(){
return{age:20}
},
methods(){changeAge(){this.age=15}
}
6. 非父子, 兄弟组件之间通信
vue2
中废除了broadcast
播送和散发事件的办法。父子组件中能够用props
和$emit()
。如何实现非父子组件间的通信,能够通过实例一个vue
实例Bus
作为媒介,要互相通信的兄弟组件之中,都引入Bus
,而后通过别离调用 Bus 事件触发和监听来实现通信和参数传递。Bus.js
能够是这样:
// Bus.js
// 创立一个地方工夫总线类
class Bus {constructor() {this.callbacks = {}; // 寄存事件的名字
}
$on(name, fn) {this.callbacks[name] = this.callbacks[name] || [];
this.callbacks[name].push(fn);
}
$emit(name, args) {if (this.callbacks[name]) {this.callbacks[name].forEach((cb) => cb(args));
}
}
}
// main.js
Vue.prototype.$bus = new Bus() // 将 $bus 挂载到 vue 实例的原型上
// 另一种形式
Vue.prototype.$bus = new Vue() // Vue 曾经实现了 Bus 的性能
<template>
<button @click="toBus"> 子组件传给兄弟组件 </button>
</template>
<script>
export default{
methods: {toBus () {this.$bus.$emit('foo', '来自兄弟组件')
}
}
}
</script>
另一个组件也在钩子函数中监听 on
事件
export default {data() {
return {message: ''}
},
mounted() {this.$bus.$on('foo', (msg) => {this.message = msg})
}
}
7. $root 拜访根组件中的属性或办法
- 作用:拜访根组件中的属性或办法
- 留神:是根组件,不是父组件。
$root
只对根组件有用
var vm = new Vue({
el: "#app",
data() {
return {rootInfo:"我是根元素的属性"}
},
methods: {alerts() {alert(111)
}
},
components: {
com1: {data() {
return {info: "组件 1"}
},
template: "<p>{{info}} <com2></com2></p>",
components: {
com2: {
template: "<p> 我是组件 1 的子组件 </p>",
created() {this.$root.alerts()// 根组件办法
console.log(this.$root.rootInfo)// 我是根元素的属性
}
}
}
}
}
});
8. vuex
- 实用场景: 简单关系的组件数据传递
- Vuex 作用相当于一个用来存储共享变量的容器
state
用来寄存共享变量的中央getter
,能够减少一个getter
派生状态,(相当于store
中的计算属性),用来取得共享变量的值mutations
用来寄存批改state
的办法。actions
也是用来寄存批改 state 的办法,不过action
是在mutations
的根底上进行。罕用来做一些异步操作
小结
- 父子关系的组件数据传递抉择
props
与$emit
进行传递,也可抉择ref
- 兄弟关系的组件数据传递可抉择
$bus
,其次能够抉择$parent
进行传递 - 先人与后辈组件数据传递可抉择
attrs
与listeners
或者Provide
与Inject
- 简单关系的组件数据传递能够通过
vuex
寄存共享的变量
个别在哪个生命周期申请异步数据
咱们能够在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 曾经创立,能够将服务端端返回的数据进行赋值。
举荐在 created 钩子函数中调用异步申请,因为在 created 钩子函数中调用异步申请有以下长处:
- 能更快获取到服务端数据,缩小页面加载工夫,用户体验更好;
- SSR 不反对 beforeMount、mounted 钩子函数,放在 created 中有助于一致性。
既然 Vue 通过数据劫持能够精准探测数据变动,为什么还须要虚构 DOM 进行 diff 检测差别
- 响应式数据变动,
Vue
的确能够在数据变动时,响应式零碎能够立即得悉。然而如果给每个属性都增加watcher
用于更新的话,会产生大量的watcher
从而升高性能 - 而且粒度过细也得导致更新不精确的问题,所以
vue
采纳了组件级的watcher
配合diff
来检测差别
v-model 实现原理
咱们在
vue
我的项目中次要应用v-model
指令在表单input
、textarea
、select
等元素上创立双向数据绑定,咱们晓得v-model
实质上不过是语法糖(能够看成是value + input
办法的语法糖),v-model
在外部为不同的输出元素应用不同的属性并抛出不同的事件:
text
和textarea
元素应用value
属性和input
事件checkbox
和radio
应用checked
属性和change
事件select
字段将value
作为prop
并将change
作为事件
所以咱们能够 v -model 进行如下改写:
<input v-model="sth" />
<!-- 等同于 -->
<input :value="sth" @input="sth = $event.target.value" />
当在
input
元素中应用v-model
实现双数据绑定,其实就是在输出的时候触发元素的input
事件,通过这个语法糖,实现了数据的双向绑定
- 这个语法糖必须是固定的,也就是说属性必须为
value
,办法名必须为:input
。 - 晓得了
v-model
的原理,咱们能够在自定义组件上实现v-model
//Parent
<template>
{{num}}
<Child v-model="num">
</template>
export default {data(){
return {num: 0}
}
}
//Child
<template>
<div @click="add">Add</div>
</template>
export default {props: ['value'], // 属性必须为 value
methods:{add(){
// 办法名为 input
this.$emit('input', this.value + 1)
}
}
}
原理
会将组件的 v-model
默认转化成value+input
const VueTemplateCompiler = require('vue-template-compiler');
const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></el- checkbox>');
// 察看输入的渲染函数:// with(this) {
// return _c('el-checkbox', {
// model: {// value: (check),
// callback: function ($$v) {check = $$v},
// expression: "check"
// }
// })
// }
// 源码地位 core/vdom/create-component.js line:155
function transformModel (options, data: any) {const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
;(data.attrs || (data.attrs = {}))[prop] = data.model.value
const on = data.on || (data.on = {})
const existing = on[event]
const callback = data.model.callback
if (isDef(existing)) {if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback ) {on[event] = [callback].concat(existing)
}
} else {on[event] = callback
}
}
原生的 v-model
,会依据标签的不同生成不同的事件和属性
const VueTemplateCompiler = require('vue-template-compiler');
const ele = VueTemplateCompiler.compile('<input v-model="value"/>');
// with(this) {
// return _c('input', {// directives: [{ name: "model", rawName: "v-model", value: (value), expression: "value" }],
// domProps: {"value": (value) },
// on: {"input": function ($event) {// if ($event.target.composing) return;
// value = $event.target.value
// }
// }
// })
// }
编译时:不同的标签解析出的内容不一样
platforms/web/compiler/directives/model.js
if (el.component) {genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime
return false
} else if (tag === 'select') {genSelect(el, value, modifiers)
} else if (tag === 'input' && type === 'checkbox') {genCheckboxModel(el, value, modifiers)
} else if (tag === 'input' && type === 'radio') {genRadioModel(el, value, modifiers)
} else if (tag === 'input' || tag === 'textarea') {genDefaultModel(el, value, modifiers)
} else if (!config.isReservedTag(tag)) {genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime
return false
}
运行时:会对元素解决一些对于输入法的问题
platforms/web/runtime/directives/model.js
inserted (el, binding, vnode, oldVnode) {if (vnode.tag === 'select') { // #6903
if (oldVnode.elm && !oldVnode.elm._vOptions) {mergeVNodeHook(vnode, 'postpatch', () => {directive.componentUpdated(el, binding, vnode)
})
} else {setSelected(el, binding, vnode.context)
}
el._vOptions = [].map.call(el.options, getValue)
} else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
el._vModifiers = binding.modifiers
if (!binding.modifiers.lazy) {el.addEventListener('compositionstart', onCompositionStart)
el.addEventListener('compositionend', onCompositionEnd)
// Safari < 10.2 & UIWebView doesn't fire compositionend when
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
el.addEventListener('change', onCompositionEnd) /* istanbul ignore if */
if (isIE9) {el.vmodel = true}
}
}
}
Vue 的性能优化有哪些
(1)编码阶段
- 尽量减少 data 中的数据,data 中的数据都会减少 getter 和 setter,会收集对应的 watcher
- v-if 和 v -for 不能连用
- 如果须要应用 v -for 给每项元素绑定事件时应用事件代理
- SPA 页面采纳 keep-alive 缓存组件
- 在更多的状况下,应用 v -if 代替 v -show
- key 保障惟一
- 应用路由懒加载、异步组件
- 防抖、节流
- 第三方模块按需导入
- 长列表滚动到可视区域动静加载
- 图片懒加载
(2)SEO 优化
- 预渲染
- 服务端渲染 SSR
(3)打包优化
- 压缩代码
- Tree Shaking/Scope Hoisting
- 应用 cdn 加载第三方模块
- 多线程打包 happypack
- splitChunks 抽离公共文件
- sourceMap 优化
(4)用户体验
- 骨架屏
- PWA
- 还能够应用缓存 (客户端缓存、服务端缓存) 优化、服务端开启 gzip 压缩等。
Vue 生命周期钩子是如何实现的
vue
的生命周期钩子就是回调函数而已,当创立组件实例的过程中会调用对应的钩子办法- 外部会对钩子函数进行解决,将钩子函数保护成数组的模式
Vue
的生命周期钩子外围实现是利用公布订阅模式先把用户传入的的生命周期钩子订阅好(外部采纳数组的形式存储)而后在创立组件实例的过程中会一次执行对应的钩子办法(公布)
<script>
// Vue.options 中会寄存所有全局属性
// 会用本身的 + Vue.options 中的属性进行合并
// Vue.mixin({// beforeCreate() {// console.log('before 0')
// },
// })
debugger;
const vm = new Vue({
el: '#app',
beforeCreate: [function() {console.log('before 1')
},
function() {console.log('before 2')
}
]
});
console.log(vm);
</script>
相干代码如下
export function callHook(vm, hook) {
// 顺次执行生命周期对应的办法
const handlers = vm.$options[hook];
if (handlers) {for (let i = 0; i < handlers.length; i++) {handlers[i].call(vm); // 生命周期外面的 this 指向以后实例
}
}
}
// 调用的时候
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = mergeOptions(vm.constructor.options, options);
callHook(vm, "beforeCreate"); // 初始化数据之前
// 初始化状态
initState(vm);
callHook(vm, "created"); // 初始化数据之后
if (vm.$options.el) {vm.$mount(vm.$options.el);
}
};
// 销毁实例实现
Vue.prototype.$destory = function() {
// 触发钩子
callHook(vm, 'beforeDestory')
// 本身及子节点
remove()
// 删除依赖
watcher.teardown()
// 删除监听
vm.$off()
// 触发钩子
callHook(vm, 'destoryed')
}
原理流程图
Vue2.x 响应式数据原理
整体思路是数据劫持 + 观察者模式
对象外部通过 defineReactive
办法,应用 Object.defineProperty
来劫持各个属性的 setter
、getter
(只会劫持曾经存在的属性),数组则是通过 重写数组 7 个办法
来实现。当页面应用对应属性时,每个属性都领有本人的 dep
属性,寄存他所依赖的 watcher
(依赖收集),当属性变动后会告诉本人对应的 watcher
去更新(派发更新)
Object.defineProperty 根本应用
function observer(value) { // proxy reflect
if (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
包,使得咱们能够更灵便的应用它,第三方的扩大开发起来更加灵便了
vue-router 动静路由是什么
咱们常常须要把某种模式匹配到的所有路由,全都映射到同个组件。例如,咱们有一个
User
组件,对于所有ID
各不相同的用户,都要应用这个组件来渲染。那么,咱们能够在vue-router
的路由门路中应用“动静门路参数”(dynamic segment) 来达到这个成果
const User = {template: "<div>User</div>",};
const router = new VueRouter({
routes: [
// 动静门路参数 以冒号结尾
{path: "/user/:id", component: User},
],
});
问题: vue-router
组件复用导致路由参数生效怎么办?
解决办法:
- 通过
watch
监听路由参数再发申请
watch: { // 通过 watch 来监听路由变动
"$route": function(){this.getData(this.$route.params.xxx);
}
}
- 用
:key
来阻止“复用”
<router-view :key="$route.fullPath" />
答复范例
- 很多时候,咱们须要将给定匹配模式的路由映射到同一个组件,这种状况就须要定义动静路由
- 例如,咱们可能有一个
User
组件,它应该对所有用户进行渲染,但用户ID
不同。在Vue Router
中,咱们能够在门路中应用一个动静字段来实现,例如:{path: '/users/:id', component: User}
,其中:id
就是门路参数 - 门路参数 用冒号
:
示意。当一个路由被匹配时,它的params
的值将在每个组件中以this.$route.params
的模式裸露进去。 - 参数还能够有多个,例如 /
users/:username/posts/:postId
;除了$route.params
之外,$route
对象还公开了其余有用的信息,如$route.query
、$route.hash
等
组件通信
组件通信的形式如下:
(1)props / $emit
父组件通过 props
向子组件传递数据,子组件通过 $emit
和父组件通信
1. 父组件向子组件传值
props
只能是父组件向子组件进行传值,props
使得父子组件之间造成了一个单向上行绑定。子组件的数据会随着父组件不断更新。props
能够显示定义一个或一个以上的数据,对于接管的数据,能够是各种数据类型,同样也能够传递一个函数。props
属性名规定:若在props
中应用驼峰模式,模板中须要应用短横线的模式
// 父组件
<template>
<div id="father">
<son :msg="msgData" :fn="myFunction"></son>
</div>
</template>
<script>
import son from "./son.vue";
export default {
name: father,
data() {msgData: "父组件数据";},
methods: {myFunction() {console.log("vue");
},
},
components: {son},
};
</script>
// 子组件
<template>
<div id="son">
<p>{{msg}}</p>
<button @click="fn"> 按钮 </button>
</div>
</template>
<script>
export default {name: "son", props: ["msg", "fn"] };
</script>
2. 子组件向父组件传值
$emit
绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过v-on
监听并接管参数。
// 父组件
<template>
<div class="section">
<com-article
:articles="articleList"
@onEmitIndex="onEmitIndex"
></com-article>
<p>{{currentIndex}}</p>
</div>
</template>
<script>
import comArticle from "./test/article.vue";
export default {
name: "comArticle",
components: {comArticle},
data() {return { currentIndex: -1, articleList: ["红楼梦", "西游记", "三国演义"] };
},
methods: {onEmitIndex(idx) {this.currentIndex = idx;},
},
};
</script>
// 子组件
<template>
<div>
<div
v-for="(item, index) in articles"
:key="index"
@click="emitIndex(index)"
>
{{item}}
</div>
</div>
</template>
<script>
export default {props: ["articles"],
methods: {emitIndex(index) {this.$emit("onEmitIndex", index); // 触发父组件的办法,并传递参数 index
},
},
};
</script>
(2)eventBus 事件总线($emit / $on
)
eventBus
事件总线实用于 父子组件 、 非父子组件 等之间的通信,应用步骤如下:(1)创立事件核心治理组件之间的通信
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
(2)发送事件 假如有两个兄弟组件firstCom
和secondCom
:
<template>
<div>
<first-com></first-com>
<second-com></second-com>
</div>
</template>
<script>
import firstCom from "./firstCom.vue";
import secondCom from "./secondCom.vue";
export default {components: { firstCom, secondCom} };
</script>
在 firstCom
组件中发送事件:
<template>
<div>
<button @click="add"> 加法 </button>
</div>
</template>
<script>
import {EventBus} from "./event-bus.js"; // 引入事件核心
export default {data() {return { num: 0};
},
methods: {add() {EventBus.$emit("addition", { num: this.num++});
},
},
};
</script>
(3)接管事件 在secondCom
组件中发送事件:
<template>
<div> 求和: {{count}}</div>
</template>
<script>
import {EventBus} from "./event-bus.js";
export default {data() {return { count: 0};
},
mounted() {EventBus.$on("addition", (param) => {this.count = this.count + param.num;});
},
};
</script>
在上述代码中,这就相当于将 num
值存贮在了事件总线中,在其余组件中能够间接拜访。事件总线就相当于一个桥梁,不必组件通过它来通信。
尽管看起来比较简单,然而这种办法也有不变之处,如果我的项目过大,应用这种形式进行通信,前期保护起来会很艰难。
(3)依赖注入(provide / inject)
这种形式就是 Vue 中的 依赖注入 ,该办法用于 父子组件之间的通信。当然这里所说的父子不肯定是真正的父子,也能够是祖孙组件,在层数很深的状况下,能够应用这种办法来进行传值。就不必一层一层的传递了。
provide / inject
是 Vue 提供的两个钩子,和 data
、methods
是同级的。并且 provide
的书写模式和 data
一样。
provide
钩子用来发送数据或办法inject
钩子用来接收数据或办法
在父组件中:
provide() {
return {num: this.num};
}
在子组件中:
inject: ['num']
还能够这样写,这样写就能够拜访父组件中的所有属性:
provide() {
return {app: this};
}
data() {
return {num: 1};
}
inject: ['app']
console.log(this.app.num)
留神: 依赖注入所提供的属性是 非响应式 的。
(3)ref / $refs
这种形式也是实现 父子组件 之间的通信。
ref
:这个属性用在子组件上,它的援用就指向了子组件的实例。能够通过实例来拜访组件的数据和办法。
在子组件中:
export default {data () {
return {name: 'JavaScript'}
},
methods: {sayHello () {console.log('hello')
}
}
}
在父组件中:
<template>
<child ref="child"></component-a>
</template>
<script>
import child from "./child.vue";
export default {components: { child},
mounted() {console.log(this.$refs.child.name); // JavaScript
this.$refs.child.sayHello(); // hello},
};
</script>
(4)$parent / $children
- 应用
$parent
能够让组件拜访父组件的实例(拜访的是上一级父组件的属性和办法) - 应用
$children
能够让组件拜访子组件的实例,然而,$children
并不能保障程序,并且拜访的数据也不是响应式的。
在子组件中:
<template>
<div>
<span>{{message}}</span>
<p> 获取父组件的值为: {{parentVal}}</p>
</div>
</template>
<script>
export default {data() {return { message: "Vue"};
},
computed: {parentVal() {return this.$parent.msg;},
},
};
</script>
在父组件中:
// 父组件中
<template>
<div class="hello_world">
<div>{{msg}}</div>
<child></child>
<button @click="change"> 点击扭转子组件值 </button>
</div>
</template>
<script>
import child from "./child.vue";
export default {components: { child},
data() {return { msg: "Welcome"};
},
methods: {change() {
// 获取到子组件
this.$children[0].message = "JavaScript";
},
},
};
</script>
在下面的代码中,子组件获取到了父组件的 parentVal
值,父组件扭转了子组件中 message
的值。须要留神:
- 通过
$parent
拜访到的是上一级父组件的实例,能够应用$root
来拜访根组件的实例 - 在组件中应用
$children
拿到的是所有的子组件的实例,它是一个数组,并且是无序的 - 在根组件
#app
上拿$parent
失去的是new Vue()
的实例,在这实例上再拿$parent
失去的是undefined
,而在最底层的子组件拿$children
是个空数组 $children
的值是 数组 ,而$parent
是个 对象
(5)$attrs / $listeners
思考一种场景,如果 A 是 B 组件的父组件,B 是 C 组件的父组件。如果想要组件 A 给组件 C 传递数据,这种隔代的数据,该应用哪种形式呢?
如果是用 props/$emit
来一级一级的传递,的确能够实现,然而比较复杂;如果应用事件总线,在多人开发或者我的项目较大的时候,保护起来很麻烦;如果应用 Vuex,确实也能够,然而如果仅仅是传递数据,那可能就有点节约了。
针对上述情况,Vue 引入了$attrs / $listeners
,实现组件之间的跨代通信。
先来看一下 inheritAttrs
,它的默认值 true,继承所有的父组件属性除props
之外的所有属性;inheritAttrs:false
只继承 class 属性。
$attrs
:继承所有的父组件属性(除了 prop 传递的属性、class 和 style),个别用在子组件的子元素上$listeners
:该属性是一个对象,外面蕴含了作用在这个组件上的所有监听器,能够配合v-on="$listeners"
将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)
A 组件(APP.vue
):
<template>
<div id="app">
// 此处监听了两个事件,能够在 B 组件或者 C 组件中间接触发
<child1
:p-child1="child1"
:p-child2="child2"
@test1="onTest1"
@test2="onTest2"
></child1>
</div>
</template>
<script>
import Child1 from "./Child1.vue";
export default {components: { Child1},
methods: {onTest1() {console.log("test1 running");
},
onTest2() {console.log("test2 running");
},
},
};
</script>
B 组件(Child1.vue
):
<template>
<div class="child-1">
<p>props: {{pChild1}}</p>
<p>$attrs: {{$attrs}}</p>
<child2 v-bind="$attrs" v-on="$listeners"></child2>
</div>
</template>
<script>
import Child2 from "./Child2.vue";
export default {props: ["pChild1"],
components: {Child2},
inheritAttrs: false,
mounted() {this.$emit("test1"); // 触发 APP.vue 中的 test1 办法
},
};
</script>
C 组件 (Child2.vue
):
<template>
<div class="child-2">
<p>props: {{pChild2}}</p>
<p>$attrs: {{$attrs}}</p>
</div>
</template>
<script>
export default {props: ["pChild2"],
inheritAttrs: false,
mounted() {this.$emit("test2"); // 触发 APP.vue 中的 test2 办法
},
};
</script>
在上述代码中:
- C 组件中能间接触发 test 的起因在于 B 组件调用 C 组件时 应用 v-on 绑定了
$listeners
属性 - 在 B 组件中通过 v -bind 绑定
$attrs
属性,C 组件能够间接获取到 A 组件中传递下来的 props(除了 B 组件中 props 申明的)
(6)总结
(1)父子组件间通信
- 子组件通过 props 属性来承受父组件的数据,而后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
- 通过 ref 属性给子组件设置一个名字。父组件通过
$refs
组件名来取得子组件,子组件通过$parent
取得父组件,这样也能够实现通信。 - 应用 provide/inject,在父组件中通过 provide 提供变量,在子组件中通过 inject 来将变量注入到组件中。不管子组件有多深,只有调用了 inject 那么就能够注入 provide 中的数据。
(2)兄弟组件间通信
- 应用 eventBus 的办法,它的实质是通过创立一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现音讯的传递。
- 通过
$parent/$refs
来获取到兄弟组件,也能够进行通信。
(3)任意组件之间
- 应用 eventBus,其实就是创立一个事件核心,相当于中转站,能够用它来传递事件和接管事件。
如果业务逻辑简单,很多组件之间须要同时解决一些公共的数据,这个时候采纳下面这一些办法可能不利于我的项目的保护。这个时候能够应用 vuex,vuex 的思维就是将这一些公共的数据抽离进去,将它作为一个全局的变量来治理,而后其余组件就能够对这个公共数据进行读写操作,这样达到理解耦的目标。
Vue3.0 和 2.0 的响应式原理区别
Vue3.x 改用 Proxy 代替 Object.defineProperty。因为 Proxy 能够间接监听对象和数组的变动,并且有多达 13 种拦挡办法。
相干代码如下
import {mutableHandlers} from "./baseHandlers"; // 代理相干逻辑
import {isObject} from "./util"; // 工具办法
export function reactive(target) {
// 依据不同参数创立不同响应式对象
return createReactiveObject(target, mutableHandlers);
}
function createReactiveObject(target, baseHandler) {if (!isObject(target)) {return target;}
const observed = new Proxy(target, baseHandler);
return observed;
}
const get = createGetter();
const set = createSetter();
function createGetter() {return function get(target, key, receiver) {
// 对获取的值进行喷射
const res = Reflect.get(target, key, receiver);
console.log("属性获取", key);
if (isObject(res)) {
// 如果获取的值是对象类型,则返回以后对象的代理对象
return reactive(res);
}
return res;
};
}
function createSetter() {return function set(target, key, value, receiver) {const oldValue = target[key];
const hadKey = hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
if (!hadKey) {console.log("属性新增", key, value);
} else if (hasChanged(value, oldValue)) {console.log("属性值被批改", key, value);
}
return result;
};
}
export const mutableHandlers = {
get, // 当获取属性时调用此办法
set, // 当批改属性时调用此办法
};
Vue 的事件绑定原理
原生事件绑定是通过
addEventListener
绑定给实在元素的,组件事件绑定是通过Vue
自定义的$on
实现的。如果要在组件上应用原生事件,须要加.native
修饰符,这样就相当于在父组件中把子组件当做一般html
标签,而后加上原生事件。
$on
、$emit
是基于公布订阅模式的,保护一个事件核心,on
的时候将事件按名称存在事件中心里,称之为订阅者,而后 emit
将对应的事件进行公布,去执行事件中心里的对应的监听器
EventEmitter(公布订阅模式 – 简略版)
// 手写公布订阅模式 EventEmitter
class EventEmitter {constructor() {this.events = {};
}
// 实现订阅
on(type, callBack) {if (!this.events) this.events = Object.create(null);
if (!this.events[type]) {this.events[type] = [callBack];
} else {this.events[type].push(callBack);
}
}
// 删除订阅
off(type, callBack) {if (!this.events[type]) return;
this.events[type] = this.events[type].filter(item => {return item !== callBack;});
}
// 只执行一次订阅事件
once(type, callBack) {function fn() {callBack();
this.off(type, fn);
}
this.on(type, fn);
}
// 触发事件
emit(type, ...rest) {this.events[type] && this.events[type].forEach(fn => fn.apply(this, rest));
}
}
// 应用如下
const event = new EventEmitter();
const handle = (...rest) => {console.log(rest);
};
event.on("click", handle);
event.emit("click", 1, 2, 3, 4);
event.off("click", handle);
event.emit("click", 1, 2);
event.once("dbClick", () => {console.log(123456);
});
event.emit("dbClick");
event.emit("dbClick");
源码剖析
- 原生 dom 的绑定
Vue
在创立真是dom
时会调用createElm
, 默认会调用invokeCreateHooks
- 会遍历以后平台下绝对的属性解决代码, 其中就有
updateDOMListeners
办法, 外部会传入add
办法
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {return}
const on = vnode.data.on || {}
const oldOn = oldVnode.data.on || {}
target = vnode.elm normalizeEvents(on)
updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
target = undefined
}
function add (name: string, handler: Function, capture: boolean, passive: boolean) {
target.addEventListener( // 给以后的 dom 增加事件
name,
handler,
supportsPassive ? {capture, passive} : capture
)
}
vue
中绑定事件是间接绑定给实在dom
元素的
- 组件中绑定事件
export function updateComponentListeners (vm: Component, listeners: Object, oldListeners: ?Object) {target = vm updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
function add (event, fn) {target.$on(event, fn)
}
组件绑定事件是通过
vue
中自定义的$on
办法来实现的