跳到主要内容

模拟实现

2023年07月14日
柏拉文
越努力,越幸运

一、/src/compiler/mountComponent.js mountComponent()


/**
* @param {*} vm Vue 实例
*/
export default function mountComponent(vm) {
// 更新组件的的函数
const updateComponent = () => {
vm._update(vm._render())
}

// 实例化一个渲染 Watcher,当响应式数据更新时,这个更新函数会被执行
new Watcher(updateComponent)
}

二、/src/compiler/mountComponent.js vm._render()


/**
* 负责执行 vm.$options.render 函数
*/
Vue.prototype._render = function () {
// 给 render 函数绑定 this 上下文为 Vue 实例
return this.$options.render.apply(this)
}

三、/src/compiler/renderHelper.js


/**
* 在 Vue 实例上安装运行时的渲染帮助函数,比如 _c、_v,这些函数会生成 Vnode
* @param {VueContructor} target Vue 实例
*/
export default function renderHelper(target) {
target._c = createElement
target._v = createTextNode
}

四、/src/compiler/renderHelper.js createElement()


/**
* 根据标签信息创建 Vnode
* @param {string} tag 标签名
* @param {Map} attr 标签的属性 Map 对象
* @param {Array<Render>} children 所有的子节点的渲染函数
*/
function createElement(tag, attr, children) {
return VNode(tag, attr, children, this)
}

五、/src/compiler/renderHelper.js createTextNode()


/**
* 生成文本节点的 VNode
* @param {*} textAst 文本节点的 AST 对象
*/
function createTextNode(textAst) {
return VNode(null, null, null, this, textAst)
}

六、/src/compiler/vnode.js VNode


/**
* VNode
* @param {*} tag 标签名
* @param {*} attr 属性 Map 对象
* @param {*} children 子节点组成的 VNode
* @param {*} text 文本节点的 ast 对象
* @param {*} context Vue 实例
* @returns VNode
*/
export default function VNode(tag, attr, children, context, text = null) {
return {
// 标签
tag,
// 属性 Map 对象
attr,
// 父节点
parent: null,
// 子节点组成的 Vnode 数组
children,
// 文本节点的 Ast 对象
text,
// Vnode 的真实节点
elm: null,
// Vue 实例
context
}
}

七、/src/compiler/mountComponent.js vm._update


Vue.prototype._update = function (vnode) {
// 老的 VNode
const prevVNode = this._vnode
// 新的 VNode
this._vnode = vnode
if (!prevVNode) {
// 老的 VNode 不存在,则说明时首次渲染根组件
this.$el = this.__patch__(this.$el, vnode)
} else {
// 后续更新组件或者首次渲染子组件,都会走这里
this.$el = this.__patch__(prevVNode, vnode)
}
}

八、/src/index.js 安装 patch


/**
* 初始化配置对象
* @param {*} options
*/
Vue.prototype._init = function (options) {
// ...
initData(this)
// 安装运行时的渲染工具函数
renderHelper(this)
// 在实例上安装 patch 函数
this.__patch__ = patch
// 如果存在 el 配置项,则调用 $mount 方法编译模版
if (this.$options.el) {
this.$mount()
}
}

九、/src/compiler/patch.js patch()


/**
* 初始渲染和后续更新的入口
* @param {VNode} oldVnode 老的 VNode
* @param {VNode} vnode 新的 VNode
* @returns VNode 的真实 DOM 节点
*/
export default function patch(oldVnode, vnode) {
if (oldVnode && !vnode) {
// 老节点存在,新节点不存在,则销毁组件
return
}

if (!oldVnode) { // oldVnode 不存在,说明是子组件首次渲染
createElm(vnode)
} else {
if (oldVnode.nodeType) { // 真实节点,则表示首次渲染根组件
// 父节点,即 body
const parent = oldVnode.parentNode
// 参考节点,即老的 vnode 的下一个节点 —— script,新节点要插在 script 的前面
const referNode = oldVnode.nextSibling
// 创建元素
createElm(vnode, parent, referNode)
// 移除老的 vnode
parent.removeChild(oldVnode)
} else {
console.log('update')
}
}
return vnode.elm
}

十、/src/compiler/patch.js createElm()


/**
* 创建元素
* @param {*} vnode VNode
* @param {*} parent VNode 的父节点,真实节点
* @returns
*/
function createElm(vnode, parent, referNode) {
// 记录节点的父节点
vnode.parent = parent
// 创建自定义组件,如果是非组件,则会继续后面的流程
if (createComponent(vnode)) return

const { attr, children, text } = vnode
if (text) { // 文本节点
// 创建文本节点,并插入到父节点内
vnode.elm = createTextNode(vnode)
} else { // 元素节点
// 创建元素,在 vnode 上记录对应的 dom 节点
vnode.elm = document.createElement(vnode.tag)
// 给元素设置属性
setAttribute(attr, vnode)
// 递归创建子节点
for (let i = 0, len = children.length; i < len; i++) {
createElm(children[i], vnode.elm)
}
}
// 如果存在 parent,则将创建的节点插入到父节点内
if (parent) {
const elm = vnode.elm
if (referNode) {
parent.insertBefore(elm, referNode)
} else {
parent.appendChild(elm)
}
}
}

十一、/src/compiler/patch.js createTextNode()


/**
* 创建文本节点
* @param {*} textVNode 文本节点的 VNode
*/
function createTextNode(textVNode) {
let { text } = textVNode, textNode = null
if (text.expression) {
// 存在表达式,这个表达式的值是一个响应式数据
const value = textVNode.context[text.expression]
textNode = document.createTextNode(typeof value === 'object' ? JSON.stringify(value) : String(value))
} else {
// 纯文本
textNode = document.createTextNode(text.text)
}
return textNode
}

十二、/src/compiler/patch.js setAttribute()


/**
* 给节点设置属性
* @param {*} attr 属性 Map 对象
* @param {*} vnode
*/
function setAttribute(attr, vnode) {
// 遍历属性,如果是普通属性,直接设置,如果是指令,则特殊处理
for (let name in attr) {
if (name === 'vModel') {
// v-model 指令
const { tag, value } = attr.vModel
setVModel(tag, value, vnode)
} else if (name === 'vBind') {
// v-bind 指令
setVBind(vnode)
} else if (name === 'vOn') {
// v-on 指令
setVOn(vnode)
} else {
// 普通属性
vnode.elm.setAttribute(name, attr[name])
}
}
}

十三、/src/compiler/patch.js setVModel()


/**
* v-model 的原理
* @param {*} tag 节点的标签名
* @param {*} value 属性值
* @param {*} node 节点
*/
function setVModel(tag, value, vnode) {
const { context: vm, elm } = vnode
if (tag === 'select') {
// 下拉框,<select></select>
Promise.resolve().then(() => {
// 利用 promise 延迟设置,直接设置不行,
// 因为这会儿 option 元素还没创建
elm.value = vm[value]
})
elm.addEventListener('change', function () {
vm[value] = elm.value
})
} else if (tag === 'input' && vnode.elm.type === 'text') {
// 文本框,<input type="text" />
elm.value = vm[value]
elm.addEventListener('input', function () {
vm[value] = elm.value
})
} else if (tag === 'input' && vnode.elm.type === 'checkbox') {
// 选择框,<input type="checkbox" />
elm.checked = vm[value]
elm.addEventListener('change', function () {
vm[value] = elm.checked
})
}
}

十四、/src/compiler/patch.js setVBind()


/**
* v-bind 原理
* @param {*} vnode
*/
function setVBind(vnode) {
const { attr: { vBind }, elm, context: vm } = vnode
for (let attrName in vBind) {
elm.setAttribute(attrName, vm[vBind[attrName]])
elm.removeAttribute(`v-bind:${attrName}`)
}
}

十五、/src/compiler/patch.js setVOn()


/**
* v-on 原理
* @param {*} vnode
*/
function setVOn(vnode) {
const { attr: { vOn }, elm, context: vm } = vnode
for (let eventName in vOn) {
elm.addEventListener(eventName, function (...args) {
vm.$options.methods[vOn[eventName]].apply(vm, args)
})
}
}

十六、/src/compiler/patch.js createComponent()


/**
* 创建自定义组件
* @param {*} vnode
*/
function createComponent(vnode) {
if (vnode.tag && !isReserveTag(vnode.tag)) { // 非保留节点,则说明是组件
// 获取组件配置信息
const { tag, context: { $options: { components } } } = vnode
const compOptions = components[tag]
const compIns = new Vue(compOptions)
// 将父组件的 VNode 放到子组件的实例上
compIns._parentVnode = vnode
// 挂载子组件
compIns.$mount()
// 记录子组件 vnode 的父节点信息
compIns._vnode.parent = vnode.parent
// 将子组件添加到父节点内
vnode.parent.appendChild(compIns._vnode.elm)
return true
}
}

十七、/src/utils.js isReserveTag()


/**
* 是否为平台保留节点
*/
export function isReserveTag(tagName) {
const reserveTag = ['div', 'h3', 'span', 'input', 'select', 'option', 'p', 'button', 'template']
return reserveTag.includes(tagName)
}