vue源码阅读—11—扩展:原生内置指令和自定义指令
1.v-mode既可以用在表单控件上比如input、selet、textarea、checkbox,
也可以用在组件上;
举例:
let vm = new Vue({ el: '#app', template: '<div>' + '<input v-model="message" placeholder="edit me">' +
'<childComp v-model=message></childComp>' '<p>Message is: {{ message }}</p>' + '</div>', data() { return { message: '' } } })
2.如何用原生js在表单元素上实现v-model原理
<body> <!--使用vue框架就是这样写: <input class='foo' :value='xx' @input='fangfa' /> --> <input type="text" class="foo"> <div> <h5>通过下方,我们可以看到v-model绑定的值会双向变化: </h5> <h5 class="foo2"</h1> </div> <script> const foo = document.querySelector(".foo"); const foo2 = document.querySelector('.foo2') let bar = "helloWorld"; foo.value = bar; foo2.innerHTML = bar; foo.addEventListener("input", (e) => { bar = e.target.value; foo.value = bar; foo2.innerHTML = bar; }); </script>
3.v-mode在原生html元素和组件上使用时,生成的render函数是不一样的;
- 在原生html元素上,会生成一个direactive属性,还有domProps属性,还有on属性
- 在组件上,会生成一个model属性,只不过这个model属性根props属性+on属性一样的效果;
4.在原生html元素上,会生成一个direactive属性有什么用?
原生html元素上,主要还是看domProps属性和on属性,这个和我们自己在input元素使用原生js实现双向绑定是一样的;
其次,direactive属性主要是告知我们有一个指令,然后会调用一个指令的insert钩子函数,在钩子函数里生成compositionStart监听事件和compositionEnd监听事件;
当然,我们自定义指令的时候,就可以调用insert钩子函数,在这个钩子函数里也可以添加我们想要的操作;
5.v-mode在原生html元素使用v-model和我们使用:value和@input替换v-model也有细微的差别:
都说v-model指令其实是:value+@input的语法糖,我们完全可以用:value和@input替换v-model,但是为什么在处理输入法的时候,v-model和:value+@input的表现并不一样。
假设在中文输入法,v-model在完全打出一个字符时才会显示,而:value+@input虽然可以显示;
因为v-model会在inset钩子函数中添加compositionstart和compositionend;
- compositionstart api主要是在使用输入法在表单里输入时,会触发这个监听事件
- compositionUpate 输入法输入时持续触发这个监听事件;
- compositionsend输入法输入结束时触发这个监听事件;
一、v-model在表单上的实现流程
1.1编译阶段compiler
具体流程不分析了,通过compiler会转化为render函数大概如图所示:
1.2运行时阶段runtime
当我们patch div元素时会先调用createChildren方法,
所以我们到了创建input元素的时候了,这个时候当input元素调用完createChildren时会调用invokeCreateHooks,
然后调用create钩子函数;
cbs是什么,create钩子为什么时vdom/modules/dieactives.js里定义的create,后续再说,因为是分支逻辑;
create钩子函数其实就是updateDirectives方法,那么这个updateDirectives方法是它:
本质上,如果有vnode.data里有directives的话,就调用_update();方法;
到了这里,我们想想,我们在普通html元素上定义v-model,然后生成的rende函数里,所以关于普通元素的h函数是有directives的,所以会调用_update方法;
执行normalizeDirectives去获取指令;
调用resloveAsstes方法,从vm.$options.directives里获取dir.name(即v-model)对应的数据
最后返回的res是一个对象,对象里有v-model属性,v-model对象的def属性又是一个包含了两个钩子函数insert、componentUpdated的对象;
这个insert钩子和componentUpdate钩子是在哪里定义的?就是在/platforms/runtime/directives/model.jsi定义的。因为我们在打包vue框架时,在/platfom/runtime/index.js文件里,会有extend(Vue.options.direactives, plarfomDirectives)这样一个方法去把平台自定义的指令v-model和v-show的钩子方法拓展到Vue.options.direactives上;
然后我们现在通过resolveAsset(‘ v-model’)方法取到vm.$options.direacitves里的钩子函数;
然后执行完normalizeDireactives函数后,让我们回到_update函数,
然后因为create阶段,即create钩子函数调用的updateDireactives的_update,oldDir是不存在的,所以第一步callhook bind这个钩子,但是dir没有bind这个钩子所以直接跳过;
第二步,把v-model对应的dir对象添加到dirsWithInsert数组中;
继续执行_update函数,
第一步,定义callInsert这函数;
第二步,调用mergeVnodeHook方法,把callInsert钩子函数添加到vnode.data.hook中;
因为普通节点的vnode.data.hook是为空的,只有组件节点的vnode.data.hook才有值;
现在这个通过mergeBnodeHook让所有节点的vnode.data.hook都有回调函数;
然后我们开始执行vm_render阶段和vm.update阶段,在update阶段在patch函数倒数第二步执行invokeInsertHook()是会调用这个callInsert函数;
进入callhook执行,
然后执行到了v-model的insert钩子函数,
v-model是web平台的内置指令,它的钩子函数在src/platforms/web/runtime/directives/model.js定义,所以
因为input html元素满足isTextINputType,所以命中这一块,会给input添加compositionstart和compositionend和change监听事件;
到这里初始化就结束了。
然后,我们开始使用中文输入法,输入汉字。
当我们输入wo的时候,message is 并没有任何数据显示;
这是因为,当我们输入汉字时会触发compositionstart,会将e.tartget.composing置为true,然后导致input事件失效;
等到我们把 “我”这个汉字打印完后,会触发compositionend,
然后将composing置为false,并且重新trigger input事件(因为input事件在compositionend事件已经执行过了,所以需要重新触发;)
1.初始化一个input事件,然后dispatch即可;
最后我们打印的汉字“我”就显示出来了。
总结:
v-model在html元素举例input元素时,是通过指令的insert钩子函数,给input元素添加一个addEventListener(compositionStart)、addEventListener(compositionEnd)、addEventListener(compositionChange)事件,
这样当input元素的value发生改变时就会触发这三个事件,然后又重新触发dispatchEvent,然后触发我们给input元素定义的on事件,然后调用回调函数把value赋值给父组件的data;
1.3vue内置指令v-model如何被定义到vm.$options?
你一定有一个疑问?
vue内置的指令v-model如何被定义到vm.$options上的?
我们在执行src/platforms/web/entry-runtime-with-compiler.js文件的$mount的过程后,会执行src/platforms/web/runtime/index.js文件的$mount,但是在加载index.js文件的$mount的过程中,我们会执行extend方法,extend方法把我们在web平台下特有的指令和Vue.options里的指令(所有平台共有的指令)进行了一个合并
Vue.options.directives是在src/core/global-api/index.js定义的;
web平台下,platformDirectives的值是一个包含了下面两个对象的对象;即v-model内置指令的定义和v-show内置指令的定义;
然后在vue初始化时,即执行this_init时,会执行option上的merge操作,把Vue.options和自定义的options合并并都赋值给vm.$options,所以现在vm.$options.direactives是有值的,即v-model和v-show;
1.4cbs是什么?cbs里的create钩子函数为什么是src/core/vdom/modules里的create钩子?
待解答。。。。。。。
在获取不同平台的patch函数时,我们需要通过createPatchFunction方法获取,这个方法要传递两个参数nodeOpos和modules;
其中basemodules是定义在core/vdom/modules/index里的,platformModules是定义在web平台下的,它们两个做了一个合并,然后赋值给modules;
我们可以通过modules获取很多事件、指令、属性的钩子函数,那么v-model是一个指令,指令的钩子函数create钩子和update钩子就在modules的base-modules里;
二、v-model在组件的实现流程
总结:
v-model指令定义在组件上,那么渲染这个组件的h函数会有一个model属性;
在我们创建占位符vnode时,会有一个transFromModel()方法,
然后这个方法第一步,根据sub.options.model给data.props对象赋值两个key;第二步,根据data.model现有的数据,给data.props.key赋值;
v-model在组件也还是可以用的,给组件也是一样传递一个value的prop和添加一个@input的监听事件;
<script> let Child = { template: "<div>" + '<input :value="value" @input="updateValue" placeholder="edit me">' + "</div>", props: ["value"], methods: { updateValue(e) { this.$emit("input", e.target.value); }, }, }; let vm = new Vue({ el: "#app", template: "<div>" + '<child v-model="message"></child>' + "<p>Message is: {{ message }}</p>" + "</div>", data() { return { message: "", }; }, components: { Child, }, }); </script>
2.1编译阶段compiler
上述例子经过编译后转化的render函数如下:
v-model的数据,变成了_C函数(即编译器的h函数)的第二个参数即vnode的data的model属性;
with(this){ return _c('div',[_c('child',{ model:{ value:(message), callback:function ($$v) { message=$$v }, expression:"message" } }), _c('p',[_v("Message is: "+_s(message))])],1) }
但是,其实,它和我们这种的render函数是一样的,所以我们说v-model是 :value和@input的语法糖;
with(this){ return _c('div',[_c('child',{ props:{ value:message, }, on:{ inputz:function ($$v) { message=$$v }, } }), _c('p',[_v("Message is: "+_s(message))])],1) }
2.2运行时阶段runtime
在运行时的render阶段,会先创建子组件的vnode;因为会先执行里面的_c函数,因为js的执行规范就是先把里面的函数执行完了在执行外层函数;
在创建子组件 vnode
阶段,会执行 createComponent
函数,它的定义在 src/core/vdom/create-component.js
中:
如果vnode的data属性有model属性,说明组件添加了v-model属性,那么就会执行transfomrModel函数;
看注释——“转化组件的v-model数据为props和事件”,我们也可以很明白的知道transformModel就是处理组件的v-model的。
transformModel
逻辑很简单,
第一步,判断我们子组件手写options里是否配置了model这个选项,这个选项可以配置子组件在接受父组件的v-model指令的时候,到底用什么接受,不一定是value来接受,具体可以看vue文档;
如果配置了这个选项,就通过Sub.options.model.prop赋值给prop;Sub.options.model.event赋值给event;
第二步,我们知道,data参数其实就是h()函数的第二个参数,也就是这个样子
给 data.props.prop属性
等于 data.model.value
,
给data.on.event事件等于
data.model.callback
,对这个例子而言,h函数也就是变成了这个样子
总结:第一步,通过sub.options.model给data.props属性取key;第二部,通过data.model现有的数据,给data.props.key赋值;
//子组件修改了接收的 prop
名以及派发的事件名,然而这一切父组件作为调用方是不用关心的,这样做的好处是我们可以把 value
这个 prop
作为其它的用途。
然后在实例化子组件vnode时会把data变量作为参数传递;
所以我们子组件的vnode的data会有这两个属性
data.props = { value: (message), } data.on = { input: function ($$v) { message=$$v } }
这相当于我们的根组件这样写
let vm = new Vue({ el: '#app', template: '<div>' + '<child :value="message" @input="message=arguments[0]"></child>' + '<p>Message is: {{ message }}</p>' + '</div>', data() { return { message: '' } }, components: { Child } })
这就是典型的 Vue 的父子组件通讯模式,父组件通过 prop
把数据传递到子组件,子组件修改了数据后把改变通过 $emit
事件的方式通知父组件(本质上是调用父组件的回调函数通信),所以说组件上的 v-model
也是一种语法糖。
三、自定义指令
指令其实和钩子函数密切相关,我们看看vue里的几个钩子函数
1.组件实例的钩子函数beforeCreated,created,beforeMounted,mounted
2.组件vnode的Hooks:
init, prepatch,inserted,destory
3.modules里定义的属性钩子函数:
core目录下所有平台公有的:
- 指令的 create钩子、update钩子、destory钩子
- ref属性 create钩子、update钩子、destory钩子
web平台特有的:
- v-mode的 insert钩子和updateComponent钩子
- v-show的 bind钩子、update钩子、unbind钩子
- attr、class、style、domPorp、event、transition的 create钩子和update钩子;
然后每个钩子函数,其实会保存在cbs对象数据结构上;cbs对象,会有属性create、active、update、remove、destory这5个属性;
每个属性比如cbs.create是一个数组,这个数组的元素就是modeules(平台特有modules+公有modules)里的每个文件的create钩子函数;
所以当我们执行invokeCreateHook时,是把modeules里的所有create钩子函数都执行一遍;
问题1:什么时候把自定义指令的insert钩子函数添加到insertVnodeQueue中的?
在调用指令的create钩子函数时,把指令insert钩子函数添加到普通vnode.data.hook.insert中;
然后等到普通vnode在patch完成后,会调用invokeINsertHooks,会把insertQueue里的每个vnode取出,不管是普通vnode还是组件vnode都会调用它的vnode.data.hook.insert方法;
问题2:自定义指令在普通元素上和组件上,生成的render函数有什么不同?
1.在普通元素上,我们可以看到有directives属性,和v-model是一样的。
我感觉,data属性有个directives属性表示会调用指令的钩子函数;仅此而言;
毕竟内置指令v-model在编译过程是直接被转化为domProps和on属性的,多的directives说白了就是执行下v-model的insert钩子函数,添加一个compositionStart事件;
2.在组件上,我们看到没什么区别,也有directives属性,说明也会去调用指令的钩子函数;
那么,为什么,v-model在组件上定义的时候,没有directives属性?而是直接被编译成了model属性?可能v-model内置指令在组件上不需要调用v-model的insert钩子函数吧;
<div>
<h1 v-auth='"authTest"' class='firstH'>{{message}}</h1>
<child v-auth='"authTestChild"' class="childComp"></child>
</div>
(function anonymous(
) {
with (this) {
return _c('div',
[_c('h1',
{
directives: [{ name: "auth", rawName: "v-auth", value: ("authTest"), expression: "\"authTest\"" }],
staticClass: "firstH"
},
[_v(_s(message))]),
_v(" "),
_c('child',
{
directives: [{ name: "auth", rawName: "v-auth", value: ("authTestChild"), expression: "\"authTestChild\"" }],
staticClass: "childComp"
})
], 1)
}
})
原生内置指令和自定义指令的区别:
自定义指令需要自身通过Vue.direcitves()方法,把自身指令名和指令回调函数注册到Vue.options.direactives上;
而web平台内置指令v-model,v-show等在src/platform/runtime/index.js文件夹里,就已经通过extend(Vue.options.directives, platformDirectives)方法注册到Vue.options.direactives上了。
自定义指令的流程:
1.拓展到Vue.options.direacitves上
2.在本元素创建完成,本元素的createChildren也完成,但是本元素还没patch到父元素上时,调用invokeCreateHooks,调用本指令的create钩子函数;
本指令的create钩子函数是vue自己提供的,不需要用户写;
4.create钩子函数,会调用我们写得bind函数,然后把我们写得insert钩子函数使用mergeVnodeHooks赋值给vnode.data.hook.insert上
5.等到元素patch到父元素上时,会调用invokeInsertHooks,这个时候就是调用vnode.data.hook.insert,这个时候我们手写的insert函数就会执行了。