My Little World

learn and share


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于
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

My Little World

指令

发表于 2018-12-22

指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM

简单指令

v-if 条件指令,绑定值为真时构建DOM,假时bu构建或者删除已存在DOM

1
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 属性保持一致”

自定义指令

My Little World

文件结构

发表于 2018-12-22

构建

使用命令行构建

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
My Little World

vue对象属性

发表于 2018-12-22

el

其值为vue模板挂载ID,用于标识vue的APP template挂载在index.html的什么位置

data

method

计算属性computed

通过{ {}}}插值的时候,可以在大括号里面添加一个简单的计算表达式,从而将计算结果插入,但是对于复杂的计算,需要执行多个计算表达式时不能放在大括号里面的,为方便绑定,在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
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
//computed方式
<p>Original data: "{{ value }}"</p>
<p>Computed result: {{ double }}</p>
<button @click='changeValue'><button>

export default {
data() {
value: 10
},
computed: {
double: function() {
return this.value + this.doubleValue
},
doubleValue: function() {
console.log('double') //只打印一次
return 2
}
},
method: {
changeValue: function() {
this.value++
}
}
}
//method方式

<p>Original data: "{{ value }}"</p>
<p>Computed result: {{ value + doubleValue() }}</p>
<button @click='changeValue'>1234<button>

export default {
data() {
value: 10
},
method: {
doubleValue: function() {
console.log('double') //value 改变一次,打印一次,即函数会被执行一次
return 2
},
changeValue: function() {
this.value++
}
}
}

计算属性还可以设置getter和setter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
computed: {
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 也会相应地被更新

watch

定义某个变量发生变化时需要执行的函数

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++
}
}
}

生命周期钩子函数

components

My Little World

flex-drection属性使用

发表于 2018-12-22

在display:flex基础上使用flex-direction,可用于多子模块布局

flex-direction可以为四个值

row:水平并行排列在左侧

row-reverse:水平并行排列在右侧,顺序与书写顺序相反,最前的在最右边

column:垂直排列在上方,相当于display:block状态下模块默认布局

column-reverse:垂直排列在下方,顺序与书写顺序相反,最前的在最下边

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
<!doctype html>
<html>
<head>
<style>
#main1
{
width:200px;
height: 300px;
border:1px solid black;
display: flex;
flex-direction:row;
}
#main2
{
width:200px;
height: 300px;
border:1px solid black;
display: flex;
flex-direction:row-reverse;
}
#main3
{
width:200px;
height: 300px;
border:1px solid black;
display: flex;
flex-direction:column;
}
#main4
{
width:200px;
height: 300px;
border:1px solid black;
display: flex;
flex-direction:column-reverse;
}
</style>
</head>
<body>
<h4>This is an example for flex-direction:row(default)</h4>
<div id="main1">
<div style="background-color:red;">RED</div>
<div style="background-color:lightblue;">BLUE</div>
<div style="background-color:lightgreen;">GREEN</div>
</div>
<h4>This is an example for flex-direction:row-reverse</h4>
<div id="main2">
<div style="background-color:red;">RED</div>
<div style="background-color:lightblue;">BLUE</div>
<div style="background-color:lightgreen;">GREEN</div>
</div>
<h4>This is an example for flex-direction:column</h4>
<div id="main3">
<div style="background-color:red;">RED</div>
<div style="background-color:lightblue;">BLUE</div>
<div style="background-color:lightgreen;">GREEN</div>
</div>
<h4>This is an example for flex-direction:column-reverse</h4>
<div id="main4">
<div style="background-color:red;">RED</div>
<div style="background-color:lightblue;">BLUE</div>
<div style="background-color:lightgreen;">GREEN</div>
</div>
</body>
</html>
1…91011…25
YooHannah

YooHannah

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