mini-Vue
前置知识
- Vue中使用了虚拟DOM。
The virtual DOM (VDOM) is a programming concept where an ideal, or “virtual”, representation of a UI is kept in memory and synced with the “real” DOM by a library such as ReactDOM. This process is called reconciliation. – React官方文档
可以看到 React 也使用了虚拟 DOM 😃
虚拟DOM(VDOM)是一种编程概念,其中UI“虚拟”表示保存在内存中,并通过ReactDOM之类的库与“真实”DOM同步。
- 虚拟 DOM 可以为我们带来什么?
-
操作便利:因为对于直接操作DOM来说是有很多的限制的,比如diff、clone等等,但是使用JavaScript编程语言来操作这
些,就变得非常的简单;
-
性能优势:频繁的操作真实 DOM 会在性能方面造成极大的损耗,我们可以采用 虚拟 DOM 中的 diff 算法渲染真实DOM,提升了性能。
-
跨平台性:可以将VNode节点渲染成任意你想要的节点,如渲染在canvas、WebGL、SSR、Native(iOS、Android)上,并且Vue允许你开发属于自己的渲染器(renderer),在其他的平台上渲染;
Vue主要由哪几部分构成?
- Compiler模块:编译模板系统;
- Runtime模块:也可以称之为Renderer模块,真正渲染的模块;
- Reactivity模块:响应式系统;
Vue工作流程
- 通过
h
函数创建出 vnode (虚拟节点) - 如果没啥变化,直接渲染上去
- 当数据被修改了,会创建一个 newVnode,通过 diff 算法与 oldVnode 进行比对,然后进行渲染。
Part One 渲染器的实现
该模块主要包括三个功能
- h函数,用于返回一个VNode对象;
- mount函数,用于将VNode挂载到DOM上;
- patch函数,用于对两个VNode进行对比,决定如何处理新的VNode;
// 首先实现h函数,h函数就是为了让我们返回一个 JavaScript 对象的,则:
const h = (tag, props, children) => {
return {
tag,
props,
children
}
}
// 其次我们需要实现一个 mount 函数,将我们的虚拟节点转化成真实的节点,并且挂载到container上
const mount = (vnode, container) => {
// 1. 创建出真实的原生,并在 vnode 上保留 el
const el = vnode.el = document.createElement(vnode.tag)
// 2. 处理 props
if(vnode.props) {
for(const key in vnode.props) {
// 获取值
const value = vnode.props[key]
// 如果是事件的话
if(key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
// 如果只是属性的话
el.setAttribute(key, value);
}
}
}
// 3. 处理 chilren
if(vnode.chilren) {
if(typeof vnode.chilren === "string") {
el.textContent = vnode.children
} else {
vnode.chilren.forEach((item) => {
mount(item, el)
})
}
}
}
// 最后就是要实现一个进行 diff 比对的 patch 函数
// 此处传入两个 vnode
const patch = (n1, n2) => {
if(n1.tag !== n2.tag) {
const n1ElParent = n1.el.parentElement;
n1ElParent.removeChild(n1.el);
mount(n2, n1ElParent)
} else {
// 取出 element 对象,并且在 n2 中进行保存
const el = n1.el = n2.el
// 为了获取 value
const oldProps = n1.props || []
const newProps = n2.props || []
// 遍历新对象
for(const key in newProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
if(oldValue != newValue) {
if(key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(), newValue);
} else {
el.setAttribute(key, newValue);
}
}
}
// 删除旧的 props
for(const key in oldProps) {
if(key.startsWith("on")) {
const value = oldProps[key];
// removeEventListener(type, listener)
// type ==> 一个字符串,表示需要移除的事件类型 | listener ==> 需要从目标事件中移除的事件处理函数。
el.removeEventListener(key.slice(2).toLowerCase(), value)
}
if(!(key in newProps)) {
el.removeAttribute(key)
}
}
// 3. 处理children
const oldChildren = n1.children || []
const newChildren = n2.children || []
// 如果新的 node 的 children 只是一个字符串,那直接覆盖就可以了
if (typeof newChildren === 'string') {
// 如果旧的也是字符串,那就判断一下是不是一样的,然后覆盖一下
if (typeof oldChildren === 'string') {
if (newChildren != oldChildren) {
el.textContent = newChildren
}
} else {
// 如果新的是很多很多东西,我直接覆盖就可以了
el.innerHTML = newChildren
}
} else {
// 这种情况新的 node 并不是字符串,是数组啥的
// 然后判断一下旧的的情况
// 如果是字符串
if(typeof oldChildren === 'string') {
el.innerHTML = ""
newChildren.forEach((item) => {
mount(item, el)
})
} else {
// 如果都不是字符串的话
// 拿到共同长度
const commonLength = Math.min(oldChildren.length, newChildren.length)
for(let i=0;i<commonLength;i++) {
patch(oldChildren[i], newChildren[i])
}
// 如果新的节点的子节点多一些的话,那就全部挂载上去
if(newChildren.length > oldChildren.length) {
newChildren.slice(commonLength).forEach((item) => {
mount(item, el)
})
}
// 如果旧的节点的子节点多一些,那就一个个删除!
if(newChildren.length > oldChildren.length) {
oldChildren.slice(commonLength).forEach((item) => {
el.removeChild(item)
})
}
}
}
}
}
Part Two 实现响应式
- 依赖收集过程
WeakMap
==>Map
==>set
const targetMap = new WeakMap()
function getDep(target, key) {
// 根据对应的 target 取出对应的Map对象
let depsMap = targetMap.get(target)
if(!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 取出具体的 deps 对象
let dep = depsMap.get(key)
if(!deps) {
dep = new Dep()
depsMap.set(key, dep)
}
return dep
}
class Dep {
constructor() {
this.subscribers = []
}
depend() {
if(activeEffect) {
this.subscribers.add(activeEffect);
}
}
// 更新视图
notify() {
this.subscribers.forEach(effect => {
effect();
})
}
}
- 监听函数过程
let activeEffect = null
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}
- 数据劫持过程
// vue3对raw进行数据劫持
function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const dep = getDep(target, key);
dep.depend();
return target[key];
},
set(target, key, newValue) {
const dep = getDep(target, key);
target[key] = newValue;
dep.notify();
}
})
}
其实 Vue2 也可以使用 Object.defineProperty 进行数据劫持
function reactive(raw) {
Object.keys(raw).forEach(key => {
const dep = getDep(raw, key);
let value = raw[key];
Object.defineProperty(raw, key, {
get() {
// 对于响应式对象,当他获取这个值的时候,就会把依赖进行收集
dep.depend();
return value;
},
set(newValue) {
if (value !== newValue) {
value = newValue;
dep.notify();
}
}
})
})
return raw;
}
所以我们需要来比对一下 Object.defineProperty 和 proxy
proxy 与 Object.defineProperty的优缺点
Object.defineProperty
做的是拦截,对一个对象中的某个属性进行拦截,然后实现响应式,所以如果这个对象新增了属性,Object.definProperty
无法监测到,需要使用 $set 强制添加才可以。Proxy则没有这个问题。- 对于数组的
push、pop
等操作,Object.defineProperty
无法监测到,自然没法响应式的变更,Proxy也没这个问题 - Proxy 能观察的类型比 defineProperty 更丰富:「has:in操作符的捕获器;」「deleteProperty:delete 操作符的捕捉器;」
- Proxy 作为新标准将受到浏览器厂商重点持续的性能优化;
- 缺点:Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9
Part Three 入口文件
function createApp(rootComponent) {
return {
mount(selector) {
const container = document.querySelector(selector);
let isMounted = false;
let oldVNode = null;
watchEffect(() => {
if(!isMounted) {
isMounted = true
oldVNode = rootComponent.render()
mount(oldVNode, container)
} else {
let newVnode = rootComponent.render()
patch(oldVNode, newVnode)
oldVNode = newVnode
}
})
}
}
}
- 至此,一个超精简版的Vue就实现啦!