本文共 12987 字,大约阅读时间需要 43 分钟。
在的最后一篇文章中说道:我觉得可响应的数据结构作用很大,在整理了一段时间后,这是我们的最终产出:
ok 回到整理,这篇文章我们不研究 Vue
了,而是根据我们现在的研究成果来手撸一个 MVVM
。
先看看下我们的研究成果:一个例子
let demo = new RD({ data(){ return { text: 'Hello', firstName: 'aco', lastName: 'yang' } }, watch:{ 'text'(newValue, oldValue){ console.log(newValue) console.log(oldValue) } }, computed:{ fullName(){ return this.firstName + ' ' + this.lastName } }, method:{ testMethod(){ console.log('test') } }})demo.text = 'Hello World'// console: Hello World// console: Hellodemo.fullName// console: aco yangdemo.testMethod()// console: test
写法上与 Vue
的一样,先说说拥有那些属性吧:
关于数据
关于生命周期
关于实例间关系
实例下的方法:
关于事件
其他方法
类下方法:
以上便是所有的内容,因为 RD
仅仅关注于数据的变化,所以生命周期就就只有创建和销毁。
对比与 Vue
多了一个 $initProp
,同样的由于仅仅关注于数据变化,所以当父实例相关的 prop
发生变化时,需要手动通知子组件修改相关数据。
其他的属性以及方法的使用与 Vue
一致。
ok 大概说了下,具体的内容可以
有了 RD
我们来手撸一个 MVVM
框架。
我们先确定我们大致需要什么?
dom
结构)ok 模板引擎,JSX
语法不错,来一份。
接着虚拟节点,github
上搜一搜,ok 找到了,
所有条件都具备了,我们的实现思路如下:
RD + JSX + VNode = MVVM
具体的实现我们一边写 TodoList
一边实现
首先我们得要有一个 render
函数,ok 配上,先来个标题组件 Title
和一个使用标题的 App
的组件吧。
可以对照完整的 demo
查看一下内容,。
var App = RD.extend({ render(h) { return () }})var Title = RD.extend({ render(h) { return ({this.title}
) }, data(){ return { title:'这是个标题' } }})
这里就不说明 JSX
语法了,可以在 babel
上看下转码的结果,。
至于 render
的参数为什么是 h
?这是大部分人都认可这么做,所以我们这么做就好。
根据 JSX
的语法,我们需要实现一个创建虚拟节点的方法,也就是 render
需要传入的参数 h
。
ok 实现一下,我们编写一个插件使用 RD.use
来实现对于实例的扩展
// demo/jsxPlugin/index.jsexport default { install(RD) { RD.prototype.$createElement = function (tag, properties, ...children) { return createElement(this, tag, properties, ...children) } RD.prototype.render = function () { return this.$option.render.call(this, this.$createElement.bind(this)) } }}
我们把具体的处理逻辑放在 createElement
这个方法中,而实例下的 $createElement
仅仅是为了把当前对象 this
传入这个函数中。
接着我们把传入的 render
方法包装一下,挂载到实例的 render
方法下,我们先假设这个 createElement
能生成一个树结构,这样调用 实例下的 render()
,就能获得一个节点树。
注:这里获得的并不是虚拟节点树,节点树需要涉及子组件,我们要确保这个节点树仅仅和当前实例相关,不然会比较麻烦,暂且叫它是节点模板。
ok 我们可以想象一下这节点模板会长什么样?
参考后,得到这样一个结构:
{ tagName: 'div', properties: {className: 'todo-wrap'}, children:[ tagName:'component-1',// 后面的 1 是扩展出来的类的 cid ,每个类都有一个单独的 cid parent: App, isComponent: true, componentClass: Title properties: {}, children: [] ]}原有标签的处理虚拟节点的库已经帮我们做了,我们来实现一下组件的节点:
// demo/jsxPulgin/createElemet.jsimport {h, VNode} from 'virtual-dom'export default function createElement(ctx, tag, properties, ...children) { if (typeof tag === 'function' || typeof tag === 'object') { let node = new VNode() // 构建一个空的虚拟节点,带上组件的相关信息 node.tagName = `component-${tag.cid}` node.properties = properties // prop node.children = children // 组件的子节点,也就是 slot 这里并没有实现 node.parent = ctx // 父节点信息 node.isComponent = true // 用于判断是否是组件 node.componentClass = tag // 组件的类 return node } return h(tag, properties, children) // 一般标签直接调用库提供的方法生成}
现在我们可以通过实例的 render
方法获取到了一个节点模板,但需要注意的是:这个仅仅只能算是通过 JSX
语法获取的一个模板,并没有转换为真正的虚拟节点,这是一个节点模板,当把其中的组件节点给替换掉就能得到真正的虚拟节点树。
捋一捋我们现在有的:
render
函数render
函数生成的一个节点模板接着来实现一个方法,用于将节点模板转化为虚拟节点树,具体过程看代码中的注释
// demo/jsxPlugin/getTree.jsfunction extend(source, extend) { for (let key in extend) { source[key] = extend[key] } return source}function createTree(template) { // 由于虚拟节点只接受通过 VNode 创建的对象 // 并且为了保持模板不被污染,所以新创建一个节点 let tree = extend(new VNode(), template) if (template && template.children) { // 遍历所有子节点 tree.children = template.children.map(node => { let treeNode = node // 如果是组件,则用保存的类实例化一个 RD 对象 if (node.isComponent) { // 确定 parent 实例以及 初始化 prop node.component = new node.componentClass({parent: node.parent, propData: node.properties}) // 将模板对应的节点模板指向实例的节点模板,实例下的 $vnode 用于存放节点模板 // 这样就将父组件中的组件节点替换为组件的节点模板,然后递归子组件,直到所有的组件节点都转换为了虚拟节点 // 这里使用了 $createComponentVNode 来获取节点模板,下一步我们就会实现它 treeNode = node.component.$vnode = node.component.$createComponentVNode(node.properties) // 如果是组件节点,则保存一个字段在虚拟节点下,用于区分普通节点 treeNode.component = node.component } if (treeNode.children) { // 递归生成虚拟节点树 treeNode = createTree(treeNode) } if (node.isComponent) { // 将生成的虚拟节点树保存在实例的 _vnode 字段下 node.component._vnode = treeNode } return treeNode }) } return tree}现在的流程是
render => createElement => createTree
生成了虚拟节点, $createComponentVNode
其实就是调用组件的 render
函数,现在我们写一个 $patch
方法,包装这个行为,并且通过 $mount
实现挂载到 DOM
节点的过程。 // demo/jsxPlugin/index.jsimport {create, diff, patch} from 'virtual-dom'import createElement from './createElement'export default { install(RD) { RD.$mount = function (el, rd) { // 获取节点模板 let template = rd.render.call(rd) // 初始化 prop rd.$initProp(rd.propData) // 生成虚拟节点树 rd.$patch(template) // 挂载到传入的 DOM 上 el.appendChild(rd.$el) } RD.prototype.$createElement = function (tag, properties, ...children) { return createElement(this, tag, properties, ...children) } RD.prototype.render = function () { return this.$option.render.call(this, this.$createElement.bind(this)) } // 对 render 的封装,用于获取节点模板 RD.prototype.$createComponentVNode = function (prop) { this.$initProp(prop) return this.render.call(this) } RD.prototype.$patch = function (newTemplate) { // 获取到虚拟节点树 let newTree = createTree(newTemplate) // 将生成 DOM 元素保存在 $el 下,create 为虚拟节点库提供,用于生成 DOM 元素 this.$el = create(newTree) // 保存节点模板 this.$vnode = newTemplate // 保存虚拟节点树 this._vnode = newTree } }}ok 接着我们来调用一下
// demo/index.jsimport RD from '../src/index'import jsxPlugin from './jsxPlugin/index'import App from './component/App'import './index.scss'RD.use(jsxPlugin, RD)RD.$mount(document.getElementById('app'), App)到目前为止,我们仅仅是通过了页面的组成显示出了一个页面,并没有实现数据的绑定,但是有了
RD
的支持,我们可以很简单的实现这种由数据的变化导致视图变化的效果,加几段代码即可 // demo/jsxPlugin/index.jsimport {create, diff, patch} from 'virtual-dom'import createElement from './createElement'import getTree from './getTree'export default { install(RD) { RD.$mount = function (el, rd) { let template = null rd.$initProp(rd.propData) // 监听 render 所需要用的数据,当用到的数据发生变化的时候触发回调,也就是第二个参数 // 回调的的参数新的节点模板(也就是 $watch 第一个函数参数的返回值) // 回调触发 $patch rd.$renderWatch = rd.$watch(() => { template = rd.render.call(rd) return template }, (newTemplate) => { rd.$patch(newTemplate) }) rd.$patch(template) el.appendChild(rd.$el) } RD.prototype.$createElement = function (tag, properties, ...children) { return createElement(this, tag, properties, ...children) } RD.prototype.render = function () { return this.$option.render.call(this, this.$createElement.bind(this)) } RD.prototype.$createComponentVNode = function (prop) { let template = null this.$initProp(prop) // 监听 render 所需要用的数据,当用到的数据发生变化的时候触发 $patch this.$renderWatch = this.$watch(() => { template = this.render.call(this) return template }, (newTemplate) => { this.$patch(newTemplate) }) return template } RD.prototype.$patch = function (newTemplate) { // 由于是新创建和更新都在同一个函数中处理了 // 这里的 createTree 是需要条件判断调用的 // 所以这里的 getTree 就先认为是获取虚拟节点,之后再说 // $vnode 保存着节点模板,对于更新来说,这个就是旧模板 let newTree = getTree(newTemplate, this.$vnode) // _vnode 是原来的虚拟节点,如果没有的话就说明是第一次创建,就不需要走 diff & patch if (!this._vnode) { this.$el = create(newTree) } else { this.$el = patch(this.$el, diff(this._vnode, newTree)) } // 更新保存的变量 this.$vnode = newTemplate this._vnode = newTree this.$initDOMBind(this.$el, newTemplate) } // 由于组件的更新需要一个 $el ,所以 $initDOMBind 在每次 $patch 之后都需要调用,确定子组件绑定的元素 // 这里需要明确的是,由于模板必须使用一个元素包裹,所以父组件的状态改变时,父组件的 $el 是不会变的 // 需要变的仅仅是子组件的 $el 绑定,所以这个方法是向下进行的,不回去关注父组件以上的组件 RD.prototype.$initDOMBind = function (rootDom, vNodeTemplate) { if (!vNodeTemplate.children || vNodeTemplate.children.length === 0) return for (let i = 0, len = vNodeTemplate.children.length; i < len; i++) { if (vNodeTemplate.children[i].isComponent) { vNodeTemplate.children[i].component.$el = rootDom.childNodes[i] this.$initDOMBind(rootDom.childNodes[i], vNodeTemplate.children[i].component.$vnode) } else { this.$initDOMBind(rootDom.childNodes[i], vNodeTemplate.children[i]) } } } }}
ok 现在我们大概实现了一个 MVVM
框架,缺的仅仅是 getTree
这个获取虚拟节点树的方法,我们来实现一下。
首先,getTree
需要传入两个参数,分别是新老节点模板,所以当老模板不存在时,走原来的逻辑即可
// demo/jsxPlugin/getTree.jsfunction deepClone(node) { if (node.type === 'VirtualNode') { let children = [] if (node.children && node.children.length !== 0) { children = node.children.map(node => deepClone(node)) } let cloneNode = new VNode(node.tagName, node.properties, children) if (node.component) cloneNode.component = node.component return cloneNode } else if (node.type === 'VirtualText') { return new VText(node.text) }}export default function getTree(newTemplate, oldTemplate) { let tree = null if (!oldTemplate) { // 走原来的逻辑 tree = createTree(newTemplate) } else { // 走更新逻辑 tree = changeTree(newTemplate, oldTemplate) } // 确保给出一份完全新的虚拟节点树,我们克隆一份返回 return deepClone(tree)}// 具体的更新逻辑function changeTree(newTemplate, oldTemplate) { let tree = extend(new VNode(), newTemplate) if (newTemplate && newTemplate.children) { // 遍历新模板的子节点 tree.children = newTemplate.children.map((node, index) => { let treeNode = node let isNewComponent = false if (treeNode.isComponent) { // 出于性能考虑,老节点模板中相同的 RD 类,就使用它 node.component = getOldComponent(oldTemplate.children, treeNode.componentClass.cid) if (!node.component) { // 在老模板中没有找到,就生成一个,与 createTree 中一致 node.component = new node.componentClass({parent: node.parent, propData: node.properties}) node.component.$vnode = node.component.$createComponentVNode(node.properties) treeNode = node.component.$vnode treeNode.component = node.component isNewComponent = true } else { // 更新复用组件的 prop node.component.$initProp(node.properties) // 直接引用组件的虚拟节点树 treeNode = node.component._vnode // 保存组件的实例 treeNode.component = node.component } } if (treeNode.children && treeNode.children.length !== 0) { if (isNewComponent) { // 如果是新的节点,直接调用 createTree treeNode = createTree(treeNode) } else { // 当递归的时候,有时可能出现老模板没有的情况,比如递归新节点的时候 // 所以需要判断 oldTemplate 的情况 if (oldTemplate && oldTemplate.children) { treeNode = changeTree(treeNode, oldTemplate.children[index]) } else { treeNode = createTree(treeNode) } } } if (isNewComponent) { node.component._vnode = treeNode } return treeNode }) // 注销在老模板中没有被复用的组件,释放内存 if (oldTemplate && oldTemplate.children.length !== 0) for (let i = 0, len = oldTemplate.children.length; i < len; i++) { if (oldTemplate.children[i].isComponent && !oldTemplate.children[i].used) { oldTemplate.children[i].component.$destroy() } } } return tree}// 获取在老模板中可服用的实例function getOldComponent(list = [], cid) { for (let i = 0, len = list.length; i < len; i++) { if (!list[i].used && list[i].isComponent && list[i].componentClass.cid === cid) { list[i].used = true return list[i].component } }}
ok 整个 MVVM
框架实现,具体的效果可以把啦下来,执行 npm run start:demo
即可。上诉所有的代码都在 demo
中。
我们来统计下我们一共写了几行代码来实现这个 MVVM
的框架:
所以我们仅仅使用了 22 + 111 + 65 = 198
行代码实现了一个 MVVM
的框架,可以说是很少了。
可能有的同学会说这还不算使用 RD
和虚拟节点库呢?是的我们并没有算上,因为这两个库的功能足够的独立,即使库变动了,实现相应的 api
用上面的代码我们同样能够实现,所以黑盒里的代码我们不算。
198
行的代码连接了 JSX/VNode/RD
实现了一个 MVVM
框架。 在研究 Vue
源码的过程中,在代码里看到了不少 SSR
和 WEEX
的判断,个人觉得这个没必要。这会导致 Vue
不论在哪段使用都会有较多的代码冗余。我认为一个理想的框架应该是足够的可配置的,至少对于开发人员来说应该如此。
所以我觉得应该想 react
那样,在开发哪端的项目就引入相应的库即可,而不是将代码全部都聚合到同一个库中。
以下我认为是可以做的,比如在开发 web
应用时,这样写
import vue from 'vue'import vue-dom from 'vue-dom'vue.use(vue-dom)在开发
WEEX
应用时: import vue from 'vue'import vue-dom from 'vue-weex'vue.use(vue-weex)在开发
SSR
时: import vue from 'vue'import vue-dom from 'vue-ssr'vue.use(vue-ssr)当然如果说非要一套代码统一
3
端 import vue from 'vue'import vue-dom from 'vue-dynamic-import'vue.use(vue-dynamic-import)
vue-dynamic-import
这个组件用于环境判断,动态导入相应环境的插件。
这种想法也是我想把 RD
给独立出来的原因,一个模块足够的独立,让环境的判断交给程序员来决定,因为大部分项目是仅仅需要其中的一个功能,而不需要全部的功能的。
以上,更多关于 Vue
的内容,已经关于 RD
的编写过程,可以到
原文发布时间为:2018年06月28日
原文作者:掘金
本文来源: 如需转载请联系原作者