关于vue.js:老生常谈的vue源码

6次阅读

共计 10282 个字符,预计需要花费 26 分钟才能阅读完成。

当初这个时候在聊起 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 生成 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 的依赖收集

原理剖析:

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

正文完
 0