经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » HTML/CSS » HTML5 » 查看文章
Vue.js 源码分析(二十二) 指令篇 v-model指令详解
来源:cnblogs  作者:大沙漠  时间:2019/7/10 11:31:56  对本文有异议

Vue.js提供了v-model指令用于双向数据绑定,比如在输入框上使用时,输入的内容会事实映射到绑定的数据上,绑定的数据又可以显示在页面里,数据显示的过程是自动完成的。

v-model本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。例如:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Document</title>
  6. <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
  7. </head>
  8. <body>
  9. <div id="app">
  10. <p>Message is: {{message}}</p>
  11. <input v-model="message" placeholder="edit me" type="text">
  12. </div>
  13. <script>
  14. Vue.config.productionTip=false;
  15. Vue.config.devtools=false;
  16. new Vue({el: '#app',data(){return { message:'' }}})
  17. </script>
  18. </body>
  19. </html>

渲染如下:

当我们在输入框输入内容时,Message is:后面会自动显示输入框里的内容,反过来当修改Vue实例的message时,输入框也会自动更新为该内容。

与事件的修饰符类似,v-model也有修饰符,用于控制数据同步的时机,v-model可以添加三个修饰符:lazy、number和trim,具体可以看官网。

我们如果不用v-model,手写一些事件也可以实现例子里的效果,如下:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Document</title>
  6. <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
  7. </head>
  8. <body>
  9. <div id="app">
  10. <p>Message is: {{message}}</p>
  11. <input :value="message" @input="message=$event.target.value" placeholder="edit me" type="text">
  12. </div>
  13. <script>
  14. Vue.config.productionTip=false;
  15. Vue.config.devtools=false;
  16. new Vue({el: '#app',data(){return { message:'' }}})
  17. </script>
  18. </body>
  19. </html>

我们自己手写的和用v-model有一点不同,就是当输入中文时,输入了拼音,但是没有按回车时,p标签也会显示message信息的,而用v-model实现的双向绑定是只有等到回车按下去了才会渲染的,这是因为v-model内部监听了compositionstart和compositionend事件,有兴趣的同学具体可以查看一下这两个事件的用法,网上教程挺多的。

 

源码分析


Vue是可以自定义指令的,其中v-model和v-show是Vue的内置指令,它的写法和我们的自定义指令是一样的,都保存到Vue.options.directives上,例如:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Document</title>
  6. <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
  7. </head>
  8. <body>
  9. <script>
  10. console.log(Vue.options.directives)     //打印Vue.options.directives的值
  11. </script>
  12. </body>
  13. </html>

输出如下:

Vue内部通过extend(Vue.options.directives, platformDirectives); 将v-model和v-show的指令信息保存到Vue.options.directives里面,如下:

  1. var platformDirectives = { //第8417行 内置指令 v-module和v-show platformDirectives的意思是这两个指令和平台无关的,不管任何环境都可以用这两个指令
  2. model: directive,
  3. show: show
  4. }
  5. extend(Vue.options.directives, platformDirectives); //第8515行 将两个指令信息保存到Vue.options.directives里面

Vue的源码实现代码比较多,我们一步步来,以上面的第一个例子为例,当Vue将模板解析成AST对象解析到input时会processAttrs()函数,如下:

  1. function processAttrs (el) { //第9526行 对剩余的属性进行分析
  2. var list = el.attrsList;
  3. var i, l, name, rawName, value, modifiers, isProp;
  4. for (i = 0, l = list.length; i < l; i++) { //遍历每个属性
  5. name = rawName = list[i].name;
  6. value = list[i].value;
  7. if (dirRE.test(name)) { //如果该属性以v-、@或:开头,表示这是Vue内部指令
  8. // mark element as dynamic
  9. el.hasBindings = true;
  10. // modifiers
  11. modifiers = parseModifiers(name);
  12. if (modifiers) {
  13. name = name.replace(modifierRE, '');
  14. }
  15. if (bindRE.test(name)) { // v-bind //bindRD等于/^:|^v-bind:/ ,即该属性是v-bind指令时
  16. /*v-bind逻辑*/
  17. } else if (onRE.test(name)) { // v-on //onRE等于/^@|^v-on:/,即该属性是v-on指令时
  18. /*v-on逻辑*/
  19. } else { // normal directives //普通指令
  20. name = name.replace(dirRE, ''); //去掉指令前缀,比如v-model执行后等于model
  21. // parse arg
  22. var argMatch = name.match(argRE); //argRE等于:(.*)$/,如果name以:开头的话
  23. var arg = argMatch && argMatch[1];
  24. if (arg) {
  25. name = name.slice(0, -(arg.length + 1));
  26. }
  27. addDirective(el, name, rawName, value, arg, modifiers); //执行addDirective给el增加一个directives属性,值是一个数组,例如:[{name: "model", rawName: "v-model", value: "message", arg: null, modifiers: undefined}]
  28. if ("development" !== 'production' && name === 'model') {
  29. checkForAliasModel(el, value);
  30. }
  31. }
  32. } else {
  33. /*普通特性的逻辑*/
  34. }
  35. }
  36. }

addDirective会给AST对象增加一个directives属性,用于保存对应的指令信息,如下:

  1. function addDirective ( //第6561行 指令相关,给el这个AST对象增加一个directives属性,值为该指令的信息,比如:
  2. el,
  3. name,
  4. rawName,
  5. value,
  6. arg,
  7. modifiers
  8. ) {
  9. (el.directives || (el.directives = [])).push({ name: name, rawName: rawName, value: value, arg: arg, modifiers: modifiers });
  10. el.plain = false;
  11. }

例子里的 <input v-model="message" placeholder="edit me" type="text">对应的AST对象如下:

接下来在generate生成rendre函数的时候,获取data属性时会执行genDirectives()函数,该函数会执行全局的model函数,也就是v-model的初始化函数,如下:

  1. function genDirectives (el, state) { //第10352行 获取指令
  2. var dirs = el.directives; //获取元素的directives属性,是个数组,例如:[{name: "model", rawName: "v-model", value: "message", arg: null, modifiers: undefined}]
  3. if (!dirs) { return } //如果没有directives则直接返回
  4. var res = 'directives:[';
  5. var hasRuntime = false;
  6. var i, l, dir, needRuntime;
  7. for (i = 0, l = dirs.length; i < l; i++) { //遍历dirs
  8. dir = dirs[i]; //每一个directive,例如:{name: "model", rawName: "v-model", value: "message", arg: null, modifiers: undefined}
  9. needRuntime = true;
  10. var gen = state.directives[dir.name]; //获取对应的指令函数,如果是v-model,则对应model函数,可能为空的,只有内部指令才有
  11. if (gen) {
  12. // compile-time directive that manipulates AST.
  13. // returns true if it also needs a runtime counterpart.
  14. needRuntime = !!gen(el, dir, state.warn); //执行指令对应的函数,也就是全局的model函数
  15. }
  16. if (needRuntime) {
  17. hasRuntime = true;
  18. res += "{name:\"" + (dir.name) + "\",rawName:\"" + (dir.rawName) + "\"" + (dir.value ? (",value:(" + (dir.value) + "),expression:" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (",arg:\"" + (dir.arg) + "\"") : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},";
  19. }
  20. }
  21. if (hasRuntime) {
  22. return res.slice(0, -1) + ']' //去掉最后的逗号,并加一个],最后返回
  23. }
  24. }

model()函数会根据不同的tag(select、input的不同)做不同的处理,如下:

  1. function model ( //第6854行 v-model指令的初始化
  2. el,
  3. dir,
  4. _warn
  5. ) {
  6. warn$1 = _warn;
  7. var value = dir.value; //
  8. var modifiers = dir.modifiers; //修饰符
  9. var tag = el.tag; //标签名,比如:input
  10. var type = el.attrsMap.type;
  11. {
  12. // inputs with type="file" are read only and setting the input's
  13. // value will throw an error.
  14. if (tag === 'input' && type === 'file') {
  15. warn$1(
  16. "<" + (el.tag) + " v-model=\"" + value + "\" type=\"file\">:\n" +
  17. "File inputs are read only. Use a v-on:change listener instead."
  18. );
  19. }
  20. }
  21. if (el.component) {
  22. genComponentModel(el, value, modifiers);
  23. // component v-model doesn't need extra runtime
  24. return false
  25. } else if (tag === 'select') { //如果typ为select下拉类型
  26. genSelect(el, value, modifiers);
  27. } else if (tag === 'input' && type === 'checkbox') {
  28. genCheckboxModel(el, value, modifiers);
  29. } else if (tag === 'input' && type === 'radio') {
  30. genRadioModel(el, value, modifiers);
  31. } else if (tag === 'input' || tag === 'textarea') { //如果是input标签,或者是textarea标签
  32. genDefaultModel(el, value, modifiers);             //则执行genDefaultModel()函数
  33. } else if (!config.isReservedTag(tag)) {
  34. genComponentModel(el, value, modifiers);
  35. // component v-model doesn't need extra runtime
  36. return false
  37. } else {
  38. warn$1(
  39. "<" + (el.tag) + " v-model=\"" + value + "\">: " +
  40. "v-model is not supported on this element type. " +
  41. 'If you are working with contenteditable, it\'s recommended to ' +
  42. 'wrap a library dedicated for that purpose inside a custom component.'
  43. );
  44. }
  45. // ensure runtime directive metadata
  46. return true
  47. }

genDefaultModel会在el的value绑定对应的值,并调用addHandler()添加对应的事件,如下:

  1. function genDefaultModel ( //第6965行 nput标签 和textarea标签 el:AST对象 value:对应值
  2. el,
  3. value,
  4. modifiers
  5. ) {
  6. var type = el.attrsMap.type; //获取type值,比如text,如果未指定则为undefined
  7.  
  8. // warn if v-bind:value conflicts with v-model
  9. // except for inputs with v-bind:type
  10. {
  11. var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value']; //尝试获取动态绑定的value值
  12. var typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type']; //尝试获取动态绑定的type值
  13. if (value$1 && !typeBinding) { //如果动态绑定了value 且没有绑定type,则报错
  14. var binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value';
  15. warn$1(
  16. binding + "=\"" + value$1 + "\" conflicts with v-model on the same element " +
  17. 'because the latter already expands to a value binding internally'
  18. );
  19. }
  20. }
  21. var ref = modifiers || {};
  22. var lazy = ref.lazy; //获取lazy修饰符
  23. var number = ref.number; //获取number修饰符
  24. var trim = ref.trim; //获取trim修饰符
  25. var needCompositionGuard = !lazy && type !== 'range';
  26. var event = lazy //如果有lazy修饰符则绑定为change事件,否则绑定input事件
  27. ? 'change'
  28. : type === 'range'
  29. ? RANGE_TOKEN
  30. : 'input';
  31. var valueExpression = '$event.target.value';
  32. if (trim) { //如果有trim修饰符,则在值后面加上trim()
  33. valueExpression = "$event.target.value.trim()";
  34. }
  35. if (number) { //如果有number修饰符,则加上_n函数,就是全局的toNumber函数
  36. valueExpression = "_n(" + valueExpression + ")";
  37. }
  38. var code = genAssignmentCode(value, valueExpression); //返回一个表达式,例如:message=$event.target.value
  39. if (needCompositionGuard) { //如果需要composing配合,则在前面加上一段if语句
  40. code = "if($event.target.composing)return;" + code;
  41. }
  42. //双向绑定就是靠着两行代码的
  43. addProp(el, 'value', ("(" + value + ")")); //添加一个value的prop
  44. addHandler(el, event, code, null, true); //添加event事件
  45. if (trim || number) {
  46. addHandler(el, 'blur', '$forceUpdate()');
  47. }
  48. }

渲染完成后对应的render函数如下:

  1. with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',[_v("Message is: "+_s(message))]),_v(" "),_c('input',{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"placeholder":"edit me","type":"text"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return;message=$event.target.value}}})])}

我们整理一下就看得清楚一点,如下:

  1. with(this) {
  2. return _c('div', {
  3. attrs: {
  4. "id": "app"
  5. }
  6. },
  7. [_c('p', [_v("Message is: " + _s(message))]), _v(" "), _c('input', {
  8. directives: [{
  9. name: "model",
  10. rawName: "v-model",
  11. value: (message),
  12. expression: "message"
  13. }],
  14. attrs: {
  15. "placeholder": "edit me",
  16. "type": "text"
  17. },
  18. domProps: {
  19. "value": (message)
  20. },
  21. on: {
  22. "input": function($event) {
  23. if ($event.target.composing) return;
  24. message = $event.target.value
  25. }
  26. }
  27. })])
  28. }

最后等DOM节点渲染成功后就会执行events模块的初始化事件 并且会执行directive模块的inserted钩子函数:

  1. var directive = {
  2. inserted: function inserted (el, binding, vnode, oldVnode) { //第7951行
  3. if (vnode.tag === 'select') {
  4. // #6903
  5. if (oldVnode.elm && !oldVnode.elm._vOptions) {
  6. mergeVNodeHook(vnode, 'postpatch', function () {
  7. directive.componentUpdated(el, binding, vnode);
  8. });
  9. } else {
  10. setSelected(el, binding, vnode.context);
  11. }
  12. el._vOptions = [].map.call(el.options, getValue);
  13. } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) { //如果tag是textarea节点,或者type为这些之一:text,number,password,search,email,tel,url
  14. el._vModifiers = binding.modifiers; //保存修饰符
  15. if (!binding.modifiers.lazy) { //如果没有lazy修饰符,先后绑定三个事件
  16. el.addEventListener('compositionstart', onCompositionStart);
  17. el.addEventListener('compositionend', onCompositionEnd);
  18. // Safari < 10.2 & UIWebView doesn't fire compositionend when
  19. // switching focus before confirming composition choice
  20. // this also fixes the issue where some browsers e.g. iOS Chrome
  21. // fires "change" instead of "input" on autocomplete.
  22. el.addEventListener('change', onCompositionEnd);
  23. /* istanbul ignore if */
  24. if (isIE9) {
  25. el.vmodel = true;
  26. }
  27. }
  28. }
  29. },

onCompositionStart和onCompositionEnd分别对应compositionstart和compositionend事件,如下:

  1. function onCompositionStart (e) { //第8056行
  2. e.target.composing = true;
  3. }
  4. function onCompositionEnd (e) {
  5. // prevent triggering an input event for no reason
  6. if (!e.target.composing) { return } //如果e.target.composing为false,则直接返回,即保证不会重复触发
  7. e.target.composing = false;
  8. trigger(e.target, 'input'); //触发e.target的input事件
  9. }
  10. function trigger (el, type) { //触发el上的type事件 例如type等于:input
  11. var e = document.createEvent('HTMLEvents'); //创建一个HTMLEvents类型
  12. e.initEvent(type, true, true); //初始化事件
  13. el.dispatchEvent(e); //向el这个元素派发e这个事件
  14. }

最后执行的el.dispatchEvent(e)就会触发我们生成的render函数上定义的input事件

原文链接:http://www.cnblogs.com/greatdesert/p/11154239.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号