初始渲染原理
大约 5 分钟约 1506 字
一、组件挂载入口
// src/init.js
import { initState } from './state'
import { compileToFunctions } from './compiler/index'
export function initMixin(Vue) {
Vue.prototype._init = function (options) {
const vm = this
// 这里的this代表调用_init方法的对象(实例对象)
// this.$options就是用户new Vue的时候传入的属性
vm.$options = options
// 初始化状态
initState(vm)
// 如果有el属性 进行模板渲染
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
// 这块代码在源码里面的位置其实是放在entry-runtime-with-compiler.js里面
// 代表的是Vue源码里面包含了compile编译功能 这个和runtime-only版本需要区分开
Vue.prototype.$mount = function (el) {
const vm = this
const options = vm.$options
el = document.querySelector(el)
// 如果不存在render属性
if (!options.render) {
// 如果存在template属性
let template = options.template
if (!template && el) {
// 如果不存在render和template 但是存在el属性 直接将模板赋值到el所在的外层html结构(就是el本身 并不是父元素)
template = el.outerHTML
}
// 最终需要把tempalte模板转化成render函数
if (template) {
const render = compileToFunctions(template)
options.render = render
}
}
// 将当前组件实例挂载到真实的el节点上面
return mountComponent(vm, el)
}
}
接着看$mount 方法 我们主要关注最后一句话 mountComponent 就是组件实例挂载的入口函数 这个方法放在源码的 lifecycle 文件里面 代表了与生命周期相关 因为我们组件初始渲染前后对应有 beforeMount 和 mounted 生命周期钩子
二、组件挂载核心方法 mountComponent
// src/lifecycle.js
export function mountComponent(vm, el) {
// 上一步模板编译解析生成了render函数
// 下一步就是执行vm._render()方法 调用生成的render函数 生成虚拟dom
// 最后使用vm._update()方法把虚拟dom渲染到页面
// 真实的el选项赋值给实例的$el属性 为之后虚拟dom产生的新的dom替换老的dom做铺垫
vm.$el = el
// _update和._render方法都是挂载在Vue原型的方法 类似_init
vm._update(vm._render())
}
新建 lifecycle.js 文件 表示生命周期相关功能 核心导出 mountComponent 函数 主要使用 vm._update(vm._render())方法进行实例挂载
三、render 函数转化成虚拟 dom 核心方法 _render
// src/index.js
import { initMixin } from './init'
import { renderMixin } from './vdom/index'
function Vue(options) {
this._init(options) // 入口方法,做初始化操作
}
// 写成一个个的插件进行对原型的扩展
initMixin(Vue)
// _render
renderMixin(Vue)
export default Vue
主要在原型定义了_render 方法 然后执行了 render 函数 我们知道模板编译出来的 render 函数核心代码主要 return 了 类似于_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))这样的代码 那么我们还需要定义一下_c _v_s 这些函数才能最终转化成为虚拟 dom
// src/vdom/index.js
export function renderMixin(Vue) {
Vue.prototype._render = function () {
const vm = this
// 获取模板编译生成的render方法
const { render } = vm.$options
// 生成vnode--虚拟dom
const vnode = render.call(vm)
return vnode
}
// render函数里面有_c _v _s方法需要定义
Vue.prototype._c = function (...args) {
// 创建虚拟dom元素
return createElement(...args)
}
Vue.prototype._v = function (text) {
// 创建虚拟dom文本
return createTextNode(text)
}
Vue.prototype._s = function (val) {
// 如果模板里面的是一个对象 需要JSON.stringify
return val == null
? ''
: typeof val === 'object'
? JSON.stringify(val)
: val
}
}
export function createElement(vm, tag, data = {}, ...children) {
return vnode(vm, tag, data, data.key, children, undefined)
}
export function createTextElement(vm, text) {
return vnode(vm, undefined, undefined, undefined, undefined, text)
}
function vnode(vm, tag, data, key, children, text) {
return {
vm,
tag,
data,
key,
children,
text,
// .....
}
}
四、虚拟 dom 转化成真实 dom 核心方法 _update
import { initMixin } from './init'
import { lifecycleMixin } from './lifecycle'
import { renderMixin } from './vdom/index'
function Vue(options) {
this._init(options) // 入口方法,做初始化操作
}
// 写成一个个的插件进行对原型的扩展
initMixin(Vue)
// 混合生命周期 组件挂载、组件更新
lifecycleMixin(Vue)
// _render
renderMixin(Vue)
export default Vue
// src/lifecycle.js
import { patch } from './vdom/patch'
export function lifecycleMixin(Vue) {
// 把_update挂载在Vue的原型
Vue.prototype._update = function (vnode) {
const vm = this
// patch是渲染vnode为真实dom核心
patch(vm.$el, vnode)
}
}
// src/vdom/patch.js
// patch用来渲染和更新视图 今天只介绍初次渲染的逻辑
export function patch(oldVnode, vnode) {
// 判断传入的oldVnode是否是一个真实元素
// 这里很关键 初次渲染 传入的vm.$el就是咱们传入的el选项 所以是真实dom
// 如果不是初始渲染而是视图更新的时候 vm.$el就被替换成了更新之前的老的虚拟dom
const isRealElement = oldVnode.nodeType
if (isRealElement) {
// 这里是初次渲染的逻辑
const oldElm = oldVnode
const parentElm = oldElm.parentNode
// 将虚拟dom转化成真实dom节点
let el = createElm(vnode)
// 插入到 老的el节点下一个节点的前面 就相当于插入到老的el节点的后面
// 这里不直接使用父元素appendChild是为了不破坏替换的位置
parentElm.insertBefore(el, oldElm.nextSibling)
// 删除老的el节点
parentElm.removeChild(oldVnode)
return el
}
}
// 虚拟dom转成真实dom 就是调用原生方法生成dom树
function createElm(vnode) {
let { tag, data, key, children, text } = vnode
// 判断虚拟dom 是元素节点还是文本节点
if (typeof tag === 'string') {
// 虚拟dom的el属性指向真实dom
vnode.el = document.createElement(tag)
// 解析虚拟dom属性
updateProperties(vnode)
// 如果有子节点就递归插入到父节点里面
children.forEach((child) => {
return vnode.el.appendChild(createElm(child))
})
} else {
// 文本节点
vnode.el = document.createTextNode(text)
}
return vnode.el
}
// 解析vnode的data属性 映射到真实dom上
function updateProperties(vnode) {
let newProps = vnode.data || {}
let el = vnode.el //真实节点
for (let key in newProps) {
// style需要特殊处理下
if (key === 'style') {
for (let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName]
}
} else if (key === 'class') {
el.className = newProps.class
} else {
// 给这个元素添加属性 值就是对应的值
el.setAttribute(key, newProps[key])
}
}
}
_update 核心方法就是 patch 初始渲染和后续更新都是共用这一个方法 只是传入的第一个参数不同 初始渲染总体思路就是根据虚拟 dom(vnode) 调用原生 js 方法创建真实 dom 节点并替换掉 el 选项的位置
源码
https://github.com/luotianxu1/learn/tree/main/vue2/03.lifecycle