从零开始的Vue世界-02

1,120 阅读2分钟

从零开始的Vue世界-01

上节已经搭建完开发环境,并且实现了数据响应式,这一节来实现vue页面的渲染

核心思想就是将模板变成js语法,然后通过js语法生成虚拟dom,当数据改变后比较虚拟dom差异的部分,组后更新需要更新的地方

模板编译

获取标签和内容

首先要找到需要解析的标签 init.js


import { initState } from "./state";

export function initMixin(Vue) { // 就是给Vue增加init方法的
    Vue.prototype._init = function (options) { // 用于初始化操作
        // vue  vm.$options 就是获取用户的配置
        const vm = this;
        vm.$options = options; // 将用户的选项挂载到实例上

        // 初始化状态
        initState(vm);
        console.log(options)
        if (options.el) {
            console.log(options.el)
            vm.$mount(options.el); // 实现数据的挂载
        }
    }

    Vue.prototype.$mount = function (el) {
        const vm = this;
        el = document.querySelector(el);
        let ops = vm.$options
        if (!ops.render) { // 先进行查找有没有render函数
            let template; // 没有render看一下是否写了tempate, 没写template采用外部的template
            if (!ops.template && el) { // 没有写模板 但是写了el
                template = el.outerHTML
            }else{
                if(el){
                    template = ops.template // 如果有el 则采用模板的内容
                }
            }
            // 写了temlate 就用 写了的template
            if(template && el){
                console.log('templatetemplate')
               console.log(template)
            }
        }


    }

}


index.html

<div id="app" style="color:red;background:yellow">
    <div style="color:green" key="123">
        {{ name }} hello {{age}} hello
    </div>
    <li> world </li>
</div>
    <script src="vue.js"></script>
 <script>
        const el = new Vue({
            data(){
                return {
                    name: '张三',
                    age: 12,
                    obj:{
                        a:1
                    }
                }
            },
            el: '#app'
        })
    </script>

这样就取到了需要解析的标签内容

image.png

解析标签和内容,生成ast树

获取到标签内容后需要将其变为js语法ast树,用正则来匹配标开始标签-入栈,结束标签-出栈,新建

parse.js

const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 他匹配到的分组是一个 标签名  <xxx 匹配到的是开始 标签的名字
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);  // 匹配的是</xxxx>  最终匹配到的分组就是结束标签的名字
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;  // 匹配属性
// 第一个分组就是属性的key value 就是 分组3/分组4/分组五
const startTagClose = /^\s*(\/?)>/;  // <div> <br/>


// 对模板进行编译处理  


export function parseHTML(html) { // html最开始肯定是一个  </div>

    const ELEMENT_TYPE = 1;
    const TEXT_TYPE = 3;
    const stack = []; // 用于存放元素的
    let currentParent; // 指向的是栈中的最后一个
    let root;

    // 最终需要转化成一颗抽象语法树
    function createASTElement(tag, attrs) {
        return {
            tag,
            type: ELEMENT_TYPE,
            children: [],
            attrs,
            parent: null
        }
    }
    // 利用栈型结构 来构造一颗树
    function start(tag, attrs) {
        let node =  createASTElement(tag,attrs); // 创造一个ast节点
        if(!root){ // 看一下是否是空树
            root = node; // 如果为空则当前是树的根节点
        } 
        if(currentParent){
            node.parent = currentParent; // 只赋予了parent属性
            currentParent.children.push(node); // 还需要让父亲记住自己
        }
        stack.push(node);
        currentParent = node; // currentParent为栈中的最后一个
    }
    function chars(text) { // 文本直接放到当前指向的节点中
        text = text.replace(/\s/g,' '); // 如果空格超过2就删除2个以上的
        text && currentParent.children.push({
            type:TEXT_TYPE,
            text,
            parent:currentParent
        });
    }
    function end(tag) {
       let node =  stack.pop();  // 弹出最后一个, 校验标签是否合法
       currentParent = stack[stack.length - 1];
    }
    function advance(n) {
        html = html.substring(n);
    }
    function parseStartTag() {
        const start = html.match(startTagOpen);
        if (start) {
            const match = {
                tagName: start[1], // 标签名
                attrs: []
            }
            advance(start[0].length);
            // 如果不是开始标签的结束 就一直匹配下去
            let attr, end
            while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
                advance(attr[0].length);
                match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] || true })
            }
            if (end) {
                advance(end[0].length)
            }
            return match;
        }
        return false; // 不是开始标签
    }
    while (html) {
        // 如果textEnd 为0 说明是一个开始标签或者结束标签
        // 如果textEnd > 0说明就是文本的结束位置
        let textEnd = html.indexOf('<');  // 如果indexOf中的索引是0 则说明是个标签
        if (textEnd == 0) {
            const startTagMatch = parseStartTag(); // 开始标签的匹配结果
            if (startTagMatch) { // 解析到的开始标签
                start(startTagMatch.tagName, startTagMatch.attrs)
                continue
            }
            let endTagMatch = html.match(endTag);
            if (endTagMatch) {
                advance(endTagMatch[0].length);
                end(endTagMatch[1])
                continue;
            }
        }
        if (textEnd > 0) {
            let text = html.substring(0, textEnd); // 文本内容
            if (text) {
                chars(text)
                advance(text.length); // 解析到的文本 
            }
        }
    }
    

    return root;
}
import { parseHTML } from "./parse";

export function compileToFunction(template) {

    将template 转化成ast语法树
    let ast = parseHTML(template);
    return ast;
}

init.js中$mount获取模板后进行转换

import { compileToFunction } from "./compiler";
 Vue.prototype.$mount = function (el) {
        const vm = this;
        el = document.querySelector(el);
        let ops = vm.$options
        if (!ops.render) { // 先进行查找有没有render函数
            let template; // 没有render看一下是否写了tempate, 没写template采用外部的template
            if (!ops.template && el) { // 没有写模板 但是写了el
                template = el.outerHTML
            }else{
                if(el){
                    template = ops.template // 如果有el 则采用模板的内容
                }
            }
            // 写了temlate 就用 写了的template
            if(template && el){
                const ast = compileToFunction(template);
                console.log('astast--')
                console.log(ast)
            }
        }


    }

这样就获取了ast树,将模板变为了js语法

image.png

将ast树生成render函数

<div style="color:red">hello {{name}} <span></span></div>
render(){
   return _c('div',{style:{color:'red'}},_v('hello'+_s(name)),_c('span',undefined,''))
}

标签_c(tag,attrs,...children), 值_v(content+_s([key])),内容_c(tag,content)

import { parseHTML } from "./parse";

function genProps(attrs) {
    let str = ''// {name,value}
    for (let i = 0; i < attrs.length; i++) {
        let attr = attrs[i];
        if (attr.name === 'style') {
            // color:red;background:red => {color:'red'}
            let obj = {};
            attr.value.split(';').forEach(item => { // qs 库
                let [key, value] = item.split(':');
                obj[key] = value;
            });
            attr.value = obj
        }
        str += `${attr.name}:${JSON.stringify(attr.value)},` // a:b,c:d,
    }
    return `{${str.slice(0, -1)}}`
}
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // {{ asdsadsa }}  匹配到的内容就是我们表达式的变量
function gen(node) {
    if (node.type === 1) {
        return codegen(node);
    } else {
        // 文本
        let text = node.text
        if (!defaultTagRE.test(text)) {
            return `_v(${JSON.stringify(text)})`
        } else {
            //_v( _s(name)+'hello' + _s(name))
            let tokens = [];
            let match;
            defaultTagRE.lastIndex = 0;
            let lastIndex = 0;
            // split
            while (match = defaultTagRE.exec(text)) {
                let index = match.index; // 匹配的位置  {{name}} hello  {{name}} hello
                if (index > lastIndex) {
                    tokens.push(JSON.stringify(text.slice(lastIndex, index)))
                }
                tokens.push(`_s(${match[1].trim()})`)
                lastIndex = index + match[0].length
            }
            if (lastIndex < text.length) {
                tokens.push(JSON.stringify(text.slice(lastIndex)))
            }
            return `_v(${tokens.join('+')})`
        }
    }
}
function genChildren(children) {
    return children.map(child => gen(child)).join(',')
}
function codegen(ast) {
    let children = genChildren(ast.children);
    let code = (`_c('${ast.tag}',${ast.attrs.length > 0 ? genProps(ast.attrs) : 'null'
        }${ast.children.length ? `,${children}` : ''
        })`)

    return code;
}
export function compileToFunction(template) {

    // 1.就是将template 转化成ast语法树
    let ast = parseHTML(template);

    // 2.生成render方法 (render方法执行后的返回的结果就是 虚拟DOM)

    let code = codegen(ast);
    code = `with(this){return ${code}}`; // 函数里是向当前的vm取参数,使用with来去指定实例
    let render = new Function(code); // 根据代码生成render函数

    //  _c('div',{id:'app'},_c('div',{style:{color:'red'}},  _v(_s(vm.name)+'hello'),_c('span',undefined,  _v(_s(age))))

    return render;
}

index.html

 <div id="app" style="color:red;background:yellow"><div style="color:green" key="123"></div><li> {{name}}world{{age}} </li></div>
    <script src="vue.js"></script>
    <script>
        const el = new Vue({
            data(){
                return {
                    name: '张三',
                    age: 12,
                    obj:{
                        a:1
                    }
                }
            },
            el: '#app'
        })
    </script>

得到render函数

image.png

执行render函数,生成虚拟dom,patch函数渲染页面

上一步得到render函数后,下面就要执行render函数,渲染出页面来了,新建lifecycle/index.js index.js

import { initMixin } from "./init";
import { initLifeCycle } from "./lifecycle";

function Vue(options){ // options就是用户的选项
    this._init(options); // 默认就调用了init
}

initMixin(Vue); // 扩展了init方法
initLifeCycle(Vue); 
export default Vue

init.js

import { mountComponent } from "./lifecycle";
export function initMixin(Vue) { // 就是给Vue增加init方法的
 //...
 Vue.prototype.$mount = function (el) {
       
        const vm = this;
        el = document.querySelector(el);
        let ops = vm.$options
        if (!ops.render) { // 先进行查找有没有render函数 
            let template; // 没有render看一下是否写了tempate, 没写template采用外部的template
            if (!ops.template && el) { // 没有写模板 但是写了el
                template = el.outerHTML
            }else{
                if(el){
                    template = ops.template // 如果有el 则采用模板的内容
                }
            }
            // 写了temlate 就用 写了的template
            if(template && el){
                // 这里需要对模板进行编译 
                const render = compileToFunction(template);
                ops.render = render;
            }
        }
        mountComponent(vm,el); // 组件的挂载  时候不能使用template
    }
    }

lifecycle/index.js

export function initLifeCycle(Vue){
    Vue.prototype._update = function(vnode){ // 将vnode转化成真实dom
        const vm = this;
        const el = vm.$el;

        // patch既有初始化的功能  又有更新 
        vm.$el = patch(el,vnode);
    }

    // _c('div',{},...children)
    Vue.prototype._c = function(){
       return  createElementVNode(this,...arguments)
    }
    // _v(text)
    Vue.prototype._v = function(){
        return createTextVNode(this,...arguments)
    }
    Vue.prototype._s = function(value){
        if(typeof value !== 'object') return value
        return JSON.stringify(value)
    }
    Vue.prototype._render = function(){
        // 当渲染的时候会去实例中取值,我们就可以将属性和视图绑定在一起
        
        return this.$options.render.call(this); // 通过ast语法转义后生成的render方法
    }
}

export function mountComponent(vm,el){ // 这里的el 是通过querySelector处理过的
    vm.$el = el;

    // 1.调用render方法产生虚拟节点 虚拟DOM

    vm._update(vm._render()); // vm.$options.render() 虚拟节点

    // 2.根据虚拟DOM产生真实DOM 

    // 3.插入到el元素中

}

这里执行了vm.$options.render()_c,_v等会执行,生成虚拟dom,新建vnode/index.js

vnode/index.js


// h()  _c()
export function createElementVNode(vm, tag, data, ...children) {
    if (data == null) {
        data = {}
    }
    let key = data.key;
    if (key) {
        delete data.key
    }
    return vnode(vm, tag, key, data, children);
}
// _v();
export function createTextVNode(vm, text) {
    return vnode(vm, undefined, undefined, undefined, undefined, text);
}
// ast一样吗? ast做的是语法层面的转化 他描述的是语法本身 (可以描述js css html)
// 我们的虚拟dom 是描述的dom元素,可以增加一些自定义属性  (描述dom的)
function vnode(vm, tag, key, data, children, text) {
    return {
        vm,
        tag,
        key,
        data,
        children,
        text
        // ....
    }
}

这样就执行render生成了虚拟的dom,然后通过_update()生成/更新dom lifecycle/index.js

import { createElementVNode, createTextVNode } from "./vdom"


function createElm(vnode){
    let {tag,data,children,text} = vnode;
    if(typeof tag === 'string'){ // 标签
        vnode.el =  document.createElement(tag); // 这里将真实节点和虚拟节点对应起来,后续如果修改属性了
        patchProps(vnode.el,data);
        children.forEach(child => {
            vnode.el.appendChild( createElm(child))
        });
    }else{
        vnode.el = document.createTextNode(text)
    }
    return vnode.el
}
function patchProps(el,props){
    for(let key in props){
        if(key === 'style'){ // style{color:'red'}
            for(let styleName in props.style){
                el.style[styleName] = props.style[styleName];
            }
        }else{
            el.setAttribute(key,props[key]);
        }
    }
}
function patch(oldVNode,vnode){
    // 写的是初渲染流程 
    const isRealElement = oldVNode.nodeType;
    if(isRealElement){
        const elm = oldVNode; // 获取真实元素
        const parentElm = elm.parentNode; // 拿到父元素
        let newElm =  createElm(vnode);
        parentElm.insertBefore(newElm,elm.nextSibling);
        parentElm.removeChild(elm); // 删除老节点

        return newElm
    }else{
        // diff算法
    }
}


export function initLifeCycle(Vue){
    Vue.prototype._update = function(vnode){ // 将vnode转化成真实dom
        const vm = this;
        const el = vm.$el;

        // patch既有初始化的功能  又有更新 
        vm.$el = patch(el,vnode);
    }

    // _c('div',{},...children)
    Vue.prototype._c = function(){
       return  createElementVNode(this,...arguments)
    }
    // _v(text)
    Vue.prototype._v = function(){
        return createTextVNode(this,...arguments)
    }
    Vue.prototype._s = function(value){
        if(typeof value !== 'object') return value
        return JSON.stringify(value)
    }
    Vue.prototype._render = function(){
        // 当渲染的时候会去实例中取值,我们就可以将属性和视图绑定在一起
        
        return this.$options.render.call(this); // 通过ast语法转义后生成的render方法
    }
}

export function mountComponent(vm,el){ // 这里的el 是通过querySelector处理过的
    vm.$el = el;

    // 1.调用render方法产生虚拟节点 虚拟DOM

    vm._update(vm._render()); // vm.$options.render() 虚拟节点

    // 2.根据虚拟DOM产生真实DOM 

    // 3.插入到el元素中

}
// vue核心流程 1) 创造了响应式数据  2) 模板转换成ast语法树  
// 3) 将ast语法树转换了render函数 4) 后续每次数据更新可以只执行render函数 (无需再次执行ast转化的过程)
// render函数会去产生虚拟节点(使用响应式数据)
// 根据生成的虚拟节点创造真实的DOM

至此页面渲染完成。