vue框架解读1.0


权衡的艺术

1.3 虚拟 dom

虚拟 dom 的耗时:创建 javascript 的计算量 + 创建真实 dom 的计算量 (数据变化量有关)

innerHtml 的耗时:拼接字符串的计算量 + innerHtml 的 DOM 计算量 (模板大小有关)

原生 JavaScript > 虚拟 dom > innerHTML

1.4 运行和编译时

1 框架设计的核心要素

2.1 缩减框架代码的体积

例:vue3 源码的 warn 函数

1
2
3
4
5
6
7
8
9
if(_DEV_ && ires) {
warn(
'faile to ....')
};
//vue.js使用rolllup.js对项目进项构建 这里的_DEV_实际上就是通过rollup.js的设置来预定义的 类似于webpack的DefinePlugin插件
当用于开发环境时 _DEV_为true 即 if(true && ires)
而在构建生产环境中 _DEV_会被翻译为false 即 if(false && _DEV_) 这样就永远不会执行
**这种永远不会执行的代码称之为dead code 在构建最终项目的时候就会被移除 从而可以在开发环境中提供友好的提示 生产环境中减少代码的体积
**vue3源码多出利用相似逻辑保证了开发环境的友好体验和生产环境的代码体积控制

2.2Tree-Shaking

Tree-Shaking:消除永远不会使用到的代码 如上面说的DEV

能使用 Tree-Shaking 的必须是满足 ESM

Tree-Shaking 会产生副作用

1
2
3
4
5
**加上标记来告诉系统可以安心清除副作用
import {foo} from './utils';
/*#_PURE_*/ foo();// /*#_PURE_*/会告诉roupull.js可以放心进行Tree-Shaking
vue3的源码多出使用/*#_PURE_*/来标记 都用于顶级调用
export const isHTMLTag = /*#_PURE_*/ makeMap(HTML_TAGS);

2.3 框架输出的产物

无论是 rollup.js 还是 webpack 在寻找资源文件的时候 如果 package.json 中存在 module 字段 都会优先使用 module 字段指向的资源代替 main 字段指向的资源

1
2
3
4
{
"main": 'index.js',
"module": 'dist/vue.runtime.esm-bundler.js'
}

用处:当我们使用构建提供工具打包的 ESM 格式的资源时 不能直接把DEV转换成 true 或 false 需要使用(process.env.NODE* !== ‘production’)替换—DEV*常量

1
2
3
if((process.env.NODE) !== 'production') {
....
}

需求场景:当进行服务端渲染时 vue.js 的代码是在 Node.js 的环境中运行的 但是 Node.js 时 CommonJS 形式 所以可以更改 roullup.js 中的 format 配置

1
format: 'cjs'//指定模块形式

2.4 特定开关

2.5 错误处理

VUE.JS3 的设计思路

3.2 渲染器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function renderer(vnode, container) {
// 使用 vnode.tag 作为标签名称创建 DOM 元素
const el = document.createElement(vnode.tag)
// 遍历 vnode.props 将属性、事件添加到 DOM 元素
for (const key in vnode.props) {
if (/^on/.test(key)) {
// 如果 key 以 on 开头,那么说明它是事件
el.addEventListener(
key.substr(2).toLowerCase(), // 事件名称 onClick ---> click
vnode.props[key] // 事件处理函数
)
}
}

// 处理 children
if (typeof vnode.children === 'string') {
// 如果 children 是字符串,说明是元素的文本子节点
el.appendChild(document.createTextNode(vnode.children))
} else if (Array.isArray(vnode.children)) {
// 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
vnode.children.forEach(child => renderer(child, el))
}

// 将元素添加到挂载点下
container.appendChild(el)
}

const vnode = {
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}

renderer(vnode, document.body);

响应系统的作用和实现

4.1 响应式数据与副作用

副作用函数:会产生副作用的函数 即该函数的执行会直接或间接地影响到其他函数或变量的执行

4.2 响应式数据的基本实现

  • 通过拦截一个对象的读取和设置操作

    • ```
      //当读取obj.text的时候 就将副作用函数储存到一个桶里面
      //当设置obj.text的时候 就将副作用从桶里面取出执行便可
      / 存储副作用函数的桶
      const bucket = new Set()

      // 原始数据
      const data = { text: ‘hello world’ }
      // 对原始数据的代理
      const obj = new Proxy(data, {
      // 拦截读取操作
      get(target, key) {

      // 将副作用函数 effect 添加到存储副作用函数的桶中
      bucket.add(effect)
      // 返回属性值
      return target[key]
      

      },
      // 拦截设置操作
      set(target, key, newVal) {

      // 设置属性值
      target[key] = newVal
      // 把副作用函数从桶里取出并执行
      bucket.forEach(fn => fn())
      

      }
      })

      function effect() {
      document.body.innerText = obj.text
      }
      effect()

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33

      **缺陷**:如果副作用改名或者是匿名函数 则该响应系统失效

      - 设计一个完善的响应系统

      - 提供一个用来注册副作用函数的机制来解决上面的问题

      - ```
      //用一个变量春促当前激活的effect函数
      let activeEffect;
      function effect(fn) {
      //当调用effect注册副作用函数是 将副作用函数赋值给activeEffect
      activeEffect = fn;
      fn(0);//执行副作用函数
      };
      effect(() => {
      console.log('effect do');
      documrnt.body.innerText = obj.text;//进行读取操作
      })
      const obj = new Proxy(data, {
      get(target, key) {
      //将当前被调用的副作用函数加入到桶中
      if(activeEffect) {
      bucket.add(activeEffect)//新增
      }
      return target[key];
      },
      set(target, key, newVal) {
      target[key] = newVal;
      bucket.forEach( fn => fn());
      return true;
      }
      })

      完成了可以添加匿名或任意名字的副作用函数

      缺陷:无法监听指定属性,即如果进行类似于新增不存在的属性 该副作用函数依然会被执行(正常不应该被执行)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      测试:新增不存在的obj.text2属性
      effect(() => {
      obj.body.innerText = obj.text

      console.log('effect done')
      });
      setTimeout(() => {
      obj.text2 = 'none exists';//新增一个不存在的属性
      })
      //运行结果:effect done 执行两次
      //分析:每次修改对象obj都会将effect存入桶中 导致读取时执行不该执行的副作用
      //没有在副作用函数与被操作的目标之间建立明确的联系
      **解决方法:在副作用函数与被操作的字段之间建立联系 可以使用weakMap
      //储存副作用的桶
      const bucket = new WeakMap();
      //修改拦截器
      const obj = new Proxy(data, {
      //拦截读取行为
      get(target, key) {
      //没有activeEffect 直接return
      if(!activeEffect) return;
      //根据traget从桶中取得desMap, :key --> effects
      let desMap = bucket.get(target);
      //如果不存在desMap 那么就创建一个WeakMap
      if(!desMap) {
      bucket.set(target, (desMap = new Map()))
      }
      //根据key自从desMap中读取deps deps时一个set类型
      //里面储存着所有与当前key有相关的副作用函数:effects
      let deps = desMap.get(key);
      if(!deps) {
      desMap.set(key, (deps = new Set()));
      }
      deps.add(activeEffect);//将激活的副作用函数存储到桶里
      return target[key];//返回属性值
      };
      //拦截设置操作
      set(target, key, newVal) {
      //设置属性值
      target[key] = newVal;
      //根据target从decket中取出desMap
      let desMap = ducket.get(target);
      if(!desMap) return;
      //根据key取得所有的副作用函数并依次执行
      let deps = desMap.get(key);
      deps && deps.forEach( fn => fn());//判空再执行

      }
      })

    • 为什么使用 WeakMap 不使用 Map

      • weakMap 是弱引用 不会影响垃圾回收
      • 只有当 key 所引用的对象存在时(没有被回收时)才有价值的信息都优先使用 weakMap 来存储
    • **存在问题:代码耦合度高 **

    • 解决方法: 抽离封装

    • function track(target, key) {
        let depsMap = bucket.get(target)
        if (!depsMap) {
          bucket.set(target, (depsMap = new Map()))
        }
        let deps = depsMap.get(key)
        if (!deps) {
          depsMap.set(key, (deps = new Set()))
        }
        deps.add(activeEffect)
      }
      
      function trigger(target, key) {
        const depsMap = bucket.get(target)
        if (!depsMap) return
        const effects = depsMap.get(key)
        effects && effects.forEach(fn => fn())
      }
      // 对原始数据的代理
      const obj = new Proxy(data, {
        // 拦截读取操作
        get(target, key) {
          // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
          track(target, key)
          // 返回属性值
          return target[key]
        },
        // 拦截设置操作
        set(target, key, newVal) {
          // 设置属性值
          target[key] = newVal
          // 把副作用函数从桶里取出并执行
          trigger(target, key)
        }
      })
      
      1
      2
      3
      4
      5

      ## 4.4 切换分支与 cleanup

      分支切换可能会产生遗留的副作用函数

      effect(function effectFn() { document.body.innerText = obj.ok? obj.text : 'not' }) //此时依赖函数被obj.ok和obj.text同时依赖 当obj.ok为false时 无论obj.text怎么变化 都是'not' 所以不应该触发副作用函数 //解决方法:每次副作用函数执行时 都把它从所有与之关联的依赖集合中删除 //当副作用执行完毕后 会建立联系 但再新的联系中不会包含遗留的副作用函数
      1
      2
      3

      **副作用依赖函数集合**

      //用一个全局变量储存被注册的副作用函数 let activeEffect; function effect(fn) { //当effectFn被执行时 将其设置为当前的激活的副作用函数 const effectFn = () => { activeEffect = effectFn; fn(); } //effectEffect.deps 用来储存所有与该副作用函数想关联的依赖集合 effectEn.dep = []; //执行副作用函数 effectFn(); }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20

      - **Track 函数**

      - ```
      function track(target, key) {
      if(!activeEffect) return;
      let depsMap = bucket.get(key);
      if(!depsMap) {
      bucket.set(target, (desMap => new Map()));
      }
      let deps = depsMap.get(key);
      if(!deps) {
      depsMap.set(key, ( deps => new Set()))
      };
      //把当前激活的副作用函数添加到依赖集合deps中
      deps.add(activeEffect);
      //deps就是一个与当前副作用函数存在联系的集合
      //将其添加到activeEffects.deps中
      activeEffect.deps.push(deps);
      }
      - 有了这个联系后 再每次执行副作用函数时 根据 effectFn.deps 获取所有的相关联的依赖集合 从而将副作用函数从依赖集合中清除 - ``` let activeEffect; function effect(fn) { const effectFn = () => { //调用cleanUp函数完成清除依赖 cleanup(effectFn); activeEffect = effectFn; fn(); } } function cleanup(effectFn) { //遍历effectFn.deps数组 for (let i = 0; i < effectFn.deps.length; i++) { //deps时依赖集合 const deps = effectFn.deps[i] //将effectFn从依赖集合中删除 deps.delete(effectFn) } //重置effectFn.deps数组 effectFn.deps.length = 0 }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13

      -

      # 非原始值的响应式方案

      ## 5.1Proxy 以及 Reflect

      **proxy 只能代理对象 不能代理其他类型的数据**

      ### 5.1.2 Reflect

      Reflect 可以接受第三个参数 即 receiver 相当于函数调用过程中的 this

      const obj = {foo: 1}; Reflect.get(obj, foo, {foo: 2});//输出2

文章作者: olddog
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 olddog !
  目录