My Little World

learn and share


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于
My Little World

多请求loading处理

发表于 2019-03-03

背景

项目中所有http请求走同一个全局公共函数,函数参数为:请求方法,url, 请求参数,回调函数以及是否需要加loading的一个布尔值

一般情况下都需要加loading,所以第5个参数一般不填,默认为true,需要加loading
不加loading的情况也有,例如下载一个文件的过程,如果是由前端来组装文件流,这个时候是不加loading的

loading效果的添加是通过jquery判断有没有loading效果对应的DOM,有的话就先remove再append
loading效果的消失通过jquery remove掉相关DOM元素

即在全局公共函数发出http请求前,根据第5个参数,添加loading,请求结束后取消loading

问题

一个配置页面,跳转过来时需要加载多项数据进行初始化,而且数据之间有一定关联
比如,异步请求A,B,C,D四个数据,请求完D之后,在回调函数中,需要根据D数据发出请求E,再根据E返回数据请求F
导致的现象是,ABCD数据回来比较快,然后loading消失,发出E时loading重现,消失和重现时间距离较短,
造成页面闪烁

目标

加载ABCDEF请求loading仅显示一次,消失一次

分析

1.其实本身发出ABCD四个异步请求时也会发生loading问题,即,如果A请求回来的比其他三个早很多,loading会提前结束
2.从D返回的数据可能为空这时就不需要去请求E,F请求也就不用发出,同样如果E没数据,也就不要请求F,这时候loading在哪结束需要判断

解决

1.在加载loading和消失loading时,添加一层计数,加载loading,计数count加1,消失loading计数count减1,当count减为0时,loading再消失
这样就可以解决异步请求loading提前消失
2.D请求完才会发出E请求,这时如果ABC请求早已结束,count已经被减为0 ,会重新开始计数,loading会重现造成闪烁,还是不能解决直接问题,
解决办法就是,在请求回来时,利用settimeout包裹计数减1,loading消失过程,先执行回调函数再减1,
这样,如果D有数据,count会因为要发请求E先加1,然后执行settimeout减1,这样loading就不会因为减到1而消失
如果 D没数据不发出请求E,同步执行完回调函数后,执行settimeout减1,count减到0,loading消失

后续

回调函数中,有同事代码编写存在BUG,比如没有判断对象是否存在直接去使用其身上的属性值,这样的error会在请求的catch中被捕捉到,
因为请求发生错误,状态码非200都会被捕捉到catch中,这时也需要将loading关闭,
这样就会造成在回调函数发生错误时,count被减了两次,第一次是请求正常回来时,第二次是回调函数发生错误在catch中被减1,
这样count就被减成了小于0,之前的判断以及count等于0,loading消失不再适用,
因此将判断界限定为小于1即loading消失,进行容错处理

My Little World

关于CSS3样式的一点小结

发表于 2019-02-21

border-image

会覆盖border属性设置的边框样式
原理依据border-image-slice将图片切成九宫格
borderOri
EFGH位于边框四角,不参与拉伸或重复
ABCD分别在各自所在边上进行拉伸或者重复
I不参加修饰

border-image-source

图片地址
格式url(imgurl)

border-image-slice

从距离top,right,bottom,left边多少切割图片,切4刀,形成9宫格
可为数字(图像的像素(位图图像)或向量的坐标(如果图像是矢量图像))/百分数(水平/垂直偏移图像宽度的百分之多少)
不设置时,图片整体位于边框四角。
一个值时表示4个距离一致;两个值表示垂直方向两刀和水平两刀距离相同,四个值表示距离四个边各自的距离
特殊情况,
如果距上下边的长度等于高的一半,即在图片水平中线来一刀,则BD两个切片不会被切出来,左右边框会因为没有切片覆盖显示background-color
同理,距左右边的长度等于宽的一半,在图片垂直中线来一刀,则AC两个切片不会被切出来,上下边框会因为没有切片覆盖显示background-color
所以如果仅装饰边框4个角,可以采用距离边等于宽高一半的方法切割图片

border-image-repeat

水平边要不要repeat 垂直边要不要repeat 默认拉伸
可取值:stretch 默认拉伸;repeat 重复切片;round;重复切片,但会适当调整大小,避免出现半截图片
borderimg
borderimg1

border-radius

设置边框外圆半径
5px ———— 4个角都设半径为5px的圆形圆角
5px/10px ———— 4个角都设水平半径为5px,垂直半径为10px的椭圆形圆角
5px 10px 左上和右下角半径5px圆形,右上和左下半径10px的圆形圆角
5px 10px 20px 左上角半径5px圆形,右上和左下半径10px的圆形,右下角半径20px圆形圆角
5px 10px 20px 15px 左上角半径5px圆形,右上半径10px的圆形,右下角半径20px圆形,左下15p圆形圆角
5px 10px 20px 15px/10px 15px 25px 20px 四个角同时设置椭圆型圆角

当半径值小于等于边框border宽度时,border内部不会具有圆角效果,内圆半径=外圆半径-边框宽度
若相邻边宽不同,则角会从宽边平滑到窄边
表格table设置圆角时,只有将border-collapse:separate时才正常显示
如果想取消原有圆角设置,可以将对应值设置为0
特殊应用:直接通过设置容器长宽和border-radius绘制圆形,半圆,扇形,椭圆

1
2
3
4
5
6
7
8
9
width:300px
height:300px
border-radius:150px;//圆形
border-radius: 0 300px 0px 0px;//扇形

width:600px
height:300px
border-radius:150px;//椭圆形
border-radius: 300px 300px 0px 0px;//半圆

box-sizing

content-box ———— 元素width代表内容宽度
border-box ———— 元素width = border+padding + 内容宽度

resize

容器能否拖拽以及拖拽方式
none | both | horizontal | vertial | inherit

outline

相当于border,但不占文档流,不破坏布局
颜色 样式 宽度 偏移

box-shadow

边框阴影,由多个值配置
insert ? 水平方向偏移(正右负左)垂直偏移(正下负上)模糊半径 伸缩半径(相当于阴影宽度)
设置insert时,设置内阴影,不设时,设置外阴影
内阴影用在img标签上无效
实例应用
1.单边阴影
box-shadow:red 0px -5px 5px -3px,给top边设置了阴影,通过设置伸缩半径-3px防止其他边模糊半径5px效果
2.四边阴影
box-shadow:0 0 10px red;//10px为模糊半径
box-shadow:0 0 0 10px red;//10px为伸缩半径,此效果相当于设置了一个宽10px的红边,但不占文档流
层级关系:外阴影—>背景色—>背景图—>内阴影—>边框
3.多层阴影
每组用逗号隔开,靠前的设置面积(伸缩半径)太大的话会遮盖之后的设置

1
2
//彩虹色
box-shadow: red 0px 0px 10px 10px, inset orange 0px 0px 10px 15px, inset yellow 0px 0px 10px 30px, inset green 0px 0px 10px 45px, inset blue 0px 0px 10px 60px, inset purple 0px 0px 10px 75px;

text-shadow

水平方向偏移(正右负左) 垂直偏移(正下负上) 模糊半径 颜色(位置随意)

text-overflow

超出容器是否显示省略号
使用条件
width:500px;容器有具体宽度
overflow: hidden;超出隐藏
white-space: nowrap;//禁止换行

word-wrap

长单词url换行
normal 默认,半角空格/连字符地方换行,长文本UrL会伸到容器外
break-word 在边界换行,不截断英文单词

word-break

自动换行处理方法
normal 默认,整字,英文单词整个换行
break-all 可截断单词换行
keep-all 不允许断开换行,chrome,safari不支持

white-space

页面显示与html文本书写格式处理
normal 根据容器宽度换行,连续空格合并成一个
pre 空白换行跟html书写格式一致,行为类似pre标签
nowrap 不换行,连续空格合并成一个,超出容器尺寸按容器overflow处理
pre-line 换行跟html书写格式一致,续空格合并成一个
pre-wrap 保留空白,正常换行,与pre区别在于pre标签对一些符号上的支持和规范

文本换行

1.pre标签自动换行

1
2
3
4
pre{
white-space:pre;
word-wrap:break-word
}

2.td自动换行

1
2
3
4
5
6
7
8
table{
table-layout:fixed;
width:***px;
}
table td{
overflow:hidden;
word-wrap:break-word;
}

3.除以上两种标签自动换行

1
2
3
4
element{
overflow:hidden;
word-wrap:break-word;
}

4.标签内容强制不换行

1
2
3
4
element{
white-space:nowrap;
word-break:keep-all;
}

background-attachment

背景图是否随页面滚动条滚动,始终不会随容器滚动条滚动
scroll ———— 滚动
fixed ———— 不滚动

background-image

1.起始位置
background-color始终从border与margin交界线的左上角(用A表示)开始
background-image默认从padding与border交界线的左上角(用B表示)开始,但是可以通过设置background-position决定起始点
2.在Z轴上,覆盖顺序
background-color ——> background-image ——> border,conten的背景色
background-repeat值为repeat和no-repeat时,对覆盖有不同影响

background-repeat

图片比容器小时,重复方式
no-repeat ———— 仅从B点开始铺一次,background-image不会填充border部分的background-color,图片不够大时会显示background-color
bgimg2
repeat ———— 从B点开始,先垂直重复,再整体水平重复,background-image会覆盖border部分的background-color,图片起点还是B,然后垂直重复,整体水平重复
bgimg1
repeat-x ———— 从B点开始,仅水平重复,不再整体垂直重复,覆盖border
repeat-y ———— 从B点开始,仅垂直重复,不再整体水平重复,覆盖border

background-position

背景图片开始位置在padding与border交界线什么地方
两个值为长度或者百分数时,第一个值表示容器水平方向的位置,相对于left边位置;第二个值表示容器垂直方向的位置,相对于TOP边位置,可以混用
但只设一个值时,表示距离left位置,垂直方向默认为居中
两个值为关键字时,left,right,center表示水平位置,top,bottom,center表示垂直位置,
前后位置可随意,left bottom 等于 bottom left
仅设置一个值时,另一个方向默认center,例left 相当于left center,背景图位于左边垂直中间位置;center,即水平垂直居中
bgimg3

background-origin

同样可以重置图片开始位置,指定居于哪条线,
同时指定background-position时,先根据background-origin确定线,依线根据background-position确定位置
border-box ———— margin与border交界线
padding-box ———— border与padding交界线,默认值
content-box ———— padding与内容块交界线

background-clip

设置背景色background-color铺设范围
border-box ———— 从margin与border交界线开始铺
padding-box ———— 从border与padding交界线开始铺
content-box ———— 从padding与内容块交界线开始铺

background-size

以background-origin设置的值为容器,设置图片大小
auto 原始尺寸
具体长度值1个或2个
百分数1个或2个 相对容器的百分之多少,不是图片大小
contain 放大,铺满容器
cover 按照图片宽高比例,缩放

opacity

透明度:0-1的值,值越小越透明
表示颜色的rgba,hsla的第四位都表示透明度,取值同opacity

linear-gradient

线性渐变,用于background-image
到达方向,开始颜色,(中间过渡色,)结束颜色
到达方向:to left 从右到左;to top left 从右下到左上角;xx deg,使用角度正值顺时针,负值逆时针
颜色名后空格加百分数,百分数指色标,色标即颜色在整个距离上开始的位置

radical-gradient

径向渐变,用于background-image
半径 (形状)(at 圆心位置),开始颜色,(中间过渡色,)结束颜色
半径:两值相同即为圆形渐变,否则为椭圆渐变;
距离容器中心 closed-side 最近边| closed-corner 最近角| farthest-side 最远边| farthest-corner最远角距离
形状: circle | ellipse
圆心位置:left bottom |100px 200px |20% 15% 默认center

transform

transform-origin

重置变形函数作用起点,但translate()移动函数始终以元素中心点进行位移
left bottom | 20% 10%

transform-style

子元素以2D还是3D形式处理变换过程
flat :2D
present-3d: 3D
如果同时有设置overflow:hidden,那么设置present-3d不生效,相当于flat

perspective

父元素是否呈现3d效果,这越小3D效果越明显,none不显示

perspective-origin

perspective 原点,从哪个点开始呈现3d效果

perspective()

用于子元素,参数大于0时,激活3d效果

back-visibility

旋转180度,背面是否显示为正面的镜面效果,visible | hidden
3D表示是否透视

功能函数

可在transform中空格隔开同时使用多个
2D
位移
translate(_ |X,Y) X正值为向右,Y正值为向下
translateX()
translateY(
)
放缩
scale(_ |X,Y) 0-1缩小,大于1放大,取负值时,先翻转再放缩
scaleX()
scaleY(
)
旋转
rotate()角度值,顺时针正,逆时针负
倾斜
skew(
|X,Y) 水平 垂直方向倾斜
skewX()
skewY(
)

3D
位移
translate3d(X,Y,Z) X正值为向右,Y正值为向下
translateZ()
放缩
scale3d(
|X,Y,Z) 0-1缩小,大于1放大,取负值时,先翻转再放缩
scaleZ()
旋转
rotate3d(X,Y,Z,角度)X,Y,Z值为0-1,表示绕轴旋转的矢量值,角度值,顺时针正,逆时针负
rotateX(
)
rotateY()
rotateZ(
)

以上功能都能让矩阵函数表示
2d时使用matrix()
3d时使用matrix3d()

transition

过度属性(css样式)none/all,具备过渡效果的属性,color,阴影,渐变
过渡时间 s/ms 整个变化持续时间
过度函数 ease|linear|ease-in|ease-out|ease-in-out|step,以上函数表示以什么样的速度变化,都可以用三次贝塞尔曲线实现
延迟时间 s/ms 变化延迟多长时间再开始,赋值为负值时,立即开始,之前的变换被截断
配置多项时,每组配置逗号隔开
终状态一般定义于各种触发伪类:hover,active,focus,checked
或者根据媒体查询结果

@keyframes

定义动画每一帧样式
@keyframes 取个名字apple{
0%{动画第一帧样式}
各种百分数{动画过渡帧样式}
100%{动画最后一帧样式}
}

animation

animation各个子属性
animation-name:@keyframes声明的动画帧的名字,例如apple
animation-duration:播放一遍的时间
animation-timing-function:播放方式
animation-delay:延迟多长时间再播放
animation-iteration-count:播放次数
animation-direction:倒放还是正放动画normal正放alternate倒放
animation-play-state:播放状态running播放|paused暂停
animation-fill-mode:结束定格在哪一帧

@media

媒体查询
@media 10种媒体类型 and (13种设备特性) and (13种设备特性){样式}

@font-face

从服务器加载字体类型

1
2
3
4
@font-face{
font=family:xxx;
src:url()
}
My Little World

探索flex布局

发表于 2019-02-01

flex布局
盒状弹性布局,盒子大小可以随容器大小变化

容器属性

display 决定开始作为弹性容器

—— flex : 将容器设置为弹性伸缩容器
—— inline-flex :使弹性容器成为单个不可分的行内级元素
注意,columns属性此时在flex容器上不在有效果

flex-direction 决定主轴方向
—— row:水平向右,子模块书写顺序横向排列,开头的放在最左端,子模块不设宽度情况下宽度由内容决定
—— row-reverse:水平向左,子模块书写顺序横向排列,开头的放在最右端,子模块不设宽度情况下宽度由内容决定
—— column:从上到下,子模块书写顺序纵向排列,开头的放在最上面,子模块不设高度情况下高度由内容决定
—— column-reverse:从下到上,子模块书写顺序纵向排列,开头的放在最下面,子模块不设高度情况下高度由内容决定
flex1

flex-wrap 对于子模块是否超出最大宽度或高度时要不要换行/列,默认不换行/列,宽/高度自动按所有模块比例缩小
—— nowrap:默认值
—— wrap:换行,水平排列的子模块多出的部分沿column方向排第二行;纵向排列的子模块多出的部分沿row方向排第二列;
—— wrap-reverse:换行,水平排列的子模块多出的部分沿column-reverse方向排第二行;纵向排列的子模块多出的部分沿row-reverse方向排第二列;
flex2.png
flex3.png
flex4.png

flex-flow :[flex-direction][flex-wrap]flex-direction和 flex-wrap的复合属性,
先按flex-direction排列,超出部分用flex-wrap决定
flex5.png

justify-content 在剩余子模块允许换行或者有剩余宽度或者高度时相对主轴的对齐方式,超出但不换行时,按比例放缩
—— center:按实际间距位于主轴中心
—— flex-start:按实际间距位于主轴开始排列的位置,第一个贴边
—— flex-end :按实际间距和顺序向主轴方向排列,最后一个贴边
—— space-between :第一个和最后一个挨着边框,其它间距相等
—— space-around:第一个和最后一个离边框距离为子模块间距的一半
处理主轴为侧轴的布局时,设置了具体高度才会生效
flex6.png

align-items 子模块整体相对侧轴的位置,row和row-reverse侧轴为column,column和column-reverse侧轴为row
—— center:侧轴中心
—— flex-start:侧轴起点位置
—— flex-end :侧轴终点位置
—— stretch:侧轴方向属性值未设置时,拉伸子模块该属性为容器对应属性值,弹性元素被在侧轴方向被拉伸到与容器相同的高度或宽度。
—— baseline:所有模块的内容第一行对齐
flex7.png

align-content 决定多行或者多列在侧轴排列方式,row和row-reverse侧轴为column,column和column-reverse侧轴为row
—— center:整体在侧轴中心
—— flex-start:整体在侧轴起点位置
—— flex-end :整体在侧轴终点位置
—— stretch:侧轴方向属性值未设置时,拉伸子模块该属性填满容器对应属性值
—— space-between :第一行/列和最后一行/列挨着边框,其它间距相等
—— space-around:第一行/列和最后一行/列离边框距离为子模块间距的一半
flex8.png

模块属性

align-self 自己决定相对侧轴的位置
属性值同align-items,默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch。

order 决定模块排列顺序,值越大,越最后被排列

flex-grow 放大比例,当容器空间有剩余时按此比列分配内存空间,填满,比如
宽度1000px的容器,4个子模块一共占了800,剩200,4个模块该值分别设为2 :1:1:1,
则200被分为5份,一份40,每个模块按比例 分别拿80,40,40,40 放大自己的面积

flex-shrink 缩小比例,当容器空间有不足时按此比列分配缩小子模块,比如
容器宽800,5个模块需要占1000,差200,5个模块该值比为4:1:1:1:1,
则200被分为8份,一份25,,每个模块按比例分别减少100,25,25,25,25 缩小自己的面积

flex-basis 在分配多余空间/缩小之前,项目占据的主轴空间,用于计算一共需要多少空间时放大还是缩小
auto 默认值,子模块本来大小

flex :none | [ <’flex-grow’> <’flex-shrink’>? || <’flex-basis’> ] 以上三项复合属性

一些特殊flex 值的表现情况
flex:auto | 1 1 auto; 空间充裕时,模块等比例分配多余空间进行扩展;空间不足时,模块等比例缩小
flex:none | 0 0 auto; 不论空间是否充裕,模块大小根据自身设置的尺寸不变,间距变化
flex:initial | 0 1 auto | 0 auto; 空间即使充裕也保持原始大小,但空间不足时,模块等比例缩小
flex:正值; 模块可根据空间大小收缩,同时多个模块设置时,以该值作为比例进行收缩,放大比等于缩小比

注意,float,clear和vertical-align在伸缩项目上没有效果
flex 实现 圣杯布局

My Little World

Slot复用

发表于 2019-01-13

背景

项目列表组件需要实现点击自动增加一行,可展示自定义内容的可扩展内容
列表table组件被应用在page组件,page组件被应用在具体业务页面,slot的具体内容在业务页面传递进去
tableextend1
问题难点
1.多层组件slot传递
2.同一个slot在table组件中循环使用在多处时,不会被渲染

原理

1.slot 只能一层一层传递,所以解决多层传递,就是在page层应用table时,增加slot接收业务页面传递进来的slot具体内容
2.同名slot不能在同一组件被重复渲染,即不允许有重名slot

1
2
重名的 Slots 移除
同一模板中的重名 <slot> 已经弃用。当一个 slot 已经被渲染过了,那么就不能在同一模板其它地方被再次渲染了。如果要在不同位置渲染同一内容,可一用 prop 来传递。

回归问题

多层组件slot传递

在page层使用table组件时将<slot></slot>放在table组件中,将slot内容当作table的slot传递给table

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
1.业务页面具体自定义内容传入
<PageComponent :pageConfig="pageConfig" ref="child">
<div slot='tableExtend'>
<extendTable :detailConfig="pageConfig.table.isExtend.detailConfig"
v-if="pageConfig.table.isExtend.detailConfig[0].data.length>0">
</extendTable>
<div style="padding: 10px;" v-if="pageConfig.table.isExtend.detailConfig[0].data.length === 0">暂无数据</div>
</div>
</PageComponent>

2.page组件table部分
<section class="page-table">
<tableTemp :table="pageConfig.table" :pageConfig="pageConfig" @sendIds="receiveIds" ref="refTest">
<slot :name='pageConfig.table.isExtend.slot' v-if="pageConfig.table.isExtend"></slot> //将slot放在这里
</tableTemp>
</section>
3.table部分
利用template实现列表行和扩展行一对一
<template v-for="(value,index) in table.tableData">
<tr :key="index">
<td v-for="(val,i) in table.tableEle" v-if="val.display" :key="i">
<div class="extendStyle" v-if='table.isExtend && i==firstShow'>
<a v-show="index != currentActive" @click="loadDetail(value,index)"><i class="ivu-icon ivu-icon-ios-arrow-forward"></i></a> //loadDetail去调父组件函数获取详情数据
<a v-show="index === currentActive" @click="loadDetail(value,index)" class="active"><i class="ivu-icon ivu-icon-ios-arrow-forward"></i></a>
</div>
<div v-if='!table.isExtend'>
...
</div>
</td>
</tr>
//在组装行的时候增加备用扩展行,利用第三方变量控制扩展行显示隐藏
<tr v-if="index === currentActive" :key="index+0.5" class='bgc'>
<td :colspan='tdNumber' id='extend1'>//合并单元格
<slot :name='table.isExtend.slot'></slot>//直接这样是不能渲染的,因为这样通过循环会出现多个同名slot
</td>
</tr>
</template>
//去调父组件函数获取详情数据
loadDetail(item, index) {
this.currentActive = this.currentActive === index ? -1 : index
this.$store.commit('changeTableExtendActive',this.currentActive)
if(this.currentActive === -1){
return
}
let func = this.pageConfig.table.isExtend.func
this.$parent.$parent[func](item,index)
},

slot重用

方法一 作用域插槽

在子组件中将slot中用到的数据传递给slot标签的data属性,
父组件借助slot-scope属性,获取子组件中slot标签的data属性传递的数据
即可应用在slot模板中,另外slot模板中如果用到点击事件回调函数,可以直接在父组件中定义,直接调用

注意slot-scope绑定的变量在使用时,子组件传递的实际值被包含在一个data属性中,所以需要通过slotscope.data.xxxxx去获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
爷爷组件
<PageComponent :pageConfig="pageConfig" ref="child">
<template slot-scope="isExtend">
<extendTable :detailConfig="isExtend.data.detailConfig" //包含的data中
v-if="isExtend.data.detailConfig[0].data.length>0">
</extendTable>
<div style="padding: 10px;" v-if="isExtend.data.detailConfig[0].data.length === 0">暂无数据</div>
</template>
</PageComponent>
父组件
<section class="page-table">
<tableTemp :table="pageConfig.table" :pageConfig="pageConfig" @sendIds="receiveIds" ref="refTest">
<slot :data='pageConfig.table.isExtend' v-if="pageConfig.table.isExtend"></slot>
</tableTemp>
</section>
子组件
<tr v-if="index === currentActive" :key="index+0.5" class='bgc'>
<td :colspan='tdNumber' id='extend1'>
<slot :data='table.isExtend'></slot> //直接使用
</td>
</tr>

插曲

一个业务场景需要同时渲染多个table,即通过一次配置,可渲染多个table的组件,
一个同事将循环table的逻辑放在了table组件本身里面,造成table本身是一个可循环输出多个table的组件
这样如果一开始在爷爷组件放入多个具名slot,因为不能在子组件slot标签添加name属性(会循环出同名slot),
那所有的slot都会出现在每一个table组件中
因此有了方法二
tableextend2

方法二

利用page层传递slot到table组件思想,在table中使用slot时,将其包裹在一个新组件中,
利用新组件复用,实现slot组件复用,(相当于将slot传递到新组件——->待查原理)
而新组件通过render函数依据slot节点生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
子组件
<tr v-if="(indexx+'-'+index) === currentActive" :key="index+0.5" class='bgc'>
<td :colspan='itemConfig.config.length'>
<!-- slot标签要放在tdslot标签充当slot,否则详情数据拉回来之后不会进行更新组件 -->
<tdSlot :name='itemConfig.isExtend.slot'> <slot :name='itemConfig.isExtend.slot'></slot> </tdSlot>
<!-- tdslot在main.js中定义 -->
</td>
</tr>
loadDetail(item, indexx, index) {
this.currentActive = this.currentActive === indexx+'-'+index ? -1 : indexx+'-'+index //通过多层key判断展开
if(this.currentActive === -1){
return
}
let func = this.detailConfig[indexx].isExtend.func
this.$parent.$parent[func](item,indexx, index)
}
新组件带着slot生成
Vue.component('tdSlot', {

render(createElement) {
function deepClone(vnodes, createElement) {
function cloneVNode (vnode) {对slot节点进行深度复制
const clonedChildren = vnode.children && vnode.children.map(vnode => cloneVNode(vnode))
const cloned = createElement(vnode.tag, vnode.data, clonedChildren)
cloned.text = vnode.text
cloned.isComment = vnode.isComment
cloned.componentOptions = vnode.componentOptions
cloned.elm = vnode.elm
cloned.context = vnode.context
cloned.ns = vnode.ns
cloned.isStatic = vnode.isStatic
cloned.key = vnode.key

return cloned
}
const clonedVNodes = vnodes.map(vnode => cloneVNode(vnode))
return clonedVNodes
}
var slots = this.$parent.$slots.default //从父组件拿到slot
var slot = null
for(let i=0;i<slots.length;i++){
if(slots[i].data && this.name === slots[i].data.slot){ //多个slot,拿到自己table.isExtend配置的那一个
slot = slots[i]
break
}
}
return createElement('div',{class:'tdslot'},deepClone([slot], createElement)) //做一下深度复制
<!-- return createElement('div',{class:'tdslot'},[slot]) --> 这样也可以
},
props:{
name:{
type:String,
default:''
}
}
})

父组件
<detailsTable v-if="detailPageConfig.detailConfig" :detailConfig="detailPageConfig.detailConfig">
<slot v-for='(item,index) in detailPageConfig.detailConfig' v-if='item.isExtend' :name='detailPageConfig.detailConfig[index].isExtend.slot' ></slot>
</detailsTable>
爷爷组件
<detailsPage :detailPageConfig="detailPageConfig">
<div slot='tableExtend-1'> //多个slot 并列写即可
<detailsTable :detailConfig="detailPageConfig.detailConfig[1].isExtend.detailConfig"
v-if="this.detailPageConfig.detailConfig[1].isExtend.detailConfig[0].data.length>0">
</detailsTable>
<div v-if="this.detailPageConfig.detailConfig[1].isExtend.detailConfig[0].data.length<1">暂无数据</div>
</div>
</detailsPage>

方法三

其实之所以会出现重名slot是因为table的大循环逻辑放在了自己组件内,
其实输出多个table的逻辑放在page层即可,table还是独立的table,
这样插入slot时就不是同名slot,各自slot,插入各自table

1
2
3
4
5
6
7
8
9
10
11
12
父组件
<template v-for='(item,index) in detailPageConfig.detailConfig'>
<detailsTable :detailConfig="item" :key='index'>
<slot v-if='item.isExtend' :name='item.isExtend.slot' ></slot>
</detailsTable>
</template>
子组件
<tr v-if="index === currentActive" :key="index+0.5" class='bgc'>
<td :colspan='detailConfig.config.length'>
<slot></slot>//只要将自己的slot放进来即可
</td>
</tr>

参考资料
参考资料
参考资料

My Little World

react 合成事件原理

发表于 2019-01-05

事件注册

注册到document

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
1. reactDOMComponent.js
export function setInitialProperties(
domElement: Element,
tag: string,
rawProps: Object,
rootContainerElement: Element | Document,
): void {
const isCustomComponentTag = isCustomComponent(tag, rawProps);
if (__DEV__) { ... }
let props: Object;
switch (tag) {
case 'iframe':
case 'object':
trapBubbledEvent(TOP_LOAD, domElement); //trapBubbledEvent
props = rawProps;
break;
case 'video':
case 'audio':
// Create listener for each media event
for (let i = 0; i < mediaEventTypes.length; i++) {
trapBubbledEvent(mediaEventTypes[i], domElement); //trapBubbledEvent
}
props = rawProps;
break;
case 'source':
trapBubbledEvent(TOP_ERROR, domElement); //trapBubbledEvent
props = rawProps;
break;
case 'img':
case 'image':
case 'link':
trapBubbledEvent(TOP_ERROR, domElement); //trapBubbledEvent
trapBubbledEvent(TOP_LOAD, domElement); //trapBubbledEvent
props = rawProps;
break;
case 'form':
trapBubbledEvent(TOP_RESET, domElement); //trapBubbledEvent
trapBubbledEvent(TOP_SUBMIT, domElement); //trapBubbledEvent
props = rawProps;
break;
case 'details':
trapBubbledEvent(TOP_TOGGLE, domElement); //trapBubbledEvent
props = rawProps;
break;
case 'input':
ReactDOMInputInitWrapperState(domElement, rawProps);
props = ReactDOMInputGetHostProps(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement); //trapBubbledEvent
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange'); // ensureListeningTo
break;
case 'option':
ReactDOMOptionValidateProps(domElement, rawProps);
props = ReactDOMOptionGetHostProps(domElement, rawProps);
break;
case 'select':
ReactDOMSelectInitWrapperState(domElement, rawProps);
props = ReactDOMSelectGetHostProps(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement); //trapBubbledEvent
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange'); // ensureListeningTo
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
props = ReactDOMTextareaGetHostProps(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement); //trapBubbledEvent
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange'); // ensureListeningTo
break;
default:
props = rawProps;
}

assertValidProps(tag, props);

setInitialDOMProperties(
tag,
domElement,
rootContainerElement,
props,
isCustomComponentTag,
);//这个函数中也会用到 ensureListeningTo

switch (tag) {
//给一些DOM标签通过setAttribute赋值
}
}

function setInitialDOMProperties(
tag: string,
domElement: Element,
rootContainerElement: Element | Document,
nextProps: Object,
isCustomComponentTag: boolean,
): void {
if(...){
...
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp != null) {
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
ensureListeningTo(rootContainerElement, propKey); // ensureListeningTo
}
} else if (nextProp != null) {
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
}
}
}

function ensureListeningTo(rootContainerElement, registrationName) {
const isDocumentOrFragment =
rootContainerElement.nodeType === DOCUMENT_NODE ||
rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
const doc = isDocumentOrFragment
? rootContainerElement
: rootContainerElement.ownerDocument;
listenTo(registrationName, doc);
}

2.ReactBrowerEventEmitter.js

export function listenTo(
registrationName: string,
mountAt: Document | Element,
) {
const isListening = getListeningForDocument(mountAt);
const dependencies = registrationNameDependencies[registrationName];

for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
switch (dependency) {
case TOP_SCROLL:
trapCapturedEvent(TOP_SCROLL, mountAt);
break;
case TOP_FOCUS:
case TOP_BLUR:
trapCapturedEvent(TOP_FOCUS, mountAt);
trapCapturedEvent(TOP_BLUR, mountAt);
// We set the flag for a single dependency later in this function,
// but this ensures we mark both as attached rather than just one.
isListening[TOP_BLUR] = true;
isListening[TOP_FOCUS] = true;
break;
case TOP_CANCEL:
case TOP_CLOSE:
if (isEventSupported(getRawEventName(dependency))) {
trapCapturedEvent(dependency, mountAt);
}
break;
case TOP_INVALID:
case TOP_SUBMIT:
case TOP_RESET:
// We listen to them on the target DOM elements.
// Some of them bubble so we don't want them to fire twice.
break;
default:
// By default, listen on the top level to all non-media events.
// Media events don't bubble so adding the listener wouldn't do anything.
const isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1;
if (!isMediaEvent) {
trapBubbledEvent(dependency, mountAt);
}
break;
}
isListening[dependency] = true;
}
}
}

3. ReactDOMEventListener.js

export function trapBubbledEvent(
topLevelType: DOMTopLevelEventType,
element: Document | Element,
) {
if (!element) {
return null;
}
const dispatch = isInteractiveTopLevelEventType(topLevelType)
? dispatchInteractiveEvent
: dispatchEvent;

addEventBubbleListener(
element,//document
getRawEventName(topLevelType),//事件类型
// Check if interactive and wrap in interactiveUpdates
dispatch.bind(null, topLevelType), //回调函数是分发函数 详见执行过程
);
}
export function trapCapturedEvent(topLevelType, element){...}
export function dispatchEvent(topLevelType,nativeEvent) {... }

4. EventListener.js
export function addEventBubbleListener(
element: Document | Element,
eventType: string,
listener: Function,
): void {
element.addEventListener(eventType, listener, false);
}
export function addEventCaptureListener(){...}

在使用setInitialProperties对标签属性初始化的时候,
会根据不同标签自动绑定上冒泡事件或者监听事件
document事件回调函数是分发功能函数,不会对任何事件的直接回调函数进行处理,只会根据事件类型进行分发

存储回调函数

事件执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
//ReactDOMEventListener
export function dispatchEvent(
topLevelType: DOMTopLevelEventType,
nativeEvent: AnyNativeEvent,
) {
if (!_enabled) {
return;
}

const nativeEventTarget = getEventTarget(nativeEvent);
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
if (
targetInst !== null &&
typeof targetInst.tag === 'number' &&
!isFiberMounted(targetInst)
) {
// If we get an event (ex: img onload) before committing that
// component's mount, ignore it for now (that is, treat it as if it was an
// event on a non-React tree). We might also consider queueing events and
// dispatching them after the mount.
targetInst = null;
}

const bookKeeping = getTopLevelCallbackBookKeeping(
topLevelType,
nativeEvent,
targetInst,
);

try {
// Event queue being processed in the same cycle allows
// `preventDefault`.
batchedUpdates(handleTopLevel, bookKeeping);
} finally {
releaseTopLevelCallbackBookKeeping(bookKeeping);
}
}

function handleTopLevel(bookKeeping) {
let targetInst = bookKeeping.targetInst;

// Loop through the hierarchy, in case there's any nested components.
// It's important that we build the array of ancestors before calling any
// event handlers, because event handlers can modify the DOM, leading to
// inconsistencies with ReactMount's node cache. See #1105.
let ancestor = targetInst;
do {
if (!ancestor) {
bookKeeping.ancestors.push(ancestor);
break;
}
const root = findRootContainerNode(ancestor);
if (!root) {
break;
}
bookKeeping.ancestors.push(ancestor);
ancestor = getClosestInstanceFromNode(root);
} while (ancestor);

for (let i = 0; i < bookKeeping.ancestors.length; i++) {
targetInst = bookKeeping.ancestors[i];
runExtractedEventsInBatch(
bookKeeping.topLevelType,
targetInst,
bookKeeping.nativeEvent,
getEventTarget(bookKeeping.nativeEvent),
);
}
}

//ReactGenericBatching.js
export function batchedUpdates(fn, bookkeeping) {
if (isBatching) {
// If we are currently inside another batch, we need to wait until it
// fully completes before restoring state.
return fn(bookkeeping);
}
isBatching = true;
try {
return _batchedUpdatesImpl(fn, bookkeeping);
} finally {
// Here we wait until all updates have propagated, which is important
// when using controlled components within layers:
// https://github.com/facebook/react/issues/1698
// Then we restore state of any controlled component.
isBatching = false;
const controlledComponentsHavePendingUpdates = needsStateRestore();
if (controlledComponentsHavePendingUpdates) {
// If a controlled event was fired, we may need to restore the state of
// the DOM node back to the controlled value. This is necessary when React
// bails out of the update without touching the DOM.
_flushInteractiveUpdatesImpl();
restoreStateIfNeeded();
}
}
}
let _batchedUpdatesImpl = function(fn, bookkeeping) {
return fn(bookkeeping);
};

//EventPluginHub
export function runExtractedEventsInBatch(
topLevelType: TopLevelType,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
) {
const events = extractEvents(//生成合成事件
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
runEventsInBatch(events); //批量更新
}
export function runEventsInBatch(
events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null,
) {
if (events !== null) {
eventQueue = accumulateInto(eventQueue, events);
}

// Set `eventQueue` to null before processing it so that we can tell if more
// events get enqueued while processing.
const processingEventQueue = eventQueue;
eventQueue = null;

if (!processingEventQueue) {
return;
}

forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);//批量执行
invariant(
!eventQueue,
'processEventQueue(): Additional events were enqueued while processing ' +
'an event queue. Support for this has not yet been implemented.',
);
// This would be a good time to rethrow if any of the event handlers threw.
rethrowCaughtError();
}

生成合成事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//EventPluginHub
function extractEvents(
topLevelType: TopLevelType,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null {
let events = null;
for (let i = 0; i < plugins.length; i++) {
// Not every plugin in the ordering may be loaded at runtime.
const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i];
if (possiblePlugin) {
const extractedEvents = possiblePlugin.extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
if (extractedEvents) {
events = accumulateInto(events, extractedEvents);
}
}
}
return events;
}

//DOMEventPluginOrder
const DOMEventPluginOrder = [
'ResponderEventPlugin',
'SimpleEventPlugin',
'EnterLeaveEventPlugin',
'ChangeEventPlugin',
'SelectEventPlugin',
'BeforeInputEventPlugin',
];

export default DOMEventPluginOrder;

获取回调函数

批量执行

My Little World

VUE 数据绑定初探

发表于 2018-12-30

背景

项目中遇到一个需求是需要渲染一个列表,然后点击每一行展开按钮,显示该行一个详情
vuedata1
初步实现想法是在拿到列表数据时,给每一项添加属性show,初始值为false,点击按钮将show改为!show,从而控制详情显示隐藏
但发现不生效,详情没有按照预期显示
探究其原因是因为数据的改变没有引起界面的重新渲染
那么数据是怎么引发界面重新渲染的呢?什么样的数据改变才会引起界面重新渲染呢?

原理

VUE 在进行数据双向绑定时,主要用到了两个思想:数据劫持和订阅发布模式

数据劫持

vue 在拿到export default 的data中声明的变量后,会进行跟踪处理,
这个跟踪处理主要就是将变量的赋值和读取,参照对象属性的setter和getter进行使用Object.defineProperty重写成响应对象
即读取一个变量的数据时会通过调用getter方法return获取值,
重新赋值时会通过setter方法判断新值旧值是否相同,不同则进行更新,从而调用更新函数,进行视图更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()

const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}

// cater for pre-defined getter/setters
const getter = property && property.get
if (!getter && arguments.length === 2) {
val = obj[key]
}
const setter = property && property.set

let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
/******/
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()//获取这个值时,即将获取的这个地方当作订阅者放到订阅者列表中
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
/******/
/******/
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) { //与旧值相同,直接return 不更新
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal //与旧值不同,重新赋值
}
childOb = !shallow && observe(newVal)
dep.notify() //通知更新
}
/******/
})
}

订阅发布模式

进行视图更新时,更新函数需要知道视图哪些地方需要更新,以及由于该变量发生变化可能引起的其他变量变化或者触发一定的功能函数,
所以我们需要对数据的变化进行监听,并告知用到变量的地方(即订阅者),变量发生了变化,需要执行依据此变化要做的事
因为订阅者很可能不止一个(即一个变量被多处用到,包括js和html模板),所以我们需要一个地方存放订阅者,并批量通知订阅者进行更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
mport type Watcher from './watcher' //或初始化watcher
import { remove } from '../util/index'

let uid = 0

/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;

constructor () {
this.id = uid++
this.subs = [] //订阅者集合
}

addSub (sub: Watcher) { //添加订阅者集合
this.subs.push(sub)
}

removeSub (sub: Watcher) {
remove(this.subs, sub)
}

depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}

notify () { //批量更新
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
const targetStack = []

export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target //将watcher缓存到Dep.target
}

export function popTarget () {
Dep.target = targetStack.pop()
}

定义订阅者,订阅者需要自己将自己放到变量的订阅者列表中,采用的方法是将自己暂存在Dep.target上,
在初始化的时候调用一下变量,利用变量getter方法,将自己添加到订阅者列表,然后将Dep.target至空,取消暂存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
getter: Function;
value: any;

constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.value = this.lazy
? undefined
: this.get() //将watcher 自己装进变量的订阅者列表
}

/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this) //暂存到Dep.target
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) //调用变量的getter函数,将自己装到订阅者列表
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()//取消watcher暂存,释放该watcher的绑定
this.cleanupDeps()
}
return value
}


/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () { //dep批量更新调用
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue) //执行更新回调
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue) //执行更新回调
}
}
}
}
}

解析模板

在解析模板时遇到指令或者{{}}时,将变量值替换到dom节点中,然后生成一个订阅者(根据参数将自己添加到对应绑定变量的订阅者列表中),将自己的更新订阅到变量的变化中,从而在变量变化时,更新视图,
每解析到一个地方用到同一个变量,就生成一个watcher,从而反应出需要使用订阅者列表进行批量更新

小结

综上,当变量在声明时会被劫持转成可响应对象,变量的读取更新,通过get和set完成,
在get时添加自己的订阅者,在set时告诉订阅者进行批量更新
订阅者管家由一个Dep对象负责,进行添加,触发更新等工作
订阅者对象需要表明自己是谁,能够将自己添加到订阅者列表,根据变量变化执行更新回调
页面上的变量会在编译时生成订阅器,将自己订阅到变量的订阅列表中,从而在变量变化时得到更新

vuedata2

回归问题

vue 在进行数据劫持时,会对对象进行递归处理,直到递归到的属性值是一个基本变量,即监听到基本变量变化,值监听
所以变量本身值是一个基本变量,则直接进行响应对象转化
但会根据变量是否是数组进行特殊处理,只是对数组每一项进行监听,除非值又是一个数组才会递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data

constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) { //数组
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}

/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}

/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]) //判断对数组值进行判断
}
}
}
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) && //值又是数组
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value) //递归转化
}
if (asRootData && ob) {
ob.vmCount++
}
return ob

带来的缺陷就是
对于对象来说,初始化以后,对象新增的属性不会被纳入监听范围,属性的删除也不能被监听到
对于数组来说,如果是一个对象数组,则数组里每一个对象里的属性值的增删改均不会被监听到

解决办法
对于数组来说,通过使用代码规定的
‘push’,
‘pop’,
‘shift’,
‘unshift’,
‘splice’,
‘sort’,
‘reverse’
几种方法可触发更新,实现对数组的修改,
例如,这里的问题,使用this.data.splice(index,1,obj),将原来的数组项替换掉,
从而达到更新

对于对象来说,可以使用如下两中方法进行更新
this.$set(obj,keyStr,val)//新增属性
obj = Object.assign({},obj,newItemObj) //删除/更改

在本问题中,目前的解决办法是引入新变量存放点击的行数,根据该变量值是否等于当前行,
更改class值,从而控制展开关闭

参照资料
源码链接

My Little World

结合VeeValidate实现对select校验

发表于 2018-12-22

1.对校验项进行手动触发校验

配置 VeeValidate 时,触发事件event设置为‘blur’,而表单在点击提交按钮时,没有对整个表单进行校验,用户如果点开配置表单,不做任何操作就提交的话,会导致页面上不会有任何反应,因此调用 VeeValidate手动触发函数this.$validator.validate(),进行整体校验

this.$validator.validate()会返回promise, resolve的回调函数的参数result 为false时校验不通过,true时校验通过

2.添加select 验证

因为 VeeValidate 只对 input 进行,所以当表单配置项以select呈现时,就算在标签上添加v-validate,也不会对其进行校验,反而还会导致之前配置的blur触发事件失效,所以手动添加对select的校验

之前调用this.$validator.validate()进行手动触发校验input ,返回的是promise,因此在校验完input之后先不管input的校验结果,直接对select 进行校验

select 出现在配置项中有两种情况,一种是通过配置表单时,配置过来的,另外一种是通过slot套嵌进来的,slot也属于配置对象config里面的一种type

之所以会出现需要通过slot 来添加select的情况,是因为这类的select一般需要绑定change事件,与其他配置项形成关联关系,如果以配置项的方式给select添加绑定change事件,会导致infinity loop错误,

导致出现这个错误的原因是渲染配置项的时候用的是一个v-for,一项一项把配置的input,select根据type渲染出来,这时,如果给select绑上change事件,而且change事件的内容是对v-for的对象的属性进行修改vue的机制就会认为触发了无限循环,开始死循环,因此不为配置的项添加change事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
父组件配置对象
modelConfig: {
title: '授权',
modalId: 'modelConfig',
isAdd: true,
config: [
{label: '策略', value: 'v_policy_selected', option: 'v_policy_option', placeholder: '', disabled: false, type: 'select',v_validate: 'required:true',isError: false},
{name: 'selectAz', type: 'slot', v_validate:[
{label: '租户', value: 'v_tenant_selected', isError: 'v_tenant_isError', type: 'select'},
{label: '项目', value: 'v_project_selected', isError: 'v_project_isError', type: 'select'}
]}],
addRow: { // 其他配置项绑定的数据

},
v_select_configs: {
v_policy_selected: null, // 选中数据
v_policy_option: [], // 选择数据
v_project_selected: null, // 选中数据
v_project_option: [], // 选择数据
v_project_isError: false,
v_tenant_selected: null, // 选中数据
v_tenant_option: [], // 选择数据
v_tenant_isError: false
},
saveFunc: 'authSave'
}

主要验证逻辑如下:

校验之前设置flag = true,标志select的校验结果,最后用于与input校验结果result共同决定校验结果

先判断该配置项是否显示在表单中,因为有时新增和编辑的配置项不同,如果不在就不对该项进行校验,continue

对通过配置项展示的select,进行校验,如配置了校验规则v_validate,而且值为未选状态,则显示校验错误提示,同时设置flag = false,否则,不显示校验错误提示

这种错误提示同input,其显示通过配置项对象里面的一个属性决定,因此效果就是,显示提示该属性就设为true,否则设为false

1
2
<label v-show="errors.has(item.value) && isHide(item.hide)" class="col-md-7 help is-danger">{{item.label}} {{errors.first(item.value)}}</label>
<label v-if="item.type === 'select' && item.isError" class="col-md-7 help is-danger">{{item.label}} 不能为空</label>

对通过slot展示的select, 进行校验, 所有需要校验的配置放在v_validate属性中,遍历v_validate,判断select的值是否为未选,未选则显示提示,已选则不显示

错误提示放在slot的代码中,每一项配置自己的错误提示label,label的显示通过v_validate里面每一项配置的对应的错误提示属性的绑定值决定

1
2
3
4
5
6
7
8
9
10
11
<div slot="selectAz">
<v-select
v-model="modelConfig.v_select_configs.v_region_selected"
label="name"
class="col-md-7 v-selectss "
:on-change="changeRegion"
:options="modelConfig.v_select_configs.v_region_option">
</v-select>
<label class="required-tip">*</label>
<label v-if="modelConfig.v_select_configs.v_region_isError" class="col-md-7 help is-danger">xxx 不能为空</label>
</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
具体代码
formValidate () {
return this.$validator.validate().then(result => {
//result 为false插件验证input没有填写完整,true为验证填写完整
/** 验证 select是否进行了选填 实例可参照 [manage][authorizations]user-authorized.vue **/
let flag = true
for(let i=0; i< this.modelConfig.config.length; i++){
if(!this.isHide (this.modelConfig.config[i].hide)){
continue
}
/* ****** 配置里面的select ***** */
//配置规则为:在配置type:selcet时,如需校验则添加v_validate: 'required:true',isError: false==>控制错误提示label显示
//如果无需校验,则不添加
if(this.modelConfig.config[i].type === 'select' && this.modelConfig.config[i].v_validate) {
let obj = this.modelConfig.config[i]
if(!this.modelConfig.v_select_configs[obj.value]){
this.modelConfig.config[i].isError = true
flag = false
}else{
this.modelConfig.config[i].isError = false
}
}
/* ****** slot里面的select ****** */
//配置规则为:在配置type:slot 时,添加 v_validate:[],数组里面存放需要校验的select的配置信息
//value:绑定值,isError:错误标签显示绑定值,type:select ===> 如果以后再校验其他类型,再增加判断逻辑
//{name:'xxxx',type:'slot',v_validate:[{value: 'v_xxx_selected', isError: 'v_xxx_isError', type: 'select'}]}
//同时在this.modelConfig.v_select_configs里面定义v_xxx_isError:false
//slot里面错误提示label显示用 v-if="modelConfig.v_select_configs.v_xxx_isError" 搭配其他具体规则进行组合
if(this.modelConfig.config[i].type === 'slot') {
let arr = this.modelConfig.config[i].v_validate ? this.modelConfig.config[i].v_validate :[]
for(let j =0;j<arr.length;j++){
let key = arr[j].isError
let value = arr[j].value
if(arr[j].type === 'select' && !this.modelConfig.v_select_configs[value]) {
this.modelConfig.v_select_configs[key] = true
flag = false
}else{
this.modelConfig.v_select_configs[key] = false
}
}
}
}
return result && flag
})
},

3.勾选选项后错误消失

在公共组件里面为prop添加watch方法,当对象属性发生变化时就检查select是否有值了,有的话,就将提示隐藏掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
data () {
return {
configCopy:this.modelConfig,
FLAG:false
}
},
props: ['modelConfig'],
watch:{
configCopy: {
handler(){
//select选择完将提示隐藏,如果日后保留'X'删除功能,如需要,再增加显示处理逻辑
for(let key in this.modelConfig.v_select_configs) { //slot select
if(key.endsWith('isError')){
let prefix = key.slice(0,-7)
if(this.modelConfig.v_select_configs[key] && this.modelConfig.v_select_configs[prefix+'selected']){
this.modelConfig.v_select_configs[key] = false
}
}
}
for(let i=0; i< this.modelConfig.config.length; i++){ //config select
if(this.modelConfig.config[i].type === 'select' && this.modelConfig.config[i].v_validate) {
let obj = this.modelConfig.config[i]
if(this.modelConfig.v_select_configs[obj.value]){
this.modelConfig.config[i].isError = false
}
}
}
},
deep:true
}
},

4.关闭配置页面的清空

检查配置项对象里面所有的select配置项,将其负责错误提示显示的项设置为false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 清除表单selected
for(let i=0; i<this.modelConfig.config.length; i++){
if(this.modelConfig.config[i].type === 'select' && this.modelConfig.config[i].v_validate) {
this.modelConfig.config[i].isError = false
}
if(this.modelConfig.config[i].type === 'slot') {
let arr = this.modelConfig.config[i].v_validate ? this.modelConfig.config[i].v_validate :[]
for(let j =0;j<arr.length;j++){
let key = arr[j].isError
let value = arr[j].value
if(arr[j].type === 'select' && !this.modelConfig.v_select_configs[value]) {
this.modelConfig.v_select_configs[key] = false
}
}
}
}

My Little World

自定义VUE指令

发表于 2018-12-22

使用marquee标签实现文字悬浮滚动效果

1
2
3
4
5
6
7
8
9
10
11
//一般实现
<div id="demo">
<div v-if="show" style="z-index:99999;width200px;margin-left:20px;margin-top:-20px;background-color:#00a4ff;width:200px;position: absolute;border-radius:5px;padding-top: 5px;">
<marquee >
<span style="font-weight: bolder;font-size: 10px;color: white;">Welcom CMRH!</span>
</marquee>
</div>
<button @mouseover="show = !show" @mouseout="show = !show">
Toggle
</button>
</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//全局定义
Vue.directive('demo', function (el, binding) {
console.log(el,binding)
let str = '<div id="pointer" style="z-index:99999;width200px;margin-left:20px;margin-top:-20px;background-color:#00a4ff;width:200px;position: absolute;border-radius:5px;padding-top: 5px;"><marquee><span style="font-weight: bolder;font-size: 10px;color: white;">'
+ binding.value.text +
'</span></marquee></div>'
$(el).mouseover (function() {
$(el).prepend(str)
})
$(el).mouseout (function() {
$('#pointer').remove()
})
})

//使用
<div v-demo="{ color: 'white', text: 'hello!' }">111111</div>

通过指令传递的数据,通过binding.value读取

My Little World

定时器

发表于 2018-12-22

1.使用 setInterval

1
2
this.timerID = setInterval(fn, time);
clearInterval(this.timerID);

2.angular 中使用 $interval

1
2
this.timerID = $interval(fn, time);
$interval.cancel(this.timerID)

My Little World

触发render

发表于 2018-12-22

在react中,触发render的有4条路径。

以下假设shouldComponentUpdate都是按照默认返回true的方式。

首次渲染Initial Render

调用this.setState (并不是一次setState会触发一次render,React可能会合并操作,再一次性进行render)

父组件发生更新(一般就是props发生改变,但是就算props没有改变或者父子组件之间没有数据交换也会触发render)

调用this.forceUpdate

https://www.cnblogs.com/AnnieBabygn/p/6560833.html

1…101112…26
YooHannah

YooHannah

260 日志
1 分类
23 标签
RSS
© 2025 YooHannah
由 Hexo 强力驱动
主题 - NexT.Pisces