⑴ vue3源码分析-实现props,emit,事件处理等
>
本期来实现, setup里面使用props,父子组件通信props和emit等 ,所有的源码请查看
在render函数中, 可以通过this,来访问setup返回的内容,还可以访问this.$el等
由于是测试dom,jest需要提前注入下面的内容,让document里面有app节点,下面测试用例类似在html中定义一个app节点哦
本功能的测试用例正式开始
上面的测试用例
解决这两个需求:
针对上面的分析,需要在setupStatefulComponent中来创建proxy并且绑定到instance当中,并且setup的执行结果如果是对象,也已经存在instance中了,可以通过instance.setupState来进行获取
通过上面的操作,从render中this.xxx获取setup返回对象的内容就ok了,接下来处理el
需要在mountElement中,创建节点的时候,在vnode中绑定下,el,并且在setupStatefulComponent 中的代理对象中判断当前的key
看似没有问题吧,但是实际上是有问题的,请仔细思考一下, mountElement是不是比setupStatefulComponent 后执行,setupStatefulComponent执行的时候,vnode.el不存在,后续mountelement的时候,vnode就会有值,那么上面的测试用例肯定是报错的,$el为null
解决这个问题的关键,mountElement的加载顺序是 render -> patch -> mountElement,并且render函数返回的subtree是一个vnode,改vnode中上面是mount的时候,已经赋值好了el,所以在patch后执行下操作
在vue中,可以使用onEvent来写事件,那么这个功能是怎么实现的呢,咋们一起来看看
在本功能的测试用例中,可以分析以下内容:
解决问题:
这个功能比较简单,在处理prop中做个判断, 属性是否满足 /^on[A-Z]/i这个格式,如果是这个格式,则进行事件注册,但是vue3会做事件缓存,这个是怎么做到?
缓存也好实现,在传入当前的el中增加一个属性 el._vei || (el._vei = {}) 存在这里,则直接使用,不能存在则创建并且存入缓存
事件处理就ok啦
父子组件通信,在vue中是非常常见的,这里主要实现props与emit
根据上面的测试用例,分析props的以下内容:
解决问题:
问题1: 想要在子组件的setup函数中第一个参数, 使用props,那么在setup函数调用的时候,把当前组件的props传入到setup函数中即可 问题2: render中this想要问题,则在上面的那个代理中,在 加入一个判断,key是否在当前instance的props中 问题3: 修改报错,那就是只能读,可以使用以前实现的 api shallowReadonly来包裹一下 既可
做完之后,可以发现咋们的测试用例是运行没有毛病的
上面实现了props,那么emit也是少不了的,那么接下来就来实现下emit
根据上面的测试用例,可以分析出:
解决办法: 问题1: emit 是setup的第二个参数, 那么可以在setup函数调用的时候,传入第二个参数 问题2: 关于emit的第一个参数, 可以做条件判断,把xxx-xxx的形式转成xxxXxx的形式,然后加入on,最后在props中取找,存在则调用,不存在则不调用 问题3:emit的第二个参数, 则使用剩余参数即可
到此就圆满成功啦!
⑵ 【面试题解析】从 Vue 源码分析 key 的作用
最近看了面试题中有一个这样的题, v-for 为什么要绑定 key?
Vue 中 key 很多人都弄不清楚有什么作用,甚至还有些人认为不绑定 key 就会报错。
其实没绑定 key 的话,Vue 还是可以正常运行的,报警告是因为没通过 Eslint 的检查。
接下来将通过源码一步步分析这个 key 的作用。
Virtual DOM 最主要保留了 DOM 元素的层级关系和一些基本属性,本质上就是一个 JS 对象。相对于真实的 DOM,Virtual DOM 更简单,操作起来速度更快。
如果需要改变 DOM,则会通过新旧 Virtual DOM 对比,找出需要修改的节点进行真实的 DOM 操作,从而减小性能消耗。
传统的 Diff 算法需要遍历一个树的每个节点,与另一棵树的每个节点对比,时间复杂度为 O(n²)。
Vue 采用的 Diff 算法则通过逐级对比,大大降低了复杂性,时间复杂度为 O(n)。
VNode 更新首先会经过 patch 函数, patch 函数源码如下:
vnode 表示更新后的节点,oldVnode 表示更新前的节点,通过对比新旧节点进行操作。
1、vnode 未定义,oldVnode 存在则触发 destroy 的钩子函数
2、oldVnode 未定义,则根据 vnode 创建新的元素
3、oldVnode 不为真实元素并且 oldVnode 与 vnode 为同一节点,则会调用 patchVnode 触发更新
4、oldVnode 为真实元素或者 oldVnode 与 vnode 不是同一节点,另做处理
接下来会进入 patchVnode 函数,源码如下:
1、vnode 的 text 不存在,则会比对 oldVnode 与 vnode 的 children 节点进行更新操作
2、vnode 的 text 存在,则会修改 DOM 节点的 text
接下来在 updateChildren 函数内就可以看到 key 的用处。
key 的作用主要是给 VNode 添加唯一标识,通过这个 key,可以更快找到新旧 VNode 的变化,从而进一步操作。
key 的作用主要表现在以下这段源码中。
updateChildren 过程为:
1、分别用两个指针(startIndex, endIndex)表示 oldCh 和 newCh 的头尾节点
2、对指针所对应的节点做一个两两比较,判断是否属于同一节点
3、如果4种比较都没有匹配,那么判断是否有 key,有 key 就会用 key 去做一个比较;无 key 则会通过遍历的形式进行比较
4、比较的过程中,指针往中间靠,当有一个 startIndex > endIndex,则表示有一个已经遍历完了,比较结束
从 VNode 的渲染过程可以得知,Vue 的 Diff 算法先进行的是同级比较,然后再比较子节点。
子节点比较会通过 startIndex、endIndex 两个指针进行两两比较,再通过 key 比对子节点。如果没设置 key,则会通过遍历的方式匹配节点,增加性能消耗。
所以不绑定 key 并不会有问题,绑定 key 之后在性能上有一定的提升。
综上,key 主要是应用在 Diff 算法中,作用是为了更快速定位出相同的新旧节点,尽量减少 DOM 的创建和销毁的操作。
希望以上内容能够对各位小伙伴有所帮助,祝大家面试顺利。
Vue 的文档中对 key 的说明如下:
关于就地修改,关键在于 sameVnode 的实现,源码如下:
可以看出,当 key 未绑定时,主要通过元素的标签等进行判断,在 updateChildren 内会将 oldStartVnode 与 newStartVnode 判断为同一节点。
如果 VNode 中只包含了文本节点,在 patchVnode 中可以直接替换文本节点,而不需要移动节点的位置,确实在不绑定 key 的情况下效率要高一丢丢。
某些情况下不绑定 key 的效率更高,那为什么大部分Eslint的规则还是要求绑定 key 呢?
因为在实际项目中,大多数情况下 v-for 的节点内并不只有文本节点,那么 VNode 的字节点就要进行销毁和创建的操作。
相比替换文本带来的一丢丢提升,这部分会消耗更多的性能,得不偿失。
了解了就地修改,那么我们在一些简单节点上可以选择不绑定 key,从而提高性能。
如果你喜欢我的文章,希望可以关注一下我的公众号【前端develop】
⑶ Vue组件中prop属性使用说明实例代码详解
Prop
的大小写
(camelCase
vs
kebab-case)
HTML
中的特性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用
DOM
中的模板时,camelCase
(驼峰命名法)
的
prop
名需要使用其等价的
kebab-case
(短横线分隔命名)
命名:
Vue.component('blog-post',
{
//
在
JavaScript
中是
camelCase
的
props:
['postTitle'],
template:
'<h3>{{
postTitle
}}</h3>'
})
<!--
在
HTML
中是
kebab-case
的
-->
<blog-post
post-title="hello!"></blog-post>
重申桥岩一次,如果你使用字符串模板,那么这个限制就不存在了。
静态的和动态的
Prop
像这样,你已经知道了可以像这样给
prop
传入一个静态的值:
<blog-post
title="My
journey
with
Vue"></blog-post>
你也知道
prop
可以通过
v-bind
动态赋值,例如:
<blog-post
v-bind:title="post.title"></blog-post>
在上述两个示例中,我们传入的值都是字符串类型的,但实际上任何类型的值都可以传给一个
prop。
传入一个数字
<!--
即便
`42`
是静态的,我们仍然需要
`v-bind`
来告诉
Vue
-->
<!--
这是一个
JavaScript
表达式而不是一个字符串。-->
<blog-post
v-bind:likes="42"></blog-post>
<!--
用一个变量进行动态赋值。-->
<blog-post
v-bind:likes="post.likes"></blog-post>
传入一个布尔值
<!--
包含该
prop
没有值的情况在内,都意味着
`true`。-->
<blog-post
favorited></blog-post>
<!--
即便
`false`
是静态的,我们仍然需要
`v-bind`
来告诉
Vue
-->
<!--
这是一个
JavaScript
表达式而不是一个字符串。-->
<base-input
v-bind:favorited="false">
<!--
用一个变量进行动态赋值。-->
<base-input
v-bind:favorited="post.currentUserFavorited">
传入一个数组
<!--
即便数组是静态的,我们仍然需要
`v-bind`
来告诉
Vue
-->
<!--
这是一个
JavaScript
表达式而不是一个字符串。-->
<blog-post
v-bind:comment-ids="[234,
266,
273]"></blog-post>
<!--
用一个变量进行动态赋值。-->
<blog-post
v-bind:comment-ids="post.commentIds"></blog-post>
传入一个对象
<!--
即便对象是静态的,我们仍然需要
`v-bind`
来告诉
Vue
-->
<!--
这是一个
JavaScript
表达式而不是一个字符串。-->
<blog-post
v-bind:comments="{
id:
1,
title:
'My
Journey
with
Vue'
}"></blog-post>
<!--
用一个变量进行动态赋值。-->
<blog-post
v-bind:post="post"></blog-post>
传入一个对象的所有属性
如果你想要将一个对象的所有属性都作为
prop
传入,你可以使用不带参数的
v-bind
(取代
v-bind:prop-name)。例如,对于一个给斗漏定的对象
post:
post:
{
id:
1,
title:
'My
Journey
with
Vue'
}
下面的模板:
<blog-post
v-bind="post"></blog-post>
等价于:
<blog-post
v-bind:id="post.id"
v-bind:title="post.title"
></blog-post>
Vue的组件中的props属性单向数据流
所有的
prop都使得其父子prop之间形成了一个单向下行绑定:父级
prop
的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父级组件发生更新时,子组件中所有的
prop
都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变
prop。如果你这样做了,Vue
会在浏览器的控制台中发出警告。
这敏销御里有两种常见的试图改变一个
prop
的情形:
这个
prop
用来传递一个初始值;这个子组件接下来希望将其作为一个本地的
prop
数据来使用。在这种情况下,最好定义一个本地的
data
属性并将这个
prop
用作其初始值:
props:
['initialCounter'],
data:
function
()
{
return
{
counter:
this.initialCounter
}
}
这个
prop
以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个
prop
的值来定义一个计算属性:
props:
['size'],
computed:
{
normalizedSize:
function
()
{
return
this.size.trim().toLowerCase()
}
}
注意在
JavaScript
中对象和数组是通过引用传入的,所以对于一个数组或对象类型的
prop
来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态。
Prop
验证
我们可以为组件的
prop
指定需求。如果有一个需求没有被满足,则
Vue
会在浏览器控制台中警告你。这在开发一个会被别人用到的组件时尤其有帮助。
为了定制
prop
的验证方式,你可以为
props
中的值提供一个带有验证需求的对象,而不是一个字符串数组。例如:
Vue.component('my-component',
{
props:
{
//
基础的类型检查
(`null`
匹配任何类型)
propA:
Number,
//
多个可能的类型
propB:
[String,
Number],
//
必填的字符串
propC:
{
type:
String,
required:
true
},
//
带有默认值的数字
propD:
{
type:
Number,
default:
100
},
//
带有默认值的对象
propE:
{
type:
Object,
//
对象或数组且一定会从一个工厂函数返回默认值
default:
function
()
{
return
{
message:
'hello'
}
}
},
//
自定义验证函数
propF:
{
validator:
function
(value)
{
//
这个值必须匹配下列字符串中的一个
return
['success',
'warning',
'danger'].indexOf(value)
!==
-1
}
}
}
})
当
prop
验证失败的时候,(开发环境构建版本的)
Vue
将会产生一个控制台的警告。
注意那些
prop
会在一个组件实例创建之前进行验证,所以实例的属性
(如
data、computed
等)
在
default
或
validator
函数中是不可用的。
类型检查
type
可以是下列原生构造函数中的一个:
String
Number
Boolean
Function
Object
Array
Symbol
额外的,type
还可以是一个自定义的构造函数,并且通过
instanceof
来进行检查确认。例如,给定下列现成的构造函数:
function
Person
(firstName,
lastName)
{
this.firstName
=
firstName
this.lastName
=
lastName
}
你可以使用:
Vue.component('blog-post',
{
props:
{
author:
Person
}
})
来验证
author
prop
的值是否是通过
new
Person
创建的。
非
Prop
的特性
一个非
prop
特性是指传向一个组件,但是该组件并没有相应
prop
定义的特性。
因为显式定义的
prop
适用于向一个子组件传入信息,然而组件库的作者并不总能预见组件会被用于怎样的场景。这也是为什么组件可以接受任意的特性,而这些特性会被添加到这个组件的根元素上。
例如,想象一下你通过一个
Bootstrap
插件使用了一个第三方的
组件,这个插件需要在其
上用到一个
data-date-picker
特性。我们可以将这个特性添加到你的组件实例上:
<bootstrap-date-input
data-date-picker="activated"></bootstrap-date-input>
然后这个
data-date-picker=”activated”
特性就会自动添加到<bootstrap-date-input>
的根元素上。
替换/合并已有的特性
想象一下
<bootstrap-date-input>
的模板是这样的:
<input
type="date"
class="form-control">
为了给我们的日期选择器插件定制一个主题,我们可能需要像这样添加一个特别的类名:
<bootstrap-date-input
data-date-picker="activated"
class="date-picker-theme-dark"
></bootstrap-date-input>
在这种情况下,我们定义了两个不同的
class
的值:
form-control,这是在组件的模板内设置好的
date-picker-theme-dark,这是从组件的父级传入的
对于绝大多数特性来说,从外部提供给组件的值会替换掉组件内部设置好的值。所以如果传入
type=”text”
就会替换掉
type=”date”
并把它破坏!庆幸的是,class
和
style
特性会稍微智能一些,即两边的值会被合并起来,从而得到最终的值:form-control
date-picker-theme-dark。
禁用特性继承
如果你不希望组件的根元素继承特性,你可以设置在组件的选项中设置
inheritAttrs:
false。例如:
Vue.component('my-component',
{
inheritAttrs:
false,
//
...
})
这尤其适合配合实例的
$attrs
属性使用,该属性包含了传递给一个组件的特性名和特性值,例如:
{
class:
'username-input',
placeholder:
'Enter
your
username'
}
有了
inheritAttrs:
false
和
$attrs,你就可以手动决定这些特性会被赋予哪个元素。在撰写基础组件的时候是常会用到的:
Vue.component('base-input',
{
inheritAttrs:
false,
props:
['label',
'value'],
template:
`
<label>
{{
label
}}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input',
$event.target.value)"
>
</label>
`
})
这个模式允许你在使用基础组件的时候更像是使用原始的
HTML
元素,而不会担心哪个元素是真正的根元素:
<base-input
v-model="username"
class="username-input"
placeholder="Enter
your
username"
></base-input>
总结
以上所述是小编给大家介绍的Vue组件中prop属性使用说明,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对脚本之家网站的支持!
您可能感兴趣的文章:VueJs组件prop验证简单介绍Vue组件选项props实例详解vue父组件通过props如何向子组件传递方法详解详解vue2父组件传递props异步数据到子组件的问题详解vue父子组件间传值(props)Vue2.0利用
v-model
实现组件props双向绑定的优美解决方案
⑷ Vue源码系列之生命钩子beforeCreate&created
在实习面试中,Vue相关被问到最多的就是围绕生命周期函数内部所作的一些操作,很多朋友可能了解的就是官网的下图,有时候倒霉遇上硬核点的面试官,深入问些东西就会被怼的哑口无言,本文就是在之前源码的基础上,分析一下,生命周期函数究竟发生了什么
先上图
在分析每个钩子之间指坦究竟干了什么之前,先来看看钩子是怎么触发的,以第一个钩子为例
调用callHook函数并向其传入this和'beforeCreate'字符串,那来看看callHook函数究竟是何方神圣
注意到,该函数一开始,也就是钩子函数进入准备触发前,进行了一个pushTarget()的操作,注释写的是“在钩子函数触发时,禁用依赖收集”,那这个操作是干嘛呢
言归正传,禁用了依赖收集后,创建handlers数组存入合并后options的hook,本例中就是找自定义或者继承来的beforeCreate钩子,然后在invokeWithErroeHandling函数中以此触发,顺序是先触发父级,后自己定义的
而这个invokeWithErroeHandling函数如下
该函数直接就在内部call了钩子,所以钩子内部的this指向vm实例
以上就是钩子函数的触发过程,下面来看不同的生命周期之间究竟干了什么
beforeCreate之前
beforeCreate之后到created
1.对于props:合法化,缓存key进数组方便下次迭代,defineReactive
2.对于data:检验props,methods中是否有重名属性,defineReactive
3.对于methods:代理到vm实例上,方便使用this.method.name调用
4.对于computed:封装成watcher并用该watcher的value缓存蚂兆该计算属性的value,再在每个计算属性上劫持一层getter和setter,在第一次调用getter的时候,取得最新闷逗租的value,并将依赖缓存下来,之后再依赖不变的前提下,getter只返回watcher的value而不是又去取一遍值,再依赖发生变化的时候,通知watcher更新,watcher取的最新值作为value,从而实现依赖更新计算属性才更新
5.对于watch:调用$watch封装成一个user watcher,如果有immediate options传入,就在封装的时候就调用一遍callback,有deep options传进来的话就将该属性的所有嵌套属性记为依赖
beforeCreate之前主要是做准备工作,将该实例的options合并整理出来,再把$那些初始话
created之前就是对options做操作,data,props设置数据劫持,methods代理在vm实例上,computed,watch封装成不同类型的watcher
⑸ Vue中的props理解
Vue实例中的data属性是一个对象,然而组件中的data属性是一个函数。这是因为一个组件可以在同一个页面上被多次引用,你大概不枝镇希望他们共享一个data对象(因为同一个组件的每个实例的data属性是同一个对象的引用,当该组件的某个实例修改了自身的data属性,相当于所有组件的data属性都被修改了。)所以组件的data属性应该是一个函数,在组件初始化是Vue会调用这个函数来生成data对象。
App.vue
DisplayNumber.vue
App.vue
再看另外一个例子:
App.vue
结果:
DisplayNumber.vue
将Javascript表达式(Javascript expression)和HTML 元素和组件(HTML elements and components)绑定起来。
在大多数时候,可以简写为:
看例子:
HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时猛丛粗,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名。
在HTML中通郑稿过kebab-case形式指定的属性,会在组件内部自动转换为camelCase 形式。我们不需要作任何额外的处理,这种转换会自动完成。
官方推荐,始终使用 kebab-case(短横线隔开) 的事件名。
⑹ 面试中的网红Vue源码解析之虚拟DOM,你知多少呢深入解读diff算法
众所周知,在前端的面试中,面试官非常爱考dom和diff算法。比如,可能会出现在以下场景
滴滴滴,面试官发来一个面试邀请。接受邀请📞
我们都知道, key 的作用在前端的面试是一道很普遍的题目,但是呢,很多时候我们都只浮于知识的表面,而没有去深挖其原理所在,这个时候我们的竞争力就在这被拉下了。所以呢,深入学习原理对于提升自身的核心竞争力是一个必不可少的过程。
在接下来的这篇文章中,我们将讲解面试中很爱考的虚拟DOM以及其背后的diff算法。 请认真阅读本文~文末有学习资源免费共享!!!
虚拟DOM是用JavaScript对象描述DOM的层次结构。DOM中的一切属性都在虚拟DOM中有对应的属性。本质上是JS 和 DOM 之间的一个映射缓存。
要点:虚拟 DOM 是 JS 对象;虚拟 DOM 是对真实 DOM 的描述。
diff发生在虚拟DOM上。diff算法是在新虚拟DOM和老虚拟DOM进行diff(精细化比对),实现最小量更新,最后反映到真正的DOM上。
我们前面知道diff算法发生在虚拟DOM上,而虚拟DOM是如何实现的呢?实际上虚拟DOM是有一个个虚拟节点组成。
h函数用来产生虚拟节点(vnode)。虚拟节点有如下的属性:
1)sel: 标签类型,例如 p、div;
2)data: 标签上的数据,例如 style、class、data-*;
3)children :子节点;
4) text: 文本内容;
5)elm:虚拟节点绑定的真实 DOM 节点;
通过h函数的嵌套,从而得到虚拟DOM树。
我们编写了一个低配版的h函数,必须传入3个参数,重载较弱。
形态1:h('div', {}, '文字')
形态2:h('div', {}, [])
形态3:h('div', {}, h())
首先定义vnode节点,实际上就是把传入的参数合成对象返回。
[图片上传失败...(image-7a9966-1624019394657)]
然后编写h函数,根据第三个参数的不同进行不同的响应。
当我们进行比较的过程中,我们采用的4种命中查找策略:
1)新前与旧前:命中则指针同时往后移动。
2)新后与旧后:命中则指针同时往前移动。
3)新后与旧前:命中则涉及节点移动,那么新后指向的节点,移到 旧后之后 。
4)新前与旧后:命中则涉及节点移动,那么新前指向的节点,移到 旧前之前 。
命中上述4种一种就不在命中判断了,如果没有命中,就需要循环来寻找,移动到旧前之前。直到while(新前<=新后&&旧前<=就后)不成立则完成。
如果是新节点先循环完毕,如果老节点中还有剩余节点(旧前和旧后指针中间的节点),说明他们是要被删除的节点。
如果是旧节点先循环完毕,说明新节点中有要插入的节点。
1.什么是Virtual DOM 和Snabbdom
2.手写底层源码h函数
3.感受Vue核心算法之diff算法
4.snabbdom之核心h函数的工作原理
1、零基础入门或者有一定基础的同学、大中院校学生
2、在职从事相关工作1-2年以及打算转行前端的朋友
3、对前端开发有兴趣人群