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: 2defaults: &defaultsworking_directory: ~/project/vuedocker:- image: vuejs/ci #流水线配置是国外CI品牌jobs:install:<<: *defaultssteps:- 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: ~/projectpaths:- vuelint-flow-types:<<: *defaultssteps:- attach_workspace:at: ~/project- run: npm run lint #eslint检查- run: npm run flow #flow检查- run: npm run test:types #检查类型test-cover:<<: *defaultssteps:- attach_workspace:at: ~/project- run: npm run test:cover- run:name: report coverage stats for non-PRscommand: |if [[ -z $CI_PULL_REQUEST ]]; then./node_modules/.bin/codecovfitest-e2e:<<: *defaultssteps:- attach_workspace:at: ~/project- run: npm run test:e2e -- --env phantomjstest-ssr-weex:<<: *defaultssteps:- attach_workspace:at: ~/project- run: npm run test:ssr #打包成ssr- run: npm run test:weex #打包成weextrigger-regression-test:<<: *defaultssteps:- 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/masterworkflows:version: 2install-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-testrequires:- test-cover- lint-flow-types- test-e2e- test-ssr-weexweekly_regression_test:triggers:- schedule:# At 13:00 UTC (9:00 EDT) on every Mondaycron: "0 13 * * 1"filters:branches:only:devjobs:- 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 || []), //加上参数传来的pluginoutput: {file: opts.dest,format: opts.format,banner: opts.banner,name: opts.moduleName || 'Vue'},onwarn: (msg, warn) => {if (!/Circular/.test(msg)) {warn(msg)}}}// built-in varsconst vars = {__WEEX__: !!opts.weex,__WEEX_VERSION__: weexVersion,__VERSION__: version}// feature flagsObject.keys(featureFlags).forEach(key => {vars[`process.env.${key}`] = featureFlags[key]})// build-specific envif (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.outputconst { file, banner } = outputconst 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']}}).codereturn 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 -eif [[ -z $1 ]]; then #找到这个版本echo "Enter new version: "read -r VERSION elseVERSION=$1 firead -p "Releasing $VERSION - are you sure? (y/n) " -n 1 -r #是否要发布这个版本 echo if [[ $REPLY =~ ^[Yy]$ ]]; thenecho "Releasing $VERSION ..."if [[ -z $SKIP_TESTS ]]; then #执行指令npm run lintnpm run flownpm run test:covernpm run test:e2e -- --env phantomjsnpm run test:ssrfi# 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# buildVERSION=$VERSION npm run build# update packages# using subshells to avoid having to cd back( ( cd packages/vue-template-compilernpm version "$VERSION"if [[ -z $RELEASE_TAG ]]; thennpm publish #执行发布elsenpm publish --tag "$RELEASE_TAG" #加上tag值fi)cd packages/vue-server-renderernpm version "$VERSION"if [[ -z $RELEASE_TAG ]]; thennpm publishelsenpm publish --tag "$RELEASE_TAG"fi)# commit 执行git add,commitgit add -Agit 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.jsgit commit -m "build: build $VERSION"# generate release notenpm run release:note# tag versionnpm version "$VERSION" --message "build: release $VERSION"# publish 发布git push origin refs/tags/v"$VERSION"git pushif [[ -z $RELEASE_TAG ]]; thennpm publishelsenpm 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和prodalias: { 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的实例vmconst 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')// 注入injectedinitInjections(vm) // resolve injections before data/props// 这里注入 data,methods,propsinitState(vm)// 注入providerinitProvide(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 functionif (!options.render) {//找到模板let template = options.templateif (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中导出来的mountreturn 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 = createEmptyVNodeif (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)}}}// 这里执行beforeMountcallHook(vm, 'beforeMount')// 组件不可能只渲染一次let updateComponent/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {updateComponent = () => {const name = vm._nameconst id = vm._uidconst 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 helpersinstallRenderHelpers(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.// 定义了vnodevm.$vnode = _parentVnode// render selflet vnodetry {// 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 itif (Array.isArray(vnode) && vnode.length === 1) {vnode = vnode[0]}// return empty vnode in case the render function errored outif (!(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 parentvnode.parent = _parentVnodereturn 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 useconst options = vm.$options// handlers对已有的render进行处理,getHandler是对render加了一些环境的判断const handlers = options.render && options.render._withStripped? getHandler: hasHandler// 做了一个proxy,将vm创建过来,handlers就是rendervm._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 = thisconst 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算法,双端比较