源码实现

数据劫持

vue3.x中使用了ES6新的语法proxyreflect相结合进行数据劫持拦截,性能上有很大的提升,而且支持劫持数组

  1. //vue3.x写法
  2. export default {
  3. setup(){
  4. const state = reactive({
  5. ...
  6. });
  7. }
  8. }
  1. //项目目录:
  2. ├─package.json
  3. ├─Readme.md
  4. ├─webpack.config.js
  5. ├─vue3
  6. | ├─shared
  7. | | utils.js - 工具函数isObject/hasOwnPropery/isEqual
  8. | ├─reactivity
  9. | | ├─index.js - 响应式出口文件
  10. | | ├─mutableHandler.js - 管理handler底下的底层Object对象方法做响应式处理/新增业务逻辑编写
  11. | | reactive.js - 实例化一个Proxy对象返回一个被代理后的响应式对象
  12. ├─src
  13. | ├─index.html
  14. | index.js
  15. ├─public
  16. | index.html

源码地址:https://gitee.com/kevinleeeee/data-hijacked-vue3.x-demo

视图绑定

案例:实现视图数据绑定

技术:vite/数据响应式

功能缺少:

  • AST
  • 虚拟dom
  • 渲染函数
  • 依赖收集

功能:

  1. 编译视图模板里的事件绑定属性
  2. 关联methods定义的方法和视图事件处理函数的绑定
  3. 编译视图模板绑定的变量以及标签属性
  4. state数据发生更改时更新页面视图
  1. //项目目录:
  2. ├─src
  3. | app.js
  4. ├─reactivity
  5. | ├─index.js 出口文件/对象响应式/视图绑定/事件/状态
  6. | ├─render.js 视图绑定/绑定属性/绑定事件函数/更新视图函数
  7. | ├─shared
  8. | | utils.js 工具函数
  9. | ├─reactive
  10. | | ├─index.js 对象响应式/创建响应式对象
  11. | | mutableHandler.js 管理handler底下的底层Object对象方法
  12. | ├─compiler
  13. | | ├─event.js 绑定视图的处理函数
  14. | | state.js 绑定视图的状态变量函数

问题:如何实现编译带事件处理函数绑定视图的标签?

  1. 编译视图模板时匹配onClick="xxx"格式
  2. 随机生成一个字符串保存在定义flag标识属性
  3. 组装一个新的对象包含flag,handler,type属性
  4. 匹配xxx内容后,将其内容保存到handler属性里
  5. 将组装好的对象存入事件池数组eventPool
  6. 匹配onClick="xxx"格式最后替换为`data-dom=${_flag}的字符串实现在视图里
  7. 实现编译标签时<div onClick="add(2)"></div>用一个标识替换原有的绑定句柄onClick="add(2)",如data-dom="1640242607185"

问题:如何关联视图上绑定的事件处理函数属性和用户methods上定义的方法?

  1. 假如视图上绑定onClick="add(2)"
  2. 假如用户在methods定义的方法为add(){...}
  3. 拿到页面所有的节点
  4. 遍历事件池里面的对象和所有节点找到符合标识属性flag条件的那一项
  5. 给该项添加绑定事件处理函数addEventListener
  6. 找到methods里定义的方法名和参数
  7. 执行定义在methods里的对应的方法
  8. 实现视图和方法的关联

问题:如何关联视图上绑定的变量属性?

  1. 替换视图模板为用户定义标签名的标签<myDiv></myDiv>
  2. 给标签新增自定义属性data-dom="1640273806381"
  3. 替换视图模板里{{}}为用户定义的state数据
  4. 缓存状态池
  1. //缓存状态池包含标识flag和变量名数组varArr属性
  2. /**
  3. * console.log(statePool);
  4. * [
  5. * {
  6. * flag: 1640277702567,
  7. * state: ['count']
  8. * }
  9. * ]
  10. */

问题:视图更新时,如何找到绑定变量数据更改时所在的节点?

  1. setter函数里新增update方法实现state数据发生更改时更新视图
  2. render文件里定义update方法
  3. 遍历所有的元素节点和状态池
  4. 当状态池最后一项和state数据的key的变量名一样时
  5. 当元素节点带有data-dom的标识跟状态池的的标识字符串一致时
  6. state数据发生更改的那个节点的文本内容进行更新
  7. 实现更新state数据时,页面内容实时发生更改

源码地址:https://gitee.com/kevinleeeee/compile-template-vue3-vite-demo

vue-loader

手写webpack实现vue-loader驱动和vue轮子(非虚拟dom)

  • 实现一个加载器,来解析.vue单文件组件里的模板和样式和脚本

  • 实现v-if/v-show

通过一个程序将.vue转为.js,并且将es6转为es5并跑在node服务器上,

node的文件操作可以完成jscss文件的创建

最终形成一个完整的html引入script,style

webpack的运行过程:

  1. resolve: {extensions: ['.js', '.vue']}处理js,vue后缀文件
  2. 在模块规则定义的loader里调用相应的vue-loader(这里的vue-loader是自定义的非vue的),自定义的vue-loader最终返回

    1. 希望处理的结果:
    2. 实现import引入的style标签内容引入到html
    3. 实现script标签的内容作为一个组件对象导出使用
  3. 处理css文件调用相应的style-loader,css-loader
  4. 将处理好的结果放入到插件HtmlWebpackPlugin
  5. 打包js/css后放到目录/dist

自定义一个vue-loader加载器:

webpack处理vue后缀文件时会执行这个加载器,这个加载器会返回希望处理后的结果

  1. function vueLoader(source) {
  2. //source打印的是App.vue里写的代码字符串
  3. /**
  4. * console.log(source);
  5. * <template>
  6. <div>
  7. <div>
  8. <img v-if="isShowImg1" class="img" src="https://gimg2.baidu.com/image_search...=jpeg?sec=1649504657&t=2a48eec8e0e23a2192c960e60ce27b7f" />
  9. <img v-show="isShowImg2" class="img" src="https://gimg2.baidu.com/image_search/src...=jpeg?sec=1649504684&t=b8e0aa845d744916020406fa30d6217e" />
  10. </div>
  11. <button @click="showImg1">Show Image 1</button>
  12. <button @click="showImg2">Show Image 2</button>
  13. </div>
  14. </template>
  15. <script>
  16. export default {
  17. name: 'App',
  18. data () {
  19. return {
  20. isShowImg1: true,
  21. isShowImg2: true
  22. }
  23. },
  24. methods: {
  25. showImg1 () {
  26. this.isShowImg1 = !this.isShowImg1;
  27. },
  28. showImg2 () {
  29. this.isShowImg2 = !this.isShowImg2;
  30. }
  31. }
  32. }
  33. </script>
  34. <style>
  35. .img {
  36. width: 200px;
  37. }
  38. </style>
  39. */
  40. return '123';
  41. }

目标:希望把字符串的内容转变为vue单文件组件写法的结构

  1. 提取template标签内容
  2. 提取script标签内容
  3. 提取style标签内容
  4. 定义一个临时存放的样式文件并写入样式代码
  5. import引入的路径返回实现html引入style标签
  6. script标签的字符串返回组件对象

目标:希望编写vue里的createApp方法实现创建一个组件实例

  1. 定义vm对象
  2. 挂载component对象里的data,methods,template属性到vm实例
  3. 挂载dom节点(template里定义的内容)到vm实例

目标:希望实现数据响应式和访问属性代理

通过访问和修改vm实例的属性时,代理Object.definePropertyget/set更改this访问返回的属性

目标:希望定义两个Map数据类型的池子

  • 池子1(propsPool):保存的节点和类型(v-if/v-show)和绑定事件方法名称
  • 池子2(eventsPool):保存节点和事件类型(click/change/...)和绑定事件的处理函数方法(handler)
  1. propsPool格式为:
  2. Map(2) {
  3. img.img => {type: 'v-if', prop: 'isShowImg1'},
  4. img.img => {type: 'v-show', prop: 'isShowImg2'}
  5. }
  6. eventsPool格式为:
  7. Map(2) {
  8. button => {type: 'click', handler: 'showImg1'},
  9. button => {type: 'click', handler: 'showImg2'}
  10. }

目标:希望根据eventPool的事件类型绑定事件处理函数

给实例挂载访问this.name = handler函数

目标:希望实现v-ifv-show的显示和隐藏效果

  • 注释节点替换的方式来实现v-if
  • display: none的方式来实现v-show
  • setter里如果数据发送更改时触发update函数

项目目录:

  1. ├─package.json
  2. ├─webpack.config.js
  3. ├─__temp - vue-loader驱动里对样式文件写入的临时目录存放临时样式文件
  4. | ├─css
  5. | | __1647282035869.css
  6. ├─src
  7. | ├─App.vue - vue单文件组件
  8. | main.js - vue入口文件
  9. ├─public
  10. | index.html
  11. ├─modules
  12. | ├─vue-loader - vue驱动实现一个单文件vue组件和样式的import引入
  13. | | index.js
  14. | ├─vue - vue轮子/实例挂载/数据响应式/事件绑定/v-if/v-show指令实现
  15. | | ├─event.js - 处理绑定事件处理函数的方法
  16. | | ├─index.js - 实例入口文件/创建实例/创建DOM/挂载DOM
  17. | | ├─pools.js - map数据类型的池绑定节点和类型
  18. | | ├─propsType.js - 标识文件
  19. | | ├─reactive.js - 数据响应式gettersetter
  20. | | render.js - 渲染函数/更新函数

源码地址: https://gitee.com/kevinleeeee/vue-loader-webpack-vshow-vif-demo

setup方法

  1. //2.0写法
  2. data(){...},
  3. methods:{...}
  4. //3.0写法
  5. //创建引用
  6. import { ref } from 'vue';
  7. export default{
  8. //函数方式创建默认值为数值0
  9. const count = ref(0);
  10. const plus = () => { count.value --; }
  11. //注意:3.0注册的属性和方法必须通过return返回出去
  12. return {
  13. count,
  14. plus
  15. }
  16. }
  1. //3.0写法:逻辑方法单独提取出去
  2. const useCompute = (count) => {
  3. const plus = () => { count.value --; }
  4. return {
  5. plus
  6. }
  7. }
  8. export default{
  9. const count = ref(0);
  10. const { plus } = useCompute(count);
  11. return {
  12. count,
  13. plus
  14. }
  15. }