阅读 733

Vue3源码解析,实现mini版Vue3

在看这篇文章之前希望大家还是有一定的基础,能够了解Vue2的一些基础原理,了解Proxy代码的基础应用以及WeakMap,Map,Set这些数据结构的特性。如果对这些还不了解的同学建议先去补一下基础知识。

如果你对vue3还不了解建议先去学习下vue3。

vue3基础特性

vue3+Ts实战

声明: 本文中采用的方法名均为源码中的方法名,很多代码结构按照源码的结构来写的,目的就是在于希望能够对想看源码的同学做一些引导。

如果代码中有些地方你不能一下子想明白的,先尝试去接受它,然后回过头来再去理解它。

重中之重:对于本文的学习方式上希望大家一定要更着去手写代码,多做一些思考,看完之后我相信你一定会有所收获。

如果有什么不对的地方也欢迎大家留言指正。

响应式

我们都知道vue2中是通过defineProperty实现对数据的拦截监听,而vue3中采用的Proxy,相较于defineProperty,既能更方便的监听数组,不需要进行嵌套,不用遍历每个属性,针对整个对象更加轻便。

vue3通过reactive创建一个响应式对象,当对象被修改的时候,视图也会随着更新,对应的副作用函数也会执行(这个我们后面实现watchEffect的时候再分析)。这其中主要有两个过程:

  • 对数据进行监听并收集依赖
  • 更新视图(后话)

reactive内部就是通过Proxy实现对数据的拦截监听。

// v3.js

// reactive 的实现
function reactive(target){
    return createReactiveObject(
        target,
        mutableHandlers
    );
}

/**
 * 真正创建响应式对象的函数
 * @param {*} target 对象
 * @param {*} handler 处理程序
 */
function createReactiveObject(target,handler){
    const proxy = new Proxy(target,handler);
    return proxy;
}
复制代码

Proxy对数据的监听主要在handler中进行处理, handler是一个定义一个或多个陷阱的对象。接着我们实现一下mutableHandlers对象

// v3.js

const get = createGetter();
function createGetter(){
    return function get(target,key,receiver){
        const res = Reflect.get(target, key, receiver);
        console.log('get执行了',key);
        return res;
    }
}

const set = createSetter();
function createSetter(){
    return function set(target,key,value,receiver){
        const res = Reflect.set(target,key,value,receiver);
        console.log('set执行了',key);
        return res;
    }
}

// proxy的处理程序,是一个定义一个或多个陷阱的对象
// 只处理简单的 get 和 set
const mutableHandlers = {
    get,
    set
}

// reactive 的实现
function reactive(target){
  return createReactiveObject(
    target,
    mutableHandlers
  );
}

/**
 * 真正创建响应式对象的函数
 * @param {*} target 对象
 * @param {*} handler 处理程序
 */
function createReactiveObject(target,handler){
  const proxy = new Proxy(target,handler);
  return proxy;
}
复制代码

这样我们就实现了一个简单的reactvie,能够对数据进行监听了。可以通过以下代码测试一下:

// v3.js

const testObj = reactive({ count:10 }})
testObj.count
testObj.count = 20

// 通过node v3.js执行当前代码
// get执行了 count
// set执行了 count
复制代码

这只是实现了对简单对象的监听,如果对象复杂一点,就会存在问题,例如:

// v3.js

const testObj = reactive({ count:10, info: { name:'lucas' }})
testObj.info.name
testObj.info.name = 'viky'

// 执行结果如下
// get执行了 count
// set执行了 count
复制代码

这样监听就失效了,没有监听到name属性,优化一下get方法,如果当值为对象的时候,我们应该递归监听:

// v3.js

const isObject = (target)=>{ return target !== null && typeof target === 'object'}
function createGetter(){
    return function get(target,key,receiver){
        const res = Reflect.get(target, key, receiver);
        console.log('get执行了',key);
        if(isObject(res)){
            return reactive(res);
        }
        return res;
    }
}
复制代码

到这里我们的reactive已经实现了对数据的监听。

订阅

接下来我们开始收集依赖,依旧是在get中进行处理,通过track函数进行依赖收集,在这之前先大致介绍一下vue3中用于存储依赖的数据结构。

20210326151449 (1)_gaitubao_375x265.jpg

牢牢记住这个数据结构(计时一分钟...)

我们开始撸代码,实现track函数:

// v3.js

let targetMap = new WeakMap(); // 存储对象 
let activeEffect; // 当前要添加的effect

/**
 * 收集依赖
 * @param {*} target 目标对象
 * @param {*} key 键值
 */
function track(target,key){
    if(activeEffect === undefined){
        return;
    }
    let desMap = targetMap.get(target);
    if(!desMap){
        targetMap.set(target,(desMap=new Map()))
    }
    let deps = desMap.get(key);
    if(!deps){
        desMap.set(key,(deps = new Set()))
    }
    if(!deps.has(activeEffect)){
        deps.add(activeEffect)
        // 用于清除依赖(后面分析)
        activeEffect.deps.push(deps)
    }
}
复制代码

我们在get陷阱中进行调用:

// v3.js

function createGetter(){
    return function get(target,key,receiver){
        const res = Reflect.get(target, key, receiver);
        // 依赖收集
        track(target, key)
        if(isObject(res)){
            return reactive(res);
        }
        return res;
    }
}
复制代码

到这里你可能会产生一个疑问,activeEffect这到底是个什么东西,在哪里赋值的?

activeEffect是一个被封装过的副作用函数,里面的执行函数可能是我们自己传进去的函数,也可能是更新视图的函数。至于具体在什么时候被赋值的,我们在这里留一个问题一,后面在一一解答,先接着往下看。

发布

我们的收集工作大致完成了,接下来想一下当监听的对象被修改时,我们如何执行收集的对应的副作用函数。v3中通过trigger来实现。

//v3.js

/**
 * 触发对应的副作用函数
 * @param {*} target 目标对象
 * @param {*} key 键值
 */
function trigger(target,key){
    // 获取我们存储的target对应的Map
    const depsMap = targetMap.get(target);
    if(!depsMap){
        return;
    }
    const effects = new Set(); // 副作用函数队列
    // 定义向队列中添加的add方法
    const add = (effectsToAdd)=>{
        effectsToAdd.forEach(effect => {
            effects.add(effect)
        });
    }
    // 将对应key的副作用函数加入队列
    add(depsMap.get(key));
    // 定义执行函数
    const run = (effect)=>{
        effect();
    }
    // 执行
    effects.forEach(run);
}
复制代码

作者在保留源码痕迹的前提下简化了trigger函数,去除了一些其他情况的处理。我们继续优化下set陷阱:

// v3.js

function createSetter(){
    return function set(target,key,value,receiver){
        const res = Reflect.set(target,key,value,receiver);
        // 执行对应的副作用函数
        trigger(target,key)
        return res;
    }
}

复制代码

到这边我们基本上告一个段落,通过实现了reactive创建一个响应式对象,并在getset陷阱中进行了订阅以及发布。但我们还留了一个关于activeEffect的问题,为了更好的回答这个问题以及理解以上的代码,我们通过实现一个官方的watchEffect函数让以上代码能跑起来,验证我们的发布订阅是否生效。如果有不太了解watchEffect的同学,建议先去看看它的基础用法,然后不妨先结合我们上面的代码思考一下它的实现方式。

子曰:弗思何以得

watchEffect的实现

watchEffect是根据响应式对象自动应用和重新应用副作用的函数,接收一个副作用函数并立即执行一次。 返回一个可以停止侦听的函数。 开始一步步实现这个函数,

// v3.js

/**
 * 监测响应式对象并执行对应的副作用函数
 * @param {*} effect 副作用函数
 */
function watchEffect(effect){
    return doWatch(effect);
}

function doWatch(fn){
    // 包装处理
    const getter = () => {
        // 永远不要相信程序员的代码不会报错
        return callWithErrorHandling(fn);
    }
    // 执行器
    const runner = effect(getter);
    // 立即执行一次以收集依赖
    runner();
    // 返回一个函数以清除副作用函数
    return ()=>{
        stop(runner)
    }
}

// 执行函数并对报错进行处理
function callWithErrorHandling(fn){
    var res;
    try {
        res = fn();
    } catch (error) {
        // 报错处理
        throw new Error(error);
    }
    return res;
}

复制代码

首先将我们传进来的副作用函数通过callWithErrorHandling方法包装一下,以便做函数执行时的报错处理(这里就简单处理了),得到我们的getter,再用getter去创建一个effect函数,然后该函数立即执行一次, 最后返回一个可以停止侦听的函数。大致结构就是这样。

我们接着来看看effect函数长啥样呢?

// v3.js

/**
 * 创建effect
 * @param {*} fn 函数
 */
function effect(fn){
    const effect = createReactiveEffect(fn);
    return effect;
}

// effect标识id
let uid = 0;

function createReactiveEffect(fn){
    const effect = function reactiveEffect(){
        try {
            activeEffect = effect;
            return fn();
        } finally {
            // 及时释放,避免造成污染
            activeEffect = undefined;
        }
    }
    // 标识
    effect.id = uid++;
    // 被收集在set的数组,以便清除
    effect.deps = []
    return effect;
}
复制代码

effect是一个函数,该函数会被赋给activeEffect,当该函数被执行的时候,会执行我们最开始传进watchEffect副作用函数,如果当副作用函数中有读取到我们的响应式对象的时候就会触发track进行依赖收集,收集的就是这个effect函数(即activeEffect),这也是为什么上面说runner要立即执行一次的原因,用于收集依赖。当响应式对象改变的时候,会触发trigger再执行这个effect(activeEffect)函数。effect函数上还添加了唯一标识id和用于存放被收集(我自己取得名称)的数列deps

这里可能会有点绕,如果还有点没明白的同学建议多看两遍,“书读百遍,其义自现”,代码也一样。后面在会再总的分析下这些数据之间的联系。

接着我们先继续来看下停止侦听的stop方法。

// v3.js

function stop(effect){
    cleanup(effect)
}

// 清除副作用函数
function cleanup(effect){
    const { deps } = effect;
    if(deps.length){
        for(var i =0; i<deps.length; i++){
            deps[i].delete(effect)
        }
    }
}
复制代码

上面说到effect函数上有一个存放被收集的数列,这个数列里面存放的都是收集了这个effectSet集合。删除这些集合中对该effect的收集即可。

我用一张图再帮大家梳理一下这其中数据结构之间的一个关系。

20210401222347 (1)_gaitubao_1000x455.jpg

结合这张图,多看两遍代码,这其中的关系自然铭记在心了。

这就是watchEffect的实现了,我们来看看跑起来看一下,新建一个html页面。

// html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue3源码解析</title>
</head>
<body>
    <button onclick="add()">点击+1</button>
    <button onclick="clearFn()">停止副作用</button>
    <script src="./v3.js"></script>
    <script>
        const obj = reactive({
            count:1
        })
        const add = () => {
            obj.count++;
        }
        const clearFn = watchEffect(()=>{
            console.log('watchEffect执行',obj.count);
        })
    </script>
</body>
</html>
复制代码

效果如下如图:

rjx20-vt47e.gif

乘热打铁,接着我们来看下初始化流程。

初始化流程

我们先来看vue3是怎么创建一个应用程序的:

createApp({
    ...
}).mount("#app");
复制代码

先大胆想一下,这个调用模式是怎么实现的?

通过createApp方法返回了一个对象,然后对象里面有个mount方法?

我们先来看下大致结构:

// v3.js

// 创建应用程序入口
const createApp = (...args) => {
    const app = ensureRenderer().createApp(...args);
    return app;
}

const ensureRenderer = () => {
    // 源码返回 renderder || (renderer = createRenderder()) 
    // 保存了renderer 
    // 初始化renderer肯定为空,所以直接创建,省略中间函数
    return baseCreateRenderer();
}

// 实际创建renderer的函数
const baseCreateRenderer = () => {
    // 这里面定义了很多渲染函数
    // ...
    const render = () => {} ;
    return {
        render,
        createApp:createAppAPI(render)
    }
}

function createAppAPI(render){
    /**创建app实例的方法
     * rootComponent:createApp传进来的初始化Oject对象
     */
    return function createApp(rootComponent){
        const app = {
            // 这里面是我们熟悉的方法,字段:_uid,_props,_context等等
            _component:rootComponent,
            // use(){ 
            // 	return app 
            // },
            // component(){
            // 	return app
            // },
            // directive(){
            // 	return app
            // },

            // 装载节点
            mount(rootContainer){
                // return app 用于链式调用
                return app;
            }
        }
        return app;
    }
}
复制代码

createApp方法返回一个app实例,再通过调用实例上的mount方法将数据渲染成页面(这个跨度有点大),这是它的一个大致骨架,我们来一点点填充。

首先我们从mount入手,我们通过传进来的节点id来找到要装载的dom。源码中在createApp方法中对mount方法进行了扩展,在扩展中获取dom节点和模版信息。 然后通过render函数进行渲染。

// v3.js

const createApp = (...args) => {
    const app = ensureRenderer().createApp(...args);
    const { mount } = app ; 
    app.mount = (containerOrSelector) => {
        // 获取真实dom节点
        const container = document.querySelector(containerOrSelector);
        if(!container){
            return;
        }

        // 获取并保存template
        const component = app._component
        component.template = container.innerHTML;

        const proxy = mount(container);
        return proxy;
    }
    return app;
}

function createAppAPI(render){
    /**创建app实例的方法
     * rootComponent:createApp传进来的初始化Oject对象
     */
    return function createApp(rootComponent){
        const app = {
            // 这里面是我们熟悉的方法,字段:_uid,_props,_context等等
            _component:rootComponent,
            mount(rootContainer){
                // 创建组件的vnode
                const vnode = createVNode(rootComponent);
                render(vnode, rootContainer);
                // return app 用于链式调用
                return app;
            }
        }
        return app;
    }
}

// 创建虚拟dom
function createVNode(type){
    return {
        type,
        props:null,
        children:null 
    }
}
复制代码

注意: 我们在创建app实例的时候将rootComponent参数直接赋值给了app._component,由于rootComponent是个Object,复制的其实是内存地址,随后又在扩展的mount方法内设置了template字段,其实就是给rootComponent添加了template字段。(基础知识,提醒一下)

虚拟dom(vnode)想必大家都很熟悉了,它其实就是个Object对象,通过对象字面量的方式描述了一个dom元素的相关信息。这边作者就写了等会要用到的三个字段:type, props, children

源码中的vnode长这样:

{
    __v_isVNode: true,
    [ReactiveFlags.SKIP]: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    children: null,
    component: null,
    suspense: null,
    ssContent: null,
    ssFallback: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null
}
复制代码

继续写render函数,作者这边写的比较简单,初始化有些流程就直接跳过了,直接在render中执行mountComponent方法了,我们来更新下baseCreateRenderer方法:

// v3.js

const baseCreateRenderer = () => {
    // ...
    // 这里面定义了很多渲染函数
    
    const mountComponent = (vnode,container) =>{
        // 初始化一个组件实例子
        // const instance = createComponentInstance(vnode);
        
        const instance = { type: vnode.type }; 	

        // 初始化setup
        setupComponent(instance)

        // 初始化渲染副作用函数
        setupRenderEffect(instance,container)
    }

    // ...
    const render = (vnode,container) => {
        mountComponent(vnode,container);
    };
   
    return {
        render,
        createApp:createAppAPI(render)
    }
}
复制代码

mountComponent中首先会创建一个组件实例(作者比较懒,直接写了一个对象),会执行两个主要的函数setupComponentsetupRenderEffect

setupComponent方法主要做几件事。

  • 初始化执行setup,执行结果为result
  • result通过reactive进行响应式监听并绑定在组件实例instance上,
  • template转换为ast
  • ast转换成render函数并绑定在组件实例instance上。
  • 兼容vue2.0的options。(后面再说)

由于编译模块相对复杂,我们先实现前两个点,编译模块后面在单独分析。

setup的结果在进行响应式监听是为了在进行读取的时候能够触发依赖收集。

setupRenderEffect方法设置页面初始化渲染和更新的副作用函数(上面有提到)并执行一次副作用函数。

开始撸代码,提醒大家注意看写代码的位置:

// v3.js

const baseCreateRenderer = () => {
    // ...
    // 这里面定义了很多渲染函数
    
    // 渲染副作用函数
    const setupRenderEffect = (instance,container) => {
        instance.update = effect(function componentEffect(){
            // 直接渲染
            if(!instance.isMounted){
                // 获取渲染的树vnode
                const subTree = (instance.subTree = renderComponentRoot(instance))
                patch(
                    null,
                    subTree,
                    container
                )
                instance.isMounted = true;
            }else{
                // 要获取新旧vnode进行对比

                // 获取之前的vnode
                const prevTree = instance.subTree;
                // 下一次即将更新的树
                const nextTree = renderComponentRoot(instance);
                patch(
                    prevTree,
                    nextTree,
                    container
                )
            }
        })

        // 手动初始化执行一次
        instance.update();
    }
    
    // ...
    
    return {
        render,
        createApp:createAppAPI(render)
    }
}

// 初始化setup
function setupComponent(instance){
    const { setup } = instance.type;
    if(!setup){
        return;
    }
    // 永远不要相信程序员的代码不会报错
    const setupResult = callWithErrorHandling(setup);
    instance.setupState = reactive(setupResult);
}

// 构造的渲染树
function renderComponentRoot(instance){
    const { setupState }  = instance;
    // 由于没有模版编译,我们直接创建renderNode
    return {
        type:'div',
        props:{
            id:'demo'
        },
        children:[
            {
                type:'h1',
                props:{
                    onclick:()=>{
                        console.log('点击事件触发了~');
                    }
                },
                children: setupState.count
            }
        ]
    }
}

复制代码

componentEffect在源码中是通过effect函数的lazy字段进行控制自执行一次。函数中在第一次执行的时候会生成我们渲染节点树vnode,并保存,标记isMounted为true,当下次触发器的时候会创建新的渲染节点树vnode,然后会将两者传进patch进行比较后渲染为真实的dom

注意: 程序第一次运行的时候我们创建的vnode存放在了instance上,用于后面更新的时候取为旧节点。

我们这边在renderComponentRoot函数里面索性就直接返回了一个vnode

注意: 这边的渲染节点树(vnode)和最开始在mount创建的vnode是不一样的,这里的vnode是真正的要渲染的树结构。

又出现一个核心函数patch,相信大家应该不陌生。

// v3.js

const baseCreateRenderer = () => {
    // ...
    // 这里面定义了很多渲染函数
    
    // vnode => dom
    const mountElement = () => {} 
    
    // 更新 dom
    const patchElement = () => {}
    
    const patch = (n1,n2, container) => {
        // 初始化渲染
        if(!n1){
            mountElement(n2,container)
        }else{
            patchElement(n1,n2,container)
        }
    }
    
   
    return {
        render,
        createApp:createAppAPI(render)
    }
}
复制代码

这个patch函数相比于源码做了简化,当n1为空的时候直接渲染,否则进行对比之后渲染。

渲染Vnode

mountElement才是真正的将vnode转为真实的dom,我们将其中会涉及到的dom操作和一些对比过程封装为一个renderOptions对象,并传给baseCreateRenderer,我们先实现mountElement:

// v3.js

// 渲染操作
let rendererOptions = {
    // 插入节点
    insert: (child, parent) => {
        parent.insertBefore(child,  null)
    },
    // 创建节点
    createElement: (tag) => document.createElement(tag),
    // 创建文本
    createText: text => document.createTextNode(text),
    // 对比属性
    patchProp: (container, key, value) => {
        if (value) { 
            // 当值为方法的时候,做包裹处理
            if(typeof value === 'function'){
                container.setAttribute(key,`(${value})()`)
            }else{
                container.setAttribute(key,value)
            }
        } else {
            container.removeAttribute(key)
        }
    },
    // 设置节点文本
    setElementText: (el, text) => {
        el.textContent = text
    },
    // 获取父节点
    parentNode: node => node.parentNode,
}

const ensureRenderer = () => {
    return baseCreateRenderer(rendererOptions);
}

const baseCreateRenderer = (options) => {
    const {
        insert: hostInsert,
        patchProp: hostPatchProp,
        createElement: hostCreateElement,
        setElementText:hostSetElementText,
        parentNode:hostGetParentNode
    } = options
    
    // ...
        
    const patch = (n1,n2, container) => {
        // 初始化渲染
        if(!n1){
            mountElement(n2,container)
        }else{
            patchElement(n1,n2,container)
        }
    }

    // vnode => dom
    const mountElement = (n2,container) => {
        const { type , props, children } = n2;
        // type 创建元素
        const el = n2.el = hostCreateElement(type)
        // props 属性
        if(props){
            Object.keys(props).forEach((key)=>{
                // 直接设置
                hostPatchProp(el,key,props[key])
            })
        }
        // children 内容
        if(typeof children === 'string' || typeof children === number){
            hostSetElementText(el,children)
        }else if(Array.isArray(children)){
            children.forEach((vnode)=>mountElement(vnode,el))
        }
        
        // 添加节点
        hostInsert(el,container);
    }

    // 对比更新
    const patchElement = (n1,n2) => {
    }

    // ...
    const render = (vnode,container) => {
        mountComponent(vnode,container);
    };

    return {
        render,
        createApp:createAppAPI(render)
    }
}

复制代码

封装的基础方法就不说了,来看mountElement方法做了哪些事情:

首先根据type创建元素,创建的元素我们要保存在vnode上,用于下次更新的时候直接进行patchElement,然后在设置元素的props,判断children类型,如果是字符串或数字直接渲染,否则递归调用mountElement渲染子节点。

注意: 我们在创建节点的时候将这个节点挂载到了vnodeel字段上,用于后面做更新操作。

更新视图

patchElement函数用于对比新旧vnode,将需要更新的地方进行更新,这边需要注意的是,我们旧的vnode里面存了一个我们的真实的dom, 那我们就没必要根据新的vnode进行重新创建了,而是在旧的el上进行一个差异更新就好了。

实现patchElement函数:

// v3.js

// 对比更新
const patchElement = (n1, n2) => {
    const { type: oldType, props: oldProps, children: oldChildren,el } = n1;
    const { type: nextType, props: nextProps, children: nextChildren } = n2;
    // type
    if (oldType !== nextType) {
        // 获取父节点
        const container = hostGetParentNode(n1.el);
        // 直接重新渲染
        mountElement(n2, container);
        return;
    } else { 
        if (nextProps) { 
            Object.keys(nextProps).forEach(key => { 
                // 当旧元素的属性不等于新元素的属性直接设置新元素的元素
                if (oldProps[key] !== nextProps[key]) { 
                    hostPatchProp(el,key,nextProps[key])
                }
            })
        }

        if (oldProps) { 
            Object.keys(oldProps).forEach(key => {
                // 新元素中没有这个属性了,则移除
                if (!(key in nextProps)) { 
                    hostPatchProp(el,key,null)
                }
            });
        }

        const isStringOrNumber = (str) => {
            return str === '' || typeof str === 'string' || typeof str === 'number';
        }

        // children
        if (isStringOrNumber(nextChildren)) {
            if (isStringOrNumber(oldChildren)) {
                // 都是字符串且值不一样
                if (nextChildren !== oldChildren) {
                    hostSetElementText(el, nextChildren)
                }
            } else if (Array.isArray(oldChildren)) {
                // 新值为字符串,旧元素为数组
                hostSetElementText(el, nextChildren)
            }
        } else { 
            // 当新旧元素的children都是数组
            patchChildren(n1, n2);
        }
    }
}

// 对比children
const patchChildren = (n1, n2) => {
    const { children: oldChildren, el } = n1;
    const { children: nextChildren } = n2;
    for (var i = 0; i < nextChildren.length; i++) { 
        // 如果旧元素有值旧进行对比
        if (oldChildren[i]) {
            patchElement(oldChildren[i], nextChildren[i]);
        } else { 
            // 没有直接创建
            mountElement(nextChildren[i],el)
        }
    }
}
	
复制代码

判断节点类型,如果不一样,直接按照新的vnode重新渲染。

判断属性,遍历新vnode中的属性,如果有不一样的,直接替换为新的,再循环旧属性,保留之前旧节点的原本就有的属性。

判断子节点,这边要考虑节点的类型,有可能是数组,有可能是字符串:

  • 新旧子节点都是字符串类型

如果类型不一样直接替换。

  • 旧子节点为数组,新子节点为字符串

直接替换

  • 新旧子节点都是数组

暂且将新的children称为n2,将旧的children称为n1。既然是更新,我们肯定是以n2为主,所以循环n2,如果对应的n1也有数据的话就通过patchElement递归进行节点对比。如果n1没有的话就通过mountElement创建。

以上就是patch的大致思路了,这边的patch过程其实很不严谨,有很多情况没有考虑到,比如元素的前后顺序等。这边掌握大致思路就可以了。

激动人心的时候终于要来了,让我们来看看代码运行起来的结果,新建一个html文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue3源码解析</title>
</head>
<body>
    <div id="app"></div>
    <script src="./v3.js"></script>
    <script>
        createApp({
            setup(){
                const obj = reactive({
                    count:0
                })
                return {
                    obj
                }
            }
        }).mount("#app")
    </script>
</body>
</html>
复制代码

再来看下renderComponentRoot返回了渲染节点:

{
    type:'div',
    props:{
        id:'demo'
    },
    children:[
        {
            type:'h1',
            props:{
                onclick:()=>{
                    console.log('点击事件触发了~');
                }
            },
            children: setupState.obj.count
        }
    ]
}
复制代码

根据vnode我们推测最后渲染出来的页面结构应该是这样:

<div id="app">
    <div id="demo">
        <h1 onclick="(()=>{console.log('点击事件触发了~');})()">0</h1>
    </div>
</div>
复制代码

执行结果如下:

5eta7-89mr3.gif

我们再来看一下响应式更新:

// html


<script>
    createApp({
        setup(){
            const obj = reactive({
                count:0
            })
            const changeCount = ()=>{
                obj.count++;
            }
            return {
                obj,
                changeCount
            }
        }
    }).mount("#app")
</script>

复制代码

这边由于我们在设置元素属性的时候简单的通过setAttribute进行了设置,所以点击事件中的对象都还是调用的window的对象,我们还需要稍微改一下setupComponent的代码,将instance.setupState的执行结果在绑定在window上。顺便修改下vnode的点击事件:

// v3.js

// 初始化setup
function setupComponent(instance){
    ...
    Object.keys(instance.setupState).forEach(key=>{
        // 挂载到window上
        window[key] = instance.setupState[key]
    })
}

// 构造的渲染树
function renderComponentRoot(instance){
    const { setupState }  = instance;
    return {
        ...
                type:'h1',
                props:{
                    onclick: setupState.changeCount
                },
                children: setupState.obj.count
        ...
    }
}
复制代码

效果如下:

hw49y-800jz.gif

结尾

我们实现了响应式原理,了解了Vue3初始化流程,并实现了响应式更新页面。附带着实现了watchEffect

学而不思则罔,思而不学则殆。

打工人,今天的你收获了吗?

文章分类
前端
文章标签