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代码后,就能自动执行后续的内容,后面内容就是自动化流水线的内容
- config.yml 流水线的配置,自动化流水线
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模板的编译
…
- vue-template-compiler:vue模板的编译
- scripts 一系列的node脚本
- package.json:
- scripts 这里的命令许多是针对scripts里的脚本去执行,因为vue2使用的是roll up,所以使用的是roll up去执行scripts里的各种各样的文件。
在进行本地的开发,构建,部署的时候,我们的自定义脚本
- 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
- web
- platforms
runtime和compiler的入口,这里做的就是,将编译时候的方法compileToFunctions,和mount时候的方法在这里定义好了
这里做的是编译时的$mount
确定vue入口
vue的入口:
import Vue from 'vue'
- runtime下的Vue
- src
- platforms
- web
- runtime
- index.js
上面 Vue引用的就是这个文件里的Vue
- index.js
- runtime
- web
- platforms
这里定义的$mount是非编译下的动作
- core下的Vue
vue的核心代码:
- src
- core
- index.js
- core
initGlobalAPI:
- src
- core
- global-api.js
- core
- 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
- instance
- core
这里定义的
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初始化做的事:
- 合并了options配置
- 初始化生命周期
- 将events事件,render方法,provide,inject,data,props进行了响应式,并且调用了两个生命周期
$mount 挂载方法
- 编译时的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的方法然后加进入
- 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算法,双端比较