当初这个时候在聊起vue源码,不论是vue2还是vue3都有些陈词滥调了吧。没得方法,谁让咱卷的慢呢,so 权当是个笔记吧
了解Vue的设计思维
MVVM框架的三要素:数据响应式、模板引擎及其渲染
- 数据响应式:监听数据变动并在视图中更新
- 模版引擎:提供形容视图的模版语法
- 渲染:如何将模板转换为html
先思考new Vue之后都做了什么(vue2)
抽象的来说就做了组件实例化、初始话这么一个事
- 选项合并(mergeOptions)将全局注册的组件换入到new Vue的是实例中
- 组件是实例的一些属性办法的初始化
- 派发两个申明周期的钩子
callHook(vm, 'beforeCreate')
和callHook(vm, 'created')
- 挂载
initLifecycle(vm) // $parent $children 实例属性initEvent(vm) // 事件的监听initRender(vm) // 插槽 $slots $scopedSlots _c()/$createElement 生成vdomcallHook(vm, 'beforeCreate')initInjections(vm) // 注入祖辈传递下来的数据initState(vm) // 解决props/data/computed/watch/methodsinitProvide(vm) // 向后辈传递callHook(vm, 'created')
new Vue
class Vue { constructor(options) { // 0.保留options this.$options = options; this.$data = options.data; // 1.将data做响应式解决 new Observer(this.$data); // 2.为$data做代理 proxy(this, "$data"); // 3.编译模板 if (options.el) { this.$mount(options.el); } } // 增加$mount $mount(el) { this.$el = document.querySelector(el); // 1.申明updateComponent const updateComponent = () => { // 渲染获取视图构造 const el = this.$options.render.call(this); // 后果追加 const parent = this.$el.parentElement; parent.insertBefore(el, this.$el.nextSibling); parent.removeChild(this.$el); this.$el = el; }; // 2.new Watcher new Watcher(this, updateComponent); }}
创立Vue时候的时候第一工夫保留的了options选项并进行了数据响应式的解决,返回的app在调用$mount进行渲染挂载
其实在new Watcher和$mount()之间还有一个compile的类存在,我这里没有写,因为全写会比较复杂。
从实现数据响应式开始
- Vue.set(obj, "key", "value")
set办法向obj追加key的时候要求obj必须是一个响应式数据,此办法只能用于向响应式数据中追加字段。 - Vue.util.defineReactive(obj, "key", "value")
这个办法对obj是不是响应式数据并没有要求,通过此办法能够在obj中追加字段并且将obj变成一个响应式数据 - 间接new Vue({return {$$state: obj}})
能够在返回的数据间接变成响应式,这里加上$$或者加_是为了在Vue实例过程中防止vue对这个是字段进行代理,只须要做成响应式即可
上面两个版本的代码都标记了依赖收集的入口,以便后续接入依赖收集函数,这里就不过多赘述了! 不同是vue3开始是用createApp
不在应用render的形式
vue2的实现形式
// 依据传⼊value类型做不同操作class Observer { constructor(value) { this.value = value; // 判断⼀下value类型 // 遍历对象 this.walk(value); } walk(obj) { if (typeof obj !== "object" || obj === null) { return obj } if (Array.isArray(obj)) { // 笼罩原型,替换咱们本人的 obj.__proto__ = arrProtoType; Object.keys(obj).forEach(key => new Observer(obj[key])) } else { Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key])) } }}}// 实现对数组响应式的拦挡const methods = ["shift", "unshift", "push", "pop", "splice", "reverse", "sort"]const arrProtoType = Object.create(Array.prototype)methods.forEach(method => { // 笼罩原有数组的办法 arrProtoType[method] = function () { Array.prototype[method].call(this, ...arguments) }})// 实现对象响应式拦挡const defineReactive = (obj, key, val) => { new Observer(val) // 创立dep实例和能够对应 const dep = new Dep() return Object.defineProperty(obj, key, { get: () => { // 在这里做依赖收集 Dep.target && dep.addDep(Dep.target) return val }, set: (v) => { if (v !== val) { new Observer(v) val = v dep.notify() } } })}
数组实现是拦挡是通过批改原型的形式来操作的
vue3的实现形式
function reactive(obj) { if (typeof obj !== "object" || obj === null) { return obj } return new Proxy(obj, { get(target, key) { // 依赖收集 track(target, key) const targData = Reflect.get(target, key) return typeof targData === 'object' ? reactive(targData) : targData }, set(target, key, val) { // notify Reflect.set(target, key, val) trigger(target, key) } })}
vue3应用proxy代替defineProperty,借助proxy惰性监听的性质进步框架的性能,也修改了对于数组以及新增删除等操作的额定监听需要,因而去除了Vue.set()、Vue.delete()这样的难堪操作
编译 Compile
编译的次要工作解决各种节点以及事件监听等工作,相熟的v-modal
的双向绑定就是在这里实现的
具体实现逻辑能够查看代码
const regExp = /\{\{(.*)\}\}/;class Compile { constructor(el, vm) { // 1、首先保留下Vue的实例,后续会调用 this.$vm = vm // 编译模板树 this.compile(document.querySelector(el)) } compile(el) { // 遍历el // 判断el的子元素类型 el.childNodes.forEach(node => { if (node.nodeType === 1) { // 代表节点为元素 this.compileElement(node) // 元素须要递归不然就看不到元素内不得值,只能看到以后元素的标签 if (node.childNodes.length) { this.compile(node) } } else if (this.isInter(node)) { // 插值文本 this.compileText(node) } }) } // 对立做初始化和更新解决 update(node, exp, dir) { // 初始化 const fn = this[dir + "Updater"]; fn && fn(node, this.$vm[exp]) // 更新 new Watcher(this.$vm, exp, function (val) { fn && fn(node, val) }) } compileElement(node) { // 获取以后元素的所有属性,并判断他们是不是动静的 const nodeAttrs = node.attributes Array.from(nodeAttrs).forEach(attr => { const attrName = attr.name; const exp = attr.value // 指令的内容 // 判断是指令或者是事件是否是动静的 if (attrName.startsWith("v-")) { const dir = attrName.substring(2) // 判断this实力上是否存在dir函数如果存在则调用 this[dir] && this[dir](node, exp) } // 事件的解决 if (this.isEvent(attrName)) { const dir = attrName.substring(1) // 事件名称 // 事件监听 this.eventHandler(node, exp, dir) } }) } // 解析插值文本 compileText(node) { const regexp = regExp.exec(node.textContent) this.update(node, regexp[1], "text") // node.textContent = this.$vm[regexp[1]] } textUpdater(node, val) { node.textContent = val } text(node, exp) { this.update(node, exp, "text") // node.textContent = this.$vm[exp] } htmlUpdater(node, val) { node.innerHTML = val } html(node, exp) { this.update(node, exp, "html") // node.innerHTML = this.$vm[exp] } modelUpdater(node, val) { // 只思考大部分状况 node.value = val } model(node, exp) { // update只负责赋值 this.update(node, exp, "model") // 监听节点事件 node.addEventListener(node.tagName.toLowerCase(), e => { // 对原数据进行反向赋值 this.$vm[exp] = e.target.value }) } // {{xxoo}} isInter(node) { return node.nodeType === 3 && regExp.test(node.textContent) } isEvent(dir) { return dir.startsWith("@") } eventHandler(node, exp, dir) { const methods = this.$vm.$options.methods const fn = methods && methods[exp] // 须要批改fn函数的this指向为以后的this.$vm node.addEventListener(dir, fn.bind(this.$vm)) }}
依赖收集
视图中会⽤到data中某key,这称为依赖。同⼀个key可能呈现屡次,每次都须要收集进去⽤⼀个Watcher来保护它们,此过程称为依赖收集
。多个Watcher须要⼀个Dep来治理,须要更新时由Dep统⼀告诉。
vue2的依赖收集
原理剖析:
- new Vue() ⾸先执⾏初始化,对data执⾏响应化解决,这个过程发⽣在Observer中
- 同时对模板执⾏编译,找到其中动静绑定的数据,从data中获取并初始化视图,这个过程发⽣在Compile中
- 同时定义⼀个更新函数和Watcher,未来对应数据变动时Watcher会调⽤更新函数
- 因为data的某个key在⼀个视图中可能呈现屡次,所以每个key都须要⼀个管家Dep来治理多个Watcher
未来data中数据⼀旦发⽣变动,会⾸先找到对应的Dep,告诉所有Watcher执⾏更新函数
class Watcher { constructor(vm, fn) { this.vm = vm; this.getter = fn; this.get(); } get() { // 依赖收集触发 Dep.target = this; this.getter.call(this.vm); Dep.target = null; } update() { this.get(); }}// 管家:和某个key,⼀⼀对应,治理多个秘书,数据更新时告诉他们做更新⼯作class Dep { constructor() { this.deps = new Set(); } addDep(watcher) { this.deps.add(watcher); } notify() { this.deps.forEach((watcher) => watcher.update()); }}
响应式数据构建过程中每呈现一个obj,就会生成一个obsever对象,每一个key对应也有一个dep,
然而在源码中dep和watcher是属于多对多的关系,每一个组件会有一个watcher,
失常状况下一个watcher和dep是1对多,然而源码中提供了$watch("key", function(){})(也叫useWatcher
)的办法,导致dep和watcher是属于多对多的关系
watcher和dep的关系 dep晓得本人治理了哪些watcher,同样的每个watcher也晓得本人被哪些dep治理,目标是提供$unwatch办法用于解绑。
vue3的依赖收集
vue3中删除了Watcher,取而代之是effect,收集依赖的过程也有所变动;
相干api有
- effect(fn):传⼊fn,返回的函数将是响应式的,外部代理的数据发⽣变动,它会再次执⾏
- track(target, key):建⽴响应式函数与其拜访的⽬标(target)和键(key)之间的映射关系
- trigger(target, key):依据track()建⽴的映射关系,找到对应响应式函数并执⾏它
// 长期存储副作用函数const effectStack = []// 1.依赖收集函数: 包装fn,立即执行fn,返回包装后果function effect(fn) { const e = createReactiveEffect(fn) e() return e}function createReactiveEffect(fn) { const effect = function () { try { effectStack.push(fn) return fn() } finally { effectStack.pop() } } return effect}// 保留依赖关系的数据结构const targetMap = new WeakMap()// 依赖收集:建设target/key和fn之间映射关系function track(target, key) { // 1.获取以后的副作用函数 const effect = effectStack[effectStack.length - 1] if (effect) { // 2.取出target/key对应的map let depMap = targetMap.get(target) if (!depMap) { depMap = new Map() targetMap.set(target, depMap) } // 3.获取key对应的set let deps = depMap.get(key) if (!deps) { deps = new Set() depMap.set(key, deps) } // 4.存入set deps.add(effect) }}// 触发更新:当某个响应式数据发生变化,依据target、key获取对应的fn并执行他们function trigger(target, key) { // 1.获取target/key对应的set,并遍历执行他们 const depMap = targetMap.get(target) if (depMap) { const deps = depMap.get(key) if (deps) { deps.forEach(dep => dep()) } }}
vue最早是一个key对应一个Watcher,然而随着我的项目和组件的体积增大,这种形式内存耗费也很大,所以不实用大我的项目,在起初的降级中粒度被切分变成一个组件一个Watcher并且逐渐引入了虚构dom和diff算法,现在在最新的vue3中Watcher已被删除,也新增了compiler的优化策略
虚构dom
虚构DOM(Virtual DOM)是对DOM的JS形象示意,它们是JS对象,可能形容DOM构造和关系。利用的各种状态变动会作用于虚构DOM,最终映射到DOM上。
长处:
- 轻量、疾速:当他们发生变化的时候时通过新旧DOM比对能够失去最小DOM操作量,配合异步更新策略缩小更新频率,进步性能
- 跨平台:将虚构DOM更新转换不同运行时非凡操作实现跨平台
- 兼容性:还能够退出兼容性代码,加强操作的兼容性
必要性
vue 1.0中有细粒度的数据变动侦测,它是不须要虚构DOM的,然而细粒度造成了大量开销,这对于大型项目来说是不可承受的。因而,vue 2.0抉择了中等粒度的解决方案,每一个组件一个watcher实例,这样状态变动时只能告诉到组件,再通过引入虚构DOM去进行比对和渲染。
patch
vue3的patch函数的次要性能是将vnode转换成真是的node的过程,应用vnode能够携带更多的信息,以便后续的diff和优化策略;
vnode
新的vnode构造中携带了块 block
的相干信息,比方patchFlag
、dynamicChildren
、dynamicProps
等;
patch的过程
- 创立vnode mount()执⾏时,创立根组件VNode
- 渲染vnode render(vnode, rootContainer)⽅法将创立的vnode渲染到根容器上。
- 初始patch 传⼊oldVnode为null,初始patch为创立⾏为
- 创立⼀个渲染副作⽤(setupRenderEffec),执⾏render,取得vnode之后,在执⾏patch转换为dom
- setupRenderEffect在初始化阶段核⼼工作是执⾏instance的render函数获取subTree,最初patch这个subTree,在这个过程中会应用shapeFlag这个字段,次要作用是标记以后的节点的组件状态的,比方:以后节点是一个文本节点,那么只须要在后续的patch中创立完文本节点并设置节点的内容即可,不须要像vue2中那样patch完文本节点还要patch文本节点的内容
- 更新阶段,patch函数对⽐新旧vnode,得出dom操作内容,编译过程中通过观察patchFlag、dynamicChildren等做出优化,patchFlag是确定以后节点在更新时候应用什么形式,比方:款式、属性等,dynamicChildren是寄存子元素中动态变化的子元素,只须要将其寄存的子元素拿进去递归patch进行精准更新即可,不须要遍历以后节点下的所有子节点。
- 如果同时存在多个⼦元素,⽐如使⽤v-for时的状况:即典型的重排操作,使⽤patchChildren进行diff操作(数组中原本就是不法则的动态变化,应用dynamicChildren意义不大),是否是多个子节点的断定也是应用patchFlag来断定的
diff
diff算法这个货色属实不想写,之前的人写的太多了,都写烂了,无非就是双端比拟,新首旧首、新尾旧尾、新首旧尾、旧首新尾之间的比拟,而后剩下的做增删操作,这是vue2的算法。
咱们明天来说说vue3的吧
在vue3diff算法也有不小的改变,尽管保留了双端比拟,然而只保留了新首旧首、新尾旧尾之间的比拟,不在有穿插比拟了,在以上两种状况比拟实现当前,diff算法将残余的节点分成了以下几种状况:
- 老节点没有了,则新增
- 新节点没有了,则删除
- 新老节点都有,则将老节点转成Map,循环新节点去老节点中查找是否存在,不存在新增,存在则断定为挪动,这里和react的diff相似,不过这里有一个细节,就是用了一个很经典的算法(最长递增子序列)
编译器(compiler)
- 编译器的作用将是生成渲染函数,将模板进行编译、解析
执行时刻须要辨别不同的应用环境
- runtime-compiler
步骤:template --> ast --> render函数 --> vdom --> 实在DOM
长处:能够应用template选项,抉择更灵便
执行机会:在vue.$mount()挂载的时候执行ast2也是一个形象语法树,然而与ast1不同的是其携带的信息不同,ast2次要是的作用是用后续的patch和diff。
- runtime-only
步骤:render函数 --> vdom --> 实在DOM
长处:体积小、运行速度快
执行机会:应用webpack的vue-loader进行预编译
- runtime-compiler
这里简略说下runtime-compiler
下编译器的工作过程
- app.mount()获取template
- compile将传⼊template编译为render函数
- 第⼀步解析-parse:解析字符串template为形象语法树ast
- 第⼆步转换-transform:解析属性、款式、指令等
- 第三步⽣成-generate:将ast转换为渲染函数,这一步渲染函数是一个字符串,须要在compile中
return new Function(code)()
编译器的优化策略
动态节点晋升
将动态节点进行缓存,用内存换工夫
补丁标记和动静属性记录(patchFlag)
只关注节点动态变化的局部,对其进行标记,下次更新的时候只更新能变动的局部
缓存事件处理程序
缓存事件,防止间接书写事件函数导致的不必要更新,间接写箭头函数,会会导致每次编译到这里的时候都会生成新的函数,使得子树更新,缓存当前即可防止,性能相似于react的useCallback.
块 block
将模板切分成块,将动静的节点进行保留(保留在dynamicChildren字段中),这样下次更新就不须要遍历整棵树,而是对保留的动静节点进行遍历即可,升高复杂度。
留神:
- jsx转换过程中也会生成ast,有区别的是jsx实质还是js,没有进行预编译,那么所携带的信息会很少。所以实践上vue中的jsx也不能享受到
残缺优化策略
带来的性能晋升。 - vue也好,react也好,他们都用到了ast,然而它们都会本人独自保护本人的规定。
对于编译器的调试
在vue3源码的package.json中找到dev-compiler
命令并执行,之后框架会在packages/template-explorer
这个包上面输入一个dist文件,这个文件呈现阐明编译胜利,我能够间接查看packages/template-explorer
包下的local.html
,间接在浏览器中关上即可调试。
vue中$nexttick(异步渲染)实现过程中异步降级
在vue2版本中是有这样一个降级的过程的,次要是为了兼容不同版本的浏览器,然而在vue3中,就比拟激进了,间接应用promise.then
入队,兼容性绝对vue2会差一些。
- 先是promise.then
- 其次是MutationObsever
- 而后setImmediate
- 最初是setTimeout
降级vue3的动机
- 类型反对更敌对
- 有利于tree-shaking
- API简化、一致性:render函数、sync修饰符、指令等
- 复用性:composition API
- 性能优化:响应式、编译优化
- 扩展性:自定义渲染器
vue源码调试
叨叨了这么多想必你也想调试源码了吧,手动狗头!
获取源码
咱们能够间接在github上间接克隆迁出
- 这是vue2的源码地址
- 这是vue3源码地址
目录构造
调试环境搭建
- 装置依赖: npm i
e2e工具安装时间会很长时间,能够抉择在装置phantom.js时终止,并不会影响咱们调试 - 装置rollup:npm i rollup
- 批改dev脚本,配置sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
- 运行开发命令:npm run dev, 输入dist文件
- 新建一个index.html文件,引入vue.js文件
<script src="../../dist/vue.js"></script>
- 开始欢快的调试旅程
goodbye
手写vue代码参考
至此,vue2和vue3的区别以及两个版本大抵的流程就全过了一遍了,有趣味的能够本人去看下源码,集体感觉还是挺有意思的,心愿我的文章对你有所帮忙。