Vue3源码学习 第一次这样学习源码还是挺有难度的,希望后面可以慢慢提升,有自己阅读源码的能力叭
1. 真实的DOM渲染 传统的前端开发中,我们是编写自己的HTML,最终被渲染到浏览器上的,这个过程大概是:
解析 html 转化成 DOM 树,然后渲染到页面
2. 虚拟DOM 2.1 渲染过程
3. Vue 源码的三大核心系统
4. 实现 Mini-Vue mini-vue的实现也是,后面有时间得回来重新看看
包括三部分:
4.1 渲染系统实现 包含三个功能:
h 函数,返回一个 VNode 对象 mount 函数,用于将 VNode 挂载到 DOM 上 patch 函数,用于对比两个 VNode,决定如何处理新的VNode(diff) h函数生成 VNode const h = (tag, props, children ) => { return { tag, props, children } }
mount函数挂载 vnode const mount = (vnode, container ) => { const el = vnode.el = document .createElement (vnode.tag ) 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) } } } if (vnode.children ) { if (typeof vnode.children === 'string' ) { el.textContent = vnode.children } else { vnode.children .forEach (item => { mount (item, el) }); } } container.appendChild (el) }
patch函数 - 对比两个VNode const patch = (n1, n2 ) => { if (n1.tag !== n2.tag ) { const n1Elparent = n1.el .parentElement n1Elparent.removeChild (n1.el ) mount (n2, n1Elparent) } else { const el = n2.el = n1.el const oldProps = n1.props || {} const newProps = n2.props || {} for (const 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) } } } for (const key in oldProps) { if (key.startsWith ("on" )) { const value = oldProps[key] el.removeEventListener (key.slice (2 ).toLowerCase (), value) } if (!(key in newProps)) { el.removeAttribute (key) } } const oldChildren = n1.children || [] const newChildren = n2.children || [] if (typeof newChildren === "string" ) { if (typeof oldChildren === "string" ) { if (newChildren !== oldChildren) { el.textContent = newChildren } } else { el.innerHTML = newChildren } } else { 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 (oldChildren.length ).forEach (item => { mount (item, el) }) } if (newChildren.length < oldChildren.length ) { oldChildren.slice (newChildren.length ).forEach (item => { el.removeChild (item.el ) }) } } } } }
4.2 响应式系统 依赖收集系统 + vue2响应式系统 class Dep { constructor ( ) { this .subscribers = new Set () } depend ( ) { if (activeEffect) { this .subscribers .add (activeEffect) } } notify ( ) { this .subscribers .forEach (effect => { effect () }) } } let activeEffect = null function watchEffect (effect ) { activeEffect = effect effect () activeEffect = null } const targetMap = new WeakMap ()function getDep (target, key ) { let depsMap = targetMap.get (target) if (!depsMap) { depsMap = new Map () targetMap.set (target, depsMap) } let dep = depsMap.get (key) if (!dep) { dep = new Dep () depsMap.set (key, dep) } return dep } 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 } const info = reactive ({ counter : 100 , name : 'hillyee' }) watchEffect (function ( ) { console .log (info.counter * 2 , info.name , 'w1' ); }) watchEffect (function ( ) { console .log (info.counter * info.counter , 'w2' ); })
响应式系统 vue3-proxy实现 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 () } }) }
为什么 Vue3 选择 Proxy 呢? Object.definedProperty 是劫持对象的属性时,如果新增元素,
那么Vue2需要再次 调用definedProperty,而 Proxy 劫持的是整个对象,不需要做特殊处理
修改对象的不同: 使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截;
而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截;
4.3 框架外层 API 设计 createApp() 用于创建一个app对象,该app对象有一个mount方法,可以将根组件挂载到某一个dom元素上
function createApp (rootComponent ) { return { mount (selector ) { const container = document .querySelector (selector) 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 } }) } } }
使用案例–计数 <body > <div id ="app" > </div > <script src ="../mini_vue/renderer.js" > </script > <script src ="../mini_vue/reactive.js" > </script > <script src ="./index.js" > </script > <script > const App = { data : reactive ({ counter : 0 }), render ( ) { return h ("div" , null , [ h ("h2" , null , `当前计数:${this .data.counter} ` ), h ("button" , { onClick : () => { this .data .counter ++ console .log (this .data .counter ); } }, "+1" ) ]) }, } const app = createApp (App ) app.mount ('#app' ) </script > </body >
Vue3源码阅读 根据图的流程,加上源码,多看看叭,第一次看源码确实有点吃力了
先看熟悉流程,然后可以 debugger 在浏览器上简单过一下整个流程
createApp
源码阅读之挂载根组件
const app = {props : {message : String }instance instance.props instance.attrs instance.slots const result = setup ()instance.setupState = proxyRefs (result); <template> -> render函数 instance.render = Component .render = render函数 data/methods/computed/生命周期
组件化的初始化
Compile过程 对于不会改变的静态节点进行作用于提升
我都没找到这部分函数。。。
Block Tree 分析 vue3的一个优化:对于不会改变的静态节点进行作用域提升,仅对新的vnode进行创建
生命周期回调
template中数据的使用顺序 如果setup跟data中有同一个属性,首先选择setup的,内部做了一个判断吧