实现一个Mini-Vue可以加深对vue的理解 ,包括渲染系统模块、可响应式系统模块、 应用程序入口模块(没有实现编译系统模块) 。
vue核心模块
vue有三个核心模块 ,编译系统,渲染系统, 响应式系统。

编译系统
- 将 temp性能优化的方法late 模板解析算法的五个特性成抽象语法树(AST)。算法的时间复杂度取决于
- 对 AST 做优化处理。
- 根据 AST 生成 render 函数。
渲染系统
- reder函数返回vnode
- vnode之间算法是什么会形成树结构vdom
- 根据vdom生成真实dom,渲染到浏览器
响应式系统
- 将新旧vnode利用diff算法进行对比
- 渲染系统根据vnode重新生成dom,渲染到浏览器
渲染系统

通过h函数生成vnode
h函数包括3个参数,元素, 属性, 子元素。生html简单网页代码成的vnode是一个javascript对象
function h(tag, props, children) { // vnode --> javascript对象 return { tag, props, children } }
// 1.通过h函数创建vnode const vnode = h("div", {class: 'lin'}, [ h("span", null, '我是靓仔'), h("button", {onClick: function() {}}, 'change') ])
文档: H函数
通过mount函HTML数, 将vnode挂载到div#app上
mount(vnode, document.querySelector("#app"))
const h = (tag, props, children) => { // vnode --> javascript对象 return { tag, props, children } } const mount = (vnode, container) => { // 1. 创建出真实的元素, 并且在vnode上保存el const el = vnode.el = document.createElement(vnode.tag) // 2. 处理props if (vnode.props) { for (let 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. 处理子元素 字符串,数组 if (vnode.children) { if (typeof vnode.children === "string") { el.textContent = vnode.children } else { // 数组 多个子元素 递归 vnode.children.forEach(element => { mount(element, el) }) } } // 4.将el挂载到container上 container.appendChild(el) }

patch对比新旧节点
当节点发生变化时, 对比新旧节点并进行更新。以新的VNode为基准,改造旧的oldVNode使之成为跟新的VNode一样(打补丁javascript是干什么的)
setTimeout(() => { const vnode2 = h("div", {class: 'jin', onClick: function() {console.log("我是靓仔")}}, [ h("button", {class: "zhang"}, '按钮') ]) patch(vnode, vnode2) }, 2000)
const patch = (n1, n2) => { // n1旧 n2新 // 如果父元素不一样, 直接替换 if (n1.tag !== n2.tag) { // 获取父元素 const n1Elparent = n1.el.parentElement // 移除旧节点 n1Elparent.removeChild(n1.el) // 重新挂载新节点 mount(n2, n1Elparent) } else { // 引用, 修改一个另一个也会改变, n1.el 在n1挂载(mount)时赋值 const el = n2.el = n1.el // 对比props const oldProps = n1.props || {} const newProps = n2.props || {} // 把新的props添加到el上 for (let key in newProps) { const oldValue = oldProps[key] const newValue = newProps[key] if (newValue !== oldValue) { if (key.startsWith("on")) { el.addEventListener(key.slice(2).toLowerCase(), newValue) } else { el.setAttribute(key, newValue) } } } // 删除旧的props 移除监听器, 属性 for (let key in oldProps) { if (key.startsWith("on")) { el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]) } else { if (!key in newProps) { el.removeAttribute(key) } } } // 对比children // children 可能是字符串, 数组, 对象(插槽), 字符串跟数组比较常见 // n1 [v1, v2, v3, v4, v5] // n2 [v1, v7, v8] const oldChildren = n1.children || [] const newChidlren = n2.children || [] if (typeof newChidlren === 'string') { // 如果newChidlren是字符串 if (typeof oldChildren === "string") { if (newChidlren !== oldChildren) { // textContent属性表示一个节点及其后代的文本内容 el.textContent = newChidlren } } else { // innerHTML 返回 HTML textContent 通常具有更好的性能,因为文本不会被解析为HTML 使用 textContent 可以防止 XSS 攻击。 el.innerHtml = newChidlren } } else { // 如果newChidlren是数组 const oldLength = oldChildren.length const newLength = newChidlren.length const minLength = Math.min(oldLength, newLength) // 先对比相同长度的部分 for (let i = 0; i < minLength; i++) { patch(oldChildren[i], newChidlren[i]) } // 如果新的比较长, 则mount新增的节点 if (newLength > oldLength) { newChidlren.slice(minLength).forEach(item => { mount(item, el) }) } // 如果旧的比较长, 则移除多余节点 if (newLength < oldLength) { oldChildren.slice(minLength).forEach(item => { el.removeChild(item.el) }) } } } }
响应式系统
当数据发生改变的时候,所有使用到数据的地方也应该发生改变
let obj = { name: 'lin' } const change = () => { console.log('输出为:', obj.name) } change() obj.name = 'jin' // 当obj发生变化时,有使用到到obj的地方也会发生相应的改变 change()
定义依赖收集类
class Depend { constructor() { // Set对象允许你存储任何类型的唯一值,不会出现重复 this.reactiveFns = new Set() } addDepend(reactiveFn) { this.reactiveFns.add(reactiveFn) } notify() { this.reactiveFns.forEach(item => { item() }) } } let obj = { name: 'lin' } const change = () => { console.log('输出为:', obj.name) } const dep = new Depend() dep.addDepend(change) obj.name = 'jin' dep.notify()
自动监听对象的变化
每次对象发生改变我们都要重新调用页面性能优化一次notify
方法, 我们可以使算法导论用 proxy来监听对象的变化。
响应式函数
并不是每个函数都需要变成浏览器怎么打开网站响应式函数,我们javascript面试题可以定义一个函数来接收需要转化成响应式的函数。
let activeReactiveFn = null function watchFn(fn) { activeReactiveFn = fn fn() // 调用一次触发get(看下面代码) activeReactiveFn = null }
监听对象的变化
vue2跟vue3实现方式不同:
- vue2使用 Object.defineProperty() 劫持对象监听数据的变化
- 不能监听数组的变化
- 必须遍历对象的每个属性
- 必须深层遍历嵌套的对象
- vue3使用 proxy 监听对象浏览器历史记录设置的变算法的五个特性化
- 针对对象:针对整个对象,而不是对象的某个属性,所以也就不需要对 keys 进行遍历。
- 支持数组:Prox算法工程师y 不需要对数组的方法进行重载,省去了众多 hack,页面性能优化减少代码量等于减少了维护成本,而且标准的就是最好的。
- Proxy 的第二个参数可javascript面试题以有 13 种拦截方法,这比起 Object浏览器的历史.defineProperty() 要更加丰富
- Proxy 作为新标准受到浏览器厂商的性能优化的方法重点关注和性能优化,相比之下 Object.defineProperty() 是一个已有的老方法。
const reactive = (obj) => { let depend = new Depend() // 返回一个proxy对象, 操作代理对象, 如果代理对象发生变化, 原对象也会发生变化 return new Proxy(obj, { get: (target, key) => { // 收集依赖 depend.addDepend() return Reflect.get(target, key) }, set: (target, key, value) => { Reflect.set(target, key, value) // 当值发生改变时 触发 depend.notify() } }) } // 修改 Depend类中的addDepend方法 // addDepend() { // if (activeReactiveFn) { // this.reactiveFns.push(activeReactiveFn) // } // } let obj = { name: 'lin' } let proxyObj = reactive(obj) const foo = () => { console.log(proxyObj.name) } watchFn(foo) proxyObj.name = 'jin'
文档:
- Reflect
正确收集依赖

age
属性,即使chan算法的有穷性是指ge
函数里面没有使用到age
, 我们也会触发change
函数。 所以我们要正确收集依赖,怎样正确收集依赖呢。
- 不同算法导论的对象单独存储
- 同一个对象不安卓性能优化同属性也要单独存储
- 存储对象我们可以使用 WeakMap
WeakMap
对象是一组算法分析的目的是键/值对的集合,其中的键是弱引用(原对象销毁的时候可以被垃圾回收javascript:void(0))的。其键必须是对象
,而值可以是任意的。
- 存储对象不同属性可以使用 Map
Map
对象保存键浏览器的历史记录在哪值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值) 都可以作为一javascriptdownload个键或一个值。

const targetMap = new WeakMap() const getDepend = (target, key) => { // 根据target对象获取Map let desMap = targetMap.get(target) if (!desMap) { desMap = new Map() targetMap.set(target, desMap) } // 根据key获取 depend类 let depend = desMap.get(key) if (!depend) { depend = new Depend() desMap.set(key, depend) } return depend }
const reactive = (obj) => { return new Proxy(obj, { get: (target, key) => { // 收集依赖 const depend = getDepend(target, key) depend.addDepend() return Reflect.get(target, key) }, set: (target, key, value) => { const depend = getDepend(target, key) Reflect.set(target, key, value) // 当值发生改变时 触发 depend.notify() } }) }
应用程序入口模块
新建一个html文件,把浏览器怎么打开网站创建浏览器的历史记录在哪的函数所在的js文件都引进来
<script>
// 创建根组件
const App = {
// 需要进行响应式的数据
data: reactive({
counter: 0
}),
render() {
// h函数渲染节点
return h("div", {class: 'lin'}, [
h("div", {class: 'text'}, `${this.data.counter}`),
h("button", {onClick: () => {
this.data.counter++
console.log(this.data.counter)
}}, '+')
])
}
}
// 挂载根组件
const app = createApp(App)
app.mount("#app")
</script>
新建一个js文件保存createApp
函数,这个函数返回一个对象,对象里面有一个mount
方法
const createApp = (rootComponent) => { return { mount(selector) { const container = document.querySelector(selector) // 响应式函数 watchEffect(function() { const vNode = rootComponent.render() // 把节点挂载到 #div mount(vNode, container) }) } } }

+
按钮都js性能优化会新增节点
第一次挂载(mount) –> 值改变 –> patch
const createApp = (rootComponent) => { return { mount(selector) { const container = document.querySelector(selector) // isMounted 是否已经挂载 let isMounted = false let oldVNode = null watchEffect(function() { if (!isMounted) { oldVNode = rootComponent.render() // 第一次挂载 mount(oldVNode, container) isMounted = true } else { const newVNode = rootComponent.render() // 对比新旧节点 patch(oldVNode, newVNode) oldVNode = newVNode } }) } } }
写的很菜,等我变秃了再回来改javascript:void(0)进改进!!!
参考文档
王红元 《深入vue3 + typescript》
评论(0)