Vue2源码梳理:关于数据驱动,与new Vue时的初始化操作

news/2024/6/17 5:41:12 标签: vue.js, 前端, 前端框架

数据驱动


1 )概述

  • vue的一个核心思想,就是数据驱动

  • 所谓数据驱动,就是指视图是由数据驱动生成的

  • 对视图的修改并不会直接操作dom,而是通过修改数据

  • 它相比我们传统的前端开发,如使用 jQuery 的前端库直接去修改 dom 的的话

  • 它大大简化了代码量,特别是当交互复杂的时候,只关心数据的修改会让代码的逻辑变得非常清晰

  • 因为dom变成了数据的映射,我们所有的逻辑都是对数据的修改,而并不关注dom,这样的代码非常有利于维护

  • vue.js中,可以采用简洁的模板语法来声明式的将数据渲染为 dom,示例

    // main.js
    import Vue from 'vue'
    
    var app = new Vue({
        el: '#app',
        /*
        data: {
          message: 'Hello'
        },
        */
        data() {
          return {
            message: 'Hello'
          }
        }
    })
    
    // App.vue
    <div id="app">
      {{ message }}
    </div>
    
    • 这实例的配置有两个参数
      • el: 是它的一个挂载的dom对象
      • data: 是相关的数据
    • 在模板中就声明是起了这样的一个差值message
    • 最终映射到浏览器上,可以看到这个文本节点
    • 它实际上就生成了一个字符串文本
  • 这个例子是为了说明这个数据在js中定义的这个数据 最终是怎么渲染到dom上的

  • 这就是 new Vue 的时候帮我们做的这些事情

2 )数据驱动的关注

  • 第一,分析数据是怎么映射到dom的
    • 传入了这样一个javascript对象,最终怎么生成到dom上的
  • 第二,数据的变化驱动视图的变化
    • 对message的修改,视图是怎么跟着变化的

new Vue时,做了哪些处理

  • 在 src/core/instance/index.js 中
    function Vue (options) {
      if (process.env.NODE_ENV !== 'production' &&
        !(this instanceof Vue)
      ) {
        warn('Vue is a constructor and should be called with the `new` keyword')
      }
      this._init(options)
    }
    initMixin(Vue)
    stateMixin(Vue)
    eventsMixin(Vue)
    lifecycleMixin(Vue)
    renderMixin(Vue)
    
    export default Vue
    
  • 它实际上就是一个class,也就是一个function实现的这个class
  • 它实际上就是执行了这个原型上的 _init 方法
  • 这个 _init 方法在 src/core/instance/init.js 中定义
  • 注意,这个 _init 方法是执行下面的 initMixin(Vue) 时,才被挂载的
  • 就在这个 initMixin 方法的内部,挂载了 _init, 如下
    Vue.prototype._init = function (options?: Object) {
      const vm: Component = this
      // a uid
      vm._uid = uid++
    
      let startTag, endTag
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        startTag = `vue-perf-start:${vm._uid}`
        endTag = `vue-perf-end:${vm._uid}`
        mark(startTag)
      }
    
      // a flag to avoid this being observed
      vm._isVue = true
      // merge options
      if (options && options._isComponent) {
        // optimize internal component instantiation
        // since dynamic options merging is pretty slow, and none of the
        // internal component options needs special treatment.
        initInternalComponent(vm, options)
      } else {
        vm.$options = mergeOptions(
          resolveConstructorOptions(vm.constructor),
          options || {},
          vm
        )
      }
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        initProxy(vm)
      } else {
        vm._renderProxy = vm
      }
      // expose real self
      vm._self = vm
      initLifecycle(vm)
      initEvents(vm)
      initRender(vm)
      callHook(vm, 'beforeCreate')
      initInjections(vm) // resolve injections before data/props
      initState(vm)
      initProvide(vm) // resolve provide after data/props
      callHook(vm, 'created')
    
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        vm._name = formatComponentName(vm, false)
        mark(endTag)
        measure(`vue ${vm._name} init`, startTag, endTag)
      }
    
      if (vm.$options.el) {
        vm.$mount(vm.$options.el)
      }
    }
    
  • 在这个 _init 方法中,做了一堆初始化的工作
    • 比如定义 uid
    • 合并options
      • 它会把传入的options,最终都merge到 $options 上
      • 可以通过 $options.el 访问到我们初始化时的 el 对象
      • 可以通过 $options.data 访问到我们定义的 data
    • 后面就是一堆初始化函数
      • 比如,初始化生命周期,事件中心,render,injections, state, provide, state 这些
      • 注意,中间的两次 callHooks 的调用
    • 最后,挂载这个 el 对象
  • 在上面的demo里面, 定义了这个data,需要看下这个data怎么初始化的?
    var app = new Vue({
      el: '#app',
      /*
      data: {
        message: 'Hello'
      },
      */
      data() {
        return {
          message: 'Hello'
        }
      }
      mounted() {
        console.log(this.message); // Hello
        console.log(this._data.message); // Hello
      }
    })
    
  • 现在看下,为何能够通过 this.message 访问到数据的
  • 进入上述的 initState, 定义在 src/core/instance/state.js
    const sharedPropertyDefinition = {
      enumerable: true,
      configurable: true,
      get: noop,
      set: noop
    }
    
    export function proxy (target: Object, sourceKey: string, key: string) {
      sharedPropertyDefinition.get = function proxyGetter () {
        return this[sourceKey][key]
      }
      sharedPropertyDefinition.set = function proxySetter (val) {
        this[sourceKey][key] = val
      }
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    
    export function initState (vm: Component) {
      vm._watchers = []
      const opts = vm.$options
      if (opts.props) initProps(vm, opts.props)
      if (opts.methods) initMethods(vm, opts.methods)
      if (opts.data) {
        initData(vm)
      } else {
        observe(vm._data = {}, true /* asRootData */)
      }
      if (opts.computed) initComputed(vm, opts.computed)
      if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)
      }
    }
    
    function initData (vm: Component) {
      let data = vm.$options.data
      data = vm._data = typeof data === 'function'
        ? getData(data, vm)
        : data || {}
      if (!isPlainObject(data)) {
        data = {}
        process.env.NODE_ENV !== 'production' && warn(
          'data functions should return an object:\n' +
          'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
          vm
        )
      }
      // proxy data on instance
      const keys = Object.keys(data)
      const props = vm.$options.props
      const methods = vm.$options.methods
      let i = keys.length
      while (i--) {
        const key = keys[i]
        if (process.env.NODE_ENV !== 'production') {
          if (methods && hasOwn(methods, key)) {
            warn(
              `Method "${key}" has already been defined as a data property.`,
              vm
            )
          }
        }
        if (props && hasOwn(props, key)) {
          process.env.NODE_ENV !== 'production' && warn(
            `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
            vm
          )
        } else if (!isReserved(key)) {
          proxy(vm, `_data`, key)
        }
      }
      // observe data
      observe(data, true /* asRootData */)
    }
    
    export function getData (data: Function, vm: Component): any {
      // #7573 disable dep collection when invoking data getters
      pushTarget()
      try {
        return data.call(vm, vm)
      } catch (e) {
        handleError(e, vm, `data()`)
        return {}
      } finally {
        popTarget()
      }
    }
    
    • initState
      • 如果有 props, 则初始化 props
      • 如果有 methods, 则初始化 methods
      • 如果有 data, 则初始化 data,看 initData
    • initData
      • 从 $options 中拿到 data
      • 判断 data 是否是一个 function, 通常我们推荐写一个function, 而非直接使用一个对象
      • 如果是函数,则调用 getData, 否则直接使用 data
      • 后面处理好的 data 不是对象,在 dev 环境中进行输出警告
      • 再后面 拿到 keys, props, methods,它们之间就做了一个循环对比
        • 比如说在这个 data 里面定义了这个message
        • 那就不能在 props 下也用 message
        • 或者在 methods 里面用这个 message
        • 为什么不能用,为什么会冲突,是因为它们最终都会挂载到 vm 上
        • 也就是说, 这个message,它最终会挂到这个this
        • 这样,this.message 就可以访问到当前数据了
      • 那这个是怎么实现的呢?实际上就通过这个 proxy 函数实现的
      • proxy 顾名思义就是代理
        • 它实际上通过这个 sharedPropertyDefinition 对象定义了一个get 和一个set 两个函数
        • 然后通过 Object.defineProperty这个方法,代理了这个 target 的 key
        • 就是对 target 的 key 的访问做了一层 get 和 set
        • 这个 target 实际上就是 vm,也就是说访问 vm[key], 这个 getter 函数就会执行,返回 this[sourceKey][key]
        • 这个 sourceKey 就是 _data, 也就是说,当我们访问 this.message 实际上是从 this._data.message 中获取的
      • 因为它是通过这个 proxy 做层代理,在调用 proxy 的时候,实际上就是把这个 _data 作为 sourceKey 传入
      • 这是通过 this.message 能狗访问到 message 定义到的数据的原因,但要注意,_data在后续开发中不要使用,这个_表示私有,不对外提供
      • 类似的,props的访问也类似,这里暂时跳过
      • 之后通过调用 observe 对 data 做一个响应式处理, 这块也跳过
  • 总结
    • 当执行 new Vue 的时候,它实际上是执行了 _init 的方法,这个方法做一堆初始化的工作
    • 先是对 options做合并,接下来就执行一系列的方法
    • 在其中的 initState 过程中,对 data 做一层 proxy 的处理,最后对data做响应式处理
    • 最后会调用 vm.$mount 将 el 进行挂载

扩展


调试Vue2源码小技巧

  • 一般而言,我们都在 dev 环境下调试源码的,没有说在生产环境下调试
  • 我们如果需要调试哪个API或相关流程,需要先建一个小demo项目,之后在当前项目的 node_modules 中
  • 找到 vue 目录,查看 package.json 找到 module 配置,dist/vue.runtime.esm.js
  • 其实这里,并不是真实的入口 !!!
  • 这个demo实际上是 vue-cli 生成的,这个模板工程是由webpack构建出来的,在build/webpack.base.conf.js 中的
  • resolve 属性中,配置了 alias
    {
      'vue': resolve('node_modules/vue/dist/vue.esm.js'),
      '@': resolve('src'),
    }
    
  • 所以,真实的源码是在 node_modules/vue/dist/vue.esm.js 中
  • 这个在 vue-cli 初始化工程的时候,如果选择了 Runtime + Compiler 版本时,会添加这个 alias 的 vue 这行代码
  • 所以,真正使用的是这个文件,这个是一个大的打包后的代码
  • 可以在这个文件中搜索方法,并进行 debugger
  • 后续如何调试,就忽略不再赘述了

http://www.niftyadmin.cn/n/5377224.html

相关文章

【AI视野·今日NLP 自然语言处理论文速览 第七十八期】Wed, 17 Jan 2024

AI视野今日CS.NLP 自然语言处理论文速览 Wed, 17 Jan 2024 (showing first 100 of 163 entries) Totally 100 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computation and Language Papers Deductive Closure Training of Language Models for Coherence, Accur…

Shell脚本——提取目录名和文件名

目录 一、${} 1.${var##*/} 2.${var##*.} 3.${var#*.} 4.${var%/*} 5.${var%%.*} 6.总结 二、basename和dirname 1.basename 2.dirname 在许多场景下&#xff0c;我们都需要对文件名称或者文件所在的目录进行操作&#xff0c;已达到我们业务目的。通常的操作是由路径…

txt写代码绘图

import re import turtle import tkinter as tk from tkinter import filedialogdef remove_between_hashes_and_newlines(txt_content):# 使用正则表达式去掉#和\n之间的内容pattern re.compile(r#.*?\n)result re.sub(pattern, , txt_content)# 去掉#和\n# result result…

内网穿透与搭建私人服务器

前言 内网穿透是一种技术&#xff0c;允许用户从外部网络访问内部私有网络中的服务器或设备。这对于想要从任何地方访问家中或办公室内部网络资源的用户非常有用。以下是为初学者准备的关于如何实现内网穿透以及搭建自己的私人服务器的详细指南。 在这个数字化时代&#xff0c;…

腾讯云4核8G服务器3年600元?

腾讯云4核8G服务器3年600元&#xff1f;目前的价格是轻量应用服务器4核8G12M带宽一年446元、646元15个月&#xff0c;云服务器CVM标准型S5实例4核8G配置价格15个月1437.3元&#xff0c;5年6490.44元&#xff0c;标准型SA2服务器1444.8元一年&#xff0c;在txy.wiki可以查询详细…

【Linux学习】线程池

目录 23.线程池 23.1 什么是线程池 23.2 为什么需要线程池 23.3 线程池的应用场景 23.4 实现一个简单的线程池 23.4.1 RAII风格信号锁 23.4.2 线程的封装 23.4.3 日志打印 22.4.4 定义队列中存放Task类任务 23.4.5 线程池的实现(懒汉模式) 为什么线程池中需要有互斥锁和条件变…

AndroidStdio修改安卓模拟器的安装位置

AndroidStdio修改安卓模拟器的安装位置 1.删除原有的虚拟机 可以直接删除这个avd文件&#xff0c;放心大胆删除 在这个目录下可以看到.avd文件和.ini文件。.avd占了我10G.上图是我转移.avd后截的。发现这个.ini文件&#xff0c;.ini文件就是配置文件&#xff0c;就像mysql安装…

vivado HDL编码技术

HDL编码技术 介绍 硬件描述语言&#xff08;HDL&#xff09;编码技术使您能够&#xff1a; •描述数字逻辑电路中最常见的功能。 •充分利用AMD设备的体系结构功能。 •模板可从AMD Vivado™设计套件集成设计环境中获得&#xff08;侧面&#xff09;。要访问模板&#xff…