前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue设计与实现读后感-开发环境搭建-渲染器(二)

Vue设计与实现读后感-开发环境搭建-渲染器(二)

作者头像
吴文周
发布2022-03-14 14:39:23
8090
发布2022-03-14 14:39:23
举报
文章被收录于专栏:吴文周的专栏吴文周的专栏

这篇博客记录自己学习的感悟,也给一些学习vue3的同学一些面试技巧,实战经验。

广告-个人项目

源码阅读

对于急迫想提升的同学来说阅读源码不仅仅是面试效果提升,更是个人能力提升的关键。首先我不反感,那些源码很熟,或者某个版本更新了什么,都是十分清楚的人,至少是个有心人。但是作为面试细节我可能觉得有些矫枉过正了,个人观点。

认可源码学习的价值

好的源码有很好的代码规范,项目规范,好的设计模式使用场景,好的数据结构设计,好的方案思路。天下文章一大抄,正是基于前人的经验和方法,也是我们构建自己项目的基石和创新基础。这个真的不是废话,一定要先从学习别人的设计开始,千万不要自以为是的创造,说真的百分之99的人没有那个能力,留下的只能是坑。

技巧与步骤(以vue3为例)

纯看代码是枯燥乏味的,也是体会不深的,过段时间可能就忘却了,这是自己的真实感受,代码永远是写出来的,不是看出来的。

首先还是要看的,看什么?看尤雨溪对vue实现方式的视频,看作者关于vue3一些官方介绍。还有看什么,在细节上面可能视频表述有限,有些对于vue3解读的博客文章,书籍都可以看,例如现在我看的这本《Vue设计与实现》,对数据劫持,响应式的概念有所了解。

看书笔记,学习千万不要停留在表面一定要有自己的思考和沉淀,虽然很感谢作者的分享与总结,但自己在工作中也是有一些体会能不能和读书笔记相结合。

代码实践,自己手动去写一下响应式的实现方式,任务调度的设计。

关注设计流程,虽然不仅仅是下面这些流程,但是这些知识的梳理应该是足够我完成一个简单的响应式的ui框架核心开发。

代码语言:javascript
复制
数据劫持 --> 依赖收集  --> 任务调度 --> 虚拟dom  --> diff算法

具体实践?删除代码中无效的代码,提炼关键函数!!!

代码语言:javascript
复制
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}


function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only a whitelist of value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

因为源码中有很多,健壮性以及辅助开发的部分代码,但是并不是我们关心的核心。我们自己去编写这段代码时,可去掉这些代码。我在具体实现的时候,就删除了一些代码变成了下面的样子。

代码语言:javascript
复制
function reactive<T>(target:  T) {
  return createReactiveObject(target)
}

function createReactiveObject<T extends Object>(
  target: T,
) {
  // 创建代理对象实现数据劫持
  const proxy = new Proxy(
    target,
    {
      get(target: any, key: string) {
        // 依赖收集
        track(target, key)
        return target[key];
      },
      set<T>(target: any, key: string, value: T) {
        const oldValue = target[key];
        target[key] = value
        trigger(target,  key, value, oldValue)
        return true
      }
    }
  )
  return proxy
}

由此可见,我不仅仅删除了很多健壮性的代码,以及开发提示代码,还有合并的部分函数功能。但是缩短后的代码,我在阅读上面心智负担比较小。

开发实践

工欲善其事必先利其器,这是我一贯的思想,我开始就在想,怎么去搭建一个vue3源码的学习环境。最大的初衷就是学习,不畏难。可选的技术有webpack,vite这两个常见的打包方式,既然esbuild已经是作为vite的很重要的部分了,甚至有了webpack插件,umijs的插件等等,那我为什么不用esbuild作为构建基础呢?,更进一步为什么我不用golang作为我的构建基础吗?再也不用担心node_module,以及编译速度了。ts和js的选择也是毫无疑问的选择了ts。

golang 初始化项目

代码语言:javascript
复制
mkdir goVue
cd goVue
go mod init github.com/fodelf/goVue

启动esbuild相关服务

查看了esbuild的官方文档serve,虽然有sever服务,但是在热更新上面支持度不够友好,不能reload页面,更别说hmr了。这是我比较痛苦的点,验证影响我开发体验,虽然编译速度变快了,但是开发体验并没有得到提升还是要重新刷新页面。

基于这个serve方式的改造是一种实现方案,我就简单粗暴一点直接使用golang启动了一个web服务,然后使用websocket进行通知更新,构建完成后刷新页面。也算完成了我基本的述求。

开发永远是阶段性的,方案永远是阶段性的,人总是在趋于完美的道路上不断前行。先实现再优化永远是我的开发思路,比如我现在关注的问题核心是vue3源码的实现,我现在很low的方式也能实现开发效果,我希望把我每天晚上更多的有效时间放到,vue3的实践当中。我也肯定知道基于esbuild的修改有更好的hmr的实践,但不是我的主要矛盾。在学习vue的过程中,我应该顺带解决这个问题。

这里面有三个开发核心要素,功能需求有限,管理自己的技术负债,提升质量属性。

代码语言:javascript
复制
实现 --> 优化  --> 自动化

这个流程是要关注的,我可以实现vue3的核心逻辑,我优化了Vue3编译速度,我通过单元测试保证了后续的开发质量与提测基准。

可以看到我确实讲了很多废话,但是我相信这些都是我最真实的心路历程,希望大家在开发的道路上面共勉。

最终的实现变扭版,也相关事宜esbuild的Plugin做一些强化,但是好像场景是不匹配的。终止肯定会更好。

golang服务核心功能 开发环境就绪

  • esbuild 构建模块
代码语言:javascript
复制
/*
 * @Description: esbuild构建相关
 * @Author: 吴文周
 * @Github: https://github.com/fodelf
 * @Date: 2022-03-12 06:24:21
 * @LastEditors: 吴文周
 * @LastEditTime: 2022-03-12 06:32:43
 */
package pkg

import (
    "fmt"
    "os"
    "path"

    "github.com/evanw/esbuild/pkg/api"
)
func BuildInit (){
    dir, _ := os.Getwd()
    api.Build(api.BuildOptions{
        // LogLevel:    api.LogLevelInfo,
        EntryPoints: []string{"src/index.ts"},
        // Outdir:      "www/js",
        Outfile:   path.Join(dir, "www", "js", "index.js"),
        Bundle:    true,
        Sourcemap: api.SourceMapLinked,
        // Plugins:     []api.Plugin{hmr},
        Write:  true,
        Format: api.FormatESModule,
        Watch: &api.WatchMode{
            OnRebuild: func(result api.BuildResult) {
                if len(result.Errors) > 0 {
                    fmt.Printf("watch build failed: %d errors\n", len(result.Errors))
                } else {
                    fmt.Println(result.OutputFiles[0].Path)
                    for _, value := range NodeCache{
                        //fmt.Println(index, "\t",value)
                        b := []byte(result.OutputFiles[0].Path)
                        value.DataQueue <- b
                        // fmt.Println(value)
                 }
                }
            },
        },
    })
}
  • golang 使用websocket 进行热更新
代码语言:javascript
复制
/*
 * @Description: websocket
 * @Author: 吴文周
 * @Github: https://github.com/fodelf
 * @Date: 2022-03-12 06:20:34
 * @LastEditors: 吴文周
 * @LastEditTime: 2022-03-12 06:34:09
 */
package pkg

import (
    "fmt"
    "net/http"
    "sync"

    "github.com/gorilla/websocket"
)

// 单个websocket链接节点
type Node struct {
    Conn *websocket.Conn
    //并行转串行,
    DataQueue chan []byte
}

//读写锁
var rwlocker sync.RWMutex
//websocket 链接缓存
var NodeCache =[]Node{}


func Message(writer http.ResponseWriter, request *http.Request) {
    conn, err := (&websocket.Upgrader{
        CheckOrigin: func(r *http.Request) bool {
            return true
        },
    }).Upgrade(writer, request, nil)

    if err != nil {
        fmt.Print("出错了")
        return
    }
    rwlocker.Lock()
    //获得websocket链接conn
    node := Node{
    Conn:      conn,
    DataQueue: make(chan []byte, 1000),
    }
    NodeCache = append(NodeCache,node)
    rwlocker.Unlock()
    go sendproc(node)
}

//发送逻辑
func sendproc(node Node) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("write stop")
        }
    }()
    defer func() {
        node.Conn.Close()
        // fmt.Println("Client发送数据 defer")
    }()
    for {
        select {
        case data := <-node.DataQueue:
            // fmt.Printf("发送消息")
            err := node.Conn.WriteMessage(websocket.TextMessage, data)
            if err != nil {
                // fmt.Printf("发送消息失败")
                // for i := 0; i < len(NodeCache); i++ {
                //     // fmt.Println(NodeCache[i].Conn)
                //     // fmt.Println(node.Conn)
                //     if cmpare2(NodeCache[i].Conn , node.Conn)  {
                //         NodeCache = append(NodeCache[:i], NodeCache[i+1:]...)
                //         fmt.Printf("删除节点")
                //     }
                 // }
                return
            }
        }
    }
}
  • 打开浏览器
代码语言:javascript
复制
/*
 * @Description: 打开浏览器
 * @Author: 吴文周
 * @Github: https://github.com/fodelf
 * @Date: 2022-03-11 10:36:26
 * @LastEditors: 吴文周
 * @LastEditTime: 2022-03-12 06:31:17
 */
package pkg

import (
    "os/exec"
    "runtime"
)
func Open(url string) error {
    var cmd string
    var args []string

    switch runtime.GOOS {
    case "windows":
            cmd = "cmd"
            args = []string{"/c", "start"}
    case "darwin":
            cmd = "open"
    default: // "linux", "freebsd", "openbsd", "netbsd"
            cmd = "xdg-open"
    }
    args = append(args, url)
    return exec.Command(cmd, args...).Start()
}
  • golang web 服务入口
代码语言:javascript
复制
/*
 * @Description: web服务
 * @Author: 吴文周
 * @Github: https://github.com/fodelf
 * @Date: 2022-03-12 06:28:23
 * @LastEditors: 吴文周
 * @LastEditTime: 2022-03-12 06:31:26
 */
package pkg

import (
    "fmt"
    "net/http"
    "os"
    "path"
)

func Serveinit() {
    dir, _ := os.Getwd()
    http.Handle("/", http.FileServer(http.Dir(path.Join(dir, "www"))))
    http.HandleFunc("/message", Message)
    done := make(chan bool)
    go http.ListenAndServe(":8080", nil)
    fmt.Println("serve stsart at http://localhost:8080")
    Open("http://localhost:8080")
    <-done
}

todo

  • 本身esbuild在编译ts时有些类似装饰器这样的api友好度不行。
  • hmr需要优化。

回归正题

Vue3的设计思路

初识渲染器

渲染器的作用就是把虚拟dom转换为真正dom

代码语言:javascript
复制
虚拟dom --> 渲染器 --> 真实dom

其实大家对react的render 函数,从react的场景上面我们知道,需要一个html的模板,需要一个挂载的root就是可以了。

代码语言:javascript
复制
const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));

渲染器我理解的话就是两个入参 一个dom的模板或者虚拟dom,一个是挂载的容器。通过递归调用的方式将js对象变成了dom对象,并且插入到容器里面。

代码语言:javascript
复制
// 节点定义
interface VNode {
  tag: string;
  props: Record<string, Function>;
  children: VNode[] | String;
}
// 虚拟dom
const vnode = {
  tag: "div",
  props: {
    onClick: () => {
      console.log("hello word");
    },
  },
  children: [
    {
      tag: "span",
      props: {
        onClick: () => {
          console.log("hello child");
        },
      },
      children: "我是child",
    },
  ],
};
/**
 * @name: render
 * @description: 渲染方法
 * @param {VNode} vnode
 * @param {HTMLElement} root
 */
function render(vnode: VNode, root: HTMLElement) {
  //创建html元素
  const el = document.createElement(vnode.tag);
  // 绑定事件
  const props = vnode.props;
  for (let key in vnode.props) {
    // 截取onClick中的Click名称,转小写
    const eventName = key.substring(2).toLowerCase();
    el.addEventListener(
      eventName as keyof HTMLElementEventMap,
      props[key] as EventListenerOrEventListenerObject
    );
  }
  // 如果是文本直接渲染,如果是数组需要递归渲染逻辑
  if (typeof vnode.children === "string") {
    el.innerHTML = vnode.children;
  } else {
    const children = vnode.children as VNode[];
    children.forEach((child) => {
      render(child, el);
    });
  }
  //添加到容器中
  root.appendChild(el);
}
render(vnode, document.getElementById("container") as HTMLElement);

组件的本质

由上可知虚拟dom可以通过渲染器变成真实dom,那组件又是什么呢?组件是一个自定义命名的标签,标签是变成渲染结果进入文档节点当中的呢?

代码语言:javascript
复制
<div>
  里面是我的内容区
  <MyComponent></MyComponent>
</div>

上述代码中MyComponent组件是变成渲染dom呢?猜一下? 是不是这样的就可以 ?

代码语言:javascript
复制
// 组件函数
function MyComponent(){
    return {
     tag: "div",
      props: {
        onClick: () => {
          console.log("hello word");
        },
      },
    }
}

// 虚拟dom
const vnode = {
  tag: MyComponent,
}

由此可见 组件可以是一个函数,返回了一个虚拟dom,剩下的还是那个嵌套递归的render,这样就可以把我们的组件渲染出来了。

代码语言:javascript
复制
function render(vnode: VNode, root: HTMLElement) {
  // 判断tag类型
  if (typeof vnode.tag == "string") {
    //创建html元素
    const el = document.createElement(vnode.tag);
    // 绑定事件
    const props = vnode.props;
    for (let key in vnode.props) {
      const eventName = key.substring(2).toLowerCase();
      el.addEventListener(
        eventName as keyof HTMLElementEventMap,
        props[key] as EventListenerOrEventListenerObject
      );
    }
    // 如果是文本直接渲染,如果是数组需要递归渲染逻辑
    if (typeof vnode.children === "string") {
      el.innerHTML = vnode.children;
    } else {
      const children = vnode.children as VNode[];
      children.forEach((child) => {
        render(child, el);
      });
    }
    //添加到容器中
    root.appendChild(el);
  } else {
    // 渲染组件
    render(vnode.tag(), root);
  }
}

核心逻辑就是判断tag的数据类型如果是function就不使用createElement,而是再次调用render方法 render(vnode.tag(), root) 这样理解的话就可以推断,react的组件是否也是如此实现的?下面就是react组件实现的一种方式。场景上面react有class组件和function组件,类型判断的时候也是要注意一下。

代码语言:javascript
复制
// 借用解决大佬代码了
function createDOM(vdom){
    let {type,props} = vdom;
    let dom;
    if(type === REACT_TEXT){
        dom = document.createTextNode(props.content);
    }else if(typeof type === 'function'){ // 组件
        if(type.isReactComponent){  // 类组件
            return mountClassComponent(vdom);
        }else{                         // 函数组件
            return mountFunctionComponent(vdom);
        }
    }else{
        dom = document.createElement(props.content);
    }
    
    if(props){
        updateProps(dom,{},props);
        if(typeof props.children == 'object' && props.children.type){
           render(props.children,dom)
        }else if(Array.isArray(props.children)){
            reconcileChildren(props.children,dom)
        }
    }
    vdom.dom = dom
    return dom
}

模板语言和jsx 虽然实现方式各异,但是殊途同归,都是先转换为虚拟dom,然后再调用render 渲染器将虚拟dom转换为真实dom,组件的实现亦是如此。

本以为复杂的源码阅读之路,其实有机会举一反三,在面试时又多了一套说辞。

其实一直强调的那一点就是所有的设计都是基于前人的经验去创新,而不是自己闭门造车。

组件一定是函数吗?其实无所谓了只要传入的是一个虚拟dom的对象,这样定义的object也是可以的。其实我们在用vue3+tsx就可以知道,只要有render函数(此render和之前的render渲染器不同)返回的是一个可以渲染的模板就可以,甚至于说不用render返回,只要能返回一个渲染模板,就可以实现。

我们在vue3中写不写render都可以。

代码语言:javascript
复制
// 写法一
export default defineComponent({
  name: 'Test',
  render:() {
    return <div>hello world</div>
  },
})

// 写法二
export default defineComponent({
    name: 'Test',
    setup() { 
        return () => <div>hello world</div> 
    }
})

模板的工作原理

这里有个人观点:作者的写作顺序有些偏向概念的由浅入深模式,例如先从声明式,命令式开始往下讲,其实还有一种方式,能不能从一个库的设计理念开始讲,也是一种方案,先从核心概念开始讲数据劫持,diff算法,编译渲染。一种是概念的由浅入深,一种是真正的vu3的实现核心方案。

各有利弊作者的方式作者的方式更利于新手去感受编程的基础概念等等,另一种方式更适合有一定工作经验的人去提示,写作上面也是鱼和熊掌不可兼得。

模板是声明式ui的表现形式。核心的思想还是编译器的概念。这也是前端受人诟病的一点,所谓的前端方言话,虽然编译器帮助我们实现了组件化提升了代码的可维护性。但是每个前端框架就是一种语法,实在是有些一言难尽。

编译原理顺带提一下,后面可以大篇幅的描述。我之前关于编译原理的浅析学习

代码语言:javascript
复制
<template>
    <div @click ='handleCilck'>
      点我
    </div>
</template>
<script>
  export default{
      methods:{
          handleCilck:()=>{
           alert("Hello Word")
          }
      }
  }
</script>

这段代码是不能在浏览器里面正常运行的。需要把这样的前端方言转换为浏览器可以执行的代码。经过编译器之后就会变成下面的样子。

代码语言:javascript
复制
 // 这是手写render
 export default{
      methods:{
          handleCilck:()=>{
           alert("Hello Word")
          }
      },
      render(){
        h("div",{onClick:handleCilck},'点我')
      }
  }
  
  
  // 然后进入渲染器
  const handleCilck = ()=>{
      alert("Hello Word")
  }
  const vnode = {
      tag: "div",
      props: {
        onClick: handleCilck,
      },
      children:"点我"
  }
  // root是挂载点
  render(vnode,root)

流程再说明

把一段不能被浏览器正常执行但是可维护性较高的代码,转换为js(可能还有less/sass转css),再使用渲染器将组建渲染到容器内。

代码语言:javascript
复制
编译器 --> 渲染器

vue 是一个有机整体

很多设计其实是环环相扣的。基于场景我们去看,使用编译器编译模板,到虚拟dom,再到渲染器渲染,流程是清晰可见的。在此过程中,还有一些类似静态编译提升的场景优化。

代码语言:javascript
复制
<template>
    <div>
      {message}
    </div>
</template>
<script>
  export default{
      data():{
         return{
           message: 'hello world'
         }
      },
  }
</script>
代码语言:javascript
复制
<template>
    <div>
      hello word
    </div>
</template>

我们看到这个两个模板是有明显区别的,第一个模板明显比第二个模板具有可变性,第二个组件可以说只要生成了就不会变,只要打上这些标记,在我们代码运行的过程中旧不需要对第二个组件进行相关的数据劫持等等操作了。查看变量提升工具这是作者提供的在线查询的地址。

有这些标记的目的肯定是为了提升速度,有兴趣的可以看b站尤雨溪自己vue3宣讲会的视频对这一点有着重描述。也是这一点对比react的这些方面做得欠佳的,其实核心还是jsx和模板语言的不同场景,没什么必要强行优劣,软件从业人员没必要拉踩,场景和技术选型是极大相关的,一定要基于自己的场景选择合适的技术,仅此而已。 原视频在21分钟左右。 当然整个视频值得我们反复观看,别听那些其他人讲,包括我这篇博客都是自己的理解,我们更应该听作者讲他的设计理念再去面试。

总结

  • 工欲善其事,必先利其器。在vue3源码编写的时候一个要构建自己的开发环境,当然可以选择webpack,vite等等,我选择了golang作为项目基础。核心要素,web服务,ts编译,热更新,自动打开浏览器。
  • 渲染器就是一个递归调用函数,将虚拟dom挂载在容器之上。
  • 组件可以是一个函数,可以是一个虚拟dom对象,说白就是一个表现形式不一样,实质还是虚拟dom转换挂载的场景。
  • 编译器把浏览器不识别的模板语言进行转换,同时过程中做了一些标记和优化。
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022/03/12 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 广告-个人项目
  • 源码阅读
    • 认可源码学习的价值
      • 技巧与步骤(以vue3为例)
      • 开发实践
        • golang 初始化项目
          • 启动esbuild相关服务
            • golang服务核心功能 开发环境就绪
              • todo
              • 回归正题
              • Vue3的设计思路
                • 初识渲染器
                  • 组件的本质
                    • 模板的工作原理
                      • 流程再说明
                    • vue 是一个有机整体
                    • 总结
                    相关产品与服务
                    容器服务
                    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档