当初这个时候在聊起vue源码,不论是vue2还是vue3都有些陈词滥调了吧。没得方法,谁让咱卷的慢呢,so 权当是个笔记吧

了解Vue的设计思维

MVVM框架的三要素:数据响应式、模板引擎及其渲染

  1. 数据响应式:监听数据变动并在视图中更新
  2. 模版引擎:提供形容视图的模版语法
  3. 渲染:如何将模板转换为html

先思考new Vue之后都做了什么(vue2)

抽象的来说就做了组件实例化、初始话这么一个事

  1. 选项合并(mergeOptions)将全局注册的组件换入到new Vue的是实例中
  2. 组件是实例的一些属性办法的初始化
  3. 派发两个申明周期的钩子callHook(vm, 'beforeCreate')callHook(vm, 'created')
  4. 挂载
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的依赖收集

原理剖析:

  1. new Vue() ⾸先执⾏初始化,对data执⾏响应化解决,这个过程发⽣在Observer中
  2. 同时对模板执⾏编译,找到其中动静绑定的数据,从data中获取并初始化视图,这个过程发⽣在Compile中
  3. 同时定义⼀个更新函数和Watcher,未来对应数据变动时Watcher会调⽤更新函数
  4. 因为data的某个key在⼀个视图中可能呈现屡次,所以每个key都须要⼀个管家Dep来治理多个Watcher
  5. 未来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上。

长处:

  1. 轻量、疾速:当他们发生变化的时候时通过新旧DOM比对能够失去最小DOM操作量,配合异步更新策略缩小更新频率,进步性能
  2. 跨平台:将虚构DOM更新转换不同运行时非凡操作实现跨平台
  3. 兼容性:还能够退出兼容性代码,加强操作的兼容性

必要性

vue 1.0中有细粒度的数据变动侦测,它是不须要虚构DOM的,然而细粒度造成了大量开销,这对于大型项目来说是不可承受的。因而,vue 2.0抉择了中等粒度的解决方案,每一个组件一个watcher实例,这样状态变动时只能告诉到组件,再通过引入虚构DOM去进行比对和渲染。

patch

vue3的patch函数的次要性能是将vnode转换成真是的node的过程,应用vnode能够携带更多的信息,以便后续的diff和优化策略;

vnode



新的vnode构造中携带了块 block的相干信息,比方patchFlagdynamicChildrendynamicProps等;

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算法将残余的节点分成了以下几种状况:

  1. 老节点没有了,则新增
  2. 新节点没有了,则删除
  3. 新老节点都有,则将老节点转成Map,循环新节点去老节点中查找是否存在,不存在新增,存在则断定为挪动,这里和react的diff相似,不过这里有一个细节,就是用了一个很经典的算法(最长递增子序列)

编译器(compiler)

  1. 编译器的作用将是生成渲染函数,将模板进行编译、解析
  2. 执行时刻须要辨别不同的应用环境

    • 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下编译器的工作过程

  1. app.mount()获取template
  2. compile将传⼊template编译为render函数
  3. 第⼀步解析-parse:解析字符串template为形象语法树ast
  4. 第⼆步转换-transform:解析属性、款式、指令等
  5. 第三步⽣成-generate:将ast转换为渲染函数,这一步渲染函数是一个字符串,须要在compile中return new Function(code)()

编译器的优化策略

  1. 动态节点晋升

    将动态节点进行缓存,用内存换工夫

  2. 补丁标记和动静属性记录(patchFlag)

    只关注节点动态变化的局部,对其进行标记,下次更新的时候只更新能变动的局部

  3. 缓存事件处理程序

    缓存事件,防止间接书写事件函数导致的不必要更新,间接写箭头函数,会会导致每次编译到这里的时候都会生成新的函数,使得子树更新,缓存当前即可防止,性能相似于react的useCallback.

  4. 块 block

    将模板切分成块,将动静的节点进行保留(保留在dynamicChildren字段中),这样下次更新就不须要遍历整棵树,而是对保留的动静节点进行遍历即可,升高复杂度。

留神:

  1. jsx转换过程中也会生成ast,有区别的是jsx实质还是js,没有进行预编译,那么所携带的信息会很少。所以实践上vue中的jsx也不能享受到残缺优化策略带来的性能晋升。
  2. 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的动机

  1. 类型反对更敌对
  2. 有利于tree-shaking
  3. API简化、一致性:render函数、sync修饰符、指令等
  4. 复用性:composition API
  5. 性能优化:响应式、编译优化
  6. 扩展性:自定义渲染器

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的区别以及两个版本大抵的流程就全过了一遍了,有趣味的能够本人去看下源码,集体感觉还是挺有意思的,心愿我的文章对你有所帮忙。