最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • [Vue源码]当我们谈及v-model,我们在讨论什么?

    正文概述 掘金(村上小树)   2021-03-20   539

    前言

    当提及到v-model时,我们都会想到它时一个语法糖,相当于在使用该指令的元素或者组件上绑定了props中的value值以及注册了input事件。但其实深入挖掘其中,可以学习到更多的东西,下面就看一下相关的Vue源码吧。

    从编译出发

    以下面的例子出发分析:

    new Vue({
        el:'#app',
        template:'<input v-model="value">'
    })
    

    在初始化时,Vue构造函数会执行this._init,然后this._init内部通过vm.$mount()调用Vue.prototype.$mount的方法。Vue.prototype.$mount的方法会因Vue构建版本而不同。

    Vue构建版本分完整版运行时的版本完整版包括编译器运行时的版本。如果需要在代码编译模板 (比如传入一个字符串给 template 选项,或挂载到一个元素上并以其 DOM 内部的 HTML 作为模板),就将需要加上编译器,即完整版:

    // 需要编译器
    new Vue({
      template: '<div>{{ hi }}</div>'
    })
    
    // 不需要编译器
    new Vue({
      render (h) {
        return h('div', this.hi)
      }
    })
    

    对于我们现在例子的情况,需要用到完整版Vue.prototype.$mount(定义在src\platforms\web\entry-runtime-with-compiler.js中)来作分析,当render未定义但template已定义时,会通过compileToFunction解析template以获取render,其内部会调用baseCompile作为主要的解析函数,如下所示:

    function baseCompile (
      template: string,
      options: CompilerOptions
    ): CompiledResult {
      // 1.把模板转换成ast抽象语法树
      // 抽象语法树,用来以树形的方式描述代码结构
      const ast = parse(template.trim(), options)
      if (options.optimize !== false) {
        // 2.优化抽象语法树
        optimize(ast, options)
      }
      // 3.把抽象语法树生成字符串形式的js代码
      const code = generate(ast, options)
      return {
        ast,
        // 渲染函数
        render: code.render,
        // 静态渲染函数,生成静态VNode树
        staticRenderFns: code.staticRenderFns
      }
    }
    

    至此,我们可以知道编译的三个步骤:

    • parse : 解析模板字符串生成 AST语法树
    • optimize : 优化语法树,主要时标记静态节点,提高更新页面的性能
    • codegen : 生成js代码,主要是render函数和staticRenderFns函数

    我们接着开头的例子去代入到这些步骤中:

    parse

    parse过程中,会对模板使用大量的正则表达式去进行解析。开头的例子会被解析成以下AST节点:

    ast={
        'type': 1, // type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本
        'tag': 'input',
        'directives': [
            {
                'name': 'model',
                'value': 'value',
                'modifier': undefined
            }
        ],
        attrsMap:{
            'v-model':'value'
        },
        // ...还有很多属性省略...
    }
    

    optimize过程在此不做分析,因为本例子没有静态节点

    codegen

    首先,查看codegen过程中const code = generate(ast, options)中,generate源码如下所示:

    export function generate (
      ast: ASTElement | void,
      options: CompilerOptions
    ): CodegenResult {
      /** CodegenState构造函数是根据options生成一个处理AST语法树的代码生成器
      * CodegenState类的构造函数中有以下逻辑
      * class CodegenState{
          constructor(){
              this.directives = extend(extend({}, baseDirectives), options.directives)
          }
      * }
      * 其中baseDirectives用于处理v-on,v-bind,v-cloak指令
      * options为编译相关的配置,不同的平台下option不同,
      * 在web环境下,option会来自src\platforms\web\compiler\options.js,
      * 而option['directives']则引用自src\platforms\web\compiler\directives\index.js
      * 里面包括对v-model,v-text,v-html指令的处理
      */
      const state = new CodegenState(options)
      /**
      * 
      */
      const code = ast ? genElement(ast, state) : '_c("div")'
      return {
        render: `with(this){return ${code}}`,
        staticRenderFns: state.staticRenderFns
      }
    }
    

    genElement方法中有大量处理当前AST节点属性的逻辑,根据节点属性生成各种生成函数,在我们的例子中,只需要执行里面的genData,而genData中针对el.directives会调用genDirectives处理。genDirectives源码如下所示:

    function genDirectives (el: ASTElement, state: CodegenState): string | void {
      const dirs = el.directives
      if (!dirs) return
      let res = 'directives:['
      let hasRuntime = false
      let i, l, dir, needRuntime
      // 遍历el.directives
      for (i = 0, l = dirs.length; i < l; i++) {
        dir = dirs[i]
        needRuntime = true
        // 调用CodegenState实例的directives对应的指令处理函数进行处理
        const gen: DirectiveFunction = state.directives[dir.name]
        if (gen) {
          needRuntime = !!gen(el, dir, state.warn)
        }
        if (needRuntime) {
          hasRuntime = true
          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)}` : ''
          }},`
        }
      }
      if (hasRuntime) {
        return res.slice(0, -1) + ']'
      }
    }
    

    现在可以知道,处理v-modelCodegenState实例中的directives['model'],接下来我们看一下其相关代码:

    rc\platforms\web\compiler\directives\model.js

    export default function model (
      el: ASTElement,
      dir: ASTDirective,
      _warn: Function
    ): ?boolean {
      warn = _warn
      const value = dir.value
      const modifiers = dir.modifiers
      const tag = el.tag
      const type = el.attrsMap.type
      // 以分情况来处理v-model
      // 1. 如果节点是组件实例
      if (el.component) {
        genComponentModel(el, value, modifiers)
        // component v-model doesn't need extra runtime
        return false
      // 2. 如果节点的html标签为select
      } else if (tag === 'select') {
        genSelect(el, value, modifiers)
      // 3. 如果节点的html标签为input且type属性为'checkbox'
      } else if (tag === 'input' && type === 'checkbox') {
        genCheckboxModel(el, value, modifiers)
      // 4. 如果节点的html标签为input且type属性为'radio'
      } else if (tag === 'input' && type === 'radio') {
        genRadioModel(el, value, modifiers)
      // 5. 如果节点的html标签为input且type属性为'textarea'
      } else if (tag === 'input' || tag === 'textarea') {
        genDefaultModel(el, value, modifiers)
      // 6. 检查标记是否被保留,以便它不能被注册为组件。这是依赖于平台的,在web端此值恒为false
      } else if (!config.isReservedTag(tag)) {
        genComponentModel(el, value, modifiers)
        // component v-model doesn't need extra runtime
        return false
      } 
      // ensure runtime directive metadata
      return true
    }
    

    继续以我们的例子做分析,此时执行genDefaultModel函数,该函数源码如下:

    function genDefaultModel (
      el: ASTElement,
      value: string,
      modifiers: ?ASTModifiers
    ): ?boolean {
      const type = el.attrsMap.type
      const { lazy, number, trim } = modifiers || {}
      const needCompositionGuard = !lazy && type !== 'range'
      // 如果v-model.lazy则选择监听change事件,
      // input事件的触发机制是,当输入框内容发生变化时
      // change事件的触发机制是,当输入框失去焦点后且输入框内容发生变化时
      const event = lazy
        ? 'change'
        : type === 'range'
          ? RANGE_TOKEN
          : 'input'
      // valueExpression是用于event事件触发后,把valueExpression赋值到v-model绑定的值上,也就是value
      let valueExpression = '$event.target.value'
      // 如果v-model.trim,则事件触发把event.target.value赋值前调用trim()
      if (trim) {
        valueExpression = `$event.target.value.trim()`
      }
      // 如果v-model.number,则事件触发把event.target.value赋值前先调用parseFloat转换
      // target._n = toNumber;
      // function toNumber (val) {
      //   var n = parseFloat(val);
      //   return isNaN(n) ? val : n
      // }
      if (number) {
        valueExpression = `_n(${valueExpression})`
      }
      
      /**
      * genAssignmentCode (value: string,assignment: string): string {
          const res = parseModel(value)
          // 如果v-model="value"
          if (res.key === null) {
            return `${value}=${assignment}`
          // 如果v-model="value",即绑定的是一个对象中的属性
          } else {
            return `$set(${res.exp}, ${res.key}, ${assignment})`
          }
        }
      */
      let code = genAssignmentCode(value, valueExpression)
      if (needCompositionGuard) {
        code = `if($event.target.composing)return;${code}`
      }
      // 添加到prop中
      addProp(el, 'value', `(${value})`)
      // 添加到on事件中
      addHandler(el, event, code, null, true)
      // 以上两个处理过程相当于把节点处理为<input :value="value" @input="value=$event.target.value">
      
      if (trim || number) {
        addHandler(el, 'blur', '$forceUpdate()')
      }
    }
    

    以上针对lazy,number,trim修饰符的处理在其余五种情况下也几乎一样。

    genDirectivesmodel函数执行完后,接下来的操作会生成directives数据。directives数据将会作为生成VNode需要的参数传入到render函数内部调用的createElement函数中。

    直至codegen过程结束,生成的render代码如下所示:

    function render() {
      with(this) {
        return _c('input', {
          directives: [{
            name: "model",
            rawName: "v-model",
            value: (value),
            expression: "value"
          }],
          domProps: {
            "value": (value)
          },
          on: {
            "input": function ($event) {
              if ($event.target.composing) return;
              value = $event.target.value
            }
          }
        })
      }
    }
    

    model函数中的多种情况

    这里我直接说一下其他情况下,v-bindv-on会对应哪些值和事件,这些情况对应的函数的源码我就不做分析了,基本和genDefaultModel都大同小异。

    1. 如果节点是组件实例

    此时,v-model会转换成绑定(v-bind)value值和监听(v-on)input事件。另外,如果在组件里定义了model属性,则会绑定和监听其中设置的值,例如:

    Vue.component('base-checkbox', {
      model: {
        prop: 'checked',
        event: 'change'
      },
      props: {
        checked: Boolean
      },
      template: `
          // ...省略
      `
    })
    

    此时该组件以<base-checkbox v-model="lovingVue"></base-checkbox>的方式调用时,v-model会转换成绑定checked值和监听change事件。

    2. 如果节点为普通元素

    节点类型绑定值(v-bind)监听事件(v-on)
    input[type="radio"]checkedchangeselectvaluechangeinput[type="checkbox"]checkedchangeinputvalueinputtextareavalueinput

    后记

    支支吾吾想了很久,后记还是写 "希望大家多多支持" 吧。


    起源地下载网 » [Vue源码]当我们谈及v-model,我们在讨论什么?

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元