Vue 2.x 源码阅读记录(一):渲染过程

我心飞翔 分类:javascript

写在前面

文章为阅读笔记向,需 clone 下来 Vue 源码并加以调试服用~~

先来看看 Vue 从创建实例到渲染 DOM 经过的流程:

一个最基本的渲染,下面的分析都围绕例子进行:

new Vue({
  el: '#app',
  data: {
     msg: 'Hello Vue!',
  },
});
 

下面看看,new Vue()是怎么做的。


import Vue from 'vue'

在通常的浏览器环境下引入 Vue 的时候,会先后执行src\core\index.jssrc\platforms\web\runtime\index.jssrc\platforms\web\entry-runtime-with-compiler.js三个文件,它们进行了对 Vue 的构造函数进行了一些初始化及对特定的环境挂载了一些静态方法。

第一个文件中,为 Vue 的构造函数调用了initGlobalAPI方法,它为 Vue 挂载了一些静态方法以及做了一些初始化:

/* 在 import Vue 时执行 */
export function initGlobalAPI (Vue: GlobalAPI) {
  // 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.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)  
  initAssetRegisters(Vue)
}
 

第二个文件向 Vue 挂载了针对所有运行环境下的__patch__$mount方法:

// install platform patch function
// 在 web 环境下将 patch 赋值给 __patch__
// 否则赋值一个空函数,因为 Vue 在其它平台下使用不会有 DOM 对象
// patch 定义在 src\platforms\web\runtime\patch.js
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
// 不同环境下 $mount 挂载方法的公共入口
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 在浏览器环境中,拿到 el 对应的 DOM 元素
  el = el && inBrowser ? query(el) : undefined
  // 执行真正的挂载方法
  return mountComponent(this, el, hydrating)
}
 

第三个文件针对 web 环境为$mount方法提供适配:

// 缓存的公共 $mount
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function () { //... }
 

init

Vue 的构造函数定义在src\core\instance\index.js,在new调用前,会先对构造函数进行初始化。它的初始化主要做了合并配置、初始化生命周期、初始化事件中心、初始化渲染、初始化 data、props、computed、watcher 等等:

/* new Vue() 从这里开始 */
function Vue (options) {
  // 必须使用 new 调用
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 执行初始化方法,在 initMixin 中定义
  this._init(options)
}

// 挂载初始化方法 _init,合并 option 到 vm.$option 属性上
initMixin(Vue)
// 拦截 $data 和 $props 的访问,原型上挂载数据操作相关 $set, $delete, $watch 方法
stateMixin(Vue)
// 挂载事件相关方法:$on, $once, $off, $emit 
eventsMixin(Vue)
// 挂载生命周期相关方法:_update, $forceUpdate, $destroy
lifecycleMixin(Vue)
// 挂载渲染相关方法: $nextTick, _render
renderMixin(Vue)

export default Vue
 

$mount

initMixin中,进行了合并配置和一些初始化的工作,最后调用了$mount进入挂载流程:

// 传入 el 时实际是调用 $mount() 开始挂载
if (vm.$options.el) {
    vm.$mount(vm.$options.el)
}
 

$mount函数生成templaterender函数,最后调用缓存的公共mount函数:

// 缓存的公共 $mount
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// query() 是对元素 DOM 选择器的封装
// 返回挂载的根元素(#app),如果不存在则返回一个空 div
el = el && query(el)
/* istanbul ignore if */
// 不能是 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
}
const options = this.$options
// 解析 template/el 并转换成 render 函数
if (!options.render) {
let template = options.template
// 是否使用的是 template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
// idToTemplate 返回挂载根节点的子节点字符串
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 是一个元素,则直接取 innerHTML
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// getOuterHTML 返回挂载根节点的子节点字符串
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// 使用 template 生成 render 函数,挂载到 option 上
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
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')
}
}
}
// 最后,调用之前缓存的公共 $mount
return mount.call(this, el, hydrating)
}

而公共的mount最终调用的是mountComponent方法。对前面的例子来讲,这里主要就是调用了_render生成vnode_update实现 DOM 的挂载:

vm._update(vm._render(), hydrating /* false */)

render

先改写一下例子:

new Vue({
el: '#app',
data: {
msg: 'Hello'
},
render(createElement) {
return createElement('div', {
attrs: {
id: 'my-app'
}
}, this.msg);
}
});

_render也是一层柯里化,它实际调用的是render函数:

// $createElement 对应我们的例子,就是 render 函数的第一个参数 createElement
// _renderProxy 可以看作是一个对 Vue 实例的代理,它在渲染过程中会对实例属性的访问进行拦截操作
vnode = render.call(vm._renderProxy, vm.$createElement)

$createElementinitMixin方法中调用的initRender中被定义,它分为自动编译的render调用和用户手动调用:

// _c 自动编译成的 render 函数的 createElement 调用
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// 用户传入的 render 函数的 createElement 调用
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

createElement

createElementrender 函数提供 VNode 节点,返回一个 VNode 对象或数组,它实际上只是对真实 _createElement 函数做的一层柯里化,来为它处理一些参数:

export function createElement (
context: Component,
tag: any,  // 标签名或组件 
data: any,  // vNode data,比如 attrs...
children: any,  // 子节点,tag 或 数组,它接下来需要被规范为标准的 VNode 数组
normalizationType: any,  // 子节点规范的类型
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
// 如果第二个参数是一个数组或者是一个普通类型的值
// 则认为它直接是 children
// 意味着我们可以这样使用 createElement: createElement('div', this.msg || [this.msg, 'hi'])
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
// children 表示当前 vNode 的子节点,它被定义为任意类型的
// 它接下来需要被规范为标准的 VNode 数组
// normalizationType 取决于是自动生成的 render 函数还是用户手动调用的
// 也就是调用的是 vm._c 还是 vm.$createElement
normalizationType = ALWAYS_NORMALIZE
}
// 实际调用 _createElement 方法
return _createElement(context, tag, data, children, normalizationType)
}

children 的规范,实质上就是将 children 参数转换为一个 VNode tree,将 children 中所有层级的节点都转换为 vNode 对象,包括文本节点、注释节点等。

创建 vnode 完整流程

export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,  // 标签名或组件
data?: VNodeData,  // vNode data,比如 attrs...
children?: any,  // 子节点,tag 或 数组,它接下来需要被规范为标准的 VNode 数组
normalizationType?: number  // 子节点规范的类型
): VNode | Array<VNode> {
// debugger
if (isDef(data) && isDef((data: any).__ob__)) {
// 如果定义了响应式的 data,则报出警告,返回一个空 vNode
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
// 根据文档的内容来看
// <component v-bind:is="currentView"></component>
// 推测用户调用是 createElement('component', { is: 'currentView' }, [...])
// 表示需要创建 vNode 的是一个组件
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
// 没有正确传入 tag 属性,返回空 vNode
return createEmptyVNode()
}
// warn against non-primitive key
if (process.env.NODE_ENV !== 'production' &&
isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
if (!__WEEX__ || !('@binding' in data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
}
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// normalizationType 在 _createElement 的调用中已经被处理
// 开始规范转换 children 
if (normalizationType === ALWAYS_NORMALIZE) {
// 用户手动调用 render
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 自动编译的 render
children = simpleNormalizeChildren(children)
}
// 开始创建 vnode
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
// tag 如果是一个内置节点(div, span 等),直接创建一个 vNode 实例
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
// tag 如果是一个已注册的组件名,则创建一个组件类型的 vnode
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
// 否则创建一个未知标签的 vnode
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
// tag 直接传递了一个组件,创建组件类型的 vnode
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}

_update(patch)

_update中实际是调用了__patch__方法来进行挂载:

vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

在前面import提到过,patch是根据平台来定义的,实际的patch函数是通过createPatchFunction闭包返回的一个函数:

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

针对例子的 patch 流程

接收参数:

vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

1、获取el元素的父节点:

// vnode 中的真实 DOM 元素,#app
const oldElm = oldVnode.elm
// oldElm 的父节点,在例子中是 <body>
const parentElm = nodeOps.parentNode(oldElm)

2、调用 createElm方法将_render 生成的 vnode 转化为真实 DOM 并插入到el的父节点中:

createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)

3、删除el旧的 DOM 元素:

if (isDef(parentElm)) {
// 执行到这里 vNode 已经全部生成真实 DOM 并插入到页面中了,例子是 <div id="my-appp"><span>Hello Vue!</span></div>
// 插入完成之后,由于执行了 render 函数,会销毁旧的根节点
// 对应例子来看,就是插入 render 描述的 DOM (#my-app),销毁 el 的模板(#app)
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}

createElm 流程

接收参数:

createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)

1、创建根元素:

// 在 vnode.elm 属性上保存真实 DOM
vnode.elm = vnode.ns  // ns: namespace,暂时不明白
? nodeOps.createElementNS(vnode.ns, tag)
// 关键步骤,调用封装的原生 createElement
// 可以单纯的看作原生的 createElement(tag)
: nodeOps.createElement(tag, vnode)

2、插入 DOM 到页面上:

// 调用 appendChid 将当前 DOM 插入到父节点中
insert(parentElm, vnode.elm, refElm)
// 如果当前 vnode 没有 tag 属性,就属于注释节点或文本节点
if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
// 创建并插入注释节点
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
// 创建并插入文本节点
insert(parentElm, vnode.elm, refElm)
}

在插入 DOM 之前,会调用createChildren循环调用createElm将当前 vnode 的children也都生成 DOM 并插入到当前 child 的父节点:

// 当前 vnode 已经生成真实 DOM,需要将其 children 内的 vnode 也生成
// 内部为 vnode.children 循环调用了 createElm,因为 children 也都是 vnode
// 如果 children 是文本节点则直接插入 vnode.elm 中
createChildren(vnode, children, insertedVnodeQueue)

至此__patch__ 方法调用完成了,_update(_render(), false) 创建 vnode 和 patch 的流程也走完了。

回复

我来回复
  • 暂无回复内容