实现了简版一个Vue?

224 阅读4分钟

开始

这一段时间在阅读 Vue 2.6 的源码,太多精妙的设计和写法令人叹为观止。虽然思维导图什么的已经画了很多,整个流程和关键的逻辑已经高通,但是本着纸上学来终觉浅的原则,决定自己动手撸一个 Vue 的简版。

需求分析

Vue 有几个重要的组成部分 ---> Observe,complier,Dep,Watcher,Patch。下面会逐一分析原理和我自己的实现。

数据响应部分

这个部分相信直到 Vue 原理的人都清楚,核心是 javascript 的语言特性 api Object,definePrototype ,即使你没用过这个 api,也一定直到它是 Vue 实现响应式的关键,但是它对于 Vue 的响应式系统来说是个入口,Vue 通过它实现了依赖收集和视图更新,这其中的涉及到的部分主要有这几个:Observe,Dep,Watcher。下面是概念代码实现:

function Observer(obj) {
	const keys = Object.keys(obj)
    keys.forEach( item => {
    	const dep = new Dep()
        let value = obj[item] 
    	Object.definePrototype( obj, item, {
        	get() {
            	if( Dep.target ) {
                	dep.add(Dep.target)
                }
                return value
            },
            set(val) {
            	if( val !== value ) {
                	value = val
                    dep.nodify()
                }
            }
        } )
    } )
}

这段代码基本是相应式数据依赖收集和触发更新的全部代码,如果你对这其中的细节感兴趣,或者想知道 Dep.targrt 是什么,可以点击这里查看 watcher.jsDep.js 的更多细节实现。如果看的爽记得一个 Star

Complier 部分

这个部分是 Vue 源码中逻辑最复杂的部分,它将平时定义的模版编译成了 render 函数,也就是说,只在实例初始化时对模版编译一次,后续只需要根据数据和 render 结果去做 patch 就可以了,VueComplier 来自另一个简易版的 html 解析器,尤大将器扩充和修改,就成了现在的 Vue Complier。这里我是直接采用了这个简易版的模版解析,具体实现代码在这里

具体使用的代码在这里,它将<div>123</div> 就系出来,但是需要自己再对解析的每个节点的结果做一个有机的拼合,形成一个 vnode,以供 patch 使用,下面是配置编译器的具体代码:

const handler = {
  startElement: function (sTagName, oAttrs) {
    //标签开始
    if (tag.length === 0) {
      currentNode = { tag: sTagName, attr: parseProps(oAttrs), children: [] }
      arr.push(currentNode)
    } else {
      const parentNode = arr[arr.length - 1]
      currentNode = { tag: sTagName, attr: parseProps(oAttrs), children: [] }
      parentNode.children.push(currentNode)
    }
    tag.push(sTagName) // 遇到开始标签,就入栈
    // console.log("startElement", sTagName, oAttrs);
  },
  endElement: function (sTagName) {
    // 标签解释
    tag.pop() // 遇到结束标签,就弹出去一个
    // console.log("endElement", sTagName);
  },
  characters: function (s) {
    // 表示的是获取的字符

    (currentNode && currentNode.children) && currentNode.children.push(s)
    // console.log("characters", s);
  },
  comment: function (s) {
    // 表示获取的注释
    currentNode.children.push({ tag: "comment", text: s })
    // console.log("comment", s);
  }
};

编译过程还包含了像指令,属性等的解析,这里这个编译器只会将 <div v-model="sss"></div>v-model 解析为:

{
	name: "v-model",
    value: "sss"
}

的形式,所以,需要自己再多做一部分工作,那就是将执行转换为标签属性,比如将 v-model 转换为 value 属性和 input 事件。这一部分的内容我是放在生成 render 函数时做的,具体代码在这里。处理思路和上述描述的一致。

Patch 部分

这个部分直接使用的 Snabbdom.js,这个 Vue patch 的基础版本实现,如有兴趣可以去官网研究。

数据和 Patch 进行链接

这个部分就是 Vue 中的核心部分。我们知道,对于每个实例,Vue 只会生成一个 render 函数,这样有利于批量更新,减少更新的次数(当然,还有异步调度的配合,代码在这里)。具体 render 函数的生成逻辑在这里。 这里是模仿 Vue 官方生成的样子就行递归生成的字符串,比如使用 with圈定作用域范围,使用不同的函数去生成不同的节点等等,这一部分需要深入研究,因为目前我的算法还存在瑕疵。(工作太忙,大家见谅。)

后续

这篇文章只是一个开头,后续会针对每一部分的源码和逻辑结合源码进行更深如的分析:

  • Diff 的核心算法
  • Vue 的初始化流程
  • 实例更新的流程
  • 组件的初始化,挂载,渲染流程解析

总结

项目的完整代码在这里, 里面也有其他知识可供学习查看。需要的同学可以自行查看,看的爽的别忘了给个 Star 给予一个小小的鼓励,谢谢。