1. 首页

Vue3数据驱动源码解读

前言

DEMO地址,先下载DEMO,打开index.html体验一下吧

  • 阅读的时候最好是先把DEMO过一遍,然后带着问题来看这篇文章,不然可能会一脸懵逼
  • 我的DEMO源码简化了非常非常多的代码,所以功能很基础,为了方便把流程先理清楚
  • 不要死抓住细节不放,有些问题放一放,等把握全局之后就可以理解了
  • 可以下载DEMO源码,打开里面index.html先体验下

灵魂图

先上一张灵魂图,可能看完图就要跑了。。。为了讲清楚画的比较乱

Vue3数据驱动源码解读

总体介绍

从上图中可以大致看出,分为两条线🧵

第一条线:

new Vue -> reactive[new Proxy -> return proxy] -> baseHandler(set, get) -> effect文件中的依赖收集(track,trigger)-> trigger触发任务调度 -> 完了

第一条线遗留几个问题:

  • 依赖是什么时候生成的?
  • 依赖(dom和data之间的关系)是如何形成的?

先保留这两个问题,继续看第二条线

第二条线

从根路径app开始解析子节点 -> 解析出当前node,node里面的指令{{name}} -> 实例化effect(传入回调函数:回调里面去proxy上面获取name) -> 调用effect

第二条线的重点

调用effect主要做两件事:

  • 把当前effect push进activeReactiveEffectStack
  • 执行回调,回调函数里面从proxy获取name
  • 【从proxy获取name】这一步会触发proxy的get
  • get里面从activeReactiveEffectStack获取最后一个依赖,进行依赖收集

源码解析

reactive


import { mutableHandlers } from './baseHandler' // 这个map存储key: target, value:proxy // 作用: // 1.避免重复proxy const rawToReactive = new WeakMap() // 这个map存储key:proxy, value:target // 作用: // 1.避免proxy对象再次被proxy const reactiveToRaw = new WeakMap() export const targetMap = new WeakMap() export function reactive(target){ return createReactiveObject( target, rawToReactive, reactiveToRaw, mutableHandlers ) } // 创建响应式对象 function createReactiveObject(target, toProxy, toRaw, handlers){ // 如果当前对象已被proxy,那么直接返回 let observed = toProxy.get(target) if (observed !== void 0) { return observed } // 检测被proxy的对象,即这里的target,自身是否是个proxy,如果是的话,直接返回 if (toRaw.has(target)) { return target } // 当前的target既没有被proxy,也不是个proxy对象,那么对它proxy observed = new Proxy(target, handlers) // 实例化之后把它维护到两个map toProxy.set(target, observed) toRaw.set(observed, target) // 把当前的target维护到targetMap,targetMap的作用 -> 【继续往下看,先不管】 if (!targetMap.has(target)) { targetMap.set(target, new Map()) } return observed } // toRaw函数,传入proxy对象,获取target // export function toRaw(observed) { return reactiveToRaw.get(observed) || observed } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

reactive的作用

可以看出reactive的作用主要是:

  • 创建proxy实例并返回
  • 维护一个targetMap队列

reactive的遗留问题

  • targetMap是干嘛用的?
  • handler来自于baseHandler

ok,这两个疑问🤔️先保留,继续看baseHandler的源码

baseHandler


import { toRaw } from './reactive' import { track, trigger } from './effect' // 为了便于理解,这里只做了get和set的proxy // 其他的代码都是一般的代理,不讲,讲一下track和trigger // 从vue 2.0的源码其实可以知道: // 1.get的时候会做依赖收集:即这里的track // 2.set的时候会做更新广播:即这里的trigger export const mutableHandlers = { get(target, key, receiver){ const res = Reflect.get(target, key, receiver) track(target, 'get', key) return res }, set(target, key, value, receiver){ const oldValue = target[key] const result = Reflect.set(target, key, value, receiver) // 这里检测key是否是target的自有属性 const hadKey = target.hasOwnProperty(key) // 在reactive维护了一个reactiveToRaw队列,存储了[proxy]:[target]这样的队列,这里检测下是否是使用createReactiveObject新建的proxy if (target === toRaw(receiver)) { // 判断是否值改变,才触发更新 if (hadKey && value !== oldValue) { trigger(target, 'set', key) } } return result } } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

baseHandler的作用

  • get获取值,其次依赖收集
  • set设置值,其次触发任务调度

baseHandler的遗留问题

  • 什么是依赖?
  • 收集的依赖中,dom和data的关系是怎样的
  • 如何做任务调度?

effect

重点来了,effect就是vue3里面用于依赖管理的,主要是管理三个东西:

  • 依赖收集
  • 依赖实例化
  • 依赖存储

import { targetMap } from './reactive' const activeReactiveEffectStack = [] // 下面这两个api是初始化effect,就不过于纠结了 export function effect(fn, options){ const effect = createReactiveEffect(fn, options) return effect } function createReactiveEffect(fn, options){ const effect = function(){ if (!activeReactiveEffectStack.includes(effect)) { try { activeReactiveEffectStack.push(effect) return fn() } finally { activeReactiveEffectStack.pop() } } } effect.scheduler = options.scheduler return effect } // 作用: // 1.收集依赖 export function track(target, type, key){ const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1] // proxy初始化的时候,这个depsMap为new Map let depsMap = targetMap.get(target) if (depsMap === void 0) { targetMap.set(target, (depsMap = new Map())) } // 如果是第一次这个dep是没有的,因为depsMap是new Map let dep = depsMap.get(key) if (dep === void 0) { // 这里把依赖放进去。依赖是个Set depsMap.set(key, (dep = new Set())) } // 这里的effect就是依赖。 // 依赖是啥?可以理解为依赖保存了data <-> dom的关系 dep.add(effect) // effect.deps.push(dep) } // 作用: // 1.触发了数据更新,这时候得更新dom了 export function trigger(target, type, key){ const depsMap = targetMap.get(target) const effects = new Set() const run = effect => { scheduleRun(effect, target, type, key) } // 解析出依赖中要更新的effect addRunners(effects, depsMap.get(key)) // 任务调度执行 effects.forEach(run) } function addRunners(effects, effectsToAdd){ effectsToAdd.forEach(effect => { effects.add(effect) }) } // 任务调度,就理解为data更新之后,调用effect.scheduler去更新dom function scheduleRun(effect, target, type, key){ if (effect.scheduler !== void 0) { effect.scheduler(effect) } else { effect() } } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

effect重点

  • 依赖收集用到两个东西:activeReactiveEffectStack,targetMap
  • 触发依赖也用到两个:targetMap,scheduler的queueJob

可以看出:1.targetMap是用来存储依赖的

继续看下scheduler的queueJob任务调度

scheduler


import { callWithErrorHandling } from './errorHandling' const queue = [] const p = Promise.resolve() let isFlushing = false export function queueJob(job) { if (!queue.includes(job)) { queue.push(job) if (!isFlushing) { nextTick(flushJobs) } } } export function nextTick(fn) { return fn ? p.then(fn) : p } function flushJobs(seenJobs) { isFlushing = true let job while ((job = queue.shift())) { job() } isFlushing = false } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

scheduler queueJob

queueJob主要是利用了Promise来进行一个微任务队列的依赖更新:其实执行effect实例函数

第一条线内容结束语

到这里第一线的内容就完了,遗留一个问题:

  • 在get的时候从activeReactiveEffectStack的最后一个取依赖

这说明啥?
说明在调用了effect -> 把effect push进activeReactiveEffectStack 之后,需要调用proxy[name]来触发get

明白了这一点之后,继续来看第二条线,第二条线的compile内容属于自研,跟源码差距比较大

compile


import { effect } from './effect' import { queueJob } from './scheduler' export function compile(el, vm){ let fragment = document.createDocumentFragment(); let node; while(node = el.firstChild){ compileNode(vm, node) fragment.append(node) } return fragment } const reg = /\{\{(.*)\}\}/; function compileNode(vm, node){ let { nodeType, nodeValue, nodeName } = node; node.update = (type, bindName) => { return effect(() => { node[type] = vm[bindName] }, { scheduler: queueJob }) } let bindName; switch(nodeType){ case 1: if(nodeName == 'INPUT'){ let { attributes } = node; for(let attr of attributes){ if(attr.name === 'v-model'){ bindName = attr.value; } } if(bindName){ node.addEventListener('input', e => { vm[bindName] = e.target.value; }) } node.update('value', bindName)() } break; case 3: let isModal = reg.test(nodeValue) if(isModal){ bindName = RegExp.$1 && RegExp.$1.trim(); node.update('nodeValue', bindName)() } break; } } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

compile的重点

重点在于:当解析出node和bingName之后,其实这时候可以调用proxy[name]来直接获取值了

-> 可是这里创建了个effect来建立key和当前node之间的关系

  • effect的回调是用来调用node[’nodeValue’] = proxy[’name’]来触发get收集依赖的
  • 当set name使name改变之后,查询当前key下面的effect队列,调用各个effect的回调更新dom

写在最后

因为时间比较紧,所以写得很仓促,自己也感觉文章写的比较乱,单独看文章的话可能会看不懂。需要先把DEMO过一遍,然后带着问题来看这篇文章

作者:Houdini
链接:https://juejin.im/post/6844903969156923405

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:Vue3数据驱动源码解读

链接:https://www.javascriptc.com/4148.html

« 精选10款谷歌浏览器插件武装你的浏览器
Node.js异步编程进化论»
Flutter 中文教程资源

相关推荐

QR code