当初这个时候在聊起 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 生成 vdom
callHook(vm, 'beforeCreate')
initInjections(vm) // 注入祖辈传递下来的数据
initState(vm) // 解决 props/data/computed/watch/methods
initProvide(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 的区别以及两个版本大抵的流程就全过了一遍了,有趣味的能够本人去看下源码,集体感觉还是挺有意思的,心愿我的文章对你有所帮忙。