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赋值;

 

 

// 通过这里我们知道,v-mode所默认采用的给子组件传递一个value props和监听input事件是可以改变的;
// 我们在子组件的options里定义一个model对象,指定v-model要接受的props和event名字
// 然后父组件给子组件传递的值data.model就被赋值到指定的data.props和data.events。
// 最后,指定的data.props和data.events会被实例化子组件Vnode时使用;

//子组件修改了接收的 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函数就会执行了。

 

posted @ 2022-08-11 19:25  Eric-Shen  阅读(121)  评论(0编辑  收藏  举报