您好,欢迎来到三六零分类信息网!老站,搜索引擎当天收录,欢迎发信息

深入解析下vue3中的渲染系统

2024/4/28 16:21:22发布19次查看
本篇文章给大家深入解析一下vue3中的渲染系统,希望对大家有所帮助!
提到马拉松,大家都知道马拉松是世界上最长的田径项目(全程42.195公里),是所有体育运动中体力消耗最大,同时也是最磨练一个人的意志的项目。如果说你可以坚持跑完整个马拉松,那还有什么是你不可以坚持下来的呢。
同样,在我们学习研究一些优秀类库的源码时,整个过程也是枯燥乏味,亦如参加一场源码级的马拉松。那今天笔者就带着大家一起跑一场关于vue.js 3.0渲染系统的源码解析版的马拉松,在整个过程中,笔者也会给大家提供补给站(流程图),方便大家阅读。
思考
在开始今天的文章之前,大家可以想一下:
vue文件是如何转换成dom节点,并渲染到浏览器上的?
数据更新时,整个的更新流程又是怎么样的?
vuejs有两个阶段:编译时和运行时。
编译时
我们平常开发时写的.vue文件是无法直接运行在浏览器中的,所以在webpack编译阶段,需要通过vue-loader将.vue文件编译生成对应的js代码,vue组件对应的template模板会被编译器转化为render函数。
运行时
接下来,当编译后的代码真正运行在浏览器时,便会执行render函数并返回vnode,也就是所谓的虚拟dom,最后将vnode渲染成真实的dom节点。
了解完vue组件渲染的思路后,接下来让我们从vue.js 3.0(后续简称vue3)的源码出发,深入了解vue组件的整个渲染流程是怎么样的?
准备
本文主要是分析vue3的渲染系统,为了方便调试,我们直接通过引入vue.js文件的方式进行源码调试分析。
vue3源码下载
# 源码地址(推荐ssh方式下载)https://github.com/vuejs/vue-next# 或者下载笔者做笔记用的版本https://github.com/asyncguo/vue-next/tree/vue3_notes
生成vue.global.js文件
npm run dev# bundles .../vue-next/packages/vue/src/index.ts → packages/vue/dist/vue.global.js...# created packages/vue/dist/vue.global.js in 2.8s
启动开发环境
npm run serve
测试代码
<!-- 调试代码目录:/packages/vue/examples/test.html --><script src="./../dist/vue.global.js"></script><div id="app"> <div>static node</div> <div>{{title}}</div> <button @click="add">click</button> <item :msg="title"/></div><script> const item = { props: ['msg'], template: `<div>{{ msg }}</div>` } const app = vue.createapp({ components: { item }, setup() { return { title: vue.ref(0) } }, methods: { add() { this.title += 1 } }, }) app.mount('#app')</script>
创建应用
从上面的测试代码,我们会发现vue3和vue2的挂载方式是不同的,vue3是通过createapp这个入口函数进行应用的创建。接下来我们来看下createapp的具体实现:
// 入口文件: /vue-next/packages/runtime-dom/src/index.tsconst createapp = ((...args) => { console.log('createapp入参:', ...args); // 创建应用 const app = ensurerenderer().createapp(...args); const { mount } = app; // 重写mount app.mount = (containerorselector) => { // ... }; return app;});
ensurerenderer
首先通过ensurerenderer创建web端的渲染器,我们来看下具体实现:
// 更新属性的方法const patchprop = () => { // ...}// 操作dom的方法const nodeops = { insert: (child, parent, anchor) => { parent.insertbefore(child, anchor || null) }, remove: child => { const parent = child.parentnode if (parent) { parent.removechild(child) } }, ...}// web端的渲染器所需的参数设置const rendereroptions = extend({ patchprop }, nodeops);let renderer;// 延迟创建rendererfunction ensurerenderer() { return (renderer || (renderer = createrenderer(rendereroptions)));}
在这里可以看出,通过延迟创建渲染器,当我们只依赖响应式包的情况下,可以通过tree-shaking移除渲染相关的代码,大大减少包的体积。
createrenderer
通过ensurerenderer可以看出,真正的入口是这个createrenderer方法:
// /vue-next/packages/runtime-core/src/renderer.tsexport function createrenderer(options) { return basecreaterenderer(options)}function basecreaterenderer(options, createhydrationfns) { // 通用的dom操作方法 const { insert: hostinsert, remove: hostremove, ... } = options // ======================= // 渲染的核心流程 // 通过闭包缓存内敛函数 // ======================= const patch = () => {} // 核心diff过程 const processelement = () => {} // 处理element const mountelement = () => {} // 挂载element const mountchildren = () => {} // 挂载子节点 const processfragment = () => {} // 处理fragment节点 const processcomponent = () => {} // 处理组件 const mountcomponent = () => {} // 挂载组件 const setuprendereffect = () => {} // 运行带副作用的render函数 const render = () => {} // 渲染挂载流程 // ... // ======================= // 2000+行的内敛函数 // ======================= return { render, hydrate, // 服务端渲染相关 createapp: createappapi(render, hydrate) }}
接下来我们先跳过这些内敛函数的实现(后面的渲染流程用到时,我们再具体分析),来看下createappapi的具体实现:
createappapi
function createappapi(render, hydrate) { // 真正创建app的入口 return function createapp(rootcomponent, rootprops = null) { // 创建vue应用上下文 const context = createappcontext(); // 已安装的vue插件 const installedplugins = new set(); let ismounted = false; const app = (context.app = { _uid: uid++, _component: rootcomponent, // 根组件 use(plugin, ...options) { // ... return app }, mixin(mixin) {}, component(name, component) {}, directive(name, directive) {}, mount(rootcontainer) {}, unmount() {}, provide(key, value) {} }); return app; };}
可以看出,createappapi返回的createapp函数才是真正创建应用的入口。在createapp里会创建vue应用的上下文,同时初始化app,并绑定应用上下文到app实例上,最后返回app。
这里有个值得注意的点:app对象上的use、mixin、component和directive方法都返回了app应用实例,开发者可以链式调用。
// 一直use一直爽createapp(app).use(router).use(vuex).component('component',{}).mount("#app")
到此app应用实例已经创建好了~,打印查看下创建的app应用:
总结一下创建app应用实例的过程:
创建web端对应的渲染器(延迟创建,tree-shaking)
执行basecreaterenderer方法(通过闭包缓存内敛函数,后续挂载阶段的主流程)
执行createappapi方法(1. 创建应用上下文;2. 创建app并返回)
挂载阶段
接下来,当我们执行app.mount时,便会开始挂载组件。而我们调用的app.mount则是重写后的mount方法:
const createapp = ((...args) => { // ... const { mount } = app; // 缓存原始的mount方法 // 重写mount app.mount = (containerorselector) => { // 获取容器 const container = normalizecontainer(containerorselector); if (!container) return; const component = app._component; // 判断如果传入的根组件不是函数&根组件没有render函数&没有template,就把容器的内容设置为根组件的template if (!isfunction(component) && !component.render && !component.template) { component.template = container.innerhtml; } // 清空容器内容 container.innerhtml = ''; // 执行缓存的mount方法 const proxy = mount(container, false, container); return proxy; }; return app;});
执行完web端重写的mount方法后,才是真正挂载组件的开始,即调用createappapi返回的app应用上的mount方法:
function createappapi(render, hydrate) { // 真正创建app的入口 return function createapp(rootcomponent, rootprops = null) { // ... const app = (context.app = { // 挂载根组件 mount(rootcontainer, ishydrate, issvg) { if (!ismounted) { // 创建根组件对应的vnode const vnode = createvnode(rootcomponent, rootprops); // 根级vnode存在应用上下文 vnode.appcontext = context; // 将虚拟vnode节点渲染成真实节点,并挂载 render(vnode, rootcontainer, issvg); ismounted = true; // 记录应用的根组件容器 app._container = rootcontainer; rootcontainer.__vue_app__ = app; app._instance = vnode.component; return vnode.component.proxy; } } }); return app; };}
总结一下,mount方法主要做了什么呢?
创建根组件对应的vnode
根组件vnode绑定应用上下文context
渲染vnode成真实节点,并挂载
记录挂载状态
细心的同学可能已经发现了,这里的mount方法是一个标准的跨平台渲染流程,抽象vnode,然后通过rootcontainer实现特定平台的渲染,例如在浏览器环境下,它就是一个dom对象,在其他平台就是其他特定的值。这也就是为什么我们在调用runtime-dom包的creataapp方法时,重写mount方法,完善不同平台的渲染逻辑。
创建vnode
提到vnode,可能更多人会和高性能联想到一起,误以为vnode的性能就一定比手动操作dom的高,其实不然。vnode的底层同样是要操作dom,相反如果vnode的patch过程过长,同样会导致页面的卡顿。 而vnode的提出则是对原生dom的抽象,在跨平台设计的处理上会起到一定的抽象化。例如:服务端渲染、小程序端渲染、weex平台...
接下来,我们来看下创建vnode的过程:
function _createvnode( type, props, children, patchflag, ...): vnode { // 规范化class & style // 例如:class=[]、class={}、style=[]等格式,需规范化 if (props) { // ... } // 获取vnode类型 const shapeflag = isstring(type) ? 1 /* element */ : issuspense(type) ? 128 /* suspense */ : isteleport(type) ? 64 /* teleport */ : isobject(type) ? 4 /* stateful_component */ : isfunction(type) ? 2 /* functional_component */ : 0; return createbasevnode()}
function createbasevnode( type, props = null, children = null, ...) { // vnode的默认结构 const vnode = { __v_isvnode: true, // 是否为vnode __v_skip: true, // 跳过响应式数据化 type, // 创建vnode的第一个参数 props, // dom参数 children, component: null, // 组件实例(instance),通过createcomponentinstance创建 shapeflag, // 类型标记,在patch阶段,通过匹配shapeflag进行相应的渲染过程 ... }; // 标准化子节点 if (needfullchildrennormalization) { normalizechildren(vnode, children); } // 收集动态子代节点或子代block到父级block tree if (isblocktreeenabled > 0 && !isblocknode && currentblock && (vnode.patchflag > 0 || shapeflag & 6 /* component */) && vnode.patchflag !== 32 /* hydrate_events */) { currentblock.push(vnode); } return vnode;}
通过上面的代码,我们可以总结一下,创建vnode阶段都做了什么:
规范化class & style(例如:class=[]、class={}、style=[]等格式)
标记vnode的类型shapeflag,即根组件对应的vnode类型(type即为根组件rootcomponent,此时根组件为对象格式,所以shapeflag即为4)
标准化子节点(初始化时,children为空)
收集动态子代节点或子代block到父级block tree(这里便是vue3引入的新概念:block tree,篇幅有限,本文就不展开陈述了)
这里,我们可以打印查看一下此时根组件对应的vnode结构:
渲染vnode
通过createvnode获取到根组件对应的vnode,然后执行render方法,而这里的render函数便是basecreaterenderer通过闭包缓存的render函数:
// 实际调用的render方法即为basecreaterenderer方法中缓存的render方法function basecreaterenderer() { const render = (vnode, container) => { if (vnode == null) { if (container._vnode) { // 卸载组件 unmount() } } else { // 正常挂载 patch(container._vnode || null, vnode, container) } }}
当传入的vnode为null&存在老的vnode,则进行卸载组件
否则,正常挂载
挂载完成后,批量执行组件生命周期
绑定vnode到容器上,以便后续更新阶段通过新旧vnode进行patch
⚠️:接下来,整个渲染过程将会在basecreaterenderer这个核心函数的内敛函数中执行~
patch
接下来,我们来看下render过程中的patch函数的实现:
const patch = ( n1, // 旧的vnode n2, // 新的vnode container, // 挂载的容器 ...) => { // ... const { type, ref, shapeflag } = n2 switch (type) { case text: // 处理文本 processtext(n1, n2, container, anchor) break case comment: // 注释节点 processcommentnode(n1, n2, container, anchor) break case static: // 静态节点 if (n1 == null) { mountstaticnode(n2, container, anchor, issvg) } break case fragment: // fragment节点 processfragment(n1, n2, container, ...) break default: if (shapeflag & 1 /* element */) { // 处理dom元素 processelement(n1, n2, container, ...); } else if (shapeflag & 6 /* component */) { // 处理组件 processcomponent(n1, n2, container, ...); } else if (shapeflag & 64 /* teleport */) { type.process(n1, n2, container, ...); } else if (shapeflag & 128 /* suspense */) { type.process(n1, n2, container, ...); } }}
分析patch函数,我们会发现patch函数会通过判断type和shapeflag的不同来走不同的处理逻辑,今天我们主要分析组件类型和普通dom元素的处理。
processcomponent
初始化渲染时,type为object并且shapeflag对应的值为4(位运算4 & 6),即对应processcomponent组件的处理方法:
const processcomponent = (n1, n2, container, ...) => { if (n1 == null) { if (n2.shapeflag & 512 /* component_kept_alive */) { // 激活组件(已缓存的组件) parentcomponent.ctx.activate(n2, container, ...); } else { // 挂载组件 mountcomponent(n2, container, ...); } } else { // 更新组件 updatecomponent(n1, n2, optimized); }};
如果n1为null,则执行挂载组件;否则更新组件。
mountcomponent
接下来我们继续看挂载组件的mountcomponent函数的实现:
const mountcomponent = (initialvnode, container, ...) => { // 1. 创建组件实例 const instance = ( // 这个时候就把组件实例挂载到了组件vnode的component属性上了 initialvnode.component = createcomponentinstance(initialvnode, parentcomponent, parentsuspense) ); // 2. 设置组件实例 setupcomponent(instance); // 3. 设置并运行带有副作用的渲染函数 setuprendereffect(instance, initialvnode, container,...);};
省略掉无关主流程的代码后,可以看到,mountcomponent函数主要做了三件事:
创建组件实例
function createcomponentinstance(vnode, parent, suspense) { const type = vnode.type; // 绑定应用上下文 const appcontext = (parent ? parent.appcontext : vnode.appcontext) || emptyappcontext; // 组件实例的默认值 const instance = { uid: uid$1++, //组件唯一id vnode, // 当前组件的vnode type, // vnode节点类型 parent, // 父组件的实例instance appcontext, // 应用上下文 root: null, // 根实例 next: null, // 当前组件mounted时,为null,将设置为instance.vnode,下次update时,将执行updatecomponentprerender subtree: null, // 组件的渲染vnode,由组件的render函数生成,创建后同步 update: null, // 组件内容挂载或更新到视图的执行回调,创建后同步 scope: new effectscope(true /* detached */), render: null, // 组件的render函数,在setupstatefulcomponent阶段赋值 proxy: null, // 是一个proxy代理ctx字段,内部使用this时,指向它 // local resovled assets // resolved props and emits options // emit // props default value // inheritattrs // state // suspense related // lifecycle hooks }; { instance.ctx = createdevrendercontext(instance); } instance.root = parent ? parent.root : instance; instance.emit = emit.bind(null, instance); return instance;}
createcomponentinstance函数主要是初始化组件实例并返回,打印查看下根组件对应的instance内容:
设置组件实例
function setupcomponent(instance, isssr = false) { const { props, children } = instance.vnode; // 判断是否为状态组件 const isstateful = isstatefulcomponent(instance); // 初始化组件属性、slots initprops(instance, props, isstateful, isssr); initslots(instance, children); // 当状态组件时,挂载setup信息 const setupresult = isstateful ? setupstatefulcomponent(instance, isssr) : undefined; return setupresult;}
setupcomponent的逻辑也很简单,首先初始化组件props和slots挂载到组件实例instance上,然后根据组件类型vnode.shapeflag===4,判断是否挂载setup信息(也就是vue3的composition api)。
function setupstatefulcomponent(instance, isssr) { const component = instance.type; // 创建渲染上下文的属性访问缓存 instance.accesscache = object.create(null); // 创建渲染上下文代理 instance.proxy = markraw(new proxy(instance.ctx, publicinstanceproxyhandlers)); const { setup } = component; // 判断组件是否存在setup if (setup) { // 判断setup是否有参数,有的话,创建setup上下文并挂载组件实例 // 例如:setup(props) => {} const setupcontext = (instance.setupcontext = setup.length > 1 ? createsetupcontext(instance) : null); // 执行setup函数 const setupresult = callwitherrorhandling(setup, instance, 0 /* setup_function */, [shallowreadonly(instance.props) , setupcontext]); handlesetupresult(instance, setupresult, isssr); } else { finishcomponentsetup(instance, isssr); }}
判断组件是否设置了setup函数:
若设置了setup函数,则执行setup函数,并判断其返回值的类型。若返回值类型为函数时,则设置组件实例render的值为setupresult,否则作为组件实例setupstate的值
function handlesetupresult(instance, setupresult, isssr) { // 判断setup返回值类型 if (isfunction(setupresult)) { // 返回值为函数时,则当作组件实例的render方法 instance.render = setupresult; } else if (isobject(setupresult)) { // 返回值为对象时,则当作组件实例的setupstate instance.setupstate = proxyrefs(setupresult); } else if (setupresult !== undefined) { warn$1(`setup() should return an object. received: ${setupresult === null ? 'null' : typeof setupresult}`); } finishcomponentsetup(instance, isssr);}
设置组件实例的render方法,分析finishcomponentsetup函数,render函数有三种设置方式:
若setup返回值为函数类型,则instance.render = setupresult
若组件存在render方法,则instance.render = component.render
若组件存在template模板,则instance.render = compile(template)
组件实例的render优化级:instance.render = setup() || component.render || compile(template)
function finishcomponentsetup(instance, ...) { const component = instance.type; // 绑定render方法到组件实例上 if (!instance.render) { if (compile && !component.render) { const template = component.template; if (template) { // 通过编译器编译template,生成render函数 component.render = compile(template, ...); } } instance.render = (component.render || noop); } // support for 2.x options ...}
设置完组件后,我们可以再查看下instance的内容有发生什么变化:
这个时候组件实例instance的data、proxy、render、setupstate已经绑定上了初始值。
设置并运行带有副作用的渲染函数
const setuprendereffect = (instance, initialvnode, container, ...) => { // 创建响应式的副作用函数 const componentupdatefn = () => { // 首次渲染 if (!instance.ismounted) { // 渲染组件生成子树vnode const subtree = (instance.subtree = rendercomponentroot(instance)); patch(null, subtree, container, ...); initialvnode.el = subtree.el; instance.ismounted = true; } else { // 更新 } }; // 创建渲染effcet const effect = new reactiveeffect( componentupdatefn, () => queuejob(instance.update), instance.scope // track it in component's effect scope ); const update = (instance.update = effect.run.bind(effect)); update.id = instance.uid; update();};
接下来继续执行setuprendereffect函数,首先会创建渲染effect(响应式系统还包括其他副作用:computed effect、watch effect),并绑定副作用执行函数到组件实例的update属性上(更新流程会再次触发update函数),并立即执行update函数,触发首次更新。
function rendercomponentroot(instance) { const { proxy, withproxy, render, ... } = instance; let result; try { const proxytouse = withproxy || proxy; // 执行实例的render方法,返回vnode,然后再标准化vnode // 执行render方法时,会调用proxytouse,即会触发publicinstanceproxyhandlers的get result = normalizevnode(render.call(proxytouse, proxytouse, ...)); } return result;}
此时,rendercomponentroot函数会执行实例的render方法,即setupcomponent阶段绑定在实例render方法上的函数,同时标准化render返回的vnode并返回,作为子树vnode。
同样我们可以打印查看一下子树vnode的内容:
此时,可能有些同学开始疑惑了,为什么会有两颗vnode树呢?这两颗vnode树又有什么区别呢?
initialvnode
initialvnode就是组件的vnode,即描述整个组件对象的,组件vnode会定义一些和组件相关的属性:data、props、生命周期等。通过渲染组件vnode,生成子树vnode。
sub tree
子树vnode是通过组件vnode的render方法生成的,其实也就是对组件模板template的描述,即真正要渲染到浏览器的dom vnode。
生成subtree后,接下来就继续通过patch方法,把subtree节点挂载到container上。接下来,我们继续往下分析,大家可以看下上面subtree的截图:subtree的type值为fragment,回忆下patch方法的实现:
const patch = ( n1, // 旧的vnode n2, // 新的vnode container, // 挂载的容器 ...) => { const { type, ref, shapeflag } = n2 switch (type) { case fragment: // fragment节点 processfragment(n1, n2, container, ...) break default: // ... }}
fragment也就是vue3提到的新特性之一,在vue2中,是不支持多根节点组件,而vue3则是正式支持的。细想一下,其实还是单个根节点组件,只是vue3的底层用fragment包裹了一层。我们再看下processfragment的实现:
const processfragment = (n1, n2, container, ...) => { // 创建碎片开始、结束的文本节点 const fragmentstartanchor = (n2.el = n1 ? n1.el : hostcreatetext('')); const fragmentendanchor = (n2.anchor = n1 ? n1.anchor : hostcreatetext('')); if (n1 == null) { hostinsert(fragmentstartanchor, container, anchor); hostinsert(fragmentendanchor, container, anchor); // 挂载子节点数组 mountchildren(n2.children, container, ...); } else { // 更新 }};
接下来继续挂载子节点数组:
const mountchildren = (children, container, ...) => { for (let i = start; i < children.length; i++) { const child = (children[i] = optimized ? cloneifmounted(children[i]) : normalizevnode(children[i])); patch(null, child, container, ...); }};
遍历子节点,patch每个子节点,根据child节点的type递归处理。接下来,我们主要看下type为element类型的dom元素,即processelement:
const processelement = (n1, n2, container, ...) => { if (n1 == null) { // 挂载dom元素 mountelement(n2, container,...) } else { // 更新 }}
const mountelement = (vnode, container, ...) => { let el; let vnodehook; const { type, props, shapeflag, ... } = vnode; { // 创建dom节点,并绑定到当前vnode的el上 el = vnode.el = hostcreateelement(vnode.type, ...); } // 插入父级节点 hostinsert(el, container, anchor);};
创建dom节点,并挂载到vnode.el上,然后把dom节点挂载到container中,继续递归其他vnode的处理,最后挂载整个vnode到浏览器视图中,至此完成vue3的首次渲染整个流程。mountelement方法中提到到hostcreateelement、hostinsert也就是在最开始创建渲染器时传入的参数对应的处理方法,也就完成整个跨平台的初次渲染流程。
更新流程
分析完vue3首次渲染的整个流程后,那么在数据更新后,vue3又是怎么更新渲染呢?接下来分析更新流程阶段就要涉及到vue3的响应式系统的知识了(由于篇幅有限,我们不会展开更多响应式的知识,期待后续篇章的更加详细的分析)。
依赖收集
回忆下在首次渲染时的设置组件实例setupcomponent阶段会创建渲染上下文代理,而在生成subtree阶段,会通过rendercomponentroot函数,执行组件vnode的render方法,同时会触发渲染上下文代理的publicinstanceproxyhandlers的get,从而实现依赖收集。
function setupstatefulcomponent(instance, isssr) { ... // 创建渲染上下文代理 instance.proxy = markraw(new proxy(instance.ctx, publicinstanceproxyhandlers));}
function rendercomponentroot(instance) { const proxytouse = withproxy || proxy; // 执行render方法时,会调用proxytouse,即会触发publicinstanceproxyhandlers的get result = normalizevnode( render.call(proxytouse, proxytouse, ...) ); return result;}
我们可以查看下此时组件vnode的render方法的内容:
或者打印查看render方法内容:
(function anonymous() {const _vue = vueconst { createvnode: _createvnode, createelementvnode: _createelementvnode } = _vueconst _hoisted_1 = /*#__pure__*/_createelementvnode("div", null, "static node", -1 /* hoisted */)const _hoisted_2 = ["onclick"]return function render(_ctx, _cache) { with (_ctx) { const { createelementvnode: _createelementvnode, todisplaystring: _todisplaystring, resolvecomponent: _resolvecomponent, createvnode: _createvnode, fragment: _fragment, openblock: _openblock, createelementblock: _createelementblock } = _vue const _component_item = _resolvecomponent("item") return (_openblock(), _createelementblock(_fragment, null, [ _hoisted_1, _createelementvnode("div", null, _todisplaystring(title), 1 /* text */), _createelementvnode("button", { onclick: add }, "click", 8 /* props */, _hoisted_2), _createvnode(_component_item, { msg: title }, null, 8 /* props */, ["msg"]) ], 64 /* stable_fragment */)) }}})
仔细观察render的第一个参数_ctx,即传入的渲染上下文代理proxy,当访问title字段时,就会触发publicinstanceproxyhandlers的get方法,那publicinstanceproxyhandlers的逻辑又是怎么呢?
// 代理渲染上下文的handler实现const publicinstanceproxyhandlers = { get({ _: instance }, key) { const { ctx, setupstate, data, props, accesscache, type, appcontext } = instance; let normalizedprops; // key值不以$开头的属性 if (key[0] !== '$') { // 优先从缓存中判断当前属性需要从哪里获取 // 性能优化:缓存属性应该根据哪种类型获取,避免每次都触发hasown的开销 const n = accesscache[key]; if (n !== undefined) { switch (n) { case 0 /* setup */: return setupstate[key]; case 1 /* data */: return data[key]; case 3 /* context */: return ctx[key]; case 2 /* props */: return props[key]; // default: just fallthrough } } // 获取属性值的顺序:setupstate => data => props => ctx => 取值失败 else if (setupstate !== empty_obj && hasown(setupstate, key)) { accesscache[key] = 0 /* setup */; return setupstate[key]; } else if (data !== empty_obj && hasown(data, key)) { accesscache[key] = 1 /* data */; return data[key]; } else if ( (normalizedprops = instance.propsoptions[0]) && hasown(normalizedprops, key)) { accesscache[key] = 2 /* props */; return props[key]; } else if (ctx !== empty_obj && hasown(ctx, key)) { accesscache[key] = 3 /* context */; return ctx[key]; } else if (shouldcacheaccess) { accesscache[key] = 4 /* other */; } } }, set() {}, has() {}};
接下来我们以key为title的例子简单介绍下get的逻辑:
首先判断key值是否已$开头,明显title走否的逻辑
再看accesscache缓存中是否存在
性能优化:缓存属性应该根据哪种类型获取,避免每次都触发**hasown**的开销
最后再按照顺序获取:setupstate => data => props => ctxpublicinstanceproxyhandlers的set和has的处理逻辑,同样以这个顺序处理
若存在时,先设置缓存accesscache,再从setupstate中获取title对应的值
重点来了,当访问setupstate.title时,触发proxy的get的流程会有两个阶段:
首先触发setupstate对应的proxy的get,然后获取title的值,判断其是否为ref?
是:继续获取ref.value,即触发ref类型的依赖收集流程
否:直接返回,即为普通数据类型,不进行依赖收集
// 设置组件实例时会设置setupstate的代理prxoy// 设置流程:setupcomponent=>setupstatefulcomponent=>handlesetupresultinstance.setupstate = proxyrefs(setupresult)export function proxyrefs(objectwithrefs) { return isreactive(objectwithrefs) ? objectwithrefs : new proxy(objectwithrefs, { get: (target, key, receiver) => { return unref(reflect.get(target, key, receiver)) }, set: (target, key, value, receiver) => {} })}export function unref(ref) { return isref(ref) ? ref.value : ref}
访问ref.value时,触发ref的依赖收集。那我们先来分析vue.ref()的实现逻辑又是什么呢?
// 调用vue.ref(0),从而触发createref的流程// 省略其他无关代码function ref(value) { return createref(value, false)}function createref(rawvalue) { return new refimpl(rawvalue, false)}// ref的实现class refimpl { constructor(value) { this._rawvalue = toraw(value) this._value = toreactive(value) } get value() { trackrefvalue(this) return this._value }}function trackrefvalue(ref) { if (istracking()) { if (!ref.dep) { ref.dep = new set() } // 添加副作用,进行依赖收集 dep.add(activeeffect) activeeffect.deps.push(dep) }}
分析ref的实现,会发现当访问ref.value时,会触发refimpl实例的value方法,从而触发trackrefvalue,进行依赖收集dep.add(activeeffect)。那这时的activeeffect又是谁呢?
回忆下setuprendereffect阶段的实现:
const setuprendereffect = (instance, initialvnode, container, ...) => { // 创建响应式的副作用函数 const componentupdatefn = () => {}; // 创建渲染effcet const effect = new reactiveeffect( componentupdatefn, () => queuejob(instance.update), instance.scope ); const update = (instance.update = effect.run.bind(effect)); update();};// 创建effect类的实现class reactiveeffect { run() { try { effectstack.push((activeeffect = this)) // ... return this.fn() } finally {} }}
当执行update函数时(即渲染effect实例的run方法),从而设置全局activeeffect为当前渲染effect,也就是说此时dep.add(activeeffect)收集的activeeffect就是这个渲染effect,从而实现了依赖收集。
我们可以打印一下setupstate的内容,验证一下我们的分析:
通过截图,我们可以看到此时title收集的副作用就是渲染effect,细心的同学就发现了截图中的fn方法就是componentupdatefn函数,执行fn()继续挂载children。
派发更新
分析完依赖收集阶段,我们再看下,vue3又是如何进行派发更新呢?
当我们点击按钮执行this.title += 1时,同样会触发publicinstanceproxyhandlers的set方法,而set的触发顺序同样和get一致:setupstate=>data=>其他不允许修改的判断(例如:props、$开头的保留字段)
// 代理渲染上下文的handler实现const publicinstanceproxyhandlers = { set({ _: instance }, key, value) { const { data, setupstate, ctx } = instance; // 1. 更新setupstate的属性值 if (setupstate !== empty_obj && hasown(setupstate, key)) { setupstate[key] = value; } // 2. 更新data的属性值 else if (data !== empty_obj && hasown(data, key)) { data[key] = value; } // ... return true; }};
设置setupstate[key]从而继续触发setupstate的set方法:
const shallowunwraphandlers: proxyhandler<any> = { set: (target, key, value, receiver) => { const oldvalue = target[key] // oldvalue为ref类型&value不是ref时执行 if (isref(oldvalue) && !isref(value)) { oldvalue.value = value return true } else { // 否则,直接返回 return reflect.set(target, key, value, receiver) } }}
当设置oldvalue.value的值时继续触发ref的set方法,判断ref是否存在dep,执行副作用effect.run(),从而派发更新,完成更新流程。
class refimpl{ set value(newval) { newval = this._shallow ? newval : toraw(newval) if (haschanged(newval, this._rawvalue)) { this._rawvalue = newval this._value = this._shallow ? newval : toreactive(newval) triggerrefvalue(this, newval) } }}// 判断ref是否存在依赖,从而派发更新function triggerrefvalue(ref) { ref = toraw(ref) if (ref.dep) { triggereffects(ref.dep) }}// 派发更新function triggereffects(dep) { for (const effect of isarray(dep) ? dep : [...dep]) { if (effect !== activeeffect || effect.allowrecurse) { // 执行副作用 effect.run() } }}
总结
综上,我们分析完了vue3的整个渲染过程和更新流程,当然我们只是从主要的渲染流程分析,完整的渲染过程的复杂度不止于此,比如基于block tree的优化实现,patch阶段的diff优化以及在更新流程中的响应式阶段的优化又是怎样的等细节。
本文的初衷便是给大家提供分析vue3整个渲染过程的轮廓,有了整体的印象,再去分析了解更加细节的点的时候,也会更有思路和方向。
最后,附一张完整的渲染流程图,与君共享。
【相关推荐:《vue.js教程》】
以上就是深入解析下vue3中的渲染系统的详细内容。
该用户其它信息

VIP推荐

免费发布信息,免费发布B2B信息网站平台 - 三六零分类信息网 沪ICP备09012988号-2
企业名录 Product