当前位置: 首页 > article >正文

Vue进阶之Vue2源码解析

Vue2源码解析

  • 源码解析
    • 目录解析
    • package.json
    • 入口
      • 查找入口文件
      • 确定vue入口
      • this.\_init_ 方法
      • $mount 挂载方法
      • Vue.prototype._render
      • Vue.prototype._update
      • Vue.prototype._patch

vue2
vue3

源码解析

目录解析

vue2.6之后的版本都做的是兼容Vue3的内容,2.6版本前的内容才是纯vue2的内容,这里看的V2.6.14版本代码

这个是一个单仓库的内容,并不是多包,因此packages目录这里只是工具的集合,具体的代码在src目录下

  • flow 类似ts的类型声明,flow目录下的每一个文件,都可以理解为一个workspace
    • compiler.js 类似ts的结构
    • modules.js
      module中定义的变量,包含这些属性,和ts一模一样,可以说就是ts
      就比如:
declare module 'source-map' {
  declare class SourceMapGenerator {
    setSourceContent(filename: string, content: string): void;
    addMapping(mapping: Object): void;
    toString(): string;
  }
  declare class SourceMapConsumer {
    constructor (map: Object): void;
    originalPositionFor(position: { line: number; column: number; }): {
      source: ?string;
      line: ?number;
      column: ?number;
    };
  }
}

就能引用 SourceMapConsumer 的功能:
在这里插入图片描述

  • .flowconfig
    name_mapper — compiler映射文件 类似前面讲到的alias
  • .circleci
    • config.yml 流水线的配置,自动化流水线
      是国外CI品牌,后面会通过git action去做的,当进行指定动作的触发的时候,比如push的时候,push到master代码后,就能自动执行后续的内容,后面内容就是自动化流水线的内容
version: 2

defaults: &defaults
  working_directory: ~/project/vue
  docker:
    - image: vuejs/ci #流水线配置是国外CI品牌

jobs:
  install:
    <<: *defaults
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-vue-{{ .Branch }}-{{ checksum "yarn.lock" }}
            - v1-vue-{{ .Branch }}-
            - v1-vue-
      - run: npm install #npm安装依赖
      - save_cache: #缓存
          key: v1-vue-{{ .Branch }}-{{ checksum "yarn.lock" }}
          paths:
            - node_modules/
      - persist_to_workspace:
          root: ~/project
          paths:
            - vue

  lint-flow-types:
    <<: *defaults
    steps:
      - attach_workspace:
          at: ~/project
      - run: npm run lint #eslint检查
      - run: npm run flow #flow检查
      - run: npm run test:types #检查类型

  test-cover:
    <<: *defaults
    steps:
      - attach_workspace:
          at: ~/project
      - run: npm run test:cover
      - run:
         name: report coverage stats for non-PRs
         command: |
           if [[ -z $CI_PULL_REQUEST ]]; then
             ./node_modules/.bin/codecov
           fi

  test-e2e:
    <<: *defaults
    steps:
      - attach_workspace:
          at: ~/project
      - run: npm run test:e2e -- --env phantomjs

  test-ssr-weex:
    <<: *defaults
    steps:
      - attach_workspace:
          at: ~/project
      - run: npm run test:ssr #打包成ssr
      - run: npm run test:weex #打包成weex

  trigger-regression-test:
    <<: *defaults
    steps:
      - run:
          command: |
            curl --user ${CIRCLE_TOKEN}: \
            --data build_parameters[CIRCLE_JOB]=update \
            --data build_parameters[VUE_REVISION]=${CIRCLE_SHA1} \
            https://circleci.com/api/v1.1/project/github/vuejs/regression-testing/tree/master

workflows:
  version: 2
  install-and-parallel-test:
    jobs:
      - install
      - test-cover:
          requires:
            - install
      - lint-flow-types:
          requires:
            - install
      - test-e2e:
          requires:
            - install
      - test-ssr-weex:
          requires:
            - install
      - trigger-regression-test:
          filters:
            branches:
              only:
                - "2.6"
                - regression-test
          requires:
            - test-cover
            - lint-flow-types
            - test-e2e
            - test-ssr-weex
  weekly_regression_test:
    triggers:
      - schedule:
          # At 13:00 UTC (9:00 EDT) on every Monday
          cron: "0 13 * * 1"
          filters:
            branches:
              only:
                dev
    jobs:
      - install
      - test-cover:
          requires:
            - install
      - lint-flow-types:
          requires:
            - install
      - test-e2e:
          requires:
            - install
      - test-ssr-weex:
          requires:
            - install
      - trigger-regression-test:
          requires:
            - test-cover
            - lint-flow-types
            - test-e2e
            - test-ssr-weex

  • .github 提供的是谁提供的代码的贡献,要么是怎么提供一个commit代码的标准,社区贡献的一些资源
  • benchmarks:静态的站点/静态的说明
  • dist:打包时候的产物
  • examples:不同的模块的测试
  • flow:类型的处理
  • packages 和现在的monorepo还不一样,这个包可以理解为工具的库,
    • vue-template-compiler:vue模板的编译
  • scripts 一系列的node脚本
  • package.json:
    • scripts 这里的命令许多是针对scripts里的脚本去执行,因为vue2使用的是roll up,所以使用的是roll up去执行scripts里的各种各样的文件。
      在进行本地的开发,构建,部署的时候,我们的自定义脚本
  • src 重点
    • compiler 编译时,将vue templates最后转换为vue能识别的语言
    • core 核心代码,这里指的是runtime的核心代码,将模板编译之后的产物,在浏览器上运行。包含整体vue运行时的功能,也会包含vdom,diff
    • platforms 基于平台的,也就是host宿主,针对于浏览器web,weex端,兼容两端差异性
    • server 服务端,ssr去渲染的
    • sfc 单文件组件
      .vue文件的解析,找到对应的属性,转化成vue能够识别的语法
    • shared
      • constants 通用型的别名
      • utils 通用型的约束的定义
  • test 测试
  • types 类型 vue2使用的是flow,基本上包括了所有的类型,可能有人需要使用ts类型,vue2中使用的ts类型放在这里了
  • .babelrc babel工具
    代码编译的包,包含了vue怎么解析jsx的,怎么动态引用的
    babel/preset-env 能够在这里使用一些新的特性
  • .eslintrc 兼容的环境,ecma标准(es9),环境要支持到es6,全局中定义的环境变量
  • .flowconfig 类型的声明
  • .gitignore 忽略git提交的部分

package.json

  • config
    比较老一点版本的commit lint,进行一些commit的时候,就会执行cz-conventional-changelog,约束commit提交的标准,像feat,fix,docs等这些前缀
  • keywords:关键字
  • repository:代码仓库地址
  • lint-staged:
  • gitHooks:针对整个git生命周期做的
    • pre-commit:commit之前,校验代码规范,执行(lint-staged下的)eslint命令,并且git add
    • commit-msg:在git commit message的时候,node就会执行scripts/verify-commit-msg.js脚本,规范提交信息格式
      也可以用 git-cz 来做这个事情
  • files:代码提交之后,所有内容的产出
  • types:types类型的入口文件
  • unpkg:打包之后的文件,这里算是cdn的内容
  • jsdelivr:打包之后的内容
  • main:当前默认去引用的,umd的方式。
  • module:esm的方式
  • sideEffects:false,没有副作用
  • scripts:
    当前执行的时候,都是用的rollup的方式去做的
    rollup,一种打包工具
    • dev: rollup -w -c scripts/config.js --environment TARGET:web-full-dev
      读取的配置是:scripts/config.js,这里是打包的核心
      TARGET:能够读取到的环境变量
      –environment TARGET:注入的环境变量,就能通过process.env.TARGET找到这个环境变量,调用genConfig方法
      没有找到的话,就将getConfig这个方法透传出去

      if (process.env.TARGET) { //dev环境
        module.exports = genConfig(process.env.TARGET)
      } else { //build环境
        exports.getBuild = genConfig //将genConfig这个方法透传出去
        exports.getAllBuilds = () => Object.keys(builds).map(genConfig) //针对于所有打包的内容,都执行一遍genConfig方法,函数式编程的写法
      }
      

      genConfig:针对不同的版本,进行rollup的配置

      function genConfig(name) {
        //rollup的内容
        const opts = builds[name]
        const config = {
          input: opts.entry,
          external: opts.external,
          plugins: [
            flow(),//类型声明
            alias(Object.assign({}, aliases, opts.alias)) //别名的设置
          ].concat(opts.plugins || []), //加上参数传来的plugin
          output: {
            file: opts.dest,
            format: opts.format,
            banner: opts.banner,
            name: opts.moduleName || 'Vue'
          },
          onwarn: (msg, warn) => {
            if (!/Circular/.test(msg)) {
              warn(msg)
            }
          }
        }
      
        // built-in vars
        const vars = {
          __WEEX__: !!opts.weex,
          __WEEX_VERSION__: weexVersion,
          __VERSION__: version
        }
        // feature flags
        Object.keys(featureFlags).forEach(key => {
          vars[`process.env.${key}`] = featureFlags[key]
        })
        // build-specific env
        if (opts.env) {
          vars['process.env.NODE_ENV'] = JSON.stringify(opts.env)
        }
        config.plugins.push(replace(vars))
      
        if (opts.transpile !== false) {
          config.plugins.push(buble())
        }
      
        Object.defineProperty(config, '_name', {
          enumerable: false,
          value: name
        })
      
        return config
      }
      

      使用rollup可以进行一比一打包代码,使得打包体积变小

    • build:node scripts/build.js 执行的是build.js文件
      和刚说的区别在于,会调用bundle的generate,这样rollup会进行打包压缩,

      function buildEntry (config) {
        const output = config.output
        const { file, banner } = output
        const isProd = /(min|prod)\.js$/.test(file)
        return rollup.rollup(config)
          .then(bundle => bundle.generate(output))
          .then(({ output: [{ code }] }) => {
            if (isProd) {
              const minified = (banner ? banner + '\n' : '') + terser.minify(code, {
                toplevel: true,
                output: {
                  ascii_only: true
                },
                compress: {
                  pure_funcs: ['makeMap']
                }
              }).code
              return write(file, minified, true)
            } else {
              return write(file, code)
            }
          })
      }
      

      terser:js压缩工具

    • test:执行npm run lint,也就是执行 lint 里的命令

    • lint:执行eslint src scripts test,调用.eslintrc.js来匹配代码是否符合这里的规范

    • flow:类型检查

    • release:执行 scripts/release.sh脚本
      release.sh:

      #!/bin/bash
      set -e
      
      if [[ -z $1 ]]; then #找到这个版本
        echo "Enter new version: "
        read -r VERSION
      else
        VERSION=$1
      fi
      
      read -p "Releasing $VERSION - are you sure? (y/n) " -n 1 -r #是否要发布这个版本
      echo
      if [[ $REPLY =~ ^[Yy]$ ]]; then
        echo "Releasing $VERSION ..."
      
        if [[ -z $SKIP_TESTS ]]; then #执行指令
          npm run lint
          npm run flow
          npm run test:cover
          npm run test:e2e -- --env phantomjs
          npm run test:ssr
        fi
      
        # Sauce Labs tests has a decent chance of failing
        # so we usually manually run them before running the release script.
      
        # if [[ -z $SKIP_SAUCE ]]; then
        #   export SAUCE_BUILD_ID=$VERSION:`date +"%s"`
        #   npm run test:sauce
        # fi
      
        # build
        VERSION=$VERSION npm run build
      
        # update packages
        # using subshells to avoid having to cd back
        ( ( cd packages/vue-template-compiler
        npm version "$VERSION"
        if [[ -z $RELEASE_TAG ]]; then
          npm publish #执行发布
        else
          npm publish --tag "$RELEASE_TAG" #加上tag值
        fi
        )
      
        cd packages/vue-server-renderer
        npm version "$VERSION"
        if [[ -z $RELEASE_TAG ]]; then
          npm publish
        else
          npm publish --tag "$RELEASE_TAG"
        fi
        )
      
        # commit 执行git add,commit
        git add -A
        git add -f \
          dist/*.js \
          packages/vue-server-renderer/basic.js \
          packages/vue-server-renderer/build.dev.js \
          packages/vue-server-renderer/build.prod.js \
          packages/vue-server-renderer/server-plugin.js \
          packages/vue-server-renderer/client-plugin.js \
          packages/vue-template-compiler/build.js \
          packages/vue-template-compiler/browser.js
        git commit -m "build: build $VERSION"
        # generate release note
        npm run release:note
        # tag version
        npm version "$VERSION" --message "build: release $VERSION"
      
        # publish 发布
        git push origin refs/tags/v"$VERSION"
        git push
        if [[ -z $RELEASE_TAG ]]; then
          npm publish
        else
          npm publish --tag "$RELEASE_TAG"
        fi
      fi
      

入口

查找入口文件

  • package.json
    • main: “dist/vue.runtime.common.js”,
    • module: “dist/vue.runtime.esm.js”,
    • unpkg: “dist/vue.js”
    • scripts
      • build:“node scripts/build.js” 打包是从这里打包的,里面使用的是 config 里的builds,加上dev的TARGET:web-full-dev
  • scripts
    • config:
    'web-full-dev': {
        entry: resolve('web/entry-runtime-with-compiler.js'),
        dest: resolve('dist/vue.js'),
        format: 'umd', //打包成umd的格式
        env: 'development', //区分dev和prod
        alias: { he: './entity-decoder' },
        banner
      },
    
  • dist
    • vue.js 这个文件是 运行时和编译时在浏览器端进行的产物

关于config的builds配置项的解析:
通过resolve解析出是 src/platforms/web/entry-runtime-with-compiler.js,这也就是 vue2的入口文件
拓展:
web和weex区分:浏览器端和weex端
dev和prod区分:根据env来判断
moduleName:只有在service端需要这个部分,web端不需要
plugins:rollup引入的环境,支持到node端,commonJS端
banner:标题
runtime和compiler区分:runtime,vue本身真正能够运行的部分,compiler,将vue2的模板语言写法,转化成vue能够直接识别的语法

new Vue({
    data: xxx,
    template: '<div>{{name}}</div>'
})

通过 compiler 转换成 vue能识别的语法

// runtime,vue能够运行的:
new Vue({
    render(h) {
        return h('div', this.name)
    }
})

vue-cli 引用的项目中,在 webpack配置里,引入了 vue-loader,就会把vue模板给做这件事,vue本身是不会做这件事的

  • src
    • platforms
      • web
        • entry-runtime-with-compiler.js

runtime和compiler的入口,这里做的就是,将编译时候的方法compileToFunctions,和mount时候的方法在这里定义好了
这里做的是编译时的$mount
在这里插入图片描述

确定vue入口

vue的入口:

import Vue from 'vue'
  1. runtime下的Vue
  • src
    • platforms
      • web
        • runtime
          • index.js 上面 Vue引用的就是这个文件里的Vue

在这里插入图片描述
这里定义的$mount是非编译下的动作
在这里插入图片描述

  1. core下的Vue
    vue的核心代码:
  • src
    • core
      • index.js

在这里插入图片描述
initGlobalAPI:

  • src
    • core
      • global-api.js

在这里插入图片描述

  1. instance下的Vue —— Vue真正的入口
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

// Vue的入口就是一个关于Vue的构造函数
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)
}

// 基于这个Vue所加的拓展
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

为什么Vue通过一个函数而不是通过一个class去做呢?
使用函数去做的话,就可以通过 Vue的原型Vue.prototype给他去注入,
但是通过这种方式注入的话,代码会太乱

this._init_ 方法

this._init函数定义的位置:

  • src
    • core
      • instance
        • init.js

在这里插入图片描述
这里定义的

initMixin:

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // this指的是 new Vue的实例vm
    const vm: Component = this
    // a uid
    // 每初始化一次Vue,uid++,保证Vue节点进行区分
    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
    // 这里做的事是:初始化$options
    // options是 new Vue的时候传入的参数
    // _isComponent 传入的是Component的时候,这里已经是runtime的内容了
    // _isComponent:这个变量是在创建组件的时候,会注入这个变量
    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)
    // 就是$createElement,初始化render的方法
    initRender(vm)
    // 调用beforeCreate生命周期
    callHook(vm, 'beforeCreate')
    // 注入injected
    initInjections(vm) // resolve injections before data/props
    // 这里注入 data,methods,props
    initState(vm)
    // 注入provider
    initProvide(vm) // resolve provide after data/props
    // 调用created生命周期
    // beforeCreate和created之间的区别:1.created这里有provider, injected 2.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) {
      // 有el元素,就通过$mount进行挂载-进行内容真实dom渲染 
      vm.$mount(vm.$options.el)
    }
  }
}

Vue初始化做的事:

  1. 合并了options配置
  2. 初始化生命周期
  3. 将events事件,render方法,provide,inject,data,props进行了响应式,并且调用了两个生命周期

$mount 挂载方法

  1. 编译时的mount
    src/platforms/web/entry-runtime-with-compiler.js
// 获取当前mount
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  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
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    //找到模板
    let template = options.template
    if (template) {
      // 存在模板
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          // 进行模板的绘制
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // 不存在模板则针对这个元素创建一个模板
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      // 通过compileToFunctions获取render的方法
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      // 原本的options是没有render的
      options.render = render
      // 这里是ssr的
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  // this:当前编译时所定义的mount
  // 这里的mount:runtime中导出来的mount
  return mount.call(this, el, hydrating) //这里可以理解为,将编译时的mount塞入到运行时的mount里面
  // 先定义的是运行时候的mount,然后拿着编译时候(将template转化为render时的方法,然后放到options里)塞到运行时的mount里面
}

编译原理动作:
(1)代码的parse,将模板转化为对象
(2)优化对象
(3)代码的生成
(4)最后返回纯js对象
将代码做了一下转换,转换成render的方法然后加进入

  1. web中的mount,公用的mount
    src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
  el?: any,
  hydrating?: boolean
): Component {
  //调用mountComponent方法
  return mountComponent(
    this,
    el && query(el, this.$document),
    hydrating
  )
}

mountComponent:
src/core/instance/lifecycle.js

export function mountComponent (
  vm: Component, //vue的实例
  el: ?Element, //要渲染的元素
  hydrating?: boolean //ssr:可以忽略
): Component {
  vm.$el = el //当前内容放到实例上
  // 这里按道理来讲是应该有render方法的
  if (!vm.$options.render) {
    // 没有render的话,这里就返回一个空的对象
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  // 这里执行beforeMount
  callHook(vm, 'beforeMount')

  // 组件不可能只渲染一次
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      // 调用实例中的_update方法,传入当前实例中_render方法
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  // 创建一个Watcher实例,传入vue实例,updateComponent方法(更新当前的组件)
  // 监听updateComponent的更新
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        // 初始化并且还没有被销毁
        callHook(vm, 'beforeUpdate') //调用beforeUpdate
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  // vm.$vnode为空,意味着没有渲染过
  if (vm.$vnode == null) {
    vm._isMounted = true // 设置当前实例为已挂载
    callHook(vm, 'mounted') //调用mounted,只能调用一次,后续要更新的话,在Watcher中调用更新的方法
  }
  return vm
}

首先,判断 render 有没有
存在,则进入 beforeMount 的阶段
然后,执行_update方法
定义 Watcher 监听的方法
最后再初始化的时候进入 mounted 阶段
=> beforeMount 和 mounted 之间,就定义了这样的一个Watcher,意味着监听的话,能够监听得到。在Watcher中,对元素进行了绘制的工作,对vm进行当前节点渲染

vm._update(vm._render(), hydrating) 这句话具体做了什么事情,则是接下来要掌握的内容。

Vue.prototype._render

src/core/instance/index.js 这个文件中
在这里插入图片描述
renderMixin(Vue)执行的方法,里面包含了 _render 方法

export function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

  Vue.prototype._render = function (): VNode {
    // 获取当前实例
    const vm: Component = this
    // 调取render,_parentVnode(是组件的时候会有这个内容,没有这个的话就是根节点)
    const { render, _parentVnode } = vm.$options

    // 如果是组件的话,处理组件的内容
    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }

    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    // 定义了vnode
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm
      // _renderProxy是渲染代理,然后获取$createElement渲染元素
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
        } catch (e) {
          handleError(e, vm, `renderError`)
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    } finally {
      currentRenderingInstance = null
    }
    // if the returned array contains only a single node, allow it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }
}

render的事情,简单来说就是这句话:

// _renderProxy是渲染代理,然后获取$createElement渲染元素
vnode = render.call(vm._renderProxy, vm.$createElement)

这句话就会将元素渲染起来
这里的 $createElement是从createElement这里来的,最终从 vdom中的 createElement 得到
这个方法就是创建元素,因为当前是在浏览器端,这里能区分 weex 端还是 浏览器端,在这两端的环境下,创建element就会创建一个基础的元素节点(createEmptyVNode),这个基础的空节点既不是dom节点,也不是weex节点,它是一个对象节点
如果有子节点的话,会依次递归的调用子节点,最后生成一个完整的对象
=> 这里只需要知道,通过这个方法就能将当前的对象创建出来即可,得到的这个对象就能在浏览器端去渲染,在weex端去渲染
createElement
创建出来的就是这个东西:
在这里插入图片描述
_renderProxy是代理渲染:
src/core/instance/proxy.js

initProxy = function initProxy (vm) {
    if (hasProxy) {
      // determine which proxy handler to use
      const options = vm.$options
      // handlers对已有的render进行处理,getHandler是对render加了一些环境的判断
      const handlers = options.render && options.render._withStripped
        ? getHandler
        : hasHandler
      // 做了一个proxy,将vm创建过来,handlers就是render
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
  }

可以将_renderProxy等同于调用了 vm 里面的 render

vnode = render.call(vm._renderProxy, vm.$createElement)

那么,这句话等同于

vnode = render($createElement)

这里createElement 就相当于是创建对象,就是虚拟dom的节点

src/core/vdom/create-element.js
在这里插入图片描述

<div>
   <p>
      <span></span>
   </p>
</div>

p和span对div来说都是children

将上面的代码转化为下面的对象:

const obj={
    name:"div",
    children:[
        {
            name:"p",
            children:[
                {
                    name:"span"
                }
            ]
        }
    ]
}

这样表达出来的好处是什么?
后续通过这个对象渲染成dom,weex都简单,这个是与平台无关的,因为vue消费的是这个对象。

Vue.prototype._update

src/core/instance/lifecycle.js

// vnode:render执行完创建出来的对象,
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    // vm._vnode:上一次的vnode节点,在_render中定义的
    const prevVnode = vm._vnode
    // 拿着上一次的_vnode和这次创建的vnode进行对比,更新
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      // 上一次的节点不存在的情况,这一次要进行初始化,patch就是比较的意思
      // $el元素:最后要渲染出来的元素
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      // 如果上一次节点存在的话,要拿上一次节点和这一次节点比较 —— diff的过程
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    // 加环境的判断
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    // 有没有当前的父节点,可跳过不看
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

update的动作——其实就是进行一次patch的动作

Vue.prototype._patch

src/platforms/web/runtime/index.js:

Vue.prototype.__patch__ = inBrowser ? patch : noop

src/platforms/web/runtime/patch.js:

export const patch: Function = createPatchFunction({ nodeOps, modules })

最终是通过createPatchFunction来比较的

src/core/vdom/patch.js: 这里是vue2的diff算法,双端比较


http://www.kler.cn/a/567976.html

相关文章:

  • python 使用 venv 创建虚拟环境 (VSCode)
  • 网络运维学习笔记(DeepSeek优化版)009网工初级(HCIA-Datacom与CCNA-EI)路由理论基础与静态路由
  • 游戏引擎学习第131天
  • 定制开发开源AI大模型S2B2C商城小程序在私域流量池构建中的应用探索
  • Linux(ftrace)__mcount的实现原理
  • SpringMVC(2)传递JSON、 从url中获取参数、上传文件、cookie 、session
  • bean的管理-03.第三方bean
  • ChatGPT与DeepSeek:AI语言模型的巅峰对决
  • C++-第二十章:智能指针
  • 【Java】I/O 流篇 —— 打印流与压缩流
  • 面试【进阶】 —— 说下csr、ssr、ssg 的区别?
  • 蓝桥杯18584-个人消息同步
  • 红黑树和 STL —— set和map 【复习笔记】
  • STM32CubeMx DRV8833驱动
  • C++里面四种强制类型转换
  • ES如何打印DSL
  • Oracle 认证为有哪几个技术方向
  • Java基础语法练习34(抽象类-abstract)(抽象类最佳实践-模版设计模式)
  • 数据挖掘中特征发现与特征提取的数学原理
  • 十、Spring Boot:Spring Security(用户认证与授权深度解析)