|-core
|-grammer
|=vbind.js 分析处理v-bind指令
|=vfor.js 分析处理v-for指令
|=vmodel.js 分析处理v-model指令
|=von.js 分析处理v-on指令
|-instance
|=index.js STH框架主函数
|=init.js 给Sth构造函数加入初始化方法
|=mount.js DFS算法构建虚拟dom树
|=proxy.js 代理data对象
|=render.js 渲染页面
|-util
|=code.js 编译工具
|=objectUtil.js 其他工具函数
|-vdom
|=vnode.js 虚拟dom构造函数
|=index.js 入口函数
path:instance/index.js
这个模块完成的主要任务是
import {initMixIn} from './init.js';
import {renderMixIn} from './render.js';
function Sth(options) {
// 初始化Sth
this._init(options);
// 渲染
this._render()
}
// 添加初始化方法
initMixIn(Sth)
renderMixIn(Sth)
export default Sth
let uid = 0;
export function initMixIn(Sth) {
Sth.prototype._init = function(options) {
const vm = this;
// Sth唯一编号
this.uid = uid++;
// 记录一个对象是不是Sth对象
this.isSth = true;
};
}
function construtionProxy() {
if (当前数据是数组) {
construtionArrayProxy()
} else if (当前数据是对象) {
construtionObjectProxy()
} else {
抛出错误
}
}
// 对象代理方法
function construtionObjectProxy(vm, obj, namespace) {
let proxyObj = {};
for (let prop in obj) {
Object.defineProperty(proxyObj, prop, {
configurable: true,
get() {
return obj[prop]
},
set(value) {
console.log(`${getNameSpace(namespace, prop)}属性修改,新的值为${value}`)
obj[prop] = value;
renderData(vm, getNameSpace(namespace, prop));
// 生命周期update
if (vm._update != null) {
vm._update.call(vm);
}
}
})
Object.defineProperty(vm, prop, {
configurable: true,
get() {
return obj[prop]
},
set(value) {
console.log(`${getNameSpace(namespace, prop)}属性修改,新的值为${value}`)
obj[prop] = value;
renderData(vm, getNameSpace(namespace, prop));
// 生命周期update
if (vm._update != null) {
vm._update.call(vm);
}
}
})
// 递归 由于不知道obj[prop]是数组还是对象,所以使用construtionProxy
if (obj[prop] instanceof Object) {
proxyObj[prop] = construtionProxy(vm, obj[prop], getNameSpace(namespace, prop))
}
}
return proxyObj
}
代理数组除了要代理数组每一项之外,还要代理数组的方法,因为当数组通过push,pop等方法被改变时,我们也要检测到
// 代理数组
function construtionArrayProxy(vm, arr, namespace) {
let obj = {
eletype: "Array",
toString: () => {
let result = '';
for (let i = 0; i < arr.length; i++) {
result += arr + ', '
}
return result.slice(0, -2)
},
push() {},
pop() {},
shift() {},
unshift() {},
splice() {},
}
defArrayFunc.call(vm, obj, 'push', namespace, vm);
defArrayFunc.call(vm, obj, 'pop', namespace, vm);
defArrayFunc.call(vm, obj, 'shift', namespace, vm);
defArrayFunc.call(vm, obj, 'unshift', namespace, vm);
defArrayFunc.call(vm, obj, 'splice', namespace, vm);
arr.__proto__ = obj;
return arr
}
// 代理数组方法
const arrayProto = Array.prototype;
function defArrayFunc(obj, funcName, namespace, vm) {
Object.defineProperty(obj, funcName, {
enumerable: true,
configurable: true,
value: function(...args) {
let originFun = arrayProto[funcName];
const result = originFun.apply(this, args);
console.log(`${funcName}方法被调用`);
rebuild(vm, getNameSpace(namespace, ''));
renderData(vm, getNameSpace(namespace, ''));
// 生命周期update
if (vm._update != null) {
vm._update.call(vm);
}
return result
}
})
}
代理时还有一个很关键的概念是命名空间,通过命名空间可以准确定位被修改的值
之后预渲染时会创建节点与数据的映射关系,用到的也是命名空间
这样,通过递归,就代理了data对象
生成虚拟dom数
这是框架的核心,虚拟dom,通过虚拟dom,可以检测数据改变时,需要重新渲染的节点。
首先我们需要虚拟dom构造函数
let num = 0
export default class VNode{
constructor(
tag, // 标签名,例如DIV,SPAN,#TEXT
elm, // 真实节点
children, // 子节点
text, // 文本(仅文本节点存在)
data, // 暂时保留(v-for构建虚拟节点时,用来储存需要用到的数组命名空间)
parent, // 父级节点
nodeType, // 节点类型
) {
this.tag = tag;
this.elm = elm;
this.children = children;
this.text = text;
this.data = data;
this.parent = parent;
this.nodeType = nodeType;
this.env = {}, // 环境变量
this.instructions = null; // 存放指令
this.template = []; // 涉及模板
this.num = num++
}
}
除此之外还需要一些dom节点的基本知识
节点的nodeType 标签节点为1,文本节点为3,模板在文本节点中,所以渲染只需要渲染文本节点即可
dfs算法构建虚拟dom树
export function initMount(Sth) {
Sth.prototype.$mount = function (el) {
let vm = this;
let rootDom = document.getElementById(el);
mount(vm, rootDom);
}
}
export function mount(vm, el) {
// 进行挂载(生成虚拟dom树)
vm._vnode = constructVNode(vm, el, null);
// 进行预备渲染(将模板转换为对应的值)
prepareRender(vm, vm._vnode)
}
function constructVNode(vm, elm, parent) { // 深搜
let vnode = analysisAttr(vm, elm, parent);
if (!vnode) {
let children = [];
let text = getNodeText(elm);
let data = null;
let nodeType = elm.nodeType;
let tag = elm.nodeName;
vnode = new VNode(tag, elm, children, text, data, parent, nodeType);
if (elm.nodeType == 1 && elm.getAttribute('env')) {
vnode.env = mergeAttr(vnode.env, JSON.parse(elm.getAttribute('env')))
} else {
vnode.env = mergeAttr(vnode.env, parent ? parent.env : {})
}
}
let childs = vnode.elm.childNodes;
for (let i = 0; i < childs.length; i++) {
let childNodes = constructVNode(vm, childs, vnode);
if (childNodes instanceof VNode) {
vnode.children.push(childNodes)
} else { // v-for 返回节点数组
vnode.children = vnode.children.concat(childNodes)
}
}
return vnode
}
虚拟dom树就是与真实dom树一一对应的树形结构,用对象表示相对应的dom节点
预备渲染
// 预备渲染(创建了两个映射)
export function prepareRender(vm, vnode) {
if (vnode == null) {
return;
}
if (vnode.nodeType == 3) {
// 文本节点
analysisTemplateString(vnode);
}
if (vnode.nodeType == 0) {
setTemplate2vnode(vnode, vnode.data);
setVnode2template(vnode, vnode.data);
}
analysisAttr(vm, vnode);
for (let i = 0; i < vnode.children.length; i++) {
prepareRender(vm, vnode.children);
}
}
预备渲染主要构建了两个映射
模板到虚拟dom的映射(主要用于当模板值修改后,获取哪些节点需要修改)
虚拟dom到模板的映射(主要用于渲染时,将模板替换为对应真实值)
渲染
渲染主要渲染的是文本节点
将文本节点中的模板换成对应真实值即可
但是模板的真实值来自哪?模板真实值不一定来自sth._data,也有可能来自v-for声明的局部变量,还有可能来自计算属性,所以在取值时要注意
getTemplateValue方法就是可以从多个变量取到真实值的方法
function renderNode(vm, vnode) {
if (vnode.nodeType == 3) {
// 拿到这个文本节点用到的模板
let templates = vnode2template.get(vnode);
if (templates != null) {
let result = vnode.text;
for (let i = 0; i < templates.length; i++) {
let templateValue = getTemplateValue(
[vm._data, vnode.env, vm._computed],
templates
);
if (typeof templateValue == 'function') {
templateValue = templateValue.call(vm)
}
if (templateValue != null) {
result = result.replace(
"{{" + templates + "}}",
templateValue.toString()
);
}
}
vnode.elm.nodeValue = result;
}
} else if (vnode.nodeType == 1 && vnode.tag == "INPUT") {
// 拿到这个文本节点用到的模板
let templates = vnode2template.get(vnode);
if (templates != null) {
for (let i = 0; i < templates.length; i++) {
let templateValue = getTemplateValue(
[vm._data, vnode.env],
templates
);
if (templateValue != null) {
vnode.elm.value = templateValue;
}
}
}
} else {
for (let i = 0; i < vnode.children.length; i++) {
renderNode(vm, vnode.children);
}
}
}
这样就可以完成渲染
同时当数据修改后,可以找到使用到这个数据的节点,并且for循环这些节点,
这里有一个遗留的bug,就是当计算属性所需要的数据改变时,计算属性模板不会重新渲染,这是由于当数据改变时,只重新渲染了使用这个数据的模板,没有重新渲染相关计算属性模板,这里有个优化思路,在初始化计算属性的时候,进行语法分析,分析后建立一个属性到计算属性的映射,当数据修改后,同时查看有没有计算属性用到了这个数据,如果有,重新渲染计算属性对应的节点
v-model
v-model是一个较为简单的指令
只需要在递归挂载的时候,遇到标签节点就分析attribute,如果有v-model属性并且节点为input就去执行此方法
只需要给节点绑定onkeyup即可
export function vmodel(vm, elm, data) {
elm.onkeyup = function (event) {
setValue(vm._data, data, elm.value)
}
}
v-for
v-for是整个框架最难的部分,逻辑十分复杂
大致思路是当遇到含有v-for属性的标签时,先在真实dom中删除这个标签,然后拿到v-for循环的那个数组,通过数组长度来建立一批一模一样的节点,并且将源节点的属性也复制过来,接下来给每个节点设置局部变量,保存在vnode的env中,这样在取值的时候,可以在env中拿到对应数据
代码见文章顶部的github地址
这存在一个bug,含有v-for的dom节点的兄弟节点会被解析两遍,对应的v-on事件也会被绑定两遍,暂无很好的解决方法
数组修改后,重新渲染
当数组修改后找到用到了这个数组的节点,重新构建这个节点的父节点下的所有内容,然后清空之前的虚拟节点与vnode的映射,重新建立映射,重新建立映射是因为由于数组改变之前的映射关系可能就不准确了,这时候需要重新建立整个映射关系,由于建立映射关系不需要操作dom所以这个过程并不会慢,最后局部重新渲染即可
代码见文章顶部的github地址
v-bind
v-bind的解析需要等虚拟dom构建完成以后再解析,因为绑定的变量有可能来自v-for产生的局部变量
获取v-bind:xxx的属性值后有两种情况,分别来说一下
属性值为变量
这种比较好处理,直接获取到变量对应的值,直接赋值即可
属性为表达式
这种情况就比较麻烦,值有可能为{red:obj.x > 0};
这时候需要进行语法分析,这里用了一个很奇妙的处理方法
首先构建一个执行环境(字符串),执行环境中声明这个节点能访问到的左右变量,并且声明一个bool,初始值为false,然后将判断条件作为字符串拼接进去,eval()执行这个字符串,获取bool,如果为真,将该属性加入返回结果中,如果是为false,就不加
代码过长,见文章顶部的github地址
v-on
v-on实现较为简单,与v-bind大致相同,只是把设置attribute改成了绑定事件。
这里只需要注意使用bind改变事件的this指向即可
如果时间有参数的话,需要做一个判断,检查@xxx的属性值是否存在’(’
如果存在,就老办法获取到每一个属性值对应的真实值,并且bind的时候拼接在后面
export function checkVOn(vm, vnode) {
if (vnode.nodeType == 1) {
let attrNames = vnode.elm.getAttributeNames();
let filterAttrNames = attrNames.filter((item) => item.startsWith('v-on:') || item.startsWith('@'));
for (let i = 0; i < filterAttrNames.length; i++) {
let eventName = filterAttrNames.includes(':') ? filterAttrNames.split(':')[1] : filterAttrNames.split('@')[1];
von(vm, vnode, eventName, vnode.elm.getAttribute(filterAttrNames))
}
}
}
function von(vm, vnode, eventName, name) {
let argList = []
let index = name.indexOf('(');
let funName = name
if (index >= 0) {
funName = name.slice(0, index);
argList = name.slice(index + 1, -1).split(',');
console.log(argList);
}
for (let i = 0; i < argList.length; i++) {
argList = getTemplateValue([vm._data, vnode.env],argList)
}
let method = getValue(vm._methods, funName);
if (method) {
vnode.elm.addEventListener(eventName, method.bind(vm, ...argList))
}
}
生命周期函数很简单,只需要在init的时候初始化,并且在特定时间调用即可,注意修改this指向
例如init.js中,代理data前执行beforeCreate方法
代理后执行created方法
挂载前执行beforeMount方法等
欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) | 黑马程序员IT技术论坛 X3.2 |