4. 模板解析,生成render函数,渲染页面

解析模板,生成render函数,执行render函数,实现视图渲染

1.模板转化成ast语法树

2.ast语法树生成render函数

3.执行render函数生成虚拟dom

4.执行_update方法生成真实dom

5.真实dom替换掉模板

在初始化方法中(_init()), 对元素进行处理, 执行挂载方法

​ 在init.js

Vue.prototype._init = function(options) {
        // 获取vue实例, 这里的this指向vue实例
        const vm = this
        // 获取用户选项, 方便后续获取参数, 很多地方都是挂载到vue上面的
        vm.$options = options
      
        // 初始化状态
        initState(vm)

        // 如果有元素的话, 执行挂载方法,然后添加该方法
        if(options.el) {
            vm.$mount(options.el)
        }
    }

// 添加 $mount方法

​ 添加的$mount方法主要实现:

  1. 获取template模板, 如果没有模板, 就用包裹el的那层, 即el.outHTML作为template, 注意el.outHTML不是body, 如果有, 直接使用options的template

    let template
    if(!ops.template && el) {
        template = el.outerHTML
    } else {
        if(el) {
            template = ops.template
        }
    }
    
  2. 通过template生成render方法, 关键方法 compileToFunction, 实现内容

    1. 通过*parseHtml*方法, 将template转化为ast语法树
    2. 通过with + new Function() 生成render方法
    
    1. 执行mountComponent方法实现视图的更新,里面包括两个关键方法

    2. vm._render方法, 就是执行组转的render方法, 生成虚拟dom

    3. vm._update方法, 生成真实dom, 并替换掉模板

具体实现的方法:

dist/3.template.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <div class="bx1" style="backgroundColor: red;fontWeight: bolder">{{name}} hello</div>
        <li>{{age}}</li>
    </div>
    <script src="vue.js"></script>
    <script>
        // 新建一个vue实例
        const vm = new Vue({
            el: "#app",
            data() {
                return {
                    name: 'ywj',
                    age: 18
                }
            }
        })
        setTimeout(() => {
            vm.name = 'jerry'
            vm.age = 13
            vm._update(vm._render())
        }, 2000)
    </script>
</body>
</html>

init.js

import { compileToFunction } from "./compiler"
import { mountComponent } from "./lifecycle"
import { initState } from "./state"
import { createElementVNode, createTextVNode } from "./vdom"


export function initMixin(Vue) {
    Vue.prototype._init = function(options) {
        // 获取vue实例, 这里的this指向vue实例
        const vm = this
        // 获取用户选项, 方便后续获取参数, 很多地方都是挂载到vue上面的
        vm.$options = options
        // 初始化状态
        initState(vm)

        // 如果有元素的话, 执行挂载方法,然后添加该方法
        if(options.el) {
            vm.$mount(options.el)
        }
    }

    // 挂载方法
    Vue.prototype.$mount = function(el) {
        // 获取实例
        const vm = this
        // 将el变成一个真实的元素
        el = document.querySelector(el)
        // 获取options
        let ops = vm.$options
        // 获取render方法, 没有就生成, 如果没有, 先获取template, 有template生成render方法
        if(!ops.render) {
            let template
            if(!ops.template && el) {
                template = el.outerHTML
            } else {
                if(el) {
                    template = ops.template
                }
            }
            
            // console.log('template:', template)

            // 将template转化为render方法
            if(template) {
                // 新建文件compiler/index.js文件, 添加compileToFunction方法
                const render = compileToFunction(template)

                ops.render = render

                // console.log('render:', render)

                // 有了render之后, 挂载组件   
                // 就是执行一个render方法, 产生虚拟dom, 然后挂载到el中
                mountComponent(vm, el)
            }
        }

    }
}

新建文件 compiler/index.js

import { parseHtml } from "./parse";


export function compileToFunction(template) {
    // 先将template转化为ast语法树, 同目录下新建parse.js文件, 添加parseHtml方法
    let ast = parseHtml(template)

    // console.log('ast:' , ast)

    // 使用ast语法树生成代码
    let code = codegen(ast)

    console.log('code:', code)

    // 通过code生成render方法 , 也是模板引擎的实现原理 with + new Function
    code = `with(this){return ${code}}`

    let render = new Function(code)
    return render
}

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
}

/**
 * 
 * 生成属性
 * attrs: [{name: 'id', value: 'app'}]
 * 要拼成的结构: id:app,key:value   
 * 如 id: app, class: appcalss
 * 最外面加上一个 { id: app, class: app}
 * 注意: 需要对style特殊处理
 */
 function genProps(attrs) {
    let str = ''
    for(let i = 0; i < attrs.length; i ++) {
        let attr = attrs[i]
        // style需要特殊处理, 
        // 如不处理: style: "color: red;bgc: blue"
        // 需要变成: style: {color: 'red',bgc: 'blue'}
        // 所以 style的value是一个object
        if(attr.name === 'style') {
            let obj = {}
            // debugger
            attr.value.split(';').forEach(item => {
                let [key, value] = item.split(':')
                obj[key] = value
            })
            attr.value = obj
        }
        // 这里的value需要是一个字符串
        str += `${attr.name}:${JSON.stringify(attr.value)},`
    }
    // 最后会多出一个逗号 去掉
    return `{${str.slice(0, -1)}}`
}

// 生成孩子节点
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g     // 匹配的内容就是表达式的变量
function gen(node) {
    if(node.type === 1) {   // 如果是元素, 直接codegen
        return codegen(node)
    } else {
        // 如果是文本, 有两种情况, 'hello' 和  {{name}}
        let text = node.text
        if(!defaultTagRE.test(text)) {  // 没匹配上, 表示纯文本
            return `_v(${JSON.stringify(text)})`
        } else {
            let tokens = []
            let match
            defaultTagRE.lastIndex = 0
            let lastIndex = 0
            // match 长这样 ['{{name}}', 'name', index: 0, input: '{{name}}hello', groups: undefined]
            while(match = defaultTagRE.exec(text)) {
                let index = match.index     // 匹配的位置
                if(index > lastIndex) { // 匹配的位置大于上一次匹配的位置, 说明在匹配到的位置之前有文本, 要push进去, 需要添加json.stringify
                    tokens.push(JSON.stringify(text.slice(lastIndex, index)))
                }
                tokens.push(`_s(${match[1].trim()})`)   // 将匹配到的变量加一个 _s , 去掉前后空格 {{  name  }} 这种情况
                lastIndex = index + match[0].length // 然后将lastindex 变成本次匹配到的位置加上匹配到的长度, 循环
            }
            if(lastIndex < text.length) {   // 如果lastindex < text.length , 说明最后面还有文本, 也要stringify之后push进去
                tokens.push(JSON.stringify(text.slice(lastIndex)))
            }
            return `_v(${tokens.join('+')})`
        }
    }
}

/**
 * 
 * 生成孩子, 用逗号拼起来
 */
 function genChildren(children) {
     return children.map(child => gen(child)).join(',')
 }

新建文件 compiler/parse.js




// copy一波正则表达式
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/       // 匹配属性, 第一个分组是属性的key, value可能是分组3或4或5
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)        // 匹配到的是 <div  最终匹配到的分组是开始标签的名称
const startTagClose = /^\s*(\/?)>/  // 结束标签  </div>  <br/>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)     // 匹配到的是</xxx>, 最终匹配到的分组是结束标签的名称
const doctype = /^<!DOCTYPE [^>]+>/i
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g     // 匹配的内容就是表达式的变量

// 这里的html是字符串
export function parseHtml(html) {
    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)     // 先产生一棵树
        if(!root) {
            root = node         // 如果没有跟节点, 这个就作根节点
        }
        if(currentParent) {             // 如果有根节点, 当前节点的parent就是currentParent, 
            node.parent = currentParent     // 同时, currentParent的children是node
            currentParent.children.push(node)
        }
        stack.push(node)
        currentParent = node        // 当前节点作为栈中的最后一个
    }
    function chars(text) {
        text = text.replace(/\s/g, '');
        // 如果当前节点是文本
        text && currentParent.children.push({
            type: TEXT_TYPE,
            text,
            parent: currentParent
        })

    }
    function end(tag) {
        // 遇到结束标签, 直接当前的最后一个, 更新currentparent
        stack.pop()
        currentParent = stack[stack.length - 1]
    }
    function advance(n) {
        html = html.substring(n)
    }
    function parseStartTag() {
        const start = html.match(startTagOpen)
        if(start) {     // 如果没有匹配到, start是一个null, 直接return false, 如果匹配到, 第一个是匹配到的内容, 第二个是名称
            const match = {
                tagName : start[1],
                attrs: []
            }
            
            // 匹配到之后, 将匹配到的内容删除
            advance(start[0].length)    // start[0]标签匹配到的内容, 初次是 <div
            
            // 接下来就要匹配属性了
            let attr, end
            // 如果没有匹配到结束标签, 并且能匹配到属性, 就处理匹配信息, 之后将匹配到的内容删除
            // 这种写法是 : 判断html.match(startTagClose) 和 html.match(attribute), 前面只是赋值, 
            while(!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
                advance(attr[0].length)
                // true 标签单标签
                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('<')

        if(textEnd == 0) {
            const startTagMatch = parseStartTag()
            if(startTagMatch) {
                start(startTagMatch.tagName, startTagMatch.attrs)
                // 结束本轮循环
                continue
            }
            let tagEndMatch = html.match(endTag)
            if(tagEndMatch) {
                end(tagEndMatch[1])
                advance(tagEndMatch[0].length)
                continue
            }
        }
        if(textEnd > 0) {
            // 如果 < 的位置大于0, 那么从 0 到 textEnd 中间的e部分就是文本
            // 文本的内容就是
            let text = html.substring(0, textEnd)

            if(text) {
                chars(text)
                advance(text.length)
            }
        }
    }
    return root
}

新建文件 vdom/index.js


// h(), _c()
// with(this){return _c('div', {id:"app",style:{"color":"yellow","backgroundColor":"blue"}},_c('div', {style:{"color":" red"}},_v(_s(name)+"hello"+_s(age))),_c('span', null,_v(_s(age))))}
// render函数是自己拼起来的, 长上面的样子, 参数为 
/**
 * 
 * @param {vm} vm 实例
 * @param {标签名} tag 
 * @param {属性} data 可能没有, 给个默认值
 * @param  {...any} children 
 */
 export function createElementVNode(vm, tag, data = {}, ...children) {
    // 这里需要返回一个虚拟节点, 下面也需要返回虚拟节点, 单独创建一个方法
    // 这里的data可能是null, 需要判断一下
    // console.log('data:1', data)
    if(data==null) {
        data = {}
    }
    let key = data.key
    if(key) {
        delete data.key
    }
    // key一般在props里面, 这里的props就是data, 删除key之后把key属性从data里面删除
    // 不知道为啥, 不过不删应该也是影响不大
    return vnode(vm, tag, key, data, children)
}

// _v
export function createTextVNode(vm, text) {
    return vnode(vm, undefined, undefined, undefined, undefined, text)
}

// 看起来和ast语法树一样 ?
// ast做的是语法层面的转化, 描述的语法本身
// 虚拟dom描述的是dom元素, 可以新增一些自定义属性

/**
 * 
 * @param {实例} vm 
 * @param {标签名称} tag 
 * @param {key用于diff算法} key 
 * @param {属性} data 
 * @param {孩子} children 
 * @param {文本} text 
 */
function vnode(vm, tag, key, data, children, text) {
    return {
        vm,
        tag,
        key,
        data,
        children,
        text
    }
}

新建文件 src/lifecycle.js

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


export function mountComponent(vm, el) {
    // 将挂载的元素也放到实例上
    vm.$el = el
    // 1. 调用render方法产生虚拟节点
    // vm._render() 生成虚拟节点  vm._update 生成真实节点  需要先扩展这两个方法
    // vm._update(vm._render())
    // 2. 虚拟dom产生真实dom
    // 3. 插入到el元素中

    // const vdom = vm._render()
    // console.log('vdom:', vdom)

    vm._update(vm._render())
}

export function initLifeCycle(Vue) {
    Vue.prototype._render = function() {
        const vm = this
        // debugger
        // 返回的结果是虚拟dom
        // 注意this的指向, 需要call this
        // 就是执行$options里面的render方法
        // 需要拓展 _s _v _c方法
        return vm.$options.render.call(vm)
    }
    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._update = function(vnode) {

        const vm  = this
        const el = vm.$el

        vm.$el = patch(el, vnode)

    }
}


function patch(oldVNode, vnode) {
    // 现在是初次渲染
    // 需要判断是不是真实节点
    const isRealElement = oldVNode.nodeType     // nodeType是原生
    if(isRealElement) {
        const elm = oldVNode    // 获取真实元素
        const parentElm = elm.parentNode    // 拿到父元素
        // 创建真实元素
        let newElm = createElm(vnode)
        parentElm.insertBefore(newElm, elm.nextSibling)
        parentElm.removeChild(oldVNode)

        return newElm       // 如果是真实dom, 先返回一个新的dom,  暂时
    } else {
        // diff算法
    }
}


function createElm(vnode) {
    let {tag, data, children, text} = vnode 
    if(typeof tag === 'string') {   // 如果tag是string, 说明是一个标签, 如div
        vnode.el = document.createElement(tag)  // 生成一个真实节点, 并将真实节点挂载到虚拟节点上. 将虚拟节点和真实节点意义对应, 后续如果修改了属性, 可以直接找到虚拟节点对应的真实节点

        // 更新属性, 属性在data里面
        patchProps(vnode.el, data)


        // 标签会有儿子, 要处理儿子
        children.forEach(child => {
            // 同样生成元素并且插入到父元素的真实节点中, 递归调用
            vnode.el.appendChild(createElm(child))
        })

    } else {    // 不是元素就是文本
        vnode.el = document.createTextNode(text)    // 创建文本
    }
    // 这里返回一个真实dom是为了方便递归调用, 并且使用dom的方法
    return vnode.el
}

/**
 * 
 * @param {真实元素} el 
 * @param {属性} props 是一个对象
 */
 function patchProps(el, props) {
    for(let key in props) {

        // style单独处理
        if(key === 'style') {
            for(let styleName in props.style) {
                el.style[styleName] = props.style[styleName]
            }
        } else {
            el.setAttribute(key, props[key])
        }
    }
}

至此可以实现页面的初次渲染和手动刷新

posted @ 2022-06-26 17:02  littlelittleship  阅读(218)  评论(0编辑  收藏  举报