在实现 VUE 中 MVVM 的系列文章的最后一篇文章中说道:我以为可响应的数据结构做用很大,在整理了一段时间后,这是咱们的最终产出:RD - Reactive Datacss
ok 回到整理,这篇文章咱们不研究 Vue
了,而是根据咱们如今的研究成果来手撸一个 MVVM
。html
先看看下咱们的研究成果:一个例子vue
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: Hello demo.fullName // console: aco yang demo.testMethod() // console: test
写法上与 Vue
的同样,先说说拥有那些属性吧:node
关于数据react
关于生命周期git
关于实例间关系程序员
实例下的方法:github
关于事件web
其余方法npm
类下方法:
以上即是全部的内容,由于 RD
仅仅关注于数据的变化,因此生命周期就就只有建立和销毁。
对比与 Vue
多了一个 $initProp
,一样的因为仅仅关注于数据变化,因此当父实例相关的 prop
发生变化时,须要手动通知子组件修改相关数据。
其余的属性以及方法的使用与 Vue
一致。
ok 大概说了下,具体的内容能够点击查看
有了 RD
咱们来手撸一个 MVVM
框架。
咱们先肯定咱们大体须要什么?
dom
结构)ok 模板引擎,JSX
语法不错,来一份。
接着虚拟节点,github
上搜一搜,ok 找到了,点击查看
全部条件都具有了,咱们的实现思路以下:
RD + JSX + VNode = MVVM
具体的实现咱们一边写 TodoList
一边实现
首先咱们得要有一个 render
函数,ok 配上,先来个标题组件 Title
和一个使用标题的 App
的组件吧。
能够对照完整的 demo
查看一下内容,demo。
var App = RD.extend({ render(h) { return ( <div className='todo-wrap'> <Title/> </div> ) } }) var Title = RD.extend({ render(h) { return ( <p className='title'>{this.title}</p> ) }, data(){ return { title:'这是个标题' } } })
这里就不说明 JSX
语法了,能够在 babel
上看下转码的结果,点击查看。
至于 render
的参数为何是 h
?这是大部分人都承认这么作,因此咱们这么作就好。
根据 JSX
的语法,咱们须要实现一个建立虚拟节点的方法,也就是 render
须要传入的参数 h
。
ok 实现一下,咱们编写一个插件使用 RD.use
来实现对于实例的扩展
// demo/jsxPlugin/index.js export 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.js import {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.js function 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.js import {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.js import 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.js import {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.js function 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
的编写过程,能够到个人博客查看