Vue2.x源码解读-响应式原理剖析

本文深入探讨Vue2.x的响应式原理,从源码角度解析如何实现数据变化到视图更新的响应式过程。涉及内容包括源码结构、构建版本差异、术语解释以及关键方法如、的实现细节。通过对、和等核心概念的解读,揭示Vue实例创建、数据响应化、依赖收集、数组响应式处理以及计算属性、侦听器和用户自定义的处理方式。
摘要由CSDN通过智能技术生成

准备工作

  1. 下载vue源码,可以先将vue项目fork到自己的github仓库,然后在clone自己仓库的vue,这样在解读源码的时候可以随时添加注释,并将注释提交到自己的仓库。

  2. 源码代码主要结构说明:

    1. dist:打包生成的文件
    2. examples:实例代码目录
    3. src:源码文件目录
      1. compiler:编译器相关代码,把template模板转化成render函数
      2. core:核心代码
        1. components:定义vue自带的keep-alive组件
        2. global-api:定义vue中的静态方法,包括mixinextenduse
        3. instance:创建vue实例成员,包括构造函数、初始化和生命周期函数。
        4. observer:响应式实现
        5. util:工具方法
        6. vdom:虚拟DOM实现,重写了Snabbdom,增加了组件的机制。
      3. platforms:平台相关处理
        1. webweb平台
        2. weex:基于vue的移动端框架
      4. server:服务器端渲染
      5. sfc:单文件组件,将单文件组件转换成js模块
  3. Vue2.x使用了Flow来进行代码静态类型检查。

  4. Vue使用rollup进行打包,相对于webpack来说,rollup打包不会生成冗余代码。rollup命令行参数说明:

    1. -w:开启监视模式

    2. -c:指定配置文件

    3. --sourcemap:开启 sourceMap 功能,之后能在浏览器中的源码中看到源码对应的src文件夹
      在这里插入图片描述

    4. --environment:指定运行环境参数,TARGET指定打包生成的版本。

      "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
      
  5. Vue构建版本之间的差别
    在这里插入图片描述
    术语说明:
    完整版(Full): 同时包含编译器运行时的版本
    编译器: 将模板字符串编译成 javascript 渲染函数的代码
    运行时(Runtime-only): 用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码。基本上就是除去编译器的其它一切。运行时版本体积小,运行效率更高。
    UMD: 可以通过 <script> 标签直接用在浏览器中。
    CommonJS: CommonJS 版本用来配合老的打包工具比如 Browserifywebpack 1。这些打包工具的默认文件 (pkg.main) 是只包含运行时的 CommonJS 版本
    ES Module: 会提供两个 ES Modules (ESM) 构建文件:

    1. 为打包工具提供的 ESM:为诸如 webpack 2Rollup 提供的现代打包工具。ESM 格式被设计为可以被静态分析(编译时解析模块依赖),所以打包工具可以利用这一点来进行tree-shaking并将用不到的代码排除出最终的包。为这些打包工具提供的默认文件 pkg.module 是只有运行时的 ES Module 构建 (vue.runtime.esm.js)
    2. 为浏览器提供的 ESM (2.6+):用于在现代浏览器中通过 <script type="module"> 直接导入。
  6. 寻找入口文件(以 dev 命令为例)

    1. 根据package.json中的命令行找到配置文件
      在这里插入图片描述

    2. 找到配置文件中最后的导出语句,然后向前解析找到最后导出的内容

      // 判断环境变量是否有 TARGET
      // 如果有的话 使用 genConfig() 生成 rollup 配置文件
      if (process.env.TARGET) {
              // 如果有 TARGET 则打包生成指定的目标版本
        module.exports = genConfig(process.env.TARGET)
      } else {
              // 如果没有则打包生成所有版本
        exports.getBuild = genConfig
        exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
      }
      

      这里关键是是genConfig方法,查看方法,定位到buildsbuilds存储了不同TARGET对应的配置。

      const builds = {
             
        // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
        'web-runtime-cjs-dev': {
             
          entry: resolve('web/entry-runtime.js'),
          dest: resolve('dist/vue.runtime.common.dev.js'),
          format: 'cjs',
          env: 'development',
          banner
        },
        'web-runtime-cjs-prod': {
             
          entry: resolve('web/entry-runtime.js'),
          dest: resolve('dist/vue.runtime.common.prod.js'),
          format: 'cjs',
          env: 'production',
          banner
        },
        }
      

      resolve是用来解析模块路径中可能存在的alias别名,并返回一个绝对路径。

      const resolve = p => {
             
        // 根据路径中的前半部分去alias中找别名
        const base = p.split('/')[0]
        if (aliases[base]) {
             
          return path.resolve(aliases[base], p.slice(base.length + 1))
        } else {
             
          return path.resolve(__dirname, '../', p)
        }
      }
      

      alias别名配置

      module.exports = {
             
        vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
        compiler: resolve('src/compiler'),
        core: resolve('src/core'),
        shared: resolve('src/shared'),
        web: resolve('src/platforms/web'),
        weex: resolve('src/platforms/weex'),
        server: resolve('src/server'),
        sfc: resolve('src/sfc')
      }
      
    3. 根据配置找到入口文件,从入口文件开始解读。

  7. 按照上面的方法找到入口文件entry-runtime-with-compiler.js,通过阅读入口文件了解到该文件的主要功能是重写Vue$mount方法,用来渲染DOM。通过源码的阅读可以知道以下几处注意事项:

    1. Vue实例创建时传入的el不能是bodyhtml

      // el 不能是 body 或者 html
      if (el === document.body || el === document.documentElement) {
             
        process.env.NODE_ENV !== 'production' && warn(
          `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
        )
        return this
      }
      
    2. 创建Vue实例时如果同时传入了templaterender,则会忽略template
      在这里插入图片描述

    3. 通过调试确定$mount是在什么时候使用的。编译时开启sourceMap,直接在浏览器中定位到入口文件,并打下断点,刷新浏览器,运行到断点处,可以看到右侧Call Stack调用栈中当前正在执行Vue.$mount,下面一行则是调用Vue.$mount的运行环境,即在Vue.init中,一直向下可以追溯到Vue实例的创建的执行环境。在调用栈中可以查看函数的调用过程。
      在这里插入图片描述

  8. 根据导入的Vue查看runtime/index.js

    1. vue.config定义一些属性。

      // 判断是否是关键属性(表单元素的 input/checked/selected/muted)
      // 如果是这些属性,设置el.props属性(属性不设置到标签上)
      Vue.config.mustUseProp = mustUseProp
      Vue.config.isReservedTag = isReservedTag
      Vue.config.isReservedAttr = isReservedAttr
      Vue.config.getTagNamespace = getTagNamespace
      Vue.config.isUnknownElement = isUnknownElement
      
    2. 定义平台环境上的指令(v-show、v-modal)和组件(Transition,TransitionGroup)

      // install platform runtime directives & components
      extend(Vue.options.directives, platformDirectives)
      extend(Vue.options.components, platformComponents)
      
    3. 定义patch方法

      Vue.prototype.__patch__ = inBrowser ? patch : noop
      
    4. 定义原型上的$mount方法

      // public mount method
      Vue.prototype.$mount = function (
        el?: string | Element,
        hydrating?: boolean
      ): Component {
             
        el = el && inBrowser ? query(el) : undefined
        return mountComponent(this, el, hydrating) // 渲染组件
      }
      
    5. 执行devtoolsinit钩子,并对运行环境的不足做出提示。

      if (inBrowser) {
             
        setTimeout(() => {
             
          if (config.devtools) {
             
            if (devtools) {
             
              devtools.emit('init', Vue)
            } else if (
              process.env.NODE_ENV !== 'production' &&
              process.env.NODE_ENV !== 'test'
            ) {
             
              console[console.info ? 'info' : 'log'](
                'Download the Vue Devtools extension for a better development experience:\n' +
                'https://github.com/vuejs/vue-devtools'
              )
            }
          }
          if (process.env.NODE_ENV !== 'production' &&
            process.env.NODE_ENV !== 'test' &&
            config.productionTip !== false &&
            typeof console !== 'undefined'
          ) {
             
            console[console.info ? 'info' : 'log'](
              `You are running Vue in development mode.\n` +
              `Make sure to turn on production mode when deploying for production.\n` +
              `See more tips at https://vuejs.org/guide/deployment.html`
            )
          }
        }, 0)
      }
      
  9. Vue的初始化:通过runtime/index.js中依赖的追溯,找到core/index.js,这个文件中定义了Vue的初始化的过程。

    1. 初始化Vue的静态成员。

      initGlobalAPI(Vue)
      
      1. 初始化Vue.config对象

          // config
          const configDef = {
                 }
          configDef.get = () => config
          if (process.env.NODE_ENV !== 'production') {
                 
            configDef.set = () => {
                 
              warn(
                'Do not replace the Vue.config object, set individual fields instead.'
              )
            }
          }
          // 初始化 Vue.config 对象
          Object.defineProperty(Vue, 'config', configDef)
        
      2. 暴露util方法,但不将其作为Vue API的一部分,尽量不要依赖他们

          // exposed util methods.
          // NOTE: these are not considered part of the public API - avoid relying on
          // them unless you are aware of the risk.
          // 这些工具方法不视作全局API的一部分,除非你已经意识到某些风险,否则不要去依赖他们
          Vue.util = {
                 
            warn,
            extend,
            mergeOptions,
            defineReactive
          }
        
      3. 定义静态 setdelnextTick方法

          // 静态方法 set/delete/nextTick
          Vue.set = set
          Vue.delete = del
          Vue.nextTick = nextTick
        

        set为对象设置为响应式的属性,实现原理解析:

        function set (target: Array<any> | Object, key: any, val: any): any {
                 }
        
        1. 确保 target 是对象或数组

          if (process.env.NODE_ENV !== 'production' &&
              (isUndef(target) || isPrimitive(target))
            ) {
                     
              warn(`Cannot set reactive property on undefined, null, or primitive value: ${
                       (target: any)}`)
            }
          
        2. target是数组时,确保key是有效索引,并扩展数组长度,使数组包含key索引,通过splice方法设置值。这里的splice方法是经过处理的方法,在方法内部会调用dep.notify()发送通知。

          // 判断 target 是否是数组,key 是否是合法的索引
            if (Array.isArray(target) && isValidArrayIndex(key)) {
                     
              target.length = Math.max(target.length, key) // 扩展数组长度
              // 通过 splice 对key位置的元素进行替换
              // splice 在 array.js 进行了响应化的处理
              target.splice(key, 1, val)
              return val
            }
          
        3. keytarget对象中的属性,直接赋值。

          // 如果 key 在对象中已经存在直接赋值
          if (key in target && !(key in Object.prototype)) {
                     
            target[key] = val
            return val
          }
          
        4. 如果targetvue实例或$data,直接返回

          // 获取 target 中的 observer 对象
            const ob = (target: any).__ob__
            // 如果 target 是 vue 实例或者 $data 直接返回
            if (target._isVue || (ob && ob.vmCount)) {
                     
              process.env.NODE_ENV !== 'production' && warn(
                'Avoid adding rea
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值