vue3.0已经出来一段时间了,目前整理一下vue2.0源码的相关知识点,准备着手向3.0的进军啦~
1.准备工作
-
down源码:
git clone git@github.com:vuejs/vue.git -
下载rollup:
npm i rollup -g -
增加
--sourcemap:"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
4. npm run dev命令会在dist目录下新增vue.js和vue.js.map
5.新建一个测试的html文件,script引入dist下的vue.js文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vue.js modal component example</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app">
<p>name</p>
<p>{{name}}</p>
</div>
<script>
const app = new Vue({
el: '#app',
data:{
name: 'vue源码分析'
}
})
</script>
</body>
</html>
6.掌握断点使用方式
2.重要文件分析
步骤:根据以下代码寻找vue的入口文件,再一步步寻找vue的构造函数
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
可知开始文件为scripts/config.js,参数为web-full-dev
摘取查找到的代码:
1.src/platforms/web/entry-runtime-with-compiler.js
主要作用:覆盖$mount,执行模板解析和编译,导出render函数
主要内容就在这个方法里
分析代码可知,最终目的是为了覆盖$mountreturn mount.call(this, el, hydrating)
在new vue中的优先级是render > template > el
在接着往上找Vue的构造函数
2.src/platforms/web/runtime/index.js
主要作用:该文件主要在Vue的原型上定义了__patch_和``定义$mount
在接着往上找Vue的构造函数
3.src/core/index.js
主要作用:定义全局api
在接着往上找Vue的构造函数
4.src/core/instance/index.js
vue的构造函数文件,只是简单的执行了一个init方法
查看initMixin方法
拓展:
定义实例方法:
stateMixin(Vue) //$set,$delete.$watch
eventsMixin(Vue) //$on,$emit...
lifecycleMixin(Vue) //_updata,$forceUpdate,$destroy...
renderMixin(Vue) //$nextTick,_update...
5.src/core/instance/init.js
主要作用是初始化init方法
重要代码如下:感兴趣的可以自己深入研究看看
vm._self = vm
initLifecycle(vm) //初始化组件相关的参数,$parent,$root等等。组件创建是自上而下,挂载是自下而上
initEvents(vm) //处理父组件传入的事件添加监听
initRender(vm) //声明$slots和$createElement()
callHook(vm, 'beforeCreate') //调用生命周期函数
initInjections(vm) // resolve injections before data/props 注入数据
initState(vm) // ** 数据的初始化,响应式
initProvide(vm) // resolve provide after data/props 提供数据
callHook(vm, 'created') //调用生命周期函数
3.初始化过程分析
断点步骤:
右侧断点行可以看出初始化执行的步骤:
执行步骤是从下往上,可点击每一项查看每一个数据的变化,方便分析初始化的整个流程
断点6处可以看出来,2.0中一个组件只有一个watcher
整理以下:
new Vue() => this._init() => $mount => mountComponent => _render() => _update
this._init(): 初始化各种数据事件方法
mountComponent:声明updateComponent方法,创建组件的Watcher
_render:获取虚拟dom(vnode)
_update: 通过__patch__将虚拟dom转换成真实dom
4.数据响应实现
<body>
<div id="app">
<p>name</p>
<p>{{list[0]}}</p>
</div>
<script>
const app = new Vue({
el: '#app',
data:{
list:{
name:'响应式',
age: 18
}
}
})
</script>
</body>
分析入口:src/core/instance/init.js中的initState(vm)方法开始
在上面的重点文件讲解中提到过
断点进入initState(vm) => 断点进入initData(vm) => 断点进入observe(data, true /* asRootData */) => 进入重点文件src/core/observer/index.js
src/core/instance/state.js
主要作用:获取data去重,设置代理,启动响应式
1.observe相关
src/core/observer下都是数据响应相关
observe():主要是return ob,ob是一个观察者,负责通知和更新。如果接受的数据存ob直接返回,否则ob = new Observer(value)创建一个实例
Observer类:如果传入value是一个对象,所以设置完相关变量直接走this.walk(value)方法,循环遍历之后defineReactive(obj, keys[i])。拓展若是数组,则先该value的__proto__属性设置为Object.create(Array.prototype)。接着this.observeArray(value),目的是为了让数组里面的对象也实现响应式
defineReactive:new了一个依赖dep,所以一个key就有一个dep对象,如果接受的obj存在childOb就重复observe()方法,否则就可以走Object.defineProperty()中的get或者set方法
observe(data):总结递归遍历添加ob图:
当读取到data中的数据的时候,会触发Object.defineProperty()中的get方法,实现在拿到数据之前的数据劫持。get中主要是进行依赖收集dep.depend(),将正在使用该数据的组件进行收集,该组件我们也称为观察者。如果是修改数据,则触发set方法通知更新dep.notify()
更多的可以自己下去研究一下delete
2.dep和watcher
观察者Watcher使用:
src/core/instance/lifecycle.js中的mountComponent方法中
dep和watcher是多对多的关系
因为不只是组件创建是实例的时候才有(mountComponent),当同一个key使用watch或者$watch的时候也会new Watcher(),感兴趣的可以src/core/instance/state.js中查看$watch方法
以下dep和watcher之间的关系:
注:以上各重要方法均为简写或改写只为精简表达
5.vue异步更新实现
需要了解浏览器事件循环机制:宏任务和微任务
宏任务:settimeout、setinterval、js线程、setImmediate等。浏览器完成一个宏任务,会在当前任务下的所以微任务全部完成之后,下一个宏任务开始之前对页面进行重新渲染
微任务:promise、MutaionObserver等。当前宏任务完成之后立即执行,直到当前宏任务下所以的微任务都完成之后,才开始下一个宏任务。
实现流程:
修改数据触发set() => dep.notify() => watcher.updata() => watcher.run() => watcher.get() =>
this.getter = expOrFn =>vm._render() => vm._update() => vm.__patch__涉及diff算法
⚠️以上this.getter为vm._update(vm._render(), hydrating);
具体分析watcher.updata() :
update () {
if (this.lazy) {
this.dirty = true //if计算属性
} else if (this.sync) {
this.run() //if 同步更新
} else {
queueWatcher(this) //异步更新
}
}
queueWatcher():只保留重点代码
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
//去重,同一个watcher只添加一次到队列
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
}
if (!waiting) {
//异步刷新队列--微任务
nextTick(flushSchedulerQueue)
}
}
}
//flushSchedulerQueue =>执行watcher.run()
nextTick:src/core/util/next-tick.js
nextTick(flushSchedulerQueue)将flushSchedulerQueue方法追加到callbacks数组中(入队等待执行) => timerFunc()
判断兼容性,优雅降级:Promise > MutationObserver > setImmediate > setTimeout。
6.虚拟dom和diff算法
上面说到流程vm._update() => vm.__patch__涉及diff算法,下面就详细探索一番
虚拟dom:vnode是通过vm._render()return出来的对象哟~
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevVnode = vm._vnode
vm._vnode = vnode
if (!prevVnode) {
// initial render初始化
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates 更新
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
src/core/vdom/patch.js,相关的diff算法都在该js文件
1.界面初始化-vm.__patch__流程
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
return function patch(oldVnode, vnode, hydrating, removeOnly) {
let isInitialPatch = false
const insertedVnodeQueue = []
//如果传入的是真实节点,则是初始化,否则就是更新
const isRealElement = isDef(oldVnode.nodeType)
//初始化,创建新的dom,追加到body,删除宿主元素
if (isRealElement) {
oldVnode = emptyNodeAt(oldVnode)
}
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
//创建新的dom
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
//在此之前界面状态看下图
//删除宿主元素
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
以上,页面初始化的时候patch函数完成界面就完成更新啦~
注意⚠️:每一个知识模块都不是单独的无联系的,注意将流程结合起来,就不会迷茫了。。。
2.diff算法-界面更新vm.__patch__(prevVnode, vnode)
return function patch(oldVnode, vnode, hydrating, removeOnly) {
let isInitialPatch = false
const insertedVnodeQueue = []
//如果传入的是真实节点,则是初始化,否则就是更新
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
//新旧都存在,执行diff算法,*****
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
1.patchVnode
function patchVnode( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) {
const oldCh = oldVnode.children
const ch = vnode.children
//属性更新
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
//判断是否是元素
if (isUndef(vnode.text)) {
//双方都有孩子
if (isDef(oldCh) && isDef(ch)) {
//比孩子
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
//清空老节点文本
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
//创建孩子并追加
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
//新节点没有孩子,老节点有直接删除
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
//双方都是文本,更新文本
nodeOps.setTextContent(elm, vnode.text)
}
}
总结:比较新旧vnode,根据情况会执行三种类型的操作:属性更新、节点更新、文本更新
- diff算法:如果新旧节点都有children,执行updateChildren
- 新节点有children,老节点没有,清空老节点文本,增加子节点
- 新节点没有children,老节点有,直接删除该节点下的所以子节点
- 都无,只是文本替换
2.updateChildren
算法思路:深度优先,同级比较
比较孩子的算法是比较复杂且消耗性能的一件工作,所以vue里面优化算法,提出了四种假设,如果不满足四种假设的情况下才暴力循环查找
四种假设优化为下图:oldStartVnode、oldEndVnode和newStartVnode、newEndVnode两两比较
function sameVnode (a, b) {
return ( //是否存在key值
a.key === b.key && ( //不存在key也为true,所以就地复用
(
a.tag === b.tag && //标签是否相同
a.isComment === b.isComment && //都不是注释
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) //input的类型是否一样
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
这里的key使用是一个关键点,关于key的优点和就地复用等在diff算法中的作用可以参考文档
muyiy.cn/question/fr…中说明,再结合sameVnode就能明白。
举例分析:首首满足sameVnode
首首满足sameVnode情况下:将oldStartVnode和newStartVnode进行patchVnode,当节点下所以比较全部完成之后,在分别移动指针,进行下一轮的首位四种假设的比较。尾尾处理情况类似
首尾满足sameVnode
if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}
首尾满足sameVnode情况下:将oldStartVnode和newEndVnode进行patchVnode,当节点下所以比较全部完成之后,需要将oldStartVnode位置移动到原本的oldEndVnode之后,再分别移动指针,进行下一轮的首位四种假设的比较。尾首情况类似
如果不满足假设的四种形式,则需要循环查找
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
//查找在老得孩子数组中的索引
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
//没找到就创建
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
//找到
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
如果设置了key,用idxInOld表示在oldvnode中查找对应的newStartVnode.key索引,否则在oldvnode循环查找和newStartVnode满足sameVnode的节点索引
如果没有,就证明是新增加的,所以createElm
如果有进一步判断vnodeToMove和newStartVnode,是否满足sameVnode
满足就递归两节点下全部,全部完成之后,移动节点和指针,如上图所示
不满足则说明肯定是通过key方法找到的,但是key虽然相同,但是没有相比的价值,则需要创建新节点。侧面说明开发的时候不要用index等可能会重复的数据表示key哟~
循环♻️完毕
如果oldStartIdx > oldEndIdx ,oldvnode已经循环结束了,但是vnode还没有,所以有新添加的节点,将vnode中剩下的对应dom插入到真实dom
反正newStartIdx > newEndIdx, vnode已经循环结束了,但是oldvnode还没有,所以oldvnode中多余的节点直接删除
举例如下图:oldStartIdx > oldEndIdx
3.diff总结
1.什么是diff
diff是虚拟dom的必然产物,比较新旧dom的过程就是diff过程。
2.diff的必要性
vue2.x中一个组件就有一个watcher,相比1.x的版本中,watcher的力度变低。一个组件中data可能对应多个key,为了更好的定位变化的key,就需要引入diff
3.diff发生的地方
当数据变化需要更新视图之前,触发pathVnode()方法,开始diff算法
4.diff的高效性
提出常见的四种假设情况比较新旧虚拟dom,降低比较时间复杂度。如果都不满足,循环遍历查找节点是否存在。用户若设置了key值,通可以通过key值高效对比。
5.diff遵循的原则
深度优先,同层比较
7.部分常见问题的源码解析
1.列表渲染中key的作用
对列表渲染来说:
默认情况下,vue采用的是就地更新的策略。并不会去移动dom元素的位置。
使用key的情况下,会根据diff算法执行patchVnode函数,进行头尾四种比较,并更改元素的位置
重点是sameVnode
function sameVnode (a, b) {
return ( //是否存在key值
a.key === b.key && ( //不存在key也为true,所以就地复用
(
a.tag === b.tag && //标签是否相同
a.isComment === b.isComment && //都不是注释
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) //input的类型是否一样
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
demo:
没有设置key的时候:就地更新原则,循环分别a变更为d,b变更c,c变更为b,d变更为a
设置key的时候: diff算法执行patchVnode,移动节点元素位置
注意⚠️:
如果针对特别简单的文本内容,且标签元素均相同,没有设置key,循环新旧节点对比时均满足sameVnode(oldStartVnode, newStartVnode)方法,也就是就地更新直接更改相关的文本内容。减少节点的创建或者销毁等等
但是如果设置了key,首尾比较不满的情况下,还需循环查找是否在别的位置,不存在才会创建并添加节点,这其中无疑会浪费时间。所以不区分场景就说key提高了性能,就是耍流氓
结论:
1.不用key;
- 就地更新节点,减少节点的创建或者销毁,会有渲染性能上的提升
- 无法维持组件的状态,不能实现过渡效果
- 也有可能带来性能下降,针对复杂列表结构。节点顺序差别很大,就地复用的节点过多,导致创建或删除节点的数量过多,相比有key时性能下降
2.使用key
-
维持组件组件状态
-
元素查找性能提升
-
尽可能多的复用相同的节点,性能会得到提升
可以仔细琢磨琢磨这些文字~
2.patch函数的获取
如果翻看之前的重要文件分析部分,不难发现src/platforms/web/runtime/index.js路径主要是指定了一个打补丁的方法,并创建$mount
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
上面patch方法主要是:
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
const modules = platformModules.concat(baseModules)
//传递平台特有节点操作选项给工厂函数,返回patch
export const patch: Function = createPatchFunction({ nodeOps, modules })
从上可知patch函数是工厂函数createPatchFunction()返回的,createPatchFunction方法就是我们之前研究的diff算法的path.js中的内容,路径为src/core/vdom/patch.js
nodeOps中:主要是处理原生dom相关的方法
modules中:主要是处理属性更新的一些实现方法
3.vue中的原生事件初始化流程
创建demo
<body>
<div id="app">
<button @click="btnclick">按钮</button>
</div>
<script>
const app = new Vue({
el: '#app',
methods: {
btnclick(){
console.log('1233')
}
},
})
console.log(app.$options.render)
</script>
</body>
其中的render函数如下:
ƒ anonymous() {
with(this){return _c('div',{attrs:{"id":"app"}},[
_c('p',[_v("事件处理机制")]),_v(" "),
_c('button',{on:{"click":btnclick}},[_v("按钮")]),_v(" ")
],1)}
}
事件也是作为属性处理的:src/platforms/web/runtime/modules/events.js
event.js主要内容:
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
const on = vnode.data.on || {}
const oldOn = oldVnode.data.on || {}
target = vnode.elm
normalizeEvents(on)
updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
target = undefined
}
export default {
create: updateDOMListeners, //这个create、update不是我们所说的生命周期
update: updateDOMListeners}
初始化原生事件的监听:
依据demo,初始化组件vm.__patch()__的时候(已经处理好原生dom和属性更新相关的一些代码) => createElm() => invokeCreateHooks() => updateDOMListeners() => updateListeners() => add() => 最终执行target.addEventListener监听事件
语言描述比较笼统,为了更好的了解,最好根据demo断点源码查看相关流程栈
4.vue中的自定义事件初始化流程
demo.html
<body>
<div id="app">
<comp @myclick="myclick"></comp>
</div>
<script>
Vue.component('comp',{
template:`
<button @click="btnclick2">组件按钮</button>
`,
methods: {
btnclick2(){
this.$emit('myclick')
}
},
})
const app = new Vue({
el: '#app',
methods: {
myclick(){
console.log('898898')
}
},
})
</script>
</body>
自定义事件监听的入口:初始化init.js => initEvents方法 (src/core/instance/events.js)
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
//谁派发谁监听
//自定义组件中真正做事件监听的是事件派发着自己,也就是子组件
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
以上listeners:listeners: {myclick: ƒ}
updateComponentListeners
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
function add (event, fn) {
target.$on(event, fn)
}
updateListeners => add(**)
export function updateListeners (
on: Object,
oldOn: Object,
add: Function,
remove: Function,
createOnceHandler: Function,
vm: Component
) {
let name, def, cur, old, event
//on: {myclick: ƒ}
for (name in on) { //循环事件
def = cur = on[name]
old = oldOn[name]
event = normalizeEvent(name)
if (isUndef(old)) {
if (isUndef(cur.fns)) {
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)
}
}
}
5.v-model实现原理
语法糖 :value+@input
表单控件的v-model处理主要是在model方法中:src/platforms/web/compiler/directives/model.js
demo.html
<body>
<div id="app">
<!-- 表单控件 -->
<input type="text" v-model="name" />
<!-- 自定义事件 -->
<my-input v-model="name"></my-input>
<div>{{name}}</div>
</div>
<script>
Vue.component('myInput',{
props:['value'],
template:`
<input type="text" :value='value' @input="inputchange" />
`,
methods: {
inputchange(e){
this.$emit('input',e.target.value)
},
},
})
const app = new Vue({
el: '#app',
data:{
name:'lxy'
},
})
</script>
</body>
vue从模版到真实dom的过程:
(this._init() => Vue.$mount => compileToFunctions) =>const ast = parse(template.trim(), options)生成ast抽象语法树 => processAttrs方法 => 除去v-bind和v-on之外,使用addDirective =>普通指令会在对应的AST树上添加directives属性
接上ast相关完成之后继续执行const code = generate(ast, options)获取render函数的过程到以下步骤 => 到下图 => model方法,处理v-model中对表单控件的ast语法树进行特殊处理
感兴趣的可以看看不同的input场景具体处理方式:
最终生成render函数:语法糖设计主要部分就是domProps+on事件的处理
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},
[_c('input',{
directives:[{
name:"model",
rawName:"v-model",
value:(name),
expression:"name"
}],
attrs:{"type":"checkbox"},
domProps:{"checked":Array.isArray(name)?_i(name,null)>-1:(name)},
on:{"change":function($event){
var $$a=name,$$el=$event.target,$$c=$$el.checked?(true):(false);
if(Array.isArray($$a)){var $$v=null,$$i=_i($$a,$$v);
if($$el.checked){$$i<0&&(name=$$a.concat([$$v]))}else{$$i>-1&&(name=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}}
else{name=$$c}}}}),_v(" "),
_c('div',[_v(_s(name))])])}
})
拓展:
针对自定义组件的v-model: src/core/vdom/create-component.js
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// transform component v-model info (value and callback) into
// prop and event handler respectively.
function transformModel (options, data: any) {
const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
;(data.attrs || (data.attrs = {}))[prop] = data.model.value
const on = data.on || (data.on = {})
const existing = on[event]
const callback = data.model.callback
if (isDef(existing)) {
if (
Array.isArray(existing)
? existing.indexOf(callback) === -1
: existing !== callback
) {
on[event] = [callback].concat(existing)
}
} else {
on[event] = callback
}
}
开发中自定义组件v-model的使用方法
Vue.component('myInput',{
model:{
prop:'checked',
event:'change'
},
props:['checked'],
template:`
<input type="checkbox" :checked='checked' @change="$emit('change',$event.target.checked)" />
`,
})