事件注册
注册到document
1 | 1. reactDOMComponent.js |
在使用setInitialProperties对标签属性初始化的时候,
会根据不同标签自动绑定上冒泡事件或者监听事件
document事件回调函数是分发功能函数,不会对任何事件的直接回调函数进行处理,只会根据事件类型进行分发
存储回调函数
事件执行
1 | //ReactDOMEventListener |
生成合成事件
1 | //EventPluginHub |
learn and share
1 | 1. reactDOMComponent.js |
在使用setInitialProperties对标签属性初始化的时候,
会根据不同标签自动绑定上冒泡事件或者监听事件
document事件回调函数是分发功能函数,不会对任何事件的直接回调函数进行处理,只会根据事件类型进行分发
1 | //ReactDOMEventListener |
1 | //EventPluginHub |
项目中遇到一个需求是需要渲染一个列表,然后点击每一行展开按钮,显示该行一个详情
初步实现想法是在拿到列表数据时,给每一项添加属性show,初始值为false,点击按钮将show改为!show,从而控制详情显示隐藏
但发现不生效,详情没有按照预期显示
探究其原因是因为数据的改变没有引起界面的重新渲染
那么数据是怎么引发界面重新渲染的呢?什么样的数据改变才会引起界面重新渲染呢?
VUE 在进行数据双向绑定时,主要用到了两个思想:数据劫持和订阅发布模式
vue 在拿到export default 的data中声明的变量后,会进行跟踪处理,
这个跟踪处理主要就是将变量的赋值和读取,参照对象属性的setter和getter进行使用Object.defineProperty重写成响应对象
即读取一个变量的数据时会通过调用getter方法return获取值,
重新赋值时会通过setter方法判断新值旧值是否相同,不同则进行更新,从而调用更新函数,进行视图更新
1 | /** |
进行视图更新时,更新函数需要知道视图哪些地方需要更新,以及由于该变量发生变化可能引起的其他变量变化或者触发一定的功能函数,
所以我们需要对数据的变化进行监听,并告知用到变量的地方(即订阅者),变量发生了变化,需要执行依据此变化要做的事
因为订阅者很可能不止一个(即一个变量被多处用到,包括js和html模板),所以我们需要一个地方存放订阅者,并批量通知订阅者进行更新
1 | mport type Watcher from './watcher' //或初始化watcher |
定义订阅者,订阅者需要自己将自己放到变量的订阅者列表中,采用的方法是将自己暂存在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
104export 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对象负责,进行添加,触发更新等工作
订阅者对象需要表明自己是谁,能够将自己添加到订阅者列表,根据变量变化执行更新回调
页面上的变量会在编译时生成订阅器,将自己订阅到变量的订阅列表中,从而在变量变化时得到更新
vue 在进行数据劫持时,会对对象进行递归处理,直到递归到的属性值是一个基本变量,即监听到基本变量变化,值监听
所以变量本身值是一个基本变量,则直接进行响应对象转化
但会根据变量是否是数组进行特殊处理,只是对数组每一项进行监听,除非值又是一个数组才会递归
1 | export class Observer { |
带来的缺陷就是
对于对象来说,初始化以后,对象新增的属性不会被纳入监听范围,属性的删除也不能被监听到
对于数组来说,如果是一个对象数组,则数组里每一个对象里的属性值的增删改均不会被监听到
解决办法
对于数组来说,通过使用代码规定的
‘push’,
‘pop’,
‘shift’,
‘unshift’,
‘splice’,
‘sort’,
‘reverse’
几种方法可触发更新,实现对数组的修改,
例如,这里的问题,使用this.data.splice(index,1,obj),将原来的数组项替换掉,
从而达到更新
对于对象来说,可以使用如下两中方法进行更新
this.$set(obj,keyStr,val)//新增属性
obj = Object.assign({},obj,newItemObj) //删除/更改
在本问题中,目前的解决办法是引入新变量存放点击的行数,根据该变量值是否等于当前行,
更改class值,从而控制展开关闭
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,否则设为false1
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 | 具体代码 |
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
31data () {
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
}
}
}
}
使用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 | //全局定义 |
通过指令传递的数据,通过binding.value读取
1.使用 setInterval1
2this.timerID = setInterval(fn, time);
clearInterval(this.timerID);
2.angular 中使用 $interval
1
2this.timerID = $interval(fn, time);
$interval.cancel(this.timerID)
在react中,触发render的有4条路径。
以下假设shouldComponentUpdate都是按照默认返回true的方式。
首次渲染Initial Render
调用this.setState (并不是一次setState会触发一次render,React可能会合并操作,再一次性进行render)
父组件发生更新(一般就是props发生改变,但是就算props没有改变或者父子组件之间没有数据交换也会触发render)
调用this.forceUpdate
指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM
v-if 条件指令,绑定值为真时构建DOM,假时bu构建或者删除已存在DOM1
2
3
4
5
6
7
8
9
10<p v-if="seen">现在你看到我了</p>
export default {
name: 'vpc',
data () {
return {
seen: true
}
}
}
v-for 循环指令,根据绑定值,循环输出DOM元素1
2
3
4<div v-for="(item, index) in modalFooter">
<button @click="customFunc(item.Func)" type="button" class="btn btn-primary" v-if='item.name'>{{item.name}}</button>
</div>
//根据数据modalFooter数组循环出多个数组
v-on 监听 DOM 事件的指令,触发事件的回调函数放在method中,可以简写成‘@事件名’1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<div id="app-5">
<p>{{ message }}</p>
<button v-on:click="reverseMessage">逆转消息</button>
//或者 <button @click="reverseMessage">逆转消息</button>
</div>
var app5 = new Vue({
el: '#app-5',
data: {
message: 'Hello Vue.js!'
},
methods: {
reverseMessage: function () {
this.message = this.message.split('').reverse().join('')
}
}
})
v-model 双向绑定指令,绑定的值在页面更改后js可以及时更改,js改变该值后,页面能马上渲染响应1
2
3
4
5
6
7
8
9
10
11<div id="app-6">
<p>{{ message }}</p>
<input v-model="message">
</div>
var app6 = new Vue({
el: '#app-6',
data: {
message: 'Hello Vue!'
}
})
v-bind 绑定HTML标签属性,响应式地更新 HTML 特性 缩写形式” :属性名=’响应函数名’ “1
2
3
4
5
6
7
8
9
10
11
12
13
14<div id="app-2">
<span v-bind:title="message">
//或者<span :title="message">
鼠标悬停几秒钟查看此处动态绑定的提示信息!
</span>
</div>
var app2 = new Vue({
el: '#app-2',
data: {
message: '页面加载于 ' + new Date().toLocaleString()
}
})
//在这里,该指令的意思是:“将这个元素节点的 title 特性和 Vue 实例的 message 属性保持一致”
使用命令行构建1
2
3
4
5
npm install --global vue-cli //全局安装vue命令
vue init webpack my-project //使用vue命令构建名为my-project的项目
cd my-project //进入项目所在文件夹
npm run dev //运行项目
.
├── build/ # webpack config files
│ └── ...
├── config/
│ ├── index.js # main project config
│ └── ...
├── src/
│ ├── main.js # app entry file
│ ├── App.vue # main app component
│ ├── components/ # ui components
│ │ └── ...
│ └── assets/ # module assets (processed by webpack)
│ └── ...
├── static/ # pure static assets (directly copied)
├── test/
│ └── unit/ # unit tests
│ │ ├── specs/ # test spec files
│ │ ├── eslintrc # config file for eslint with extra settings only for unit tests
│ │ ├── index.js # test build entry file
│ │ ├── jest.conf.js # Config file when using Jest for unit tests
│ │ └── karma.conf.js # test runner config file when using Karma for unit tests
│ │ ├── setup.js # file that runs before Jest runs your unit tests
│ └── e2e/ # e2e tests
│ │ ├── specs/ # test spec files
│ │ ├── custom-assertions/ # custom assertions for e2e tests
│ │ ├── runner.js # test runner script
│ │ └── nightwatch.conf.js # test runner config file
├── .babelrc # babel config
├── .editorconfig # indentation, spaces/tabs and similar settings for your editor
├── .eslintrc.js # eslint config
├── .eslintignore # eslint ignore rules
├── .gitignore # sensible defaults for gitignore
├── .postcssrc.js # postcss config
├── index.html # index.html template
├── package.json # build scripts and dependencies
└── README.md # Default README file
其值为vue模板挂载ID,用于标识vue的APP template挂载在index.html的什么位置
通过{ {}}}插值的时候,可以在大括号里面添加一个简单的计算表达式,从而将计算结果插入,但是对于复杂的计算,需要执行多个计算表达式时不能放在大括号里面的,为方便绑定,在computed属性中定义计算属性A,A是一个函数用到变量b,然后将该A属性插入,从而实现相关值b变化,插入值A随之响应的效果
相当于计算属性因为依赖项改变而执行计算,将计算结果插入1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21例
<p>Original data: "{{ value }}"</p>
<p>Computed result: "{{ doubleValue }}"</p>
<button @click='changeValue'><button>
export default {
data() {
value: 10
},
computed: {
doubleValue: function() {
return this.value * 2
}
},
method: {
changeValue: function() {
this.value++
}
}
}
//当value 变化时,doubleValue通过运算将结果插入
与method区别
也可以在大括号里面调用method中定义的方法,达到计算响应1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<p>{{doubleVlue()}}</p>
<button @click='changeValue'><button>
export default {
data() {
value: 10
},
method: {
doubleValue: function() {
return this.value * 2
},
changeValue: function() {
this.value++
}
}
}
当计算属性函数依赖的值不发生变化时,每次调用计算属性都会去取最后一次运算的结果,即读取缓存的结果
比如有一个计算属性A需要遍历一个庞大的数组然后得到一个结果,而这个结果则用于其他计算,这样我们就不需要每次去运算A属性函数得到结果,直接读取缓存结果即可
但如果属性A结果的得来是通过运行method方法,那么每次调用A就会进行计算一次,就会造成很大开销
1 | //computed方式 |
计算属性还可以设置getter和setter1
2
3
4
5
6
7
8
9
10
11
12
13
14
15computed: {
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
},
// setter
set: function (newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
//给fullName设置值的时候,比如fullName = 'John Doe' 时,setter 会被调用,firstName 和 lastName 也会相应地被更新
定义某个变量发生变化时需要执行的函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<p>Original data: "{{ value }}"</p>
<p>Computed result: {{ result }}</p>
<button @click='changeValue'>1234<button>
export default {
data() {
value: 10
result: 0
},
watch: {
value: function() {
this.result +=10
}
},
method: {
changeValue: function() {
this.value++
}
}
}
//当value发生变化的时候,就会执行watch绑定的函数,从而可以让result发生响应
对比computed属性,假设一个结果需要依赖多个变量,如果使用watch方法则需要定义多个响应函数,而且响应函数是重复的,而如果使用computed属性则只需要定义一次即可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例
//computed方式
<p>Original data: "{{ value }}"</p>
<p>Computed result: {{ result }}</p>
<button @click='changeValue'>1234</button>
export default {
data() {
value: 10,
val:1
},
computed: {
result: function() {
return this.val + this.value
}
},
method: {
changeValue: function() {
this.value++
}
}
}
//watch方式
<p>Original data: "{{ value }}"</p>
<p>Computed result: {{ result }}</p>
<button @click='changeValue'>1234</button>
export default {
data() {
value: 10,
val:1
},
watch: {
value: function() {
this.result = this.val + this.value
},
val: function() {
this.result = this.val + this.value
}
},
method: {
changeValue: function() {
this.value++
}
}
}
在display:flex基础上使用flex-direction,可用于多子模块布局
flex-direction可以为四个值
row:水平并行排列在左侧
row-reverse:水平并行排列在右侧,顺序与书写顺序相反,最前的在最右边
column:垂直排列在上方,相当于display:block状态下模块默认布局
column-reverse:垂直排列在下方,顺序与书写顺序相反,最前的在最下边
1 | <!doctype html> |