Vue事件绑定原理

Vue中通过v-on或其语法糖@指令来给元素绑定事件并且提供了事件修饰符,基本流程是进行模板编译生成AST,生成render函数后并执行得到VNode,VNode生成真实DOM节点或者组件时候使用addEventListener方法进行事件绑定。

描述

v-on与@用于绑定事件监听器,事件类型由参数指定,表达式可以是一个方法的名字或一个内联语句,如果没有修饰符也可以省略,用在普通元素上时,只能监听原生DOM事件,用在自定义元素组件上时,也可以监听子组件触发的自定义事件,在监听原生DOM事件时,方法以事件为唯一的参数,如果使用内联语句,语句可以访问一个$event property:v-on:click="handle('param', $event)",自2.4.0开始v-on同样支持不带参数绑定一个事件或监听器键值对的对象,注意当使用对象语法时,是不支持任何修饰器的。

修饰符

  • .stop: 调用event.stopPropagation(),即阻止事件冒泡。
  • .prevent: 调用event.preventDefault(),即阻止默认事件。
  • .capture: 添加事件侦听器时使用capture模式,即使用事件捕获模式处理事件。
  • .self: 只当事件是从侦听器绑定的元素本身触发时才触发回调。
  • .{keyCode | keyAlias}: 只当事件是从特定键触发时才触发回调。
  • .native: 监听组件根元素的原生事件,即注册组件根元素的原生事件而不是组件自定义事件的。
  • .once: 只触发一次回调。
  • .left(2.2.0): 只当点击鼠标左键时触发。
  • .right(2.2.0): 只当点击鼠标右键时触发。
  • .middle(2.2.0): 只当点击鼠标中键时触发。
  • .passive(2.3.0): 以{ passive: true }模式添加侦听器,表示listener永远不会调用preventDefault()。

普通元素

<!-- 方法处理器 -->
<button v-on:click="doThis"></button> <!-- 动态事件 (2.6.0+) -->
<button v-on:[event]="doThis"></button> <!-- 内联语句 -->
<button v-on:click="doThat('param', $event)"></button> <!-- 缩写 -->
<button @click="doThis"></button> <!-- 动态事件缩写 (2.6.0+) -->
<button @[event]="doThis"></button> <!-- 停止冒泡 -->
<button @click.stop="doThis"></button> <!-- 阻止默认行为 -->
<button @click.prevent="doThis"></button> <!-- 阻止默认行为,没有表达式 -->
<form @submit.prevent></form> <!-- 串联修饰符 -->
<button @click.stop.prevent="doThis"></button> <!-- 键修饰符,键别名 -->
<input @keyup.enter="onEnter"> <!-- 键修饰符,键代码 -->
<input @keyup.13="onEnter"> <!-- 点击回调只会触发一次 -->
<button v-on:click.once="doThis"></button> <!-- 对象语法 (2.4.0+) -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>

组件元素

<!-- 自定义事件 -->
<my-component @my-event="handleThis"></my-component> <!-- 内联语句 -->
<my-component @my-event="handleThis('param', $event)"></my-component> <!-- 组件中的原生事件 -->
<my-component @click.native="onClick"></my-component>

分析

Vue源码的实现比较复杂,会处理各种兼容问题与异常以及各种条件分支,文章分析比较核心的代码部分,精简过后的版本,重要部分做出注释,commit id为ef56410。

编译阶段

Vue在挂载实例前,有相当多的工作是进行模板的编译,将template模板进行编译,解析成AST树,再转换成render函数,而在编译阶段,就是对事件的指令做收集处理。

在template模板中,定义事件的部分是属于XML的Attribute,所以收集指令时需要匹配Attributes以确定哪个Attribute是属于事件。

// dev/src/compiler/parser/index.js line 23
export const onRE = /^@|^v-on:/
export const dirRE = process.env.VBIND_PROP_SHORTHAND
? /^v-|^@|^:|^\.|^#/
: /^v-|^@|^:|^#/
// ...
const dynamicArgRE = /^\[.*\]$/
// ...
export const bindRE = /^:|^\.|^v-bind:/ // dev/src/compiler/parser/index.js line 757
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, syncGen, isDynamic
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) { // 匹配指令属性
// mark element as dynamic
el.hasBindings = true
// modifiers
modifiers = parseModifiers(name.replace(dirRE, '')) // 将修饰符解析
// support .foo shorthand syntax for the .prop modifier
if (process.env.VBIND_PROP_SHORTHAND && propBindRE.test(name)) {
(modifiers || (modifiers = {})).prop = true
name = `.` + name.slice(1).replace(modifierRE, '')
} else if (modifiers) {
name = name.replace(modifierRE, '')
}
if (bindRE.test(name)) { // v-bind // 处理v-bind的情况
// ...
} else if (onRE.test(name)) { // v-on // 处理事件绑定
name = name.replace(onRE, '') // 将事件名匹配
isDynamic = dynamicArgRE.test(name) // 动态事件绑定
if (isDynamic) { // 如果是动态事件
name = name.slice(1, -1) // 去掉两端的 []
}
addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic) // 处理事件收集
} else { // normal directives // 处理其他指令
// ...
}
} else {
// literal attribute // 处理文字属性
// ...
}
}
}

通过addHandler方法,为AST树添加事件相关的属性以及对事件修饰符进行处理。

// dev/src/compiler/helpers.js line 69
export function addHandler (
el: ASTElement,
name: string,
value: string,
modifiers: ?ASTModifiers,
important?: boolean,
warn?: ?Function,
range?: Range,
dynamic?: boolean
) {
modifiers = modifiers || emptyObject
// passive 和 prevent 不能同时使用,具体是由passive模式的性质决定的
// 详细可以参阅 https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener
// warn prevent and passive modifier
/* istanbul ignore if */
if (
process.env.NODE_ENV !== 'production' && warn &&
modifiers.prevent && modifiers.passive
) {
warn(
'passive and prevent can\'t be used together. ' +
'Passive handler can\'t prevent default event.',
range
)
}
// 标准化click.right和click.middle,因为它们实际上不会触发。
// 从技术上讲,这是特定于浏览器的,但是至少目前来说,浏览器是唯一具有右键/中间点击的目标环境。
// normalize click.right and click.middle since they don't actually fire
// this is technically browser-specific, but at least for now browsers are
// the only target envs that have right/middle clicks.
if (modifiers.right) { // 将鼠标右键点击标准化 右键点击默认的是 contextmenu 事件
if (dynamic) { // 如果是动态事件
name = `(${name})==='click'?'contextmenu':(${name})` // 动态确定事件名
} else if (name === 'click') { // 如果不是动态事件且是鼠标右击
name = 'contextmenu' // 则直接替换为contextmenu事件
delete modifiers.right // 删除modifiers的right属性
}
} else if (modifiers.middle) { // 同样标准化处理鼠标中键点击的事件
if (dynamic) { // 如果是动态事件
name = `(${name})==='click'?'mouseup':(${name})` // 动态确定事件名
} else if (name === 'click') { // 如果不是动态事件且是鼠标中键点击
name = 'mouseup' // 处理为mouseup事件
}
}
// 下面是对捕获、一次触发、passive模式的modifiers处理,主要是为事件添加 !、~、& 标记
// 这一部分标记可以在Vue官方文档中查阅
// https://cn.vuejs.org/v2/guide/render-function.html#%E4%BA%8B%E4%BB%B6-amp-%E6%8C%89%E9%94%AE%E4%BF%AE%E9%A5%B0%E7%AC%A6
// check capture modifier
if (modifiers.capture) {
delete modifiers.capture
name = prependModifierMarker('!', name, dynamic)
}
if (modifiers.once) {
delete modifiers.once
name = prependModifierMarker('~', name, dynamic)
}
/* istanbul ignore if */
if (modifiers.passive) {
delete modifiers.passive
name = prependModifierMarker('&', name, dynamic)
} // events 用来记录绑定的事件
let events
if (modifiers.native) { // 如果是要触发根元素原生事件则直接取得nativeEvents
delete modifiers.native
events = el.nativeEvents || (el.nativeEvents = {})
} else { // 否则取得events
events = el.events || (el.events = {})
} // 将事件处理函数作为handler
const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
if (modifiers !== emptyObject) {
newHandler.modifiers = modifiers
} // 绑定的事件可以多个,回调也可以多个,最终会合并到数组中
const handlers = events[name]
/* istanbul ignore if */
if (Array.isArray(handlers)) {
important ? handlers.unshift(newHandler) : handlers.push(newHandler)
} else if (handlers) {
events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
} else {
events[name] = newHandler
} el.plain = false
}

代码生成

接下来需要将AST语法树转render函数,在这个过程中会加入对事件的处理,首先模块导出了generate函数,generate函数即会返回render字符串,在这之前会调用genElement函数,而在上述addHandler方法处理的最后执行了el.plain = false,这样在genElement函数中会调用genData函数,而在genData函数中即会调用genHandlers函数。

// dev/src/compiler/codegen/index.js line 42
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`, // 即render字符串
staticRenderFns: state.staticRenderFns
}
} // dev/src/compiler/codegen/index.js line 55
export function genElement (el: ASTElement, state: CodegenState): string {
// ...
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
} const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// ...
} // dev/src/compiler/codegen/index.js line 219
export function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
// ...
// event handlers
if (el.events) {
data += `${genHandlers(el.events, false)},`
}
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true)},`
}
// ...
data = data.replace(/,$/, '') + '}'
// ...
return data
} // dev/src/compiler/to-function.js line 12
function createFunction (code, errors) {
try {
return new Function(code) // 将render字符串转为render函数
} catch (err) {
errors.push({ err, code })
return noop
}
}

可以看到无论是处理普通元素事件还是组件根元素原生事件都会调用genHandlers函数,genHandlers函数即会遍历解析好的AST树中事件属性,拿到event对象属性,并根据属性上的事件对象拼接成字符串。

// dev/src/compiler/codegen/events.js line 3
const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/
const fnInvokeRE = /\([^)]*?\);*$/
const simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/ // dev/src/compiler/codegen/events.js line 7
// KeyboardEvent.keyCode aliases
const keyCodes: { [key: string]: number | Array<number> } = {
esc: 27,
tab: 9,
enter: 13,
space: 32,
up: 38,
left: 37,
right: 39,
down: 40,
'delete': [8, 46]
}
// KeyboardEvent.key aliases
const keyNames: { [key: string]: string | Array<string> } = {
// #7880: IE11 and Edge use `Esc` for Escape key name.
esc: ['Esc', 'Escape'],
tab: 'Tab',
enter: 'Enter',
// #9112: IE11 uses `Spacebar` for Space key name.
space: [' ', 'Spacebar'],
// #7806: IE11 uses key names without `Arrow` prefix for arrow keys.
up: ['Up', 'ArrowUp'],
left: ['Left', 'ArrowLeft'],
right: ['Right', 'ArrowRight'],
down: ['Down', 'ArrowDown'],
// #9112: IE11 uses `Del` for Delete key name.
'delete': ['Backspace', 'Delete', 'Del']
}
} // dev/src/compiler/codegen/events.js line 37
// #4868: modifiers that prevent the execution of the listener
// need to explicitly return null so that we can determine whether to remove
// the listener for .once
const genGuard = condition => `if(${condition})return null;`
const modifierCode: { [key: string]: string } = {
stop: '$event.stopPropagation();',
prevent: '$event.preventDefault();',
self: genGuard(`$event.target !== $event.currentTarget`),
ctrl: genGuard(`!$event.ctrlKey`),
shift: genGuard(`!$event.shiftKey`),
alt: genGuard(`!$event.altKey`),
meta: genGuard(`!$event.metaKey`),
left: genGuard(`'button' in $event && $event.button !== 0`),
middle: genGuard(`'button' in $event && $event.button !== 1`),
right: genGuard(`'button' in $event && $event.button !== 2`)
} // dev/src/compiler/codegen/events.js line 55
export function genHandlers (
events: ASTElementHandlers,
isNative: boolean
): string {
const prefix = isNative ? 'nativeOn:' : 'on:'
let staticHandlers = ``
let dynamicHandlers = ``
for (const name in events) { // 遍历AST解析后的事件属性
const handlerCode = genHandler(events[name]) // 将事件对象转换成可拼接的字符串
if (events[name] && events[name].dynamic) {
dynamicHandlers += `${name},${handlerCode},`
} else {
staticHandlers += `"${name}":${handlerCode},`
}
}
staticHandlers = `{${staticHandlers.slice(0, -1)}}`
if (dynamicHandlers) {
return prefix + `_d(${staticHandlers},[${dynamicHandlers.slice(0, -1)}])`
} else {
return prefix + staticHandlers
}
} // dev/src/compiler/codegen/events.js line 96
function genHandler (handler: ASTElementHandler | Array<ASTElementHandler>): string {
if (!handler) {
return 'function(){}'
} // 事件绑定可以多个,多个在解析AST树时会以数组的形式存在,如果有多个则会递归调用getHandler方法返回数组。
if (Array.isArray(handler)) {
return `[${handler.map(handler => genHandler(handler)).join(',')}]`
} const isMethodPath = simplePathRE.test(handler.value) // 调用方法为 doThis 型
const isFunctionExpression = fnExpRE.test(handler.value) // 调用方法为 () => {} or function() {} 型
const isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, '')) // 调用方法为 doThis($event) 型 if (!handler.modifiers) { // 没有修饰符
if (isMethodPath || isFunctionExpression) { // 符合这两个条件则直接返回
return handler.value
}
/* istanbul ignore if */
if (__WEEX__ && handler.params) {
return genWeexHandler(handler.params, handler.value)
}
return `function($event){${ // 返回拼接的匿名函数的字符串
isFunctionInvocation ? `return ${handler.value}` : handler.value
}}` // inline statement
} else { // 处理具有修饰符的情况
let code = ''
let genModifierCode = ''
const keys = []
for (const key in handler.modifiers) { // 遍历modifiers上记录的修饰符
if (modifierCode[key]) {
genModifierCode += modifierCode[key] // 根据修饰符添加对应js的代码
// left/right
if (keyCodes[key]) {
keys.push(key)
}
} else if (key === 'exact') { // 针对exact的处理
const modifiers: ASTModifiers = (handler.modifiers: any)
genModifierCode += genGuard(
['ctrl', 'shift', 'alt', 'meta']
.filter(keyModifier => !modifiers[keyModifier])
.map(keyModifier => `$event.${keyModifier}Key`)
.join('||')
)
} else {
keys.push(key) // 如果修饰符不是以上修饰符,则会添加到keys数组中
}
}
if (keys.length) {
code += genKeyFilter(keys) // 处理其他修饰符 即keyCodes中定义的修饰符
}
// Make sure modifiers like prevent and stop get executed after key filtering
if (genModifierCode) {
code += genModifierCode
}
// 根据三种不同的书写模板返回不同的字符串
const handlerCode = isMethodPath
? `return ${handler.value}($event)`
: isFunctionExpression
? `return (${handler.value})($event)`
: isFunctionInvocation
? `return ${handler.value}`
: handler.value
/* istanbul ignore if */
if (__WEEX__ && handler.params) {
return genWeexHandler(handler.params, code + handlerCode)
}
return `function($event){${code}${handlerCode}}`
}
} // dev/src/compiler/codegen/events.js line 175
function genFilterCode (key: string): string {
const keyVal = parseInt(key, 10)
if (keyVal) { // 如果key是数字,则直接返回$event.keyCode!==${keyVal}
return `$event.keyCode!==${keyVal}`
}
const keyCode = keyCodes[key]
const keyName = keyNames[key]
// 返回_k函数,它的第一个参数是$event.keyCode,
// 第二个参数是key的值,
// 第三个参数就是key在keyCodes中对应的数字。
return (
`_k($event.keyCode,` +
`${JSON.stringify(key)},` +
`${JSON.stringify(keyCode)},` +
`$event.key,` +
`${JSON.stringify(keyName)}` +
`)`
)
}

事件绑定

前面介绍了如何编译模板提取事件收集指令以及生成render字符串和render函数,但是事件真正的绑定到DOM上还是离不开事件注册,此阶段就发生在patchVnode过程中,在生成完成VNode后,进行patchVnode过程中创建真实DOM时会进行事件注册的相关钩子处理。

// dev/src/core/vdom/patch.js line 33
const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] // dev/src/core/vdom/patch.js line 125
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// ...
} // dev/src/core/vdom/patch.js line 303
// 在之前cbs经过处理
// 这里cbs.create包含如下几个回调:
// updateAttrs、updateClass、updateDOMListeners、updateDOMProps、updateStyle、update、updateDirectives
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}

invokeCreateHooks就是一个模板指令处理的任务,他分别针对不同的指令为真实阶段创建不同的任务,针对事件,这里会调updateDOMListeners对真实的DOM节点注册事件任务。

// dev/src/platforms/web/runtime/modules/events.js line 105
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) { // on是事件指令的标志
return
}
// 新旧节点不同的事件绑定解绑
const on = vnode.data.on || {}
const oldOn = oldVnode.data.on || {}
// 拿到需要添加事件的真实DOM节点
target = vnode.elm
// normalizeEvents是对事件兼容性的处理
normalizeEvents(on)
// 调用updateListeners方法,并将on作为参数传进去
updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
target = undefined
} // dev/src/core/vdom/helpers/update-listeners.js line line 53
export function updateListeners (
on: Object,
oldOn: Object,
add: Function,
remove: Function,
createOnceHandler: Function,
vm: Component
) {
let name, def, cur, old, event
for (name in on) { // 遍历事件
def = cur = on[name]
old = oldOn[name]
event = normalizeEvent(name)
/* istanbul ignore if */
if (__WEEX__ && isPlainObject(def)) {
cur = def.handler
event.params = def.params
}
if (isUndef(cur)) { // 事件名非法的报错处理
process.env.NODE_ENV !== 'production' && warn(
`Invalid handler for event "${event.name}": got ` + String(cur),
vm
)
} else if (isUndef(old)) { // 旧节点不存在
if (isUndef(cur.fns)) { // createFunInvoker返回事件最终执行的回调函数
cur = on[name] = createFnInvoker(cur, vm)
}
if (isTrue(event.once)) { // 只触发一次的事件
cur = on[name] = createOnceHandler(event.name, cur, event.capture)
}
// 执行真正注册事件的执行函数
add(event.name, cur, event.capture, event.passive, event.params)
} else if (cur !== old) {
old.fns = cur
on[name] = old
}
}
for (name in oldOn) { // 旧节点存在,解除旧节点上的绑定事件
if (isUndef(on[name])) {
event = normalizeEvent(name)
// 移除事件监听
remove(event.name, oldOn[name], event.capture)
}
}
} // dev/src/platforms/web/runtime/modules/events.js line 32
// 在执行完回调之后,移除事件绑定
function createOnceHandler (event, handler, capture) {
const _target = target // save current target element in closure
return function onceHandler () {
const res = handler.apply(null, arguments)
if (res !== null) {
remove(event, onceHandler, capture, _target)
}
}
}

最终添加与移除事件都是调用的add与remove方法,最终调用的方法即DOM的addEventListener方法与removeEventListener方法。

// dev/src/platforms/web/runtime/modules/events.js line 46
function add (
name: string,
handler: Function,
capture: boolean,
passive: boolean
) {
// async edge case #6566: inner click event triggers patch, event handler
// attached to outer element during patch, and triggered again. This
// happens because browsers fire microtask ticks between event propagation.
// the solution is simple: we save the timestamp when a handler is attached,
// and the handler would only fire if the event passed to it was fired
// AFTER it was attached.
if (useMicrotaskFix) {
const attachedTimestamp = currentFlushTimestamp
const original = handler
handler = original._wrapper = function (e) {
if (
// no bubbling, should always fire.
// this is just a safety net in case event.timeStamp is unreliable in
// certain weird environments...
e.target === e.currentTarget ||
// event is fired after handler attachment
e.timeStamp >= attachedTimestamp ||
// bail for environments that have buggy event.timeStamp implementations
// #9462 iOS 9 bug: event.timeStamp is 0 after history.pushState
// #9681 QtWebEngine event.timeStamp is negative value
e.timeStamp <= 0 ||
// #9448 bail if event is fired in another document in a multi-page
// electron/nw.js app, since event.timeStamp will be using a different
// starting reference
e.target.ownerDocument !== document
) {
return original.apply(this, arguments)
}
}
}
target.addEventListener(
name,
handler,
supportsPassive
? { capture, passive }
: capture
)
} // dev/src/platforms/web/runtime/modules/events.js line 92
function remove (
name: string,
handler: Function,
capture: boolean,
_target?: HTMLElement
) {
(_target || target).removeEventListener(
name,
handler._wrapper || handler,
capture
)
}

每日一题

https://github.com/WindrunnerMax/EveryDay

参考

https://cn.vuejs.org/v2/api/#v-on
https://juejin.im/post/6844903919290679304
https://juejin.im/post/6844904061897015310
https://juejin.im/post/6844904126250221576
https://segmentfault.com/a/1190000009750348
https://blog.csdn.net/weixin_41275295/article/details/100549145
https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener
https://github.com/liutao/vue2.0-source/blob/master/%E4%BA%8B%E4%BB%B6%E5%A4%84%E7%90%86.md

Vue事件绑定原理的更多相关文章

  1. Vue双向绑定原理,教你一步一步实现双向绑定

    当今前端天下以 Angular.React.vue 三足鼎立的局面,你不选择一个阵营基本上无法立足于前端,甚至是两个或者三个阵营都要选择,大势所趋. 所以我们要时刻保持好奇心,拥抱变化,只有在不断的变 ...

  2. vue双向绑定原理分析

    当我们学习angular或者vue的时候,其双向绑定为我们开发带来了诸多便捷,今天我们就来分析一下vue双向绑定的原理. 简易vue源码地址:https://github.com/jiangzhenf ...

  3. vue双向绑定原理及实现

    vue双向绑定原理及实现 一.总结 一句话总结:vue中的双向绑定主要是通过发布者-订阅者模式来实现的 发布 订阅 1.单向绑定和双向绑定的区别是什么? model view 更新 单向绑定:mode ...

  4. Vue双向绑定原理(源码解析)---getter setter

       Vue双向绑定原理      大部分都知道Vue是采用的是对象的get 和set方法来实现数据的双向绑定的过程,本章将讨论他是怎么利用他实现的. vue双向绑定其实是采用的观察者模式,get和s ...

  5. vue 学习二 深入vue双向绑定原理

    vue双向绑定原理 请示总体来讲 就是为data的中的每个属性字段添加一个getter/seter属性 以此来追踪数据的变化,而执行这部操作,依赖的就是js的Object.defineProperty ...

  6. [Vue源码]一起来学Vue双向绑定原理-数据劫持和发布订阅

    有一段时间没有更新技术博文了,因为这段时间埋下头来看Vue源码了.本文我们一起通过学习双向绑定原理来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫 ...

  7. 通俗易懂了解Vue双向绑定原理及实现

    看到一篇文章,觉得写得挺好的,拿过来给大家分享一下,刚好解答了一些困扰我的一些疑惑!!! 1. 前言 每当被问到Vue数据双向绑定原理的时候,大家可能都会脱口而出:Vue内部通过Object.defi ...

  8. Vue - 事件绑定

    1.内联方式: A:将事件处理器绑定到一个方法中,以下所有事件都以click事件作为案例 注意:内联方式下事件处理器只能绑定一个方法,要是想要绑定多个方法,依旧还是使用js中的addEventList ...

  9. vue事件绑定处理

    事件监听指令 v-on 指令监听 DOM 事件来触发一些 JavaScript 代码,通常是触发一个函数,简写@ <template> <div id="app" ...

随机推荐

  1. qt事件过滤器的使用(可以用于控制屏幕背光等)

    在嵌入式qt项目中,有时并不需求屏幕一直亮着,需要一段时间不操作时,将屏幕背光关掉,以达到节能的目的: 在qt项目中,可以通过重写事件过滤器来实现屏幕操作的检测,加上定时器的时间控制,可以实现指定时间 ...

  2. Docker 搭建 RabbitMQ

    Docker RabbitMQ RabbitMQ 安装非常繁琐,使用 Docker 快速搭建一个 RabbitMQ 开发环境 步骤 拉取镜像 docker pull rabbitmq 启动容器 端口会 ...

  3. nginx进程模型解析

    nginx进程模型解析 概念   master会发送请求给worker,用于处理用户的请求,模型图如下 nginx进程分类   master进程(只有1个)    接受信号传递给worker   wo ...

  4. BiLSTM:序列标注任务的标杆

    Bidirectional LSTM-CRF Models for Sequence Tagging. Zhiheng Huang. 2015 在2015年,本文第一个提出使用BiLSTM-CRF来做 ...

  5. 浅谈python深复制与浅复制区别

    话不多说,看代码

  6. 面试题——20+Vue面试题整理

    0.那你能讲一讲MVVM吗? MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel. Model层代表数据模型,View代表UI组件, ...

  7. google protocol buffer——protobuf的基本使用和模型分析

    这一系列文章主要是对protocol buffer这种编码格式的使用方式.特点.使用技巧进行说明,并在原生protobuf的基础上进行扩展和优化,使得它能更好地为我们服务. 1.什么是protobuf ...

  8. IDEA创建聚合项目

    1.新建一个空工程 2.新建Modul New-->Modul 3.选择maven(不选模板,放在工程下) 4.选择依赖 <project xmlns="http://maven ...

  9. 准确率99.9%的离线IP地址定位库

    Ip2region是什么? ip2region - 准确率99.9%的离线IP地址定位库,0.0x毫秒级查询,ip2region.db数据库只有数MB,提供了java,php,c,python,nod ...

  10. linux root用户下没有.ssh目录

    .ssh 是记录密码信息的文件夹,如果没有登录过root的话,就没有 .ssh 文件夹,因此登录 localhost ,并输入密码就会生成了 ssh localhost