最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue源码解析(1)-数据劫持与模板编译

    正文概述 掘金(奥特曼打哥斯拉)   2021-04-05   554

    Vue源码解析(1)

    Vue源码解析(1)-数据劫持与模板编译

    Vue源码解析(1)-数据劫持与模板编译

    Vue源码解析(1)-数据劫持与模板编译

    Vue源码解析(1)-数据劫持与模板编译

    一.src入口代码

    //index.js
    import {initMixin} from './init'
    import {lifecycleMixin} from './lifecycle'
    import {renderMixin} from '../vdom'
    
    function Vue(options){
      this._init(options)
    }
    initMixin(Vue)  // 只有执行了这个方法才能将_init方法挂载到Vue的原型上
    lifecycleMixin(Vue)  // 你虽然在原型上挂载了方法 但是你得执行
    renderMixin(Vue)  // 为了使_render方法生效
    
    let vm = new Vue({
      el:"#app",
      data(){
        return {
          name:'zcl',
          age:23,
          teacher:[[1,2,3],{name:'zcl',age:22}],
          info:{a:123}
          
        }
      }
    })
    // 测试用例
    console.log(vm.teacher[0].push(4))
    console.log(vm.teacher[1].name = 'zzz')
    console.log(vm.info.a=2)
    console.log(vm)
    
    // init.js
    import {initState} from '../vue/state'
    import {compileToRenderFunction} from '../compiler'
    import {mountComponent} from './lifecycle'
    function initMixin(Vue){
      Vue.prototype._init = function(options){ // 在Vue这个构造函数的原型上添加一个_init方法
        const vm = this   // this肯定是构造函数Vue的一个实例对象 例如vm
        vm.$options = options // 将传进来的options参数 挂载到vm.$options上
        initState(vm)  // 把整个实例都传递进去 这样想获取实例上的任何参数都比较方便
    
        if(vm.$options.el){
          vm.$mount(vm.$options.el)
        }
      }
      Vue.prototype.$mount = function(el){
        const vm = this,
              options = this.$options;
        el = document.querySelector(el)
        vm.$el = el
        if(!options.render){
          // 先找是否有render函数
          let template = options.template
          if(!template && el){
            // 是否有template 如果没有只能获取html
            template = el.outerHTML
          }
          const render = compileToRenderFunction(template)
          options.render = render
        }
        mountComponent(vm)  // 上树
      }
    }
    export {initMixin}
    
    //lifecycle.js
    import {patch} from '../vdom/patch'
    function mountComponent(vm){
      vm._update(vm._render())
    }
    function lifecycleMixin(Vue){
      Vue.prototype._update = function(vnode){
        const vm = this;
        patch(vm.$el,vnode)  // 这里的vm.$el是原本html上的根节点
      }
    }
    export {lifecycleMixin,mountComponent}
    

    二.数据劫持

    //state.js
    import proxyData from "./proxy"
    import observe from './observe'
    function initState(vm){
        var options = vm.$options
        if(options.data){
          initData(vm)
        }
    }
    function initData(vm){
      var data = vm.$options.data  // 获取options选项中的data
      data = vm._data = typeof data === 'function' ? data.call(vm) : data  || {} // 将data挂载到vm._data上
      // 这里因为Vue实例中的data会呈现两种形式 一种是函数和一种是对象 所以需要进行处理
      for(var key in data){
        proxyData(vm,'_data',key)  // 进行数据代理 使数据的获取方式变成简单的vm.属性的方式 
      }
      observe(vm._data)  //  进行数据劫持
    }
    export {initState}
    
    //proxy.js
    function proxyData(vm,target,key){
      Object.defineProperty(vm,key,{
        // 因为我们在使用defineProperty之后
        // 访问数据的时候 通过vm.age访问 是通过get方法返回数据的 
        get(){
          return vm[target][key]
        },
        set(newValue){
          vm[target][key] = newValue
        }
      })
    
    }
    export default proxyData
    
    //observe.js
    import Observer from './observer'
    function observe(data){
      // 这里其实是判断是否是深层次对象 如果是就递归到底
      if(typeof data !== 'object' || data === null) return
      return new Observer(data) // 这里才是真正的数据劫持操作
    }
    export default observe
    
    //observer.js
    import defineReactiveData from './defineReactiveData'
    import observeArr from './observeArr'
    import { arrMethods } from './array'
    function Observer(data){
      if(Array.isArray(data)){
        // 如果data是数组形式
        data.__proto__ = arrMethods  // 只有真正的[]形式才添加扩展的数组方法 让其在使用这些方法的时候 进行重新劫持
        observeArr(data)  // 虽然是数组形式但是里面还有可能还有数组或{} 例如[[],[],{},{}]
      }else{
        // 如果data是{}形式
        this.walk(data)
      }
    }
    Observer.prototype.walk = function(data){
      var keys = Object.keys(data)
      for(var i=0;i<keys.length;i++){
        var key = keys[i],
            value = data[key]
        defineReactiveData(data,key,value) // 对{}形式的数据进行劫持
      }
    }
    export default Observer
    
    //defineReactiveData.js
    import defineReactiveData from './defineReactiveData'
    import observeArr from './observeArr'
    import { arrMethods } from './array'
    function Observer(data){
      if(Array.isArray(data)){
        // 如果data是数组形式
        data.__proto__ = arrMethods  // 只有真正的[]形式才添加扩展的数组方法 让其在使用这些方法的时候 进行重新劫持
        observeArr(data)  // 虽然是数组形式但是里面还有可能还有数组或{} 例如[[],[],{},{}]
      }else{
        // 如果data是{}形式
        this.walk(data)
      }
    }
    Observer.prototype.walk = function(data){
      var keys = Object.keys(data)
      for(var i=0;i<keys.length;i++){
        var key = keys[i],
            value = data[key]
        defineReactiveData(data,key,value) // 对{}形式的数据进行劫持
      }
    }
    export default Observer
    
    //config.js
    var ARR_METHODS = [
      // 以下方法会对数组的数据进行改变 我们要对新增的元素进行重新劫持
      'push','pop','shift','unshift','splice','reverse','sort'
    ]
    export {ARR_METHODS}
    
    // observeArr.js
    import observe from "./observe"
    function observeArr(data){
      for(var i=0;i<data.length;i++){
        observe(data[i])   // 我只负责遍历出来 然后让observe去判断是否需要继续去深层递归
      }
    }
    export default observeArr
    
    //array.js
    import {ARR_METHODS} from './config'
    import observeArr from './observeArr'
    
    var originArrMethods = Array.prototype,
        arrMethods = Object.create(originArrMethods)
    
    ARR_METHODS.map(function(m){
      arrMethods[m] = function(){ // 扩展数组原型上的方法
        var args = originArrMethods.slice.call(arguments), // 拷贝传递进来的参数也就是数据
            rt = originArrMethods[m].apply(this,args)  // 调用原型本身的方法
        
        var newArr
        switch(m){
          case 'push':
          case 'unshift':
            newArr = args;
            break;
          case 'splice':
            newArr = args.slice(2)
            break;
          default:
            break;
        }
        newArr && observeArr(newArr)
        return rt  // 数组调用方法是时候返回的东西 返不返回都可以
      }
    })
    export {arrMethods}
    

    三.template->Ast->render

    // index.js
    import {parseHtmlToAst} from './astParser'
    import {generate} from './generate'
    function compileToRenderFunction(template){
      const ast = parseHtmlToAst(template)  // template -> ast
      // ast -> render
      const code = generate(ast)  
      const render = new Function(`with(this)return{${code}}`)
      return render
    }
    export {compileToRenderFunction}
    
    //astParser.js
    // 匹配属性
    const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
    // 匹配开始标签
    const startTagOpen = new RegExp(`^<${qnameCapture}`)
    // 匹配开始标签的结束标签
    const startTagClose = /^\s*(\/?)>/
    // 匹配真正的结束标签
    const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
    
    function parseHtmlToAst(html){
      let root,
          currentParent,
          text,
          stack = [];
      while(html){
        let textEnd = html.indexOf('<')  // 匹配<标签判断下面一段是标签还是文本
        if(text === 0){ // 如果是标签
          const startTagMatch = parseStartTag() // 是否能匹配到开始标签
          if(startTagMatch){
            //如果匹配到了开始标签
            start(startTagMatch.tagName,startTagMatch.attrs) // 处理这段已匹配的标签
            continue;
          }
          const endTagMatch = html.match(endTag) // 是否能匹配到真正的结束标签
          if(endTagMatch){
            advance(endTagMatch[0].length)
            end()  // 保存父子级关系
            continue;
          }
        }
        // 处理文本节点
        if(textEnd>0){
          text = html.substring(0,textEnd)
        }
        if(text){
          advance(text.length)
          chars(text)
        }
      }
    
      function parseStartTag(){
        const start = html.match(startTagOpen) // 匹配开始标签
        let end,
            attr;
        if(start){
          const match = {
            tagName:start[1],
            attrs:[]
          }
          advance(start[0].length)  // 删除这段已匹配的标签
          if(!(end=html.match(startTagClose)) && (attr=html.match(attribute))){
            // 如果没有匹配到开始标签的结束标签但是匹配到了属性标签 处理属性
            match.attrs.push({
              name:attr[1],
              value:attr[3] || attr[4] || attr[5]
            })
            advance(attr[0].length)
          }
          if(end){
            // 如果匹配到了开始标签的结束标签
            advance(end[0].length)
            return match
          }
        }
      }
      function advance(length){
        html = html.substring(length)
      }
      function start(tagName,attrs){
        const element = createASTElement(tagName,attrs) // 组装AST树
        if(!root){
          root = element  // 第一个节点作为根节点
        }
        currentParent = element // 保存当前节点为父亲节点
        stack.push(element)  // 保存当前节点
      }
      function end(){
        const element = stack.pop()
        currentParent = stack[stack.length-1]
        if(currentParent){
          element.parent = currentParent
          currentParent.children.push(element)
        }
      }
      function chars(text){
        text = text.trim()
        if(text.length>0){
          currentParent.children.push({
            type:3,
            text
          })
        }
      }
      function createASTElement(tagName,attrs){
        return{
          tag:tagName,
          attrs,
          children:[],
          parent,
          type:1
        }
      }
      return root;
    }
    export {parseHtmlToAst}
    
    //generate.js
    const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
    
    function generate(el){
      let children = getChildren(el)
      return `_c('${el.tag}',${el.attrs.length>0?`${formatProps(el.attrs)}`:'undefined'}${children?`,${children}`:''})`
    }
    function getChildren(el){
      // 获取孩子并将处理后的结果进行拼接
      const children = el.children
      if(children){
        return children.map(child=>generateChild(child)).join(',')
      }
    }
    function generateChild(node){
      // 处理孩子
      if(node.type===1){
        // 如果是元素节点 则判断它是否还有孩子
        return generate(node)
      }else if(node.type === 3){
        // 文本节点
        let text = node.text
        if(!defaultTagRE.test(text)){
          // 纯文本
          return `_v(${JSON.stringify(text)})`
        }
        // 处理带有差值表达式的问呗
        let match,
            index,
            lastIndex = defaultTagRE.lastIndex = 0
        let textArr = []
        while(match = defaultTagRE.exec(text)){
          index = match.index // 所以为插值表达式第一个{的位置
          if(index>lastIndex){
            // 纯文本部分
            textArr.push(JSON.stringify(text.slice(lastIndex,index)))
          }
          // 插值表达式部分
          textArr.push(`_s(${match[1].trim()})`)
          lastIndex = index + match[0].length
        }
        if(lastIndex>text.length){
          // 仍有纯文本
          textArr.push(JSON.stringify(text.slice(lastIndex)))
        }
        return `_v(${textArr.join('+')})`
      }
    }
    function formatProps(attrs){
      // 处理属性
      let attrStr = ""
      for(var i=0;i<attrs.length;i++){
        let attr = attrs[i]
        if(attr.name === "style"){
          let styleAttrs = {}
          attr.value.split(';').map((styleAttr)=>{
            let [key,value] = styleAttr.split(':')
            styleAttrs[key] = value
          })
          attr.value  = styleAttrs
        }
        attrStr += `${attr.name}:${JSON.stringify(attr.value)},`
      }
      return `{${attrStr.slice(0,-1)}}`  // 去除多余的逗号
    }
    export {generate}
    

    四.render->vnode->上树

    //index.js
    import {createElement,createTextVnode} from './vnode'
    function renderMixin(Vue){
      Vue.prototype._c = function(){
        // 创建元素虚拟节点
        return createElement(...arguments)
      }
      Vue.prototype._v = function(text){
        // 创建文本虚拟节点
        return createTextVnode(text)
      }
      Vue.prototype._s = function(value){
        // 处理_s(name)
        if(value === null) return 
        return typeof value === 'object' ? JSON.stringify(value) : value
      }
      Vue.prototype._render = function(){
              render = this.$options.render
              vnode = render() // 生成虚拟节点
        return vnode 
      }
    }
    export {renderMixin}
    
    //vnode.js
    function vnode(tag,props,children,text){
      // 组装虚拟节点
      return {
        tag,props,children,text
      }
    }
    function createElement(tag,attrs={},...children){
      // 返回元素节点  元素节点是没有文本的
      return vnode(tag,attrs,children)
    }
    function createTextVnode(text){
      return vnode(undefined,undefined,undefined,text)
    }
    export {createElement,createTextVnode}
    
    //patch.js
    function patch(oldNode,vNode){
      let el = createElement(vNode),
          parentElement = oldNode.parentNode; // body
      parentElement.insertBefore(el,oldNode.nextSibling)
      parentElement.removeChild(oldNode)
    }
    
    function createElement(vnode){
      const {tag,props,children,text} = vnode;
      if(typeof tag === 'string'){
        // 处理标签
        vnode.el = document.createElement(tag)
        updateProps(vnode)
        children.map((child)=>{
          // 这已经是在真实DOM里面添加节点
          vnode.el.appendChild(createElement(child))
        })
      }else{
        vnode.el = document.createTextNode(text)
      }
      return vnode.el
    }
    
    function updateProps(vnode){
      const el = vnode.el,
            newProps = vnode.props || {}
      for(let key in newProps){
        if(key === 'style'){ 
          // 如果key为style 则代表是深层次对象 需要在进行遍历
          for(let sKey in newProps.style){
            el.style[sKey] = newProps.style[sKey]
          }
        }else if(key === 'class'){
          el.className = newProps[key]
        }else{
          el.setAttributes(key,newProps[key])
        }
      }
    }
    export {patch}
    

    起源地下载网 » Vue源码解析(1)-数据劫持与模板编译

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元