vue源码之数据驱动 - 3.使用柯里化实现封装
什么是柯里化?为什么要使用柯里化来实现封装?我们通过概念和案例来说明一下:
概念
一个函数原本有多个参数,传入一个参数,生成一个新的函数,新的函数接收剩余的参数来运行得到结果。
柯里化相关学习资源:
案例
1.判断元素
Vue 本质上是使用 HTML 的字符串作为模版的,将字符串的模版转换为 AST(抽象语法树),再转换为 VNode。
- 模版 -> AST 最消耗性能
- AST -> VNode
- VNode -> DOM
在 Vue 中,每一个标签可以是真正的HTML标签,也可以是自定义标签,那怎么区分?
在 Vue 源码中其实是将所有可以用的 HTML 标签已经存起来
假设只考虑几个标签
let tags = 'div,p,a,img,ul,li'.split(',');
需要一个函数,判断一个标签是否为 内置的标签
function isHTMLTag (tagName) { tagName = tagName.toLowerCase(); return tags.some(item => item===tagName) }
如果tags有6种,模版中有10的内置标签需要判断,那么就需要60次循环,显然这种方式会消耗大量内存。
下面将通过柯里化来实现该操作,将时间复杂度从O(n)->O(1):
let tags = 'div,p,a,img,ul,li'.split(','); function makeMap(keys) { let set = {} // 集合 tags.forEach(key => set[key] = true); return function (tagName) { return !!set[tagName.toLowerCase()] } } let isHTMLTag = makeMap(tags)
定义 makeMap 方法,该方法在初始时循环一次,使用 set (结构为:{div:true,p:true...})来存储标签便利之后的状态,返回一个新函数,每次在调用新函数的时候,都相当于直接取值,因此,时间复杂度为O(1)。
2.虚拟DOM都render方法
思考: Vue 项目模版转换为抽象语法树需要执行几次?
- 页面一开始加载需要渲染
- 每一个属性( 响应式 )数据在发生变化的时候需要渲染
- watch,computed等等
之前的代码 每次需要渲染的时候,模版就会解析一次
reder 的作用是将虚拟DOM转换为真正DOM加到页面中
- 虚拟DOM可以降级理解为 AST
- 一个项目运行时候,模版是不会变的,就表示 AST 是不会变的
我们可以将代码进行优化,将虚拟DOM缓存起来,生成一个函数,函数只需要传入数据,就可以得到真正的DOM
下面我们将上一节(vue源码之数据驱动 - 2.html模拟vue实现数据渲染)用到的代码进行封装:
// 虚拟DOM构造函数 class VNode { constructor(tag, data, value, type) { this.tag = tag && tag.toLowerCase() this.data = data this.value = value this.type = type this.children = [] } appendChild(vnode) { this.children.push(vnode) } } // 由 HTML DOM -> VNode : 将这个函数当做 compiler 函数 function getVNode(node) { let nodeType = node.nodeType let _vnode = null if (nodeType === 1) { // 元素 let nodeName = node.nodeName let attrs = node.attributes let _attrObj = {} for (let i = 0; i < attrs.length; i++) { _attrObj[attrs[i].nodeName] = attrs[i].nodeValue } _vnode = new VNode(nodeName, _attrObj, undefined, nodeType) // 考虑 node 的子元素 let childNodes = node.childNodes for (let i = 0; i < childNodes.length; i++) { _vnode.appendChild(getVNode(childNodes[i])) // 递归 } } else if (nodeType === 3) { // 文本 _vnode = new VNode(undefined, undefined, node.nodeValue, nodeType) } return _vnode } // 将 vnode 转换为真正的 DOM function parseVNode(vnode) { // 创建真实DOM let type = vnode.type if (type === 3) { return document.createTextNode(vnode.value) // 创建文本节点 } else if (type === 1) { let _node = document.createElement(vnode.tag) // 属性 let data = vnode.data Object.keys(data).forEach(key => { let attrName = key let attrValue = data[key] _node.setAttribute(attrName, attrValue) }) // 子元素 let children = vnode.children children.forEach(subvnode => { _node.appendChild(parseVNode(subvnode)); // 递归转换子元素 ( 虚拟DOM ) }) return _node } } function JGVue(options) { this._data = options.data || {} let elm = document.querySelector(options.el) this._template = elm this._parent = elm.parentNode this.mount() // 挂载 } // 根据路径访问对象成员 function getValueByPath(obj, path) { let paths = path.split('.') let res = obj; let prop; while (prop = paths.shift()) { res = res[prop] } return res } let rkuohao = /\{\{(.+?)\}\}/g // 将带有坑的Vnode与数据data结合,得到填充数据的VNode function combine(vnode, data) { let _type = vnode.type let _data = vnode.data let _value = vnode.value let _tag = vnode.tag let _children = vnode.children let _vnode = null if (_type === 3) { // 文本 // 对文本处理 _value = _value.replace(rkuohao, (_, g) => { return getValueByPath(data, g.trim()) }) _vnode = new VNode(_tag, _data, _value, _type) } else if (_type === 1) { // 元素 _vnode = new VNode(_tag, _data, _value, _type) _children.forEach(_subvnode => _vnode.appendChild(combine(_subvnode, data))); } return _vnode } JGVue.prototype.mount = function () { // 需要提供一个 reder 方法:生成虚拟DOM this.render = this.createRederFn() this.mountComponent() } JGVue.prototype.mountComponent = function () { // 执行 mountComponent 函数 let mount = () => { this.update(this.render()) } mount.call(this) // 本质上应该交给 watcher 来调用 } // 在真正的Vue中,使用了二次提交的设计结构 // 1. 在页面中的DOM和虚拟DOM是一一对应关系 // 2. 先有 AST 和数据生成 VNode(新,reder) // 3. 将旧VNode和新VNode比较( diff ) ,更新 ( update ) // 这里是生成reder函数, 目的是缓存抽象语法树( 我们使用虚拟DOM来模拟) JGVue.prototype.createRederFn = function () { let ast = getVNode(this._template) // Vue : 将AST + data => VNode // 带有坑的 VNode + data => 含有数据的VNode return function render() { let _tmp = combine(ast, this._data) return _tmp } } // 将虚拟DOM渲染到页面中:diff算法就在这里 JGVue.prototype.update = function (vnode) { // this._parent.replaceChild() let realDOM = parseVNode(vnode) // this._parent.replaceChild(realDOM, this._template) this._parent.replaceChild(realDOM, document.querySelector("#root")) } let app = new JGVue({ el: '#root', data: { name: '张三', age: '19', gender: "男" } })